diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-02-17 16:35:08 -0500 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-02-17 16:35:08 -0500 |
| commit | 1a9e2dfd83dbab2e9a5f277229c5994253fd8a9a (patch) | |
| tree | 157ac5c2ee2e226560d04d61fef79445b23c231d /MediaBrowser.Providers | |
| parent | 4ebba2b2e87e33f083c095957a2294b6f8ae3828 (diff) | |
fixed themoviedb search returning no results
Diffstat (limited to 'MediaBrowser.Providers')
| -rw-r--r-- | MediaBrowser.Providers/Manager/MetadataService.cs | 14 | ||||
| -rw-r--r-- | MediaBrowser.Providers/MediaBrowser.Providers.csproj | 1 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Movies/MovieDbProvider.cs | 23 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Movies/MovieDbSearch.cs | 4 | ||||
| -rw-r--r-- | MediaBrowser.Providers/TV/MissingEpisodeProvider.cs | 493 | ||||
| -rw-r--r-- | MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs | 22 | ||||
| -rw-r--r-- | MediaBrowser.Providers/TV/SeriesMetadataService.cs | 25 | ||||
| -rw-r--r-- | MediaBrowser.Providers/TV/SeriesPostScanTask.cs | 482 | ||||
| -rw-r--r-- | MediaBrowser.Providers/TV/TvdbSeriesProvider.cs | 2 |
9 files changed, 568 insertions, 498 deletions
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 1a28e5758..524df4fe2 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<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) { diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 85f988344..3a7fcd918 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/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" /> diff --git a/MediaBrowser.Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Providers/Movies/MovieDbProvider.cs index 3bfd69ef1..0a92092e5 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 b171a2178..383705e0a 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 000000000..fafd82948 --- /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<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; + } + } +} diff --git a/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs b/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs index f62922f6a..733e2c915 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 beb9ab595..72bc242f7 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<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) diff --git a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs index 9a3aaed1a..6ba9b4cdc 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<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; - } - } } diff --git a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs index 44950c4d3..c0ebd4a65 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); |
