Ver código fonte

fixed themoviedb search returning no results

Luke Pulverenti 11 anos atrás
pai
commit
1a9e2dfd83
27 arquivos alterados com 696 adições e 594 exclusões
  1. 0 5
      MediaBrowser.Api/ItemUpdateService.cs
  2. 13 31
      MediaBrowser.Api/Library/LibraryStructureService.cs
  3. 4 4
      MediaBrowser.Api/NotificationsService.cs
  4. 2 2
      MediaBrowser.Controller/Entities/BaseItem.cs
  5. 1 3
      MediaBrowser.Controller/Entities/TV/Series.cs
  6. 4 4
      MediaBrowser.Model/Entities/LocationType.cs
  7. 3 3
      MediaBrowser.Model/Notifications/NotificationLevel.cs
  8. 12 2
      MediaBrowser.Providers/Manager/MetadataService.cs
  9. 1 0
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  10. 17 6
      MediaBrowser.Providers/Movies/MovieDbProvider.cs
  11. 2 2
      MediaBrowser.Providers/Movies/MovieDbSearch.cs
  12. 493 0
      MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
  13. 16 6
      MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs
  14. 25 0
      MediaBrowser.Providers/TV/SeriesMetadataService.cs
  15. 1 481
      MediaBrowser.Providers/TV/SeriesPostScanTask.cs
  16. 1 1
      MediaBrowser.Providers/TV/TvdbSeriesProvider.cs
  17. 1 1
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  18. 15 0
      MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  19. 9 11
      MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  20. 2 1
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  21. 27 25
      MediaBrowser.Server.Implementations/Library/Validators/CountHelpers.cs
  22. 10 0
      MediaBrowser.Server.Implementations/Persistence/SqliteMediaStreamsRepository.cs
  23. 1 1
      MediaBrowser.Server.Implementations/Persistence/SqliteShrinkMemoryTimer.cs
  24. 31 0
      MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj
  25. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  26. 1 1
      Nuget/MediaBrowser.Common.nuspec
  27. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 0 - 5
MediaBrowser.Api/ItemUpdateService.cs

@@ -211,11 +211,6 @@ namespace MediaBrowser.Api
 
         private void UpdateItem(BaseItemDto request, BaseItem item)
         {
-            if (item.LocationType == LocationType.Offline)
-            {
-                throw new InvalidOperationException(string.Format("{0} is currently offline.", item.Name));
-            }
-
             item.Name = request.Name;
 
             // Only set the forced value if they changed it, or there's already one

+ 13 - 31
MediaBrowser.Api/Library/LibraryStructureService.cs

@@ -1,6 +1,5 @@
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
@@ -167,32 +166,30 @@ namespace MediaBrowser.Api.Library
         public bool RefreshLibrary { get; set; }
     }
 
-    [Route("/Library/Changes/New", "POST")]
-    public class ReportChangedPath : IReturnVoid
+    [Route("/Library/Downloaded", "POST")]
+    public class ReportContentDownloaded : IReturnVoid
     {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Path", Description = "The path that was changed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        [ApiMember(Name = "Path", Description = "The path being downloaded to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string Path { get; set; }
 
         [ApiMember(Name = "ImageUrl", Description = "Optional thumbnail image url of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string ImageUrl { get; set; }
+
+        [ApiMember(Name = "Name", Description = "The name of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Name { get; set; }
     }
 
-    [Route("/Library/Episodes/New", "POST")]
-    public class ReportNewEpisode : IReturnVoid
+    [Route("/Library/Downloading", "POST")]
+    public class ReportContentDownloading : IReturnVoid
     {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "TvdbId", Description = "The tvdb id of the new episode.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string TvdbId { get; set; }
+        [ApiMember(Name = "Path", Description = "The path being downloaded to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Path { get; set; }
 
         [ApiMember(Name = "ImageUrl", Description = "Optional thumbnail image url of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string ImageUrl { get; set; }
+
+        [ApiMember(Name = "Name", Description = "The name of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Name { get; set; }
     }
     
     /// <summary>
@@ -242,21 +239,6 @@ namespace MediaBrowser.Api.Library
             _logger = logger;
         }
 
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <exception cref="System.ArgumentException">Please supply a Path</exception>
-        public void Post(ReportChangedPath request)
-        {
-            if (string.IsNullOrEmpty(request.Path))
-            {
-                throw new ArgumentException("Please supply a Path");
-            }
-
-            _libraryMonitor.ReportFileSystemChanged(request.Path);
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 4 - 4
MediaBrowser.Api/NotificationsService.cs

@@ -35,7 +35,7 @@ namespace MediaBrowser.Api
 
     [Route("/Notifications/{UserId}", "POST")]
     [Api(Description = "Adds a notifications")]
-    public class AddNotification : IReturn<Notification>
+    public class AddUserNotification : IReturn<Notification>
     {
         [ApiMember(Name = "Id", Description = "The Id of the new notification. If unspecified one will be provided.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         public Guid? Id { get; set; }
@@ -61,7 +61,7 @@ namespace MediaBrowser.Api
         [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         public NotificationLevel Level { get; set; }
     }
-
+    
     [Route("/Notifications/{UserId}/Read", "POST")]
     [Api(Description = "Marks notifications as read")]
     public class MarkRead : IReturnVoid
@@ -93,7 +93,7 @@ namespace MediaBrowser.Api
             _notificationsRepo = notificationsRepo;
         }
 
-        public object Post(AddNotification request)
+        public object Post(AddUserNotification request)
         {
             var task = AddNotification(request);
 
@@ -107,7 +107,7 @@ namespace MediaBrowser.Api
             return result;
         }
 
-        private async Task<Notification> AddNotification(AddNotification request)
+        private async Task<Notification> AddNotification(AddUserNotification request)
         {
             var notification = new Notification
             {

+ 2 - 2
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -163,7 +163,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 var locationType = LocationType;
 
-                return locationType == LocationType.FileSystem || locationType == LocationType.Offline;
+                return locationType != LocationType.Remote && locationType != LocationType.Virtual;
             }
         }
 
@@ -581,7 +581,7 @@ namespace MediaBrowser.Controller.Entities
 
                 try
                 {
-                    var files = locationType == LocationType.FileSystem || locationType == LocationType.Offline ?
+                    var files = locationType != LocationType.Remote && locationType != LocationType.Virtual ?
                         GetFileSystemChildren(options.DirectoryService).ToList() :
                         new List<FileSystemInfo>();
 

+ 1 - 3
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -1,12 +1,10 @@
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Localization;
+using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
 using System.Runtime.Serialization;
 

+ 4 - 4
MediaBrowser.Model/Entities/LocationType.cs

@@ -9,18 +9,18 @@ namespace MediaBrowser.Model.Entities
         /// <summary>
         /// The file system
         /// </summary>
-        FileSystem,
+        FileSystem = 1,
         /// <summary>
         /// The remote
         /// </summary>
-        Remote,
+        Remote = 2,
         /// <summary>
         /// The virtual
         /// </summary>
-        Virtual,
+        Virtual = 3,
         /// <summary>
         /// The offline
         /// </summary>
-        Offline
+        Offline = 4
     }
 }

+ 3 - 3
MediaBrowser.Model/Notifications/NotificationLevel.cs

@@ -3,8 +3,8 @@ namespace MediaBrowser.Model.Notifications
 {
     public enum NotificationLevel
     {
-        Normal,
-        Warning,
-        Error
+        Normal = 1,
+        Warning = 2,
+        Error = 3
     }
 }

+ 12 - 2
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -282,7 +282,8 @@ namespace MediaBrowser.Providers.Manager
 
             foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
             {
-                Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name);
+                var providerName = provider.GetType().Name;
+                Logger.Debug("Running {0} for {1}", providerName, item.Path ?? item.Name);
 
                 var itemInfo = new ItemInfo { Path = item.Path, IsInMixedFolder = item.IsInMixedFolder };
 
@@ -309,6 +310,10 @@ namespace MediaBrowser.Providers.Manager
 
                         Logger.Error("Invalid local metadata found for: " + item.Path);
                     }
+                    else
+                    {
+                        Logger.Debug("{0} returned no metadata for {1}", providerName, item.Path ?? item.Name);
+                    }
                 }
                 catch (OperationCanceledException)
                 {
@@ -376,7 +381,8 @@ namespace MediaBrowser.Providers.Manager
 
             foreach (var provider in providers)
             {
-                Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name);
+                var providerName = provider.GetType().Name;
+                Logger.Debug("Running {0} for {1}", providerName, item.Path ?? item.Name);
 
                 if (id == null)
                 {
@@ -397,6 +403,10 @@ namespace MediaBrowser.Providers.Manager
 
                         refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload;
                     }
+                    else
+                    {
+                        Logger.Debug("{0} returned no metadata for {1}", providerName, item.Path ?? item.Name);
+                    }
                 }
                 catch (OperationCanceledException)
                 {

+ 1 - 0
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -180,6 +180,7 @@
     <Compile Include="TV\FanArtTvUpdatesPostScanTask.cs" />
     <Compile Include="TV\FanartSeasonProvider.cs" />
     <Compile Include="TV\FanartSeriesProvider.cs" />
+    <Compile Include="TV\MissingEpisodeProvider.cs" />
     <Compile Include="TV\MovieDbSeriesImageProvider.cs" />
     <Compile Include="TV\MovieDbSeriesProvider.cs" />
     <Compile Include="TV\SeriesMetadataService.cs" />

+ 17 - 6
MediaBrowser.Providers/Movies/MovieDbProvider.cs

@@ -1,9 +1,11 @@
-using MediaBrowser.Common.Configuration;
+using System.Linq;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
@@ -30,14 +32,16 @@ namespace MediaBrowser.Providers.Movies
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly ILogger _logger;
+        private readonly ILocalizationManager _localization;
 
-        public MovieDbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger)
+        public MovieDbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization)
         {
             _jsonSerializer = jsonSerializer;
             _httpClient = httpClient;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _logger = logger;
+            _localization = localization;
             Current = this;
         }
 
@@ -222,20 +226,27 @@ namespace MediaBrowser.Providers.Movies
         {
             var url = string.Format(GetMovieInfo3, id, ApiKey);
 
-            // Get images in english and with no language
-            url += "&include_image_language=en,null";
+            var imageLanguages = _localization.GetCultures()
+                .Select(i => i.TwoLetterISOLanguageName)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .ToList();
+
+            imageLanguages.Add("null");
 
             if (!string.IsNullOrEmpty(language))
             {
                 // If preferred language isn't english, get those images too
-                if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+                if (imageLanguages.Contains(language, StringComparer.OrdinalIgnoreCase))
                 {
-                    url += string.Format(",{0}", language);
+                    imageLanguages.Add(language);
                 }
 
                 url += string.Format("&language={0}", language);
             }
 
+            // Get images in english and with no language
+            url += "&include_image_language=" + string.Join(",", imageLanguages.ToArray());
+
             CompleteMovieData mainResult;
 
             cancellationToken.ThrowIfCancellationRequested();

+ 2 - 2
MediaBrowser.Providers/Movies/MovieDbSearch.cs

@@ -142,7 +142,7 @@ namespace MediaBrowser.Providers.Movies
 
                 if (result != null)
                 {
-                    return null;
+                    return result;
                 }
 
                 // Take the first result within one year
@@ -165,7 +165,7 @@ namespace MediaBrowser.Providers.Movies
 
                 if (result != null)
                 {
-                    return null;
+                    return result;
                 }
             }
 

+ 493 - 0
MediaBrowser.Providers/TV/MissingEpisodeProvider.cs

@@ -0,0 +1,493 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Providers.TV
+{
+    class MissingEpisodeProvider
+    {
+        private readonly IServerConfigurationManager _config;
+        private readonly ILogger _logger;
+
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+        public MissingEpisodeProvider(ILogger logger, IServerConfigurationManager config)
+        {
+            _logger = logger;
+            _config = config;
+        }
+
+        public async Task Run(IEnumerable<IGrouping<string, Series>> series, CancellationToken cancellationToken)
+        {
+            foreach (var seriesGroup in series)
+            {
+                await Run(seriesGroup, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        private async Task Run(IGrouping<string, Series> group, CancellationToken cancellationToken)
+        {
+            var tvdbId = group.Key;
+
+            var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, tvdbId);
+
+            var episodeFiles = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly)
+                .Select(Path.GetFileNameWithoutExtension)
+                .Where(i => i.StartsWith("episode-", StringComparison.OrdinalIgnoreCase))
+                .ToList();
+
+            var episodeLookup = episodeFiles
+                .Select(i =>
+                {
+                    var parts = i.Split('-');
+
+                    if (parts.Length == 3)
+                    {
+                        int seasonNumber;
+
+                        if (int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out seasonNumber))
+                        {
+                            int episodeNumber;
+
+                            if (int.TryParse(parts[2], NumberStyles.Integer, UsCulture, out episodeNumber))
+                            {
+                                return new Tuple<int, int>(seasonNumber, episodeNumber);
+                            }
+                        }
+                    }
+
+                    return new Tuple<int, int>(-1, -1);
+                })
+                .Where(i => i.Item1 != -1 && i.Item2 != -1)
+                .ToList();
+
+            var anySeasonsRemoved = await RemoveObsoleteOrMissingSeasons(group, episodeLookup, cancellationToken)
+                .ConfigureAwait(false);
+
+            var anyEpisodesRemoved = await RemoveObsoleteOrMissingEpisodes(group, episodeLookup, cancellationToken)
+                .ConfigureAwait(false);
+
+            var hasNewEpisodes = false;
+            var hasNewSeasons = false;
+
+            foreach (var series in group.Where(s => s.ContainsEpisodesWithoutSeasonFolders))
+            {
+                hasNewSeasons = await AddDummySeasonFolders(series, cancellationToken).ConfigureAwait(false);
+            }
+
+            var seriesConfig = _config.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, typeof(Series).Name, StringComparison.OrdinalIgnoreCase));
+
+            if (seriesConfig == null || !seriesConfig.DisabledMetadataFetchers.Contains(TvdbSeriesProvider.Current.Name, StringComparer.OrdinalIgnoreCase))
+            {
+                hasNewEpisodes = await AddMissingEpisodes(group.ToList(), seriesDataPath, episodeLookup, cancellationToken)
+                    .ConfigureAwait(false);
+            }
+
+            if (hasNewSeasons || hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved)
+            {
+                foreach (var series in group)
+                {
+                    await series.RefreshMetadata(new MetadataRefreshOptions
+                    {
+                    }, cancellationToken).ConfigureAwait(false);
+
+                    await series.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(), true)
+                        .ConfigureAwait(false);
+                }
+            }
+        }
+
+        /// <summary>
+        /// For series with episodes directly under the series folder, this adds dummy seasons to enable regular browsing and metadata
+        /// </summary>
+        /// <param name="series"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        private async Task<bool> AddDummySeasonFolders(Series series, CancellationToken cancellationToken)
+        {
+            var existingEpisodes = series.RecursiveChildren
+                .OfType<Episode>()
+                .ToList();
+
+            var hasChanges = false;
+
+            // Loop through the unique season numbers
+            foreach (var seasonNumber in existingEpisodes.Select(i => i.ParentIndexNumber ?? -1)
+                .Where(i => i >= 0)
+                .Distinct()
+                .ToList())
+            {
+                var hasSeason = series.Children.OfType<Season>()
+                    .Any(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
+
+                if (!hasSeason)
+                {
+                    await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false);
+
+                    hasChanges = true;
+                }
+            }
+
+            return hasChanges;
+        }
+
+        /// <summary>
+        /// Adds the missing episodes.
+        /// </summary>
+        /// <param name="series">The series.</param>
+        /// <param name="seriesDataPath">The series data path.</param>
+        /// <param name="episodeLookup">The episode lookup.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task<bool> AddMissingEpisodes(List<Series> series, string seriesDataPath, IEnumerable<Tuple<int, int>> episodeLookup, CancellationToken cancellationToken)
+        {
+            var existingEpisodes = series.SelectMany(s => s.RecursiveChildren.OfType<Episode>()).ToList();
+
+            var hasChanges = false;
+
+            foreach (var tuple in episodeLookup)
+            {
+                if (tuple.Item1 <= 0)
+                {
+                    // Ignore season zeros
+                    continue;
+                }
+
+                if (tuple.Item2 <= 0)
+                {
+                    // Ignore episode zeros
+                    continue;
+                }
+
+                var existingEpisode = GetExistingEpisode(existingEpisodes, tuple);
+
+                if (existingEpisode != null)
+                {
+                    continue;
+                }
+
+                var airDate = GetAirDate(seriesDataPath, tuple.Item1, tuple.Item2);
+
+                if (!airDate.HasValue)
+                {
+                    continue;
+                }
+                var now = DateTime.UtcNow;
+
+                var targetSeries = DetermineAppropriateSeries(series, tuple.Item1);
+
+                if (airDate.Value < now)
+                {
+                    // tvdb has a lot of nearly blank episodes
+                    _logger.Info("Creating virtual missing episode {0} {1}x{2}", targetSeries.Name, tuple.Item1, tuple.Item2);
+
+                    await AddEpisode(targetSeries, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false);
+
+                    hasChanges = true;
+                }
+                else if (airDate.Value > now)
+                {
+                    // tvdb has a lot of nearly blank episodes
+                    _logger.Info("Creating virtual unaired episode {0} {1}x{2}", targetSeries.Name, tuple.Item1, tuple.Item2);
+
+                    await AddEpisode(targetSeries, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false);
+
+                    hasChanges = true;
+                }
+            }
+
+            return hasChanges;
+        }
+
+        private Series DetermineAppropriateSeries(List<Series> series, int seasonNumber)
+        {
+            return series.FirstOrDefault(s => s.RecursiveChildren.OfType<Season>().Any(season => season.IndexNumber == seasonNumber)) ??
+                    series.FirstOrDefault(s => s.RecursiveChildren.OfType<Season>().Any(season => season.IndexNumber == 1)) ??
+                   series.OrderBy(s => s.RecursiveChildren.OfType<Season>().Select(season => season.IndexNumber).Min()).First();
+        }
+
+        /// <summary>
+        /// Removes the virtual entry after a corresponding physical version has been added
+        /// </summary>
+        private async Task<bool> RemoveObsoleteOrMissingEpisodes(IEnumerable<Series> series, IEnumerable<Tuple<int, int>> episodeLookup, CancellationToken cancellationToken)
+        {
+            var existingEpisodes = series.SelectMany(s => s.RecursiveChildren.OfType<Episode>()).ToList();
+
+            var physicalEpisodes = existingEpisodes
+                .Where(i => i.LocationType != LocationType.Virtual)
+                .ToList();
+
+            var virtualEpisodes = existingEpisodes
+                .Where(i => i.LocationType == LocationType.Virtual)
+                .ToList();
+
+            var episodesToRemove = virtualEpisodes
+                .Where(i =>
+                {
+                    if (i.IndexNumber.HasValue && i.ParentIndexNumber.HasValue)
+                    {
+                        var seasonNumber = i.ParentIndexNumber.Value;
+                        var episodeNumber = i.IndexNumber.Value;
+
+                        // If there's a physical episode with the same season and episode number, delete it
+                        if (physicalEpisodes.Any(p =>
+                                p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber &&
+                                p.ContainsEpisodeNumber(episodeNumber)))
+                        {
+                            return true;
+                        }
+
+                        // If the episode no longer exists in the remote lookup, delete it
+                        if (!episodeLookup.Any(e => e.Item1 == seasonNumber && e.Item2 == episodeNumber))
+                        {
+                            return true;
+                        }
+
+                        return false;
+                    }
+
+                    return true;
+                })
+                .ToList();
+
+            var hasChanges = false;
+
+            foreach (var episodeToRemove in episodesToRemove)
+            {
+                _logger.Info("Removing missing/unaired episode {0} {1}x{2}", episodeToRemove.Series.Name, episodeToRemove.ParentIndexNumber, episodeToRemove.IndexNumber);
+
+                await episodeToRemove.Parent.RemoveChild(episodeToRemove, cancellationToken).ConfigureAwait(false);
+
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
+
+        /// <summary>
+        /// Removes the obsolete or missing seasons.
+        /// </summary>
+        /// <param name="series">The series.</param>
+        /// <param name="episodeLookup">The episode lookup.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{System.Boolean}.</returns>
+        private async Task<bool> RemoveObsoleteOrMissingSeasons(IEnumerable<Series> series, IEnumerable<Tuple<int, int>> episodeLookup, CancellationToken cancellationToken)
+        {
+            var existingSeasons = series.SelectMany(s => s.Children.OfType<Season>()).ToList();
+
+            var physicalSeasons = existingSeasons
+                .Where(i => i.LocationType != LocationType.Virtual)
+                .ToList();
+
+            var virtualSeasons = existingSeasons
+                .Where(i => i.LocationType == LocationType.Virtual)
+                .ToList();
+
+            var seasonsToRemove = virtualSeasons
+                .Where(i =>
+                {
+                    if (i.IndexNumber.HasValue)
+                    {
+                        var seasonNumber = i.IndexNumber.Value;
+
+                        // If there's a physical season with the same number, delete it
+                        if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber))
+                        {
+                            return true;
+                        }
+
+                        // If the season no longer exists in the remote lookup, delete it
+                        if (episodeLookup.All(e => e.Item1 != seasonNumber))
+                        {
+                            return true;
+                        }
+
+                        return false;
+                    }
+
+                    return true;
+                })
+                .ToList();
+
+            var hasChanges = false;
+
+            foreach (var seasonToRemove in seasonsToRemove)
+            {
+                _logger.Info("Removing virtual season {0} {1}", seasonToRemove.Series.Name, seasonToRemove.IndexNumber);
+
+                await seasonToRemove.Parent.RemoveChild(seasonToRemove, cancellationToken).ConfigureAwait(false);
+
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
+
+        /// <summary>
+        /// Adds the episode.
+        /// </summary>
+        /// <param name="series">The series.</param>
+        /// <param name="seasonNumber">The season number.</param>
+        /// <param name="episodeNumber">The episode number.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken)
+        {
+            var season = series.Children.OfType<Season>()
+                .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
+
+            if (season == null)
+            {
+                season = await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false);
+            }
+
+            var name = string.Format("Episode {0}", episodeNumber.ToString(UsCulture));
+
+            var episode = new Episode
+            {
+                Name = name,
+                IndexNumber = episodeNumber,
+                ParentIndexNumber = seasonNumber,
+                Parent = season,
+                DisplayMediaType = typeof(Episode).Name,
+                Id = (series.Id + seasonNumber.ToString(UsCulture) + name).GetMBId(typeof(Episode))
+            };
+
+            await season.AddChild(episode, cancellationToken).ConfigureAwait(false);
+
+            await episode.RefreshMetadata(new MetadataRefreshOptions
+            {
+            }, cancellationToken).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Adds the season.
+        /// </summary>
+        /// <param name="series">The series.</param>
+        /// <param name="seasonNumber">The season number.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{Season}.</returns>
+        private async Task<Season> AddSeason(Series series, int seasonNumber, CancellationToken cancellationToken)
+        {
+            _logger.Info("Creating Season {0} entry for {1}", seasonNumber, series.Name);
+
+            var name = seasonNumber == 0 ? _config.Configuration.SeasonZeroDisplayName : string.Format("Season {0}", seasonNumber.ToString(UsCulture));
+
+            var season = new Season
+            {
+                Name = name,
+                IndexNumber = seasonNumber,
+                Parent = series,
+                DisplayMediaType = typeof(Season).Name,
+                Id = (series.Id + seasonNumber.ToString(UsCulture) + name).GetMBId(typeof(Season))
+            };
+
+            await series.AddChild(season, cancellationToken).ConfigureAwait(false);
+
+            await season.RefreshMetadata(new MetadataRefreshOptions
+            {
+            }, cancellationToken).ConfigureAwait(false);
+
+            return season;
+        }
+
+        /// <summary>
+        /// Gets the existing episode.
+        /// </summary>
+        /// <param name="existingEpisodes">The existing episodes.</param>
+        /// <param name="tuple">The tuple.</param>
+        /// <returns>Episode.</returns>
+        private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, Tuple<int, int> tuple)
+        {
+            return existingEpisodes
+                .FirstOrDefault(i => (i.ParentIndexNumber ?? -1) == tuple.Item1 && i.ContainsEpisodeNumber(tuple.Item2));
+        }
+
+        /// <summary>
+        /// Gets the air date.
+        /// </summary>
+        /// <param name="seriesDataPath">The series data path.</param>
+        /// <param name="seasonNumber">The season number.</param>
+        /// <param name="episodeNumber">The episode number.</param>
+        /// <returns>System.Nullable{DateTime}.</returns>
+        private DateTime? GetAirDate(string seriesDataPath, int seasonNumber, int episodeNumber)
+        {
+            // First open up the tvdb xml file and make sure it has valid data
+            var filename = string.Format("episode-{0}-{1}.xml", seasonNumber.ToString(UsCulture), episodeNumber.ToString(UsCulture));
+
+            var xmlPath = Path.Combine(seriesDataPath, filename);
+
+            DateTime? airDate = null;
+
+            // It appears the best way to filter out invalid entries is to only include those with valid air dates
+            using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
+            {
+                // Use XmlReader for best performance
+                using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings
+                {
+                    CheckCharacters = false,
+                    IgnoreProcessingInstructions = true,
+                    IgnoreComments = true,
+                    ValidationType = ValidationType.None
+                }))
+                {
+                    reader.MoveToContent();
+
+                    // Loop through each element
+                    while (reader.Read())
+                    {
+                        if (reader.NodeType == XmlNodeType.Element)
+                        {
+                            switch (reader.Name)
+                            {
+                                case "EpisodeName":
+                                    {
+                                        var val = reader.ReadElementContentAsString();
+                                        if (string.IsNullOrWhiteSpace(val))
+                                        {
+                                            // Not valid, ignore these
+                                            return null;
+                                        }
+                                        break;
+                                    }
+                                case "FirstAired":
+                                    {
+                                        var val = reader.ReadElementContentAsString();
+
+                                        if (!string.IsNullOrWhiteSpace(val))
+                                        {
+                                            DateTime date;
+                                            if (DateTime.TryParse(val, out date))
+                                            {
+                                                airDate = date.ToUniversalTime();
+                                            }
+                                        }
+
+                                        break;
+                                    }
+
+                                default:
+                                    reader.Skip();
+                                    break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            return airDate;
+        }
+    }
+}

+ 16 - 6
MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs

@@ -4,6 +4,7 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
@@ -30,13 +31,15 @@ namespace MediaBrowser.Providers.TV
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly ILogger _logger;
+        private readonly ILocalizationManager _localization;
 
-        public MovieDbSeriesProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger)
+        public MovieDbSeriesProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization)
         {
             _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _logger = logger;
+            _localization = localization;
             Current = this;
         }
 
@@ -157,6 +160,7 @@ namespace MediaBrowser.Providers.TV
             if (string.Equals(seriesInfo.status, "Ended", StringComparison.OrdinalIgnoreCase))
             {
                 series.Status = SeriesStatus.Ended;
+                series.EndDate = seriesInfo.last_air_date;
             }
             else
             {
@@ -164,7 +168,6 @@ namespace MediaBrowser.Providers.TV
             }
 
             series.PremiereDate = seriesInfo.first_air_date;
-            series.EndDate = seriesInfo.last_air_date;
 
             var ids = seriesInfo.external_ids;
             if (ids != null)
@@ -215,19 +218,26 @@ namespace MediaBrowser.Providers.TV
         {
             var url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey);
 
-            // Get images in english and with no language
-            url += "&include_image_language=en,null";
+            var imageLanguages = _localization.GetCultures()
+                .Select(i => i.TwoLetterISOLanguageName)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .ToList();
+
+            imageLanguages.Add("null");
 
             if (!string.IsNullOrEmpty(language))
             {
                 // If preferred language isn't english, get those images too
-                if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+                if (imageLanguages.Contains(language, StringComparer.OrdinalIgnoreCase))
                 {
-                    url += string.Format(",{0}", language);
+                    imageLanguages.Add(language);
                 }
 
                 url += string.Format("&language={0}", language);
             }
+            
+            // Get images in english and with no language
+            url += "&include_image_language=" + string.Join(",", imageLanguages.ToArray());
 
             cancellationToken.ThrowIfCancellationRequested();
 

+ 25 - 0
MediaBrowser.Providers/TV/SeriesMetadataService.cs

@@ -32,6 +32,31 @@ namespace MediaBrowser.Providers.TV
         protected override void MergeData(Series source, Series target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
             ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+
+            if (replaceData || target.SeasonCount == 0)
+            {
+                target.SeasonCount = source.SeasonCount;
+            }
+
+            if (replaceData || string.IsNullOrEmpty(target.AirTime))
+            {
+                target.AirTime = source.AirTime;
+            }
+
+            if (replaceData || !target.Status.HasValue)
+            {
+                target.Status = source.Status;
+            }
+
+            if (replaceData || target.AirDays.Count == 0)
+            {
+                target.AirDays = source.AirDays;
+            } 
+            
+            if (mergeMetadataSettings)
+            {
+                target.DisplaySpecialsWithSeasons = source.DisplaySpecialsWithSeasons;
+            }
         }
 
         protected override ItemUpdateType BeforeSave(Series item)

+ 1 - 481
MediaBrowser.Providers/TV/SeriesPostScanTask.cs

@@ -1,19 +1,13 @@
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
 using System.Linq;
-using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using System.Xml;
 
 namespace MediaBrowser.Providers.TV
 {
@@ -100,478 +94,4 @@ namespace MediaBrowser.Providers.TV
         }
     }
 
-    class MissingEpisodeProvider
-    {
-        private readonly IServerConfigurationManager _config;
-        private readonly ILogger _logger;
-
-        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
-        public MissingEpisodeProvider(ILogger logger, IServerConfigurationManager config)
-        {
-            _logger = logger;
-            _config = config;
-        }
-
-        public async Task Run(IEnumerable<IGrouping<string, Series>> series, CancellationToken cancellationToken)
-        {
-            foreach (var seriesGroup in series)
-            {
-                await Run(seriesGroup, cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        private async Task Run(IGrouping<string, Series> group, CancellationToken cancellationToken)
-        {
-            var tvdbId = group.Key;
-
-            var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, tvdbId);
-
-            var episodeFiles = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly)
-                .Select(Path.GetFileNameWithoutExtension)
-                .Where(i => i.StartsWith("episode-", StringComparison.OrdinalIgnoreCase))
-                .ToList();
-
-            var episodeLookup = episodeFiles
-                .Select(i =>
-                {
-                    var parts = i.Split('-');
-
-                    if (parts.Length == 3)
-                    {
-                        int seasonNumber;
-
-                        if (int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out seasonNumber))
-                        {
-                            int episodeNumber;
-
-                            if (int.TryParse(parts[2], NumberStyles.Integer, UsCulture, out episodeNumber))
-                            {
-                                return new Tuple<int, int>(seasonNumber, episodeNumber);
-                            }
-                        }
-                    }
-
-                    return new Tuple<int, int>(-1, -1);
-                })
-                .Where(i => i.Item1 != -1 && i.Item2 != -1)
-                .ToList();
-
-            var anySeasonsRemoved = await RemoveObsoleteOrMissingSeasons(group, episodeLookup, cancellationToken)
-                .ConfigureAwait(false);
-
-            var anyEpisodesRemoved = await RemoveObsoleteOrMissingEpisodes(group, episodeLookup, cancellationToken)
-                .ConfigureAwait(false);
-
-            var hasNewEpisodes = false;
-            var hasNewSeasons = false;
-
-            foreach (var series in group.Where(s => s.ContainsEpisodesWithoutSeasonFolders))
-            {
-                hasNewSeasons = await AddDummySeasonFolders(series, cancellationToken).ConfigureAwait(false);
-            }
-
-            var seriesConfig = _config.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, typeof(Series).Name, StringComparison.OrdinalIgnoreCase));
-
-            if (seriesConfig == null || !seriesConfig.DisabledMetadataFetchers.Contains(TvdbSeriesProvider.Current.Name, StringComparer.OrdinalIgnoreCase))
-            {
-                hasNewEpisodes = await AddMissingEpisodes(group.ToList(), seriesDataPath, episodeLookup, cancellationToken)
-                    .ConfigureAwait(false);
-            }
-
-            if (hasNewSeasons || hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved)
-            {
-                foreach (var series in group)
-                {
-                    await series.RefreshMetadata(new MetadataRefreshOptions
-                    {
-                    }, cancellationToken).ConfigureAwait(false);
-
-                    await series.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(), true)
-                        .ConfigureAwait(false);
-                }
-            }
-        }
-
-        /// <summary>
-        /// For series with episodes directly under the series folder, this adds dummy seasons to enable regular browsing and metadata
-        /// </summary>
-        /// <param name="series"></param>
-        /// <param name="cancellationToken"></param>
-        /// <returns></returns>
-        private async Task<bool> AddDummySeasonFolders(Series series, CancellationToken cancellationToken)
-        {
-            var existingEpisodes = series.RecursiveChildren
-                .OfType<Episode>()
-                .ToList();
-
-            var hasChanges = false;
-
-            // Loop through the unique season numbers
-            foreach (var seasonNumber in existingEpisodes.Select(i => i.ParentIndexNumber ?? -1)
-                .Where(i => i >= 0)
-                .Distinct()
-                .ToList())
-            {
-                var hasSeason = series.Children.OfType<Season>()
-                    .Any(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
-
-                if (!hasSeason)
-                {
-                    await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false);
-
-                    hasChanges = true;
-                }
-            }
-
-            return hasChanges;
-        }
-
-        /// <summary>
-        /// Adds the missing episodes.
-        /// </summary>
-        /// <param name="series">The series.</param>
-        /// <param name="seriesDataPath">The series data path.</param>
-        /// <param name="episodeLookup">The episode lookup.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        private async Task<bool> AddMissingEpisodes(List<Series> series, string seriesDataPath, IEnumerable<Tuple<int, int>> episodeLookup, CancellationToken cancellationToken)
-        {
-            var existingEpisodes = series.SelectMany(s => s.RecursiveChildren.OfType<Episode>()).ToList();
-
-            var hasChanges = false;
-
-            foreach (var tuple in episodeLookup)
-            {
-                if (tuple.Item1 <= 0)
-                {
-                    // Ignore season zeros
-                    continue;
-                }
-
-                if (tuple.Item2 <= 0)
-                {
-                    // Ignore episode zeros
-                    continue;
-                }
-
-                var existingEpisode = GetExistingEpisode(existingEpisodes, tuple);
-
-                if (existingEpisode != null)
-                {
-                    continue;
-                }
-
-                var airDate = GetAirDate(seriesDataPath, tuple.Item1, tuple.Item2);
-
-                if (!airDate.HasValue)
-                {
-                    continue;
-                }
-                var now = DateTime.UtcNow;
-
-                var targetSeries = DetermineAppropriateSeries(series, tuple.Item1);
-
-                if (airDate.Value < now)
-                {
-                    // tvdb has a lot of nearly blank episodes
-                    _logger.Info("Creating virtual missing episode {0} {1}x{2}", targetSeries.Name, tuple.Item1, tuple.Item2);
-
-                    await AddEpisode(targetSeries, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false);
-
-                    hasChanges = true;
-                }
-                else if (airDate.Value > now)
-                {
-                    // tvdb has a lot of nearly blank episodes
-                    _logger.Info("Creating virtual unaired episode {0} {1}x{2}", targetSeries.Name, tuple.Item1, tuple.Item2);
-
-                    await AddEpisode(targetSeries, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false);
-
-                    hasChanges = true;
-                }
-            }
-
-            return hasChanges;
-        }
-
-        private Series DetermineAppropriateSeries(List<Series> series, int seasonNumber)
-        {
-            return series.FirstOrDefault(s => s.RecursiveChildren.OfType<Season>().Any(season => season.IndexNumber == seasonNumber)) ??
-                    series.FirstOrDefault(s => s.RecursiveChildren.OfType<Season>().Any(season => season.IndexNumber == 1)) ??
-                   series.OrderBy(s => s.RecursiveChildren.OfType<Season>().Select(season => season.IndexNumber).Min()).First();
-        }
-
-        /// <summary>
-        /// Removes the virtual entry after a corresponding physical version has been added
-        /// </summary>
-        private async Task<bool> RemoveObsoleteOrMissingEpisodes(IEnumerable<Series> series, IEnumerable<Tuple<int, int>> episodeLookup, CancellationToken cancellationToken)
-        {
-            var existingEpisodes = series.SelectMany(s => s.RecursiveChildren.OfType<Episode>()).ToList();
-
-            var physicalEpisodes = existingEpisodes
-                .Where(i => i.LocationType != LocationType.Virtual)
-                .ToList();
-
-            var virtualEpisodes = existingEpisodes
-                .Where(i => i.LocationType == LocationType.Virtual)
-                .ToList();
-
-            var episodesToRemove = virtualEpisodes
-                .Where(i =>
-                {
-                    if (i.IndexNumber.HasValue && i.ParentIndexNumber.HasValue)
-                    {
-                        var seasonNumber = i.ParentIndexNumber.Value;
-                        var episodeNumber = i.IndexNumber.Value;
-
-                        // If there's a physical episode with the same season and episode number, delete it
-                        if (physicalEpisodes.Any(p =>
-                                p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber &&
-                                p.ContainsEpisodeNumber(episodeNumber)))
-                        {
-                            return true;
-                        }
-
-                        // If the episode no longer exists in the remote lookup, delete it
-                        if (!episodeLookup.Any(e => e.Item1 == seasonNumber && e.Item2 == episodeNumber))
-                        {
-                            return true;
-                        }
-
-                        return false;
-                    }
-
-                    return true;
-                })
-                .ToList();
-
-            var hasChanges = false;
-
-            foreach (var episodeToRemove in episodesToRemove)
-            {
-                _logger.Info("Removing missing/unaired episode {0} {1}x{2}", episodeToRemove.Series.Name, episodeToRemove.ParentIndexNumber, episodeToRemove.IndexNumber);
-
-                await episodeToRemove.Parent.RemoveChild(episodeToRemove, cancellationToken).ConfigureAwait(false);
-
-                hasChanges = true;
-            }
-
-            return hasChanges;
-        }
-
-        /// <summary>
-        /// Removes the obsolete or missing seasons.
-        /// </summary>
-        /// <param name="series">The series.</param>
-        /// <param name="episodeLookup">The episode lookup.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{System.Boolean}.</returns>
-        private async Task<bool> RemoveObsoleteOrMissingSeasons(IEnumerable<Series> series, IEnumerable<Tuple<int, int>> episodeLookup, CancellationToken cancellationToken)
-        {
-            var existingSeasons = series.SelectMany(s => s.Children.OfType<Season>()).ToList();
-
-            var physicalSeasons = existingSeasons
-                .Where(i => i.LocationType != LocationType.Virtual)
-                .ToList();
-
-            var virtualSeasons = existingSeasons
-                .Where(i => i.LocationType == LocationType.Virtual)
-                .ToList();
-
-            var seasonsToRemove = virtualSeasons
-                .Where(i =>
-                {
-                    if (i.IndexNumber.HasValue)
-                    {
-                        var seasonNumber = i.IndexNumber.Value;
-
-                        // If there's a physical season with the same number, delete it
-                        if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber))
-                        {
-                            return true;
-                        }
-
-                        // If the season no longer exists in the remote lookup, delete it
-                        if (episodeLookup.All(e => e.Item1 != seasonNumber))
-                        {
-                            return true;
-                        }
-
-                        return false;
-                    }
-
-                    return true;
-                })
-                .ToList();
-
-            var hasChanges = false;
-
-            foreach (var seasonToRemove in seasonsToRemove)
-            {
-                _logger.Info("Removing virtual season {0} {1}", seasonToRemove.Series.Name, seasonToRemove.IndexNumber);
-
-                await seasonToRemove.Parent.RemoveChild(seasonToRemove, cancellationToken).ConfigureAwait(false);
-
-                hasChanges = true;
-            }
-
-            return hasChanges;
-        }
-
-        /// <summary>
-        /// Adds the episode.
-        /// </summary>
-        /// <param name="series">The series.</param>
-        /// <param name="seasonNumber">The season number.</param>
-        /// <param name="episodeNumber">The episode number.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken)
-        {
-            var season = series.Children.OfType<Season>()
-                .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
-
-            if (season == null)
-            {
-                season = await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false);
-            }
-
-            var name = string.Format("Episode {0}", episodeNumber.ToString(UsCulture));
-
-            var episode = new Episode
-            {
-                Name = name,
-                IndexNumber = episodeNumber,
-                ParentIndexNumber = seasonNumber,
-                Parent = season,
-                DisplayMediaType = typeof(Episode).Name,
-                Id = (series.Id + seasonNumber.ToString(UsCulture) + name).GetMBId(typeof(Episode))
-            };
-
-            await season.AddChild(episode, cancellationToken).ConfigureAwait(false);
-
-            await episode.RefreshMetadata(new MetadataRefreshOptions
-            {
-            }, cancellationToken).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Adds the season.
-        /// </summary>
-        /// <param name="series">The series.</param>
-        /// <param name="seasonNumber">The season number.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{Season}.</returns>
-        private async Task<Season> AddSeason(Series series, int seasonNumber, CancellationToken cancellationToken)
-        {
-            _logger.Info("Creating Season {0} entry for {1}", seasonNumber, series.Name);
-
-            var name = seasonNumber == 0 ? _config.Configuration.SeasonZeroDisplayName : string.Format("Season {0}", seasonNumber.ToString(UsCulture));
-
-            var season = new Season
-            {
-                Name = name,
-                IndexNumber = seasonNumber,
-                Parent = series,
-                DisplayMediaType = typeof(Season).Name,
-                Id = (series.Id + seasonNumber.ToString(UsCulture) + name).GetMBId(typeof(Season))
-            };
-
-            await series.AddChild(season, cancellationToken).ConfigureAwait(false);
-
-            await season.RefreshMetadata(new MetadataRefreshOptions
-            {
-            }, cancellationToken).ConfigureAwait(false);
-
-            return season;
-        }
-
-        /// <summary>
-        /// Gets the existing episode.
-        /// </summary>
-        /// <param name="existingEpisodes">The existing episodes.</param>
-        /// <param name="tuple">The tuple.</param>
-        /// <returns>Episode.</returns>
-        private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, Tuple<int, int> tuple)
-        {
-            return existingEpisodes
-                .FirstOrDefault(i => (i.ParentIndexNumber ?? -1) == tuple.Item1 && i.ContainsEpisodeNumber(tuple.Item2));
-        }
-
-        /// <summary>
-        /// Gets the air date.
-        /// </summary>
-        /// <param name="seriesDataPath">The series data path.</param>
-        /// <param name="seasonNumber">The season number.</param>
-        /// <param name="episodeNumber">The episode number.</param>
-        /// <returns>System.Nullable{DateTime}.</returns>
-        private DateTime? GetAirDate(string seriesDataPath, int seasonNumber, int episodeNumber)
-        {
-            // First open up the tvdb xml file and make sure it has valid data
-            var filename = string.Format("episode-{0}-{1}.xml", seasonNumber.ToString(UsCulture), episodeNumber.ToString(UsCulture));
-
-            var xmlPath = Path.Combine(seriesDataPath, filename);
-
-            DateTime? airDate = null;
-
-            // It appears the best way to filter out invalid entries is to only include those with valid air dates
-            using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
-            {
-                // Use XmlReader for best performance
-                using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings
-                {
-                    CheckCharacters = false,
-                    IgnoreProcessingInstructions = true,
-                    IgnoreComments = true,
-                    ValidationType = ValidationType.None
-                }))
-                {
-                    reader.MoveToContent();
-
-                    // Loop through each element
-                    while (reader.Read())
-                    {
-                        if (reader.NodeType == XmlNodeType.Element)
-                        {
-                            switch (reader.Name)
-                            {
-                                case "EpisodeName":
-                                    {
-                                        var val = reader.ReadElementContentAsString();
-                                        if (string.IsNullOrWhiteSpace(val))
-                                        {
-                                            // Not valid, ignore these
-                                            return null;
-                                        }
-                                        break;
-                                    }
-                                case "FirstAired":
-                                    {
-                                        var val = reader.ReadElementContentAsString();
-
-                                        if (!string.IsNullOrWhiteSpace(val))
-                                        {
-                                            DateTime date;
-                                            if (DateTime.TryParse(val, out date))
-                                            {
-                                                airDate = date.ToUniversalTime();
-                                            }
-                                        }
-
-                                        break;
-                                    }
-
-                                default:
-                                    reader.Skip();
-                                    break;
-                            }
-                        }
-                    }
-                }
-            }
-
-            return airDate;
-        }
-    }
 }

+ 1 - 1
MediaBrowser.Providers/TV/TvdbSeriesProvider.cs

@@ -191,7 +191,7 @@ namespace MediaBrowser.Providers.TV
             }
 
             // Only download if not already there
-            // The prescan task will take care of updates so we don't need to re-download here
+            // The post-scan task will take care of updates so we don't need to re-download here
             if (download)
             {
                 return DownloadSeriesZip(seriesId, seriesDataPath, null, preferredMetadataLanguage, cancellationToken);

+ 1 - 1
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -902,7 +902,7 @@ namespace MediaBrowser.Server.Implementations.Dto
             {
                 var locationType = item.LocationType;
 
-                if (locationType == LocationType.FileSystem || locationType == LocationType.Offline)
+                if (locationType != LocationType.Remote && locationType != LocationType.Virtual)
                 {
                     dto.Path = GetMappedPath(item.Path);
                 }

+ 15 - 0
MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -69,6 +69,11 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
         void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
         {
+            if (e.Item.LocationType == LocationType.Virtual)
+            {
+                return;
+            }
+
             lock (_libraryChangedSyncLock)
             {
                 if (LibraryUpdateTimer == null)
@@ -97,6 +102,11 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
         void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e)
         {
+            if (e.Item.LocationType == LocationType.Virtual)
+            {
+                return;
+            }
+
             lock (_libraryChangedSyncLock)
             {
                 if (LibraryUpdateTimer == null)
@@ -120,6 +130,11 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
         void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
         {
+            if (e.Item.LocationType == LocationType.Virtual)
+            {
+                return;
+            }
+
             lock (_libraryChangedSyncLock)
             {
                 if (LibraryUpdateTimer == null)

+ 9 - 11
MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs

@@ -17,17 +17,15 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <summary>
         /// Any folder named in this list will be ignored - can be added to at runtime for extensibility
         /// </summary>
-        private static readonly Dictionary<string,string> IgnoreFolders = new List<string>
+        private static readonly Dictionary<string, string> IgnoreFolders = new List<string>
         {
-            "metadata",
-            "certificate",
-            "backup",
-            "ps3_update",
-            "ps3_vprm",
-            "adv_obj",
-            "extrafanart",
-            "extrathumbs",
-            ".actors"
+                "metadata",
+                "ps3_update",
+                "ps3_vprm",
+                "extrafanart",
+                "extrathumbs",
+                ".actors",
+                ".wd_tv"
 
         }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
 
@@ -51,7 +49,7 @@ namespace MediaBrowser.Server.Implementations.Library
             // https://github.com/MediaBrowser/MediaBrowser/issues/427
             if (filename.IndexOf("._", StringComparison.OrdinalIgnoreCase) == 0)
             {
-                return true;    
+                return true;
             }
 
             // Ignore hidden files and folders

+ 2 - 1
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -1312,7 +1312,8 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <returns>Task.</returns>
         public async Task UpdateItem(BaseItem item, ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
-            if (item.LocationType == LocationType.FileSystem)
+            var locationType = item.LocationType;
+            if (locationType != LocationType.Remote && locationType != LocationType.Virtual)
             {
                 await _providerManagerFactory().SaveMetadata(item, updateReason).ConfigureAwait(false);
             }

+ 27 - 25
MediaBrowser.Server.Implementations/Library/Validators/CountHelpers.cs

@@ -13,51 +13,46 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
     /// </summary>
     internal static class CountHelpers
     {
-        /// <summary>
-        /// Adds to dictionary.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="counts">The counts.</param>
-        internal static void AddToDictionary(BaseItem item, Dictionary<CountType, int> counts)
+        private static CountType? GetCountType(BaseItem item)
         {
             if (item is Movie)
             {
-                IncrementCount(counts, CountType.Movie);
+                return CountType.Movie;
             }
-            else if (item is Trailer)
+            if (item is Episode)
             {
-                IncrementCount(counts, CountType.Trailer);
+                return CountType.Episode;
             }
-            else if (item is Series)
+            if (item is Game)
             {
-                IncrementCount(counts, CountType.Series);
+                return CountType.Game;
             }
-            else if (item is Game)
+            if (item is Audio)
             {
-                IncrementCount(counts, CountType.Game);
+                return CountType.Song;
             }
-            else if (item is Audio)
+            if (item is Trailer)
             {
-                IncrementCount(counts, CountType.Song);
+                return CountType.Trailer;
             }
-            else if (item is MusicAlbum)
+            if (item is Series)
             {
-                IncrementCount(counts, CountType.MusicAlbum);
+                return CountType.Series;
             }
-            else if (item is Episode)
+            if (item is MusicAlbum)
             {
-                IncrementCount(counts, CountType.Episode);
+                return CountType.MusicAlbum;
             }
-            else if (item is MusicVideo)
+            if (item is MusicVideo)
             {
-                IncrementCount(counts, CountType.MusicVideo);
+                return CountType.MusicVideo;
             }
-            else if (item is AdultVideo)
+            if (item is AdultVideo)
             {
-                IncrementCount(counts, CountType.AdultVideo);
+                return CountType.AdultVideo;
             }
 
-            IncrementCount(counts, CountType.Total);
+            return null;
         }
 
         /// <summary>
@@ -129,6 +124,8 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
         /// <param name="masterDictionary">The master dictionary.</param>
         internal static void SetItemCounts(Guid userId, BaseItem media, IEnumerable<string> names, Dictionary<string, Dictionary<Guid, Dictionary<CountType, int>>> masterDictionary)
         {
+            var countType = GetCountType(media);
+
             foreach (var name in names)
             {
                 Dictionary<Guid, Dictionary<CountType, int>> libraryCounts;
@@ -148,7 +145,12 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
                     libraryCounts.Add(userLibId, userDictionary);
                 }
 
-                AddToDictionary(media, userDictionary);
+                if (countType.HasValue)
+                {
+                    IncrementCount(userDictionary, countType.Value);
+                }
+
+                IncrementCount(userDictionary, CountType.Total);
             }
         }
     }

+ 10 - 0
MediaBrowser.Server.Implementations/Persistence/SqliteMediaStreamsRepository.cs

@@ -19,6 +19,8 @@ namespace MediaBrowser.Server.Implementations.Persistence
         private IDbCommand _deleteStreamsCommand;
         private IDbCommand _saveStreamCommand;
 
+        private SqliteShrinkMemoryTimer _shrinkMemoryTimer;
+        
         public SqliteMediaStreamsRepository(IDbConnection connection, ILogManager logManager)
         {
             _connection = connection;
@@ -51,6 +53,8 @@ namespace MediaBrowser.Server.Implementations.Persistence
             _connection.RunQueries(queries, _logger);
 
             PrepareStatements();
+
+            _shrinkMemoryTimer = new SqliteShrinkMemoryTimer(_connection, _writeLock, _logger);
         }
 
         private readonly string[] _saveColumns =
@@ -356,6 +360,12 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 {
                     lock (_disposeLock)
                     {
+                        if (_shrinkMemoryTimer != null)
+                        {
+                            _shrinkMemoryTimer.Dispose();
+                            _shrinkMemoryTimer = null;
+                        }
+
                         if (_connection != null)
                         {
                             if (_connection.IsOpen())

+ 1 - 1
MediaBrowser.Server.Implementations/Persistence/SqliteShrinkMemoryTimer.cs

@@ -19,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
             _writeLock = writeLock;
             _logger = logger;
 
-            _shrinkMemoryTimer = new Timer(TimerCallback, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30));
+            _shrinkMemoryTimer = new Timer(TimerCallback, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(10));
         }
 
         private async void TimerCallback(object state)

+ 31 - 0
MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj

@@ -210,6 +210,31 @@
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
+  <PropertyGroup>
+    <PostBuildEvent>if $(ConfigurationName) == Release (
+rmdir "$(SolutionDir)..\Deploy\Server\System" /s /q
+mkdir "$(SolutionDir)..\Deploy\Server\System"
+rmdir "$(SolutionDir)..\Deploy\Server\Pismo" /s /q
+xcopy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)..\Deploy\Server\System\" /y
+xcopy "$(SolutionDir)Installation\MediaBrowser.Uninstaller.exe.config" "$(SolutionDir)..\Deploy\Server\System\" /y
+xcopy "$(SolutionDir)Installation\MediaBrowser.Uninstaller.exe" "$(SolutionDir)..\Deploy\Server\System\" /y
+xcopy "$(SolutionDir)Installation\MediaBrowser.InstallUtil.dll" "$(SolutionDir)..\Deploy\Server\System\" /y
+xcopy "$(SolutionDir)Installation\MediaBrowser.Updater.exe" "$(SolutionDir)..\Deploy\Server\System\" /y
+
+mkdir "$(SolutionDir)..\Deploy\Server\System\swagger-ui"
+xcopy "$(TargetDir)swagger-ui" "$(SolutionDir)..\Deploy\Server\System\swagger-ui" /y /s
+
+xcopy "$(TargetDir)$(TargetFileName).config" "$(SolutionDir)..\Deploy\Server\System\" /y
+
+xcopy "$(TargetDir)*.dll" "$(SolutionDir)..\Deploy\Server\System" /y
+
+mkdir "$(SolutionDir)..\Deploy\Server\System\dashboard-ui"
+xcopy "$(TargetDir)dashboard-ui" "$(SolutionDir)..\Deploy\Server\System\dashboard-ui" /y /s
+
+del "$(SolutionDir)..\Deploy\MBServer.zip"
+"$(SolutionDir)ThirdParty\7zip\7za" a -mx9 "$(SolutionDir)..\Deploy\MBServer.zip" "$(SolutionDir)..\Deploy\Server\*"
+)</PostBuildEvent>
+  </PropertyGroup>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
   <Target Name="BeforeBuild">
@@ -217,4 +242,10 @@
   <Target Name="AfterBuild">
   </Target>
   -->
+  <Target Name="AfterBuild">
+    <GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
+      <Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
+    </GetAssemblyIdentity>
+    <Exec Command="copy &quot;$(SolutionDir)..\Deploy\MBServer.zip&quot;  &quot;$(SolutionDir)..\Deploy\MBServer_%(CurrentAssembly.Version).zip&quot; /y" Condition="'$(ConfigurationName)' == 'Release'" />
+  </Target>
 </Project>

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.328</version>
+        <version>3.0.329</version>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.328" />
+            <dependency id="MediaBrowser.Common" version="3.0.329" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="SimpleInjector" version="2.4.1" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common</id>
-        <version>3.0.328</version>
+        <version>3.0.329</version>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.328</version>
+        <version>3.0.329</version>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.328" />
+            <dependency id="MediaBrowser.Common" version="3.0.329" />
         </dependencies>
     </metadata>
     <files>