Browse Source

fixed themoviedb search returning no results

Luke Pulverenti 11 năm trước cách đây
mục cha
commit
1a9e2dfd83
27 tập tin đã thay đổi với 696 bổ sung594 xóa
  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>