From 1a9e2dfd83dbab2e9a5f277229c5994253fd8a9a Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 17 Feb 2014 16:35:08 -0500 Subject: [PATCH] fixed themoviedb search returning no results --- MediaBrowser.Api/ItemUpdateService.cs | 5 - .../Library/LibraryStructureService.cs | 44 +- MediaBrowser.Api/NotificationsService.cs | 8 +- MediaBrowser.Controller/Entities/BaseItem.cs | 4 +- MediaBrowser.Controller/Entities/TV/Series.cs | 4 +- MediaBrowser.Model/Entities/LocationType.cs | 8 +- .../Notifications/NotificationLevel.cs | 6 +- .../Manager/MetadataService.cs | 14 +- .../MediaBrowser.Providers.csproj | 1 + .../Movies/MovieDbProvider.cs | 23 +- .../Movies/MovieDbSearch.cs | 4 +- .../TV/MissingEpisodeProvider.cs | 493 ++++++++++++++++++ .../TV/MovieDbSeriesProvider.cs | 22 +- .../TV/SeriesMetadataService.cs | 25 + .../TV/SeriesPostScanTask.cs | 482 +---------------- .../TV/TvdbSeriesProvider.cs | 2 +- .../Dto/DtoService.cs | 2 +- .../EntryPoints/LibraryChangedNotifier.cs | 15 + .../Library/CoreResolutionIgnoreRule.cs | 20 +- .../Library/LibraryManager.cs | 3 +- .../Library/Validators/CountHelpers.cs | 52 +- .../SqliteMediaStreamsRepository.cs | 10 + .../Persistence/SqliteShrinkMemoryTimer.cs | 2 +- .../MediaBrowser.ServerApplication.csproj | 31 ++ Nuget/MediaBrowser.Common.Internal.nuspec | 4 +- Nuget/MediaBrowser.Common.nuspec | 2 +- Nuget/MediaBrowser.Server.Core.nuspec | 4 +- 27 files changed, 696 insertions(+), 594 deletions(-) create mode 100644 MediaBrowser.Providers/TV/MissingEpisodeProvider.cs diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index 05446c6466..84f495efa8 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/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 diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index ed086398ff..ff4f78c969 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/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 { - /// - /// Gets or sets the name. - /// - /// The name. - [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 { - /// - /// Gets or sets the name. - /// - /// The name. - [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; } } /// @@ -242,21 +239,6 @@ namespace MediaBrowser.Api.Library _logger = logger; } - /// - /// Posts the specified request. - /// - /// The request. - /// Please supply a Path - public void Post(ReportChangedPath request) - { - if (string.IsNullOrEmpty(request.Path)) - { - throw new ArgumentException("Please supply a Path"); - } - - _libraryMonitor.ReportFileSystemChanged(request.Path); - } - /// /// Gets the specified request. /// diff --git a/MediaBrowser.Api/NotificationsService.cs b/MediaBrowser.Api/NotificationsService.cs index 4f9ed729d9..45a16347bc 100644 --- a/MediaBrowser.Api/NotificationsService.cs +++ b/MediaBrowser.Api/NotificationsService.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Api [Route("/Notifications/{UserId}", "POST")] [Api(Description = "Adds a notifications")] - public class AddNotification : IReturn + public class AddUserNotification : IReturn { [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 AddNotification(AddNotification request) + private async Task AddNotification(AddUserNotification request) { var notification = new Notification { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 41c4f0baed..62ea0cf5d1 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/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(); diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 4696c8a0fb..d1d393e3ed 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/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; diff --git a/MediaBrowser.Model/Entities/LocationType.cs b/MediaBrowser.Model/Entities/LocationType.cs index e6c2a843ba..09c0b20cab 100644 --- a/MediaBrowser.Model/Entities/LocationType.cs +++ b/MediaBrowser.Model/Entities/LocationType.cs @@ -9,18 +9,18 @@ namespace MediaBrowser.Model.Entities /// /// The file system /// - FileSystem, + FileSystem = 1, /// /// The remote /// - Remote, + Remote = 2, /// /// The virtual /// - Virtual, + Virtual = 3, /// /// The offline /// - Offline + Offline = 4 } } diff --git a/MediaBrowser.Model/Notifications/NotificationLevel.cs b/MediaBrowser.Model/Notifications/NotificationLevel.cs index 24946e0718..c7e68b163a 100644 --- a/MediaBrowser.Model/Notifications/NotificationLevel.cs +++ b/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 } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 1a28e57583..524df4fe2e 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -282,7 +282,8 @@ namespace MediaBrowser.Providers.Manager foreach (var provider in providers.OfType>()) { - 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) { diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 85f988344a..3a7fcd918d 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -180,6 +180,7 @@ + diff --git a/MediaBrowser.Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Providers/Movies/MovieDbProvider.cs index 3bfd69ef19..0a92092e5f 100644 --- a/MediaBrowser.Providers/Movies/MovieDbProvider.cs +++ b/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(); diff --git a/MediaBrowser.Providers/Movies/MovieDbSearch.cs b/MediaBrowser.Providers/Movies/MovieDbSearch.cs index b171a21787..383705e0a8 100644 --- a/MediaBrowser.Providers/Movies/MovieDbSearch.cs +++ b/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; } } diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs new file mode 100644 index 0000000000..fafd829485 --- /dev/null +++ b/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> series, CancellationToken cancellationToken) + { + foreach (var seriesGroup in series) + { + await Run(seriesGroup, cancellationToken).ConfigureAwait(false); + } + } + + private async Task Run(IGrouping 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(seasonNumber, episodeNumber); + } + } + } + + return new Tuple(-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(), cancellationToken, new MetadataRefreshOptions(), true) + .ConfigureAwait(false); + } + } + } + + /// + /// For series with episodes directly under the series folder, this adds dummy seasons to enable regular browsing and metadata + /// + /// + /// + /// + private async Task AddDummySeasonFolders(Series series, CancellationToken cancellationToken) + { + var existingEpisodes = series.RecursiveChildren + .OfType() + .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() + .Any(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); + + if (!hasSeason) + { + await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false); + + hasChanges = true; + } + } + + return hasChanges; + } + + /// + /// Adds the missing episodes. + /// + /// The series. + /// The series data path. + /// The episode lookup. + /// The cancellation token. + /// Task. + private async Task AddMissingEpisodes(List series, string seriesDataPath, IEnumerable> episodeLookup, CancellationToken cancellationToken) + { + var existingEpisodes = series.SelectMany(s => s.RecursiveChildren.OfType()).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, int seasonNumber) + { + return series.FirstOrDefault(s => s.RecursiveChildren.OfType().Any(season => season.IndexNumber == seasonNumber)) ?? + series.FirstOrDefault(s => s.RecursiveChildren.OfType().Any(season => season.IndexNumber == 1)) ?? + series.OrderBy(s => s.RecursiveChildren.OfType().Select(season => season.IndexNumber).Min()).First(); + } + + /// + /// Removes the virtual entry after a corresponding physical version has been added + /// + private async Task RemoveObsoleteOrMissingEpisodes(IEnumerable series, IEnumerable> episodeLookup, CancellationToken cancellationToken) + { + var existingEpisodes = series.SelectMany(s => s.RecursiveChildren.OfType()).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; + } + + /// + /// Removes the obsolete or missing seasons. + /// + /// The series. + /// The episode lookup. + /// The cancellation token. + /// Task{System.Boolean}. + private async Task RemoveObsoleteOrMissingSeasons(IEnumerable series, IEnumerable> episodeLookup, CancellationToken cancellationToken) + { + var existingSeasons = series.SelectMany(s => s.Children.OfType()).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; + } + + /// + /// Adds the episode. + /// + /// The series. + /// The season number. + /// The episode number. + /// The cancellation token. + /// Task. + private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken) + { + var season = series.Children.OfType() + .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); + } + + /// + /// Adds the season. + /// + /// The series. + /// The season number. + /// The cancellation token. + /// Task{Season}. + private async Task 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; + } + + /// + /// Gets the existing episode. + /// + /// The existing episodes. + /// The tuple. + /// Episode. + private Episode GetExistingEpisode(IEnumerable existingEpisodes, Tuple tuple) + { + return existingEpisodes + .FirstOrDefault(i => (i.ParentIndexNumber ?? -1) == tuple.Item1 && i.ContainsEpisodeNumber(tuple.Item2)); + } + + /// + /// Gets the air date. + /// + /// The series data path. + /// The season number. + /// The episode number. + /// System.Nullable{DateTime}. + 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; + } + } +} diff --git a/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs b/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs index f62922f6a6..733e2c915e 100644 --- a/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs +++ b/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(); diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index beb9ab595b..72bc242f7b 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -32,6 +32,31 @@ namespace MediaBrowser.Providers.TV protected override void MergeData(Series source, Series target, List 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) diff --git a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs index 9a3aaed1a9..6ba9b4cdc1 100644 --- a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs +++ b/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> series, CancellationToken cancellationToken) - { - foreach (var seriesGroup in series) - { - await Run(seriesGroup, cancellationToken).ConfigureAwait(false); - } - } - - private async Task Run(IGrouping 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(seasonNumber, episodeNumber); - } - } - } - - return new Tuple(-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(), cancellationToken, new MetadataRefreshOptions(), true) - .ConfigureAwait(false); - } - } - } - - /// - /// For series with episodes directly under the series folder, this adds dummy seasons to enable regular browsing and metadata - /// - /// - /// - /// - private async Task AddDummySeasonFolders(Series series, CancellationToken cancellationToken) - { - var existingEpisodes = series.RecursiveChildren - .OfType() - .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() - .Any(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); - - if (!hasSeason) - { - await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false); - - hasChanges = true; - } - } - - return hasChanges; - } - - /// - /// Adds the missing episodes. - /// - /// The series. - /// The series data path. - /// The episode lookup. - /// The cancellation token. - /// Task. - private async Task AddMissingEpisodes(List series, string seriesDataPath, IEnumerable> episodeLookup, CancellationToken cancellationToken) - { - var existingEpisodes = series.SelectMany(s => s.RecursiveChildren.OfType()).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, int seasonNumber) - { - return series.FirstOrDefault(s => s.RecursiveChildren.OfType().Any(season => season.IndexNumber == seasonNumber)) ?? - series.FirstOrDefault(s => s.RecursiveChildren.OfType().Any(season => season.IndexNumber == 1)) ?? - series.OrderBy(s => s.RecursiveChildren.OfType().Select(season => season.IndexNumber).Min()).First(); - } - - /// - /// Removes the virtual entry after a corresponding physical version has been added - /// - private async Task RemoveObsoleteOrMissingEpisodes(IEnumerable series, IEnumerable> episodeLookup, CancellationToken cancellationToken) - { - var existingEpisodes = series.SelectMany(s => s.RecursiveChildren.OfType()).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; - } - - /// - /// Removes the obsolete or missing seasons. - /// - /// The series. - /// The episode lookup. - /// The cancellation token. - /// Task{System.Boolean}. - private async Task RemoveObsoleteOrMissingSeasons(IEnumerable series, IEnumerable> episodeLookup, CancellationToken cancellationToken) - { - var existingSeasons = series.SelectMany(s => s.Children.OfType()).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; - } - - /// - /// Adds the episode. - /// - /// The series. - /// The season number. - /// The episode number. - /// The cancellation token. - /// Task. - private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken) - { - var season = series.Children.OfType() - .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); - } - - /// - /// Adds the season. - /// - /// The series. - /// The season number. - /// The cancellation token. - /// Task{Season}. - private async Task 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; - } - - /// - /// Gets the existing episode. - /// - /// The existing episodes. - /// The tuple. - /// Episode. - private Episode GetExistingEpisode(IEnumerable existingEpisodes, Tuple tuple) - { - return existingEpisodes - .FirstOrDefault(i => (i.ParentIndexNumber ?? -1) == tuple.Item1 && i.ContainsEpisodeNumber(tuple.Item2)); - } - - /// - /// Gets the air date. - /// - /// The series data path. - /// The season number. - /// The episode number. - /// System.Nullable{DateTime}. - 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; - } - } } diff --git a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs index 44950c4d3b..c0ebd4a657 100644 --- a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs +++ b/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); diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index ae08a7c3df..aba8c33537 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/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); } diff --git a/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index a91033839c..2928363e33 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -69,6 +69,11 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// The instance containing the event data. 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 /// The instance containing the event data. 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 /// The instance containing the event data. void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) { + if (e.Item.LocationType == LocationType.Virtual) + { + return; + } + lock (_libraryChangedSyncLock) { if (LibraryUpdateTimer == null) diff --git a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 98a87d03db..c830c13b86 100644 --- a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -17,17 +17,15 @@ namespace MediaBrowser.Server.Implementations.Library /// /// Any folder named in this list will be ignored - can be added to at runtime for extensibility /// - private static readonly Dictionary IgnoreFolders = new List + private static readonly Dictionary IgnoreFolders = new List { - "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 diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index b425412040..a06e03c4aa 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -1312,7 +1312,8 @@ namespace MediaBrowser.Server.Implementations.Library /// Task. 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); } diff --git a/MediaBrowser.Server.Implementations/Library/Validators/CountHelpers.cs b/MediaBrowser.Server.Implementations/Library/Validators/CountHelpers.cs index 679eadb12c..edb4e7382c 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/CountHelpers.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/CountHelpers.cs @@ -13,51 +13,46 @@ namespace MediaBrowser.Server.Implementations.Library.Validators /// internal static class CountHelpers { - /// - /// Adds to dictionary. - /// - /// The item. - /// The counts. - internal static void AddToDictionary(BaseItem item, Dictionary 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; } /// @@ -129,6 +124,8 @@ namespace MediaBrowser.Server.Implementations.Library.Validators /// The master dictionary. internal static void SetItemCounts(Guid userId, BaseItem media, IEnumerable names, Dictionary>> masterDictionary) { + var countType = GetCountType(media); + foreach (var name in names) { Dictionary> 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); } } } diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteMediaStreamsRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteMediaStreamsRepository.cs index b898398d86..f4e7fd0a6b 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteMediaStreamsRepository.cs +++ b/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()) diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteShrinkMemoryTimer.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteShrinkMemoryTimer.cs index 01784d5405..b5a0c10b1e 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteShrinkMemoryTimer.cs +++ b/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) diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index 928b7e3871..5395167b98 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -210,6 +210,31 @@ + + 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\*" +) + + + + + + + \ No newline at end of file diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index 63a6cbac91..a958415ff5 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common.Internal - 3.0.328 + 3.0.329 MediaBrowser.Common.Internal Luke ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption. Copyright © Media Browser 2013 - + diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 850ffee096..a552297d4d 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common - 3.0.328 + 3.0.329 MediaBrowser.Common Media Browser Team ebr,Luke,scottisafool diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index 6cc4e4ec64..15da720c27 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Server.Core - 3.0.328 + 3.0.329 Media Browser.Server.Core Media Browser Team ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains core components required to build plugins for Media Browser Server. Copyright © Media Browser 2013 - +