diff options
Diffstat (limited to 'MediaBrowser.Providers')
40 files changed, 1606 insertions, 990 deletions
diff --git a/MediaBrowser.Providers/AdultVideos/AdultVideoMetadataService.cs b/MediaBrowser.Providers/AdultVideos/AdultVideoMetadataService.cs new file mode 100644 index 000000000..23f370974 --- /dev/null +++ b/MediaBrowser.Providers/AdultVideos/AdultVideoMetadataService.cs @@ -0,0 +1,43 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Providers.Manager; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.AdultVideos +{ + class AdultVideoMetadataService : MetadataService<AdultVideo, ItemId> + { + private readonly ILibraryManager _libraryManager; + + public AdultVideoMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem) + { + _libraryManager = libraryManager; + } + + /// <summary> + /// Merges the specified source. + /// </summary> + /// <param name="source">The source.</param> + /// <param name="target">The target.</param> + /// <param name="lockedFields">The locked fields.</param> + /// <param name="replaceData">if set to <c>true</c> [replace data].</param> + /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param> + protected override void MergeData(AdultVideo source, AdultVideo target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + } + + protected override Task SaveItem(AdultVideo item, ItemUpdateType reason, CancellationToken cancellationToken) + { + return _libraryManager.UpdateItem(item, reason, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/AdultVideos/AdultVideoXmlProvider.cs b/MediaBrowser.Providers/AdultVideos/AdultVideoXmlProvider.cs new file mode 100644 index 000000000..3b6439b4b --- /dev/null +++ b/MediaBrowser.Providers/AdultVideos/AdultVideoXmlProvider.cs @@ -0,0 +1,59 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; +using MediaBrowser.Providers.Movies; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.AdultVideos +{ + class AdultVideoXmlProvider : BaseXmlProvider, ILocalMetadataProvider<AdultVideo> + { + private readonly ILogger _logger; + + public AdultVideoXmlProvider(IFileSystem fileSystem, ILogger logger) + : base(fileSystem) + { + _logger = logger; + } + + public async Task<MetadataResult<AdultVideo>> GetMetadata(string path, CancellationToken cancellationToken) + { + path = GetXmlFile(path).FullName; + + var result = new MetadataResult<AdultVideo>(); + + await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + result.Item = new AdultVideo(); + + new MovieXmlParser(_logger).Fetch(result.Item, path, cancellationToken); + result.HasMetadata = true; + } + catch (FileNotFoundException) + { + result.HasMetadata = false; + } + finally + { + XmlParsingResourcePool.Release(); + } + + return result; + } + + public string Name + { + get { return "Media Browser Xml"; } + } + + protected override FileInfo GetXmlFile(string path) + { + return MovieXmlProvider.GetXmlFileInfo(path, FileSystem); + } + } +} diff --git a/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs b/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs index 68602b159..2e0e21a46 100644 --- a/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs +++ b/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs @@ -1,10 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Providers.Manager; diff --git a/MediaBrowser.Providers/Games/GameMetadataService.cs b/MediaBrowser.Providers/Games/GameMetadataService.cs index afa123bf7..8ca34eb05 100644 --- a/MediaBrowser.Providers/Games/GameMetadataService.cs +++ b/MediaBrowser.Providers/Games/GameMetadataService.cs @@ -1,10 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Providers.Manager; diff --git a/MediaBrowser.Providers/Games/GameSystemMetadataService.cs b/MediaBrowser.Providers/Games/GameSystemMetadataService.cs index 9e5532a27..6dd1b1bbc 100644 --- a/MediaBrowser.Providers/Games/GameSystemMetadataService.cs +++ b/MediaBrowser.Providers/Games/GameSystemMetadataService.cs @@ -1,10 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Providers.Manager; diff --git a/MediaBrowser.Providers/LiveTv/ChannelMetadataService.cs b/MediaBrowser.Providers/LiveTv/ChannelMetadataService.cs index b0c916a61..01adb3ac3 100644 --- a/MediaBrowser.Providers/LiveTv/ChannelMetadataService.cs +++ b/MediaBrowser.Providers/LiveTv/ChannelMetadataService.cs @@ -1,10 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Providers.Manager; diff --git a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs index 02d5c1a79..037fedd79 100644 --- a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs +++ b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs @@ -1,10 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Providers.Manager; diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index da82dcb3f..c817180c5 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1,6 +1,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; @@ -230,13 +231,23 @@ namespace MediaBrowser.Providers.Manager protected virtual TIdType GetId(TItemType item) { - return new TIdType + var id = new TIdType { MetadataCountryCode = item.GetPreferredMetadataCountryCode(), MetadataLanguage = item.GetPreferredMetadataLanguage(), Name = item.Name, ProviderIds = item.ProviderIds }; + + var baseItem = item as BaseItem; + + if (baseItem != null) + { + id.IndexNumber = baseItem.IndexNumber; + id.ParentIndexNumber = baseItem.ParentIndexNumber; + } + + return id; } public bool CanRefresh(IHasMetadata item) @@ -253,6 +264,7 @@ namespace MediaBrowser.Providers.Manager }; var temp = CreateNew(); + temp.Path = item.Path; // If replacing all metadata, run internet providers first if (options.ReplaceAllMetadata) @@ -313,29 +325,32 @@ namespace MediaBrowser.Providers.Manager foreach (var provider in providers.OfType<ICustomMetadataProvider<TItemType>>()) { - Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); - - try - { - await provider.FetchAsync(item, cancellationToken).ConfigureAwait(false); - - refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload; - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - refreshResult.Status = ProviderRefreshStatus.CompletedWithErrors; - refreshResult.ErrorMessage = ex.Message; - Logger.ErrorException("Error in {0}", ex, provider.Name); - } + await RunCustomProvider(provider, item, refreshResult, cancellationToken).ConfigureAwait(false); } return refreshResult; } + private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, RefreshResult refreshResult, CancellationToken cancellationToken) + { + Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); + + try + { + refreshResult.UpdateType = refreshResult.UpdateType | await provider.FetchAsync(item, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + refreshResult.Status = ProviderRefreshStatus.CompletedWithErrors; + refreshResult.ErrorMessage = ex.Message; + Logger.ErrorException("Error in {0}", ex, provider.Name); + } + } + protected virtual TItemType CreateNew() { return new TItemType(); diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 6b0ea7ed4..dbfc97d51 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -246,11 +246,7 @@ namespace MediaBrowser.Providers.Manager cancellationToken.ThrowIfCancellationRequested(); - // Don't clog up the log with these providers - if (!(provider is IDynamicInfoProvider)) - { - _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name ?? "--Unknown--"); - } + _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name ?? "--Unknown--"); try { @@ -637,7 +633,7 @@ namespace MediaBrowser.Providers.Manager })); // Fetchers - list.AddRange(providers.Where(i => !(i is ILocalMetadataProvider)).Select(i => new MetadataPlugin + list.AddRange(providers.Where(i => (i is IRemoteMetadataProvider)).Select(i => new MetadataPlugin { Name = i.Name, Type = MetadataPluginType.MetadataFetcher diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 0ac26330a..ad8465009 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -64,6 +64,7 @@ </Reference> </ItemGroup> <ItemGroup> + <Compile Include="AdultVideos\AdultVideoMetadataService.cs" /> <Compile Include="All\LocalImageProvider.cs" /> <Compile Include="Books\BookMetadataService.cs" /> <Compile Include="BoxSets\BoxSetMetadataService.cs" /> @@ -89,7 +90,11 @@ <Compile Include="Games\GameSystemXmlProvider.cs" /> <Compile Include="ImageFromMediaLocationProvider.cs" /> <Compile Include="ImagesByNameProvider.cs" /> + <Compile Include="MediaInfo\FFProbeHelpers.cs" /> + <Compile Include="MediaInfo\FFProbeProvider.cs" /> + <Compile Include="MediaInfo\FFProbeVideoInfo.cs" /> <Compile Include="Movies\MovieDbSearch.cs" /> + <Compile Include="Movies\MovieXmlProvider.cs" /> <Compile Include="MusicGenres\MusicGenreImageProvider.cs" /> <Compile Include="GameGenres\GameGenreImageProvider.cs" /> <Compile Include="Genres\GenreImageProvider.cs" /> @@ -107,6 +112,8 @@ <Compile Include="Music\ArtistMetadataService.cs" /> <Compile Include="Music\LastfmArtistProvider.cs" /> <Compile Include="Music\MusicBrainzArtistProvider.cs" /> + <Compile Include="Music\MusicVideoMetadataService.cs" /> + <Compile Include="Music\MusicVideoXmlProvider.cs" /> <Compile Include="Omdb\OmdbProvider.cs" /> <Compile Include="Omdb\OmdbSeriesProvider.cs" /> <Compile Include="People\MovieDbPersonImageProvider.cs" /> @@ -150,9 +157,9 @@ <Compile Include="Savers\XmlSaverHelpers.cs" /> <Compile Include="Studios\StudiosImageProvider.cs" /> <Compile Include="Studios\StudioMetadataService.cs" /> - <Compile Include="TV\EpisodeImageFromMediaLocationProvider.cs" /> - <Compile Include="TV\EpisodeIndexNumberProvider.cs" /> - <Compile Include="TV\EpisodeProviderFromXml.cs" /> + <Compile Include="TV\EpisodeLocalImageProvider.cs" /> + <Compile Include="TV\EpisodeMetadataService.cs" /> + <Compile Include="TV\EpisodeXmlProvider.cs" /> <Compile Include="TV\EpisodeXmlParser.cs" /> <Compile Include="TV\FanArtTvUpdatesPrescanTask.cs" /> <Compile Include="TV\FanartSeasonProvider.cs" /> diff --git a/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs index e897eb1eb..ad4630dcf 100644 --- a/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs @@ -141,109 +141,5 @@ namespace MediaBrowser.Providers.MediaInfo { } - - /// <summary> - /// Normalizes the FF probe result. - /// </summary> - /// <param name="result">The result.</param> - protected void NormalizeFFProbeResult(InternalMediaInfoResult result) - { - if (result.format != null && result.format.tags != null) - { - result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags); - } - - if (result.streams != null) - { - // Convert all dictionaries to case insensitive - foreach (var stream in result.streams) - { - if (stream.tags != null) - { - stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags); - } - - if (stream.disposition != null) - { - stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition); - } - } - } - } - - /// <summary> - /// Gets a string from an FFProbeResult tags dictionary - /// </summary> - /// <param name="tags">The tags.</param> - /// <param name="key">The key.</param> - /// <returns>System.String.</returns> - protected string GetDictionaryValue(Dictionary<string, string> tags, string key) - { - if (tags == null) - { - return null; - } - - string val; - - tags.TryGetValue(key, out val); - return val; - } - - /// <summary> - /// Gets an int from an FFProbeResult tags dictionary - /// </summary> - /// <param name="tags">The tags.</param> - /// <param name="key">The key.</param> - /// <returns>System.Nullable{System.Int32}.</returns> - protected int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key) - { - var val = GetDictionaryValue(tags, key); - - if (!string.IsNullOrEmpty(val)) - { - int i; - - if (int.TryParse(val, out i)) - { - return i; - } - } - - return null; - } - - /// <summary> - /// Gets a DateTime from an FFProbeResult tags dictionary - /// </summary> - /// <param name="tags">The tags.</param> - /// <param name="key">The key.</param> - /// <returns>System.Nullable{DateTime}.</returns> - protected DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key) - { - var val = GetDictionaryValue(tags, key); - - if (!string.IsNullOrEmpty(val)) - { - DateTime i; - - if (DateTime.TryParse(val, out i)) - { - return i.ToUniversalTime(); - } - } - - return null; - } - - /// <summary> - /// Converts a dictionary to case insensitive - /// </summary> - /// <param name="dict">The dict.</param> - /// <returns>Dictionary{System.StringSystem.String}.</returns> - private Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict) - { - return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase); - } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs index d27b65e2a..bae719eea 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.MediaInfo cancellationToken.ThrowIfCancellationRequested(); - NormalizeFFProbeResult(result); + FFProbeHelpers.NormalizeFFProbeResult(result); cancellationToken.ThrowIfCancellationRequested(); @@ -102,7 +102,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="tags">The tags.</param> private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags) { - var title = GetDictionaryValue(tags, "title"); + var title = FFProbeHelpers.GetDictionaryValue(tags, "title"); // Only set Name if title was found in the dictionary if (!string.IsNullOrEmpty(title)) @@ -114,7 +114,7 @@ namespace MediaBrowser.Providers.MediaInfo { audio.People.Clear(); - var composer = GetDictionaryValue(tags, "composer"); + var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer"); if (!string.IsNullOrWhiteSpace(composer)) { @@ -125,9 +125,9 @@ namespace MediaBrowser.Providers.MediaInfo } } - audio.Album = GetDictionaryValue(tags, "album"); + audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album"); - var artist = GetDictionaryValue(tags, "artist"); + var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist"); if (string.IsNullOrWhiteSpace(artist)) { @@ -142,7 +142,7 @@ namespace MediaBrowser.Providers.MediaInfo } // Several different forms of albumartist - audio.AlbumArtist = GetDictionaryValue(tags, "albumartist") ?? GetDictionaryValue(tags, "album artist") ?? GetDictionaryValue(tags, "album_artist"); + audio.AlbumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist") ?? FFProbeHelpers.GetDictionaryValue(tags, "album artist") ?? FFProbeHelpers.GetDictionaryValue(tags, "album_artist"); // Track number audio.IndexNumber = GetDictionaryDiscValue(tags, "track"); @@ -150,10 +150,10 @@ namespace MediaBrowser.Providers.MediaInfo // Disc number audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc"); - audio.ProductionYear = GetDictionaryNumericValue(tags, "date"); + audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date"); // Several different forms of retaildate - audio.PremiereDate = GetDictionaryDateTime(tags, "retaildate") ?? GetDictionaryDateTime(tags, "retail date") ?? GetDictionaryDateTime(tags, "retail_date"); + audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date"); // If we don't have a ProductionYear try and get it from PremiereDate if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue) @@ -219,7 +219,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="tagName">Name of the tag.</param> private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName) { - var val = GetDictionaryValue(tags, tagName); + var val = FFProbeHelpers.GetDictionaryValue(tags, tagName); if (!string.IsNullOrEmpty(val)) { @@ -240,7 +240,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="tags">The tags.</param> private void FetchGenres(Audio audio, Dictionary<string, string> tags) { - var val = GetDictionaryValue(tags, "genre"); + var val = FFProbeHelpers.GetDictionaryValue(tags, "genre"); if (!string.IsNullOrEmpty(val)) { @@ -261,7 +261,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <returns>System.Nullable{System.Int32}.</returns> private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName) { - var disc = GetDictionaryValue(tags, tagName); + var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName); if (!string.IsNullOrEmpty(disc)) { diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeHelpers.cs b/MediaBrowser.Providers/MediaInfo/FFProbeHelpers.cs new file mode 100644 index 000000000..5a4b2beb2 --- /dev/null +++ b/MediaBrowser.Providers/MediaInfo/FFProbeHelpers.cs @@ -0,0 +1,113 @@ +using MediaBrowser.Controller.MediaInfo; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Providers.MediaInfo +{ + public static class FFProbeHelpers + { + /// <summary> + /// Normalizes the FF probe result. + /// </summary> + /// <param name="result">The result.</param> + public static void NormalizeFFProbeResult(InternalMediaInfoResult result) + { + if (result.format != null && result.format.tags != null) + { + result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags); + } + + if (result.streams != null) + { + // Convert all dictionaries to case insensitive + foreach (var stream in result.streams) + { + if (stream.tags != null) + { + stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags); + } + + if (stream.disposition != null) + { + stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition); + } + } + } + } + + /// <summary> + /// Gets a string from an FFProbeResult tags dictionary + /// </summary> + /// <param name="tags">The tags.</param> + /// <param name="key">The key.</param> + /// <returns>System.String.</returns> + public static string GetDictionaryValue(Dictionary<string, string> tags, string key) + { + if (tags == null) + { + return null; + } + + string val; + + tags.TryGetValue(key, out val); + return val; + } + + /// <summary> + /// Gets an int from an FFProbeResult tags dictionary + /// </summary> + /// <param name="tags">The tags.</param> + /// <param name="key">The key.</param> + /// <returns>System.Nullable{System.Int32}.</returns> + public static int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key) + { + var val = GetDictionaryValue(tags, key); + + if (!string.IsNullOrEmpty(val)) + { + int i; + + if (int.TryParse(val, out i)) + { + return i; + } + } + + return null; + } + + /// <summary> + /// Gets a DateTime from an FFProbeResult tags dictionary + /// </summary> + /// <param name="tags">The tags.</param> + /// <param name="key">The key.</param> + /// <returns>System.Nullable{DateTime}.</returns> + public static DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key) + { + var val = GetDictionaryValue(tags, key); + + if (!string.IsNullOrEmpty(val)) + { + DateTime i; + + if (DateTime.TryParse(val, out i)) + { + return i.ToUniversalTime(); + } + } + + return null; + } + + /// <summary> + /// Converts a dictionary to case insensitive + /// </summary> + /// <param name="dict">The dict.</param> + /// <returns>Dictionary{System.StringSystem.String}.</returns> + private static Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict) + { + return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs new file mode 100644 index 000000000..1f3723653 --- /dev/null +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -0,0 +1,103 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.MediaInfo; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.MediaInfo +{ + public class FFProbeProvider : ICustomMetadataProvider<Episode>, + ICustomMetadataProvider<MusicVideo>, + ICustomMetadataProvider<Movie>, + ICustomMetadataProvider<AdultVideo>, + ICustomMetadataProvider<LiveTvVideoRecording>, + IHasChangeMonitor + { + private readonly ILogger _logger; + private readonly IIsoManager _isoManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IItemRepository _itemRepo; + private readonly IBlurayExaminer _blurayExaminer; + private readonly ILocalizationManager _localization; + + public string Name + { + get { return "ffprobe"; } + } + + public Task<ItemUpdateType> FetchAsync(Episode item, CancellationToken cancellationToken) + { + return FetchVideoInfo(item, cancellationToken); + } + + public Task<ItemUpdateType> FetchAsync(MusicVideo item, CancellationToken cancellationToken) + { + return FetchVideoInfo(item, cancellationToken); + } + + public Task<ItemUpdateType> FetchAsync(Movie item, CancellationToken cancellationToken) + { + return FetchVideoInfo(item, cancellationToken); + } + + public Task<ItemUpdateType> FetchAsync(AdultVideo item, CancellationToken cancellationToken) + { + return FetchVideoInfo(item, cancellationToken); + } + + public Task<ItemUpdateType> FetchAsync(LiveTvVideoRecording item, CancellationToken cancellationToken) + { + return FetchVideoInfo(item, cancellationToken); + } + + public FFProbeProvider(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization) + { + _logger = logger; + _isoManager = isoManager; + _mediaEncoder = mediaEncoder; + _itemRepo = itemRepo; + _blurayExaminer = blurayExaminer; + _localization = localization; + } + + private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.Unspecified); + public Task<ItemUpdateType> FetchVideoInfo<T>(T item, CancellationToken cancellationToken) + where T : Video + { + if (item.LocationType != LocationType.FileSystem) + { + return _cachedTask; + } + + if (item.VideoType == VideoType.Iso && !_isoManager.CanMount(item.Path)) + { + return _cachedTask; + } + + if (item.VideoType == VideoType.HdDvd) + { + return _cachedTask; + } + + var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization); + + return prober.ProbeVideo(item, cancellationToken); + } + + public bool HasChanged(IHasMetadata item, DateTime date) + { + return item.DateModified > date; + } + } +} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs new file mode 100644 index 000000000..8adb75839 --- /dev/null +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -0,0 +1,587 @@ +using DvdLib.Ifo; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.MediaInfo; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.MediaInfo +{ + public class FFProbeVideoInfo + { + private readonly ILogger _logger; + private readonly IIsoManager _isoManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IItemRepository _itemRepo; + private readonly IBlurayExaminer _blurayExaminer; + private readonly ILocalizationManager _localization; + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public FFProbeVideoInfo(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization) + { + _logger = logger; + _isoManager = isoManager; + _mediaEncoder = mediaEncoder; + _itemRepo = itemRepo; + _blurayExaminer = blurayExaminer; + _localization = localization; + } + + public async Task<ItemUpdateType> ProbeVideo<T>(T item, CancellationToken cancellationToken) + where T : Video + { + var isoMount = await MountIsoIfNeeded(item, cancellationToken).ConfigureAwait(false); + + try + { + OnPreFetch(item, isoMount); + + // If we didn't find any satisfying the min length, just take them all + if (item.VideoType == VideoType.Dvd || (item.IsoType.HasValue && item.IsoType == IsoType.Dvd)) + { + if (item.PlayableStreamFileNames.Count == 0) + { + _logger.Error("No playable vobs found in dvd structure, skipping ffprobe."); + return ItemUpdateType.MetadataImport; + } + } + + var result = await GetMediaInfo(item, isoMount, cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + FFProbeHelpers.NormalizeFFProbeResult(result); + + cancellationToken.ThrowIfCancellationRequested(); + + await Fetch(item, cancellationToken, result, isoMount).ConfigureAwait(false); + + } + finally + { + if (isoMount != null) + { + isoMount.Dispose(); + } + } + + return ItemUpdateType.MetadataImport; + } + + private async Task<InternalMediaInfoResult> GetMediaInfo(BaseItem item, IIsoMount isoMount, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var type = InputType.File; + var inputPath = isoMount == null ? new[] { item.Path } : new[] { isoMount.MountedPath }; + + var video = item as Video; + + if (video != null) + { + inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type); + } + + return await _mediaEncoder.GetMediaInfo(inputPath, type, false, cancellationToken).ConfigureAwait(false); + } + + protected async Task Fetch(Video video, CancellationToken cancellationToken, InternalMediaInfoResult data, IIsoMount isoMount) + { + if (data.format != null) + { + // For dvd's this may not always be accurate, so don't set the runtime if the item already has one + var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks == null || video.RunTimeTicks.Value == 0; + + if (needToSetRuntime && !string.IsNullOrEmpty(data.format.duration)) + { + video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks; + } + } + + var mediaStreams = MediaEncoderHelpers.GetMediaInfo(data).MediaStreams; + + var chapters = data.Chapters ?? new List<ChapterInfo>(); + + if (video.VideoType == VideoType.BluRay || (video.IsoType.HasValue && video.IsoType.Value == IsoType.BluRay)) + { + var inputPath = isoMount != null ? isoMount.MountedPath : video.Path; + FetchBdInfo(video, chapters, mediaStreams, inputPath, cancellationToken); + } + + AddExternalSubtitles(video, mediaStreams); + + FetchWtvInfo(video, data); + + video.IsHD = mediaStreams.Any(i => i.Type == MediaStreamType.Video && i.Width.HasValue && i.Width.Value >= 1270); + + if (chapters.Count == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) + { + AddDummyChapters(video, chapters); + } + + var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); + + video.VideoBitRate = videoStream == null ? null : videoStream.BitRate; + video.DefaultVideoStreamIndex = videoStream == null ? (int?)null : videoStream.Index; + + video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle); + + await FFMpegManager.Instance.PopulateChapterImages(video, chapters, false, false, cancellationToken).ConfigureAwait(false); + + await _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken).ConfigureAwait(false); + + await _itemRepo.SaveChapters(video.Id, chapters, cancellationToken).ConfigureAwait(false); + } + + private void FetchBdInfo(BaseItem item, List<ChapterInfo> chapters, List<MediaStream> mediaStreams, string inputPath, CancellationToken cancellationToken) + { + var video = (Video)item; + + var result = GetBDInfo(inputPath); + + cancellationToken.ThrowIfCancellationRequested(); + + int? currentHeight = null; + int? currentWidth = null; + int? currentBitRate = null; + + var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + + // Grab the values that ffprobe recorded + if (videoStream != null) + { + currentBitRate = videoStream.BitRate; + currentWidth = videoStream.Width; + currentHeight = videoStream.Height; + } + + // Fill video properties from the BDInfo result + Fetch(video, mediaStreams, result, chapters); + + videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + + // Use the ffprobe values if these are empty + if (videoStream != null) + { + videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate; + videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width; + videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height; + } + } + + private bool IsEmpty(int? num) + { + return !num.HasValue || num.Value == 0; + } + + /// <param name="chapters">The chapters.</param> + private void Fetch(Video video, List<MediaStream> mediaStreams, BlurayDiscInfo stream, List<ChapterInfo> chapters) + { + // Check all input for null/empty/zero + + mediaStreams.Clear(); + mediaStreams.AddRange(stream.MediaStreams); + + video.MainFeaturePlaylistName = stream.PlaylistName; + + if (stream.RunTimeTicks.HasValue && stream.RunTimeTicks.Value > 0) + { + video.RunTimeTicks = stream.RunTimeTicks; + } + + video.PlayableStreamFileNames = stream.Files.ToList(); + + if (stream.Chapters != null) + { + chapters.Clear(); + + chapters.AddRange(stream.Chapters.Select(c => new ChapterInfo + { + StartPositionTicks = TimeSpan.FromSeconds(c).Ticks + + })); + } + } + + /// <summary> + /// Gets information about the longest playlist on a bdrom + /// </summary> + /// <param name="path">The path.</param> + /// <returns>VideoStream.</returns> + private BlurayDiscInfo GetBDInfo(string path) + { + return _blurayExaminer.GetDiscInfo(path); + } + + private void FetchWtvInfo(Video video, InternalMediaInfoResult data) + { + if (data.format == null || data.format.tags == null) + { + return; + } + + if (video.Genres.Count == 0) + { + if (!video.LockedFields.Contains(MetadataFields.Genres)) + { + var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre"); + + if (!string.IsNullOrEmpty(genres)) + { + video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(i => i.Trim()) + .ToList(); + } + } + } + + if (string.IsNullOrEmpty(video.Overview)) + { + if (!video.LockedFields.Contains(MetadataFields.Overview)) + { + var overview = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription"); + + if (!string.IsNullOrWhiteSpace(overview)) + { + video.Overview = overview; + } + } + } + + if (string.IsNullOrEmpty(video.OfficialRating)) + { + var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating"); + + if (!string.IsNullOrWhiteSpace(officialRating)) + { + if (!video.LockedFields.Contains(MetadataFields.OfficialRating)) + { + video.OfficialRating = officialRating; + } + } + } + + if (video.People.Count == 0) + { + if (!video.LockedFields.Contains(MetadataFields.Cast)) + { + var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits"); + + if (!string.IsNullOrEmpty(people)) + { + video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(i => new PersonInfo { Name = i.Trim(), Type = PersonType.Actor }) + .ToList(); + } + } + } + + if (!video.ProductionYear.HasValue) + { + var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime"); + + if (!string.IsNullOrWhiteSpace(year)) + { + int val; + + if (int.TryParse(year, NumberStyles.Integer, _usCulture, out val)) + { + video.ProductionYear = val; + } + } + } + } + + private IEnumerable<string> SubtitleExtensions + { + get + { + return new[] { ".srt", ".ssa", ".ass" }; + } + } + + /// <summary> + /// Adds the external subtitles. + /// </summary> + /// <param name="video">The video.</param> + /// <param name="currentStreams">The current streams.</param> + private void AddExternalSubtitles(Video video, List<MediaStream> currentStreams) + { + var useParent = !video.ResolveArgs.IsDirectory; + + if (useParent && video.Parent == null) + { + return; + } + + var fileSystemChildren = useParent + ? video.Parent.ResolveArgs.FileSystemChildren + : video.ResolveArgs.FileSystemChildren; + + var startIndex = currentStreams.Count; + var streams = new List<MediaStream>(); + + var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path); + + foreach (var file in fileSystemChildren + .Where(f => !f.Attributes.HasFlag(FileAttributes.Directory) && SubtitleExtensions.Contains(Path.GetExtension(f.FullName), StringComparer.OrdinalIgnoreCase))) + { + var fullName = file.FullName; + + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullName); + + // If the subtitle file matches the video file name + if (string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + { + streams.Add(new MediaStream + { + Index = startIndex++, + Type = MediaStreamType.Subtitle, + IsExternal = true, + Path = fullName, + Codec = Path.GetExtension(fullName).ToLower().TrimStart('.') + }); + } + else if (fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase)) + { + // Support xbmc naming conventions - 300.spanish.srt + var language = fileNameWithoutExtension.Split('.').LastOrDefault(); + + // Try to translate to three character code + // Be flexible and check against both the full and three character versions + var culture = _localization.GetCultures() + .FirstOrDefault(i => string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase) || string.Equals(i.ThreeLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase) || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase)); + + if (culture != null) + { + language = culture.ThreeLetterISOLanguageName; + } + + streams.Add(new MediaStream + { + Index = startIndex++, + Type = MediaStreamType.Subtitle, + IsExternal = true, + Path = fullName, + Codec = Path.GetExtension(fullName).ToLower().TrimStart('.'), + Language = language + }); + } + } + + currentStreams.AddRange(streams); + } + + /// <summary> + /// The dummy chapter duration + /// </summary> + private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks; + + /// <summary> + /// Adds the dummy chapters. + /// </summary> + /// <param name="video">The video.</param> + /// <param name="chapters">The chapters.</param> + private void AddDummyChapters(Video video, List<ChapterInfo> chapters) + { + var runtime = video.RunTimeTicks ?? 0; + + if (runtime < 0) + { + throw new ArgumentException(string.Format("{0} has invalid runtime of {1}", video.Name, runtime)); + } + + if (runtime < _dummyChapterDuration) + { + return; + } + + long currentChapterTicks = 0; + var index = 1; + + // Limit to 100 chapters just in case there's some incorrect metadata here + while (currentChapterTicks < runtime && index < 100) + { + chapters.Add(new ChapterInfo + { + Name = "Chapter " + index, + StartPositionTicks = currentChapterTicks + }); + + index++; + currentChapterTicks += _dummyChapterDuration; + } + } + + /// <summary> + /// Called when [pre fetch]. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="mount">The mount.</param> + private void OnPreFetch(Video item, IIsoMount mount) + { + if (item.VideoType == VideoType.Iso) + { + item.IsoType = DetermineIsoType(mount); + } + + if (item.VideoType == VideoType.Dvd || (item.IsoType.HasValue && item.IsoType == IsoType.Dvd)) + { + FetchFromDvdLib(item, mount); + } + } + + private void FetchFromDvdLib(Video item, IIsoMount mount) + { + var path = mount == null ? item.Path : mount.MountedPath; + var dvd = new Dvd(path); + + item.RunTimeTicks = dvd.Titles.Select(GetRuntime).Max(); + + var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault(); + + uint? titleNumber = null; + + if (primaryTitle != null) + { + titleNumber = primaryTitle.TitleNumber; + } + + item.PlayableStreamFileNames = GetPrimaryPlaylistVobFiles(item, mount, titleNumber) + .Select(Path.GetFileName) + .ToList(); + } + + private long GetRuntime(Title title) + { + return title.ProgramChains + .Select(i => (TimeSpan)i.PlaybackTime) + .Select(i => i.Ticks) + .Sum(); + } + + /// <summary> + /// Mounts the iso if needed. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>IsoMount.</returns> + protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken) + { + if (item.VideoType == VideoType.Iso) + { + return _isoManager.Mount(item.Path, cancellationToken); + } + + return Task.FromResult<IIsoMount>(null); + } + + /// <summary> + /// Determines the type of the iso. + /// </summary> + /// <param name="isoMount">The iso mount.</param> + /// <returns>System.Nullable{IsoType}.</returns> + private IsoType? DetermineIsoType(IIsoMount isoMount) + { + var folders = Directory.EnumerateDirectories(isoMount.MountedPath).Select(Path.GetFileName).ToList(); + + if (folders.Contains("video_ts", StringComparer.OrdinalIgnoreCase)) + { + return IsoType.Dvd; + } + if (folders.Contains("bdmv", StringComparer.OrdinalIgnoreCase)) + { + return IsoType.BluRay; + } + + return null; + } + + private IEnumerable<string> GetPrimaryPlaylistVobFiles(Video video, IIsoMount isoMount, uint? titleNumber) + { + // min size 300 mb + const long minPlayableSize = 314572800; + + var root = isoMount != null ? isoMount.MountedPath : video.Path; + + // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size + // Once we reach a file that is at least the minimum, return all subsequent ones + var allVobs = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories) + .Where(file => string.Equals(Path.GetExtension(file), ".vob", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + // If we didn't find any satisfying the min length, just take them all + if (allVobs.Count == 0) + { + _logger.Error("No vobs found in dvd structure."); + return new List<string>(); + } + + if (titleNumber.HasValue) + { + var prefix = string.Format("VTS_0{0}_", titleNumber.Value.ToString(_usCulture)); + var vobs = allVobs.Where(i => Path.GetFileName(i).StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (vobs.Count > 0) + { + return vobs; + } + + _logger.Debug("Could not determine vob file list for {0} using DvdLib. Will scan using file sizes.", video.Path); + } + + var files = allVobs + .SkipWhile(f => new FileInfo(f).Length < minPlayableSize) + .ToList(); + + // If we didn't find any satisfying the min length, just take them all + if (files.Count == 0) + { + _logger.Warn("Vob size filter resulted in zero matches. Taking all vobs."); + files = allVobs; + } + + // Assuming they're named "vts_05_01", take all files whose second part matches that of the first file + if (files.Count > 0) + { + var parts = Path.GetFileNameWithoutExtension(files[0]).Split('_'); + + if (parts.Length == 3) + { + var title = parts[1]; + + files = files.TakeWhile(f => + { + var fileParts = Path.GetFileNameWithoutExtension(f).Split('_'); + + return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase); + + }).ToList(); + + // If this resulted in not getting any vobs, just take them all + if (files.Count == 0) + { + _logger.Warn("Vob filename filter resulted in zero matches. Taking all vobs."); + files = allVobs; + } + } + } + + return files; + } + } +} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs index afbea7f8b..8d69e6ba2 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs @@ -192,7 +192,7 @@ namespace MediaBrowser.Providers.MediaInfo cancellationToken.ThrowIfCancellationRequested(); - NormalizeFFProbeResult(result); + FFProbeHelpers.NormalizeFFProbeResult(result); cancellationToken.ThrowIfCancellationRequested(); @@ -401,7 +401,7 @@ namespace MediaBrowser.Providers.MediaInfo { if (!video.LockedFields.Contains(MetadataFields.Genres)) { - var genres = GetDictionaryValue(data.format.tags, "genre"); + var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre"); if (!string.IsNullOrEmpty(genres)) { @@ -417,7 +417,7 @@ namespace MediaBrowser.Providers.MediaInfo { if (!video.LockedFields.Contains(MetadataFields.Overview)) { - var overview = GetDictionaryValue(data.format.tags, "WM/SubTitleDescription"); + var overview = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription"); if (!string.IsNullOrWhiteSpace(overview)) { @@ -428,7 +428,7 @@ namespace MediaBrowser.Providers.MediaInfo if (force || string.IsNullOrEmpty(video.OfficialRating)) { - var officialRating = GetDictionaryValue(data.format.tags, "WM/ParentalRating"); + var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating"); if (!string.IsNullOrWhiteSpace(officialRating)) { @@ -443,7 +443,7 @@ namespace MediaBrowser.Providers.MediaInfo { if (!video.LockedFields.Contains(MetadataFields.Cast)) { - var people = GetDictionaryValue(data.format.tags, "WM/MediaCredits"); + var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits"); if (!string.IsNullOrEmpty(people)) { @@ -457,7 +457,7 @@ namespace MediaBrowser.Providers.MediaInfo if (force || !video.ProductionYear.HasValue) { - var year = GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime"); + var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime"); if (!string.IsNullOrWhiteSpace(year)) { diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index 21d3ae3e8..70b849e1c 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -1,90 +1,28 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaInfo; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; using System; -using System.Collections.Concurrent; -using System.IO; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Providers.MediaInfo { - class VideoImageProvider : BaseMetadataProvider + public class VideoImageProvider : IDynamicImageProvider { - /// <summary> - /// The _locks - /// </summary> - private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>(); - - /// <summary> - /// The _media encoder - /// </summary> - private readonly IMediaEncoder _mediaEncoder; private readonly IIsoManager _isoManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IServerConfigurationManager _config; - public VideoImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IMediaEncoder mediaEncoder, IIsoManager isoManager) - : base(logManager, configurationManager) + public VideoImageProvider(IIsoManager isoManager, IMediaEncoder mediaEncoder, IServerConfigurationManager config) { - _mediaEncoder = mediaEncoder; _isoManager = isoManager; - } - - /// <summary> - /// Gets a value indicating whether [refresh on version change]. - /// </summary> - /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value> - protected override bool RefreshOnVersionChange - { - get - { - return true; - } - } - - /// <summary> - /// Gets the provider version. - /// </summary> - /// <value>The provider version.</value> - protected override string ProviderVersion - { - get - { - return "1"; - } - } - - /// <summary> - /// Supportses the specified item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - public override bool Supports(BaseItem item) - { - return item.LocationType == LocationType.FileSystem && item is Video; - } - - /// <summary> - /// Needses the refresh internal. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="providerInfo">The provider info.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) - { - var video = (Video)item; - - if (!QualifiesForExtraction(video)) - { - return false; - } - - return base.NeedsRefreshInternal(item, providerInfo); + _mediaEncoder = mediaEncoder; + _config = config; } /// <summary> @@ -94,16 +32,6 @@ namespace MediaBrowser.Providers.MediaInfo /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> private bool QualifiesForExtraction(Video item) { - if (!ConfigurationManager.Configuration.EnableVideoImageExtraction) - { - return false; - } - - if (!string.IsNullOrEmpty(item.PrimaryImagePath)) - { - return false; - } - // No support for this if (item.VideoType == VideoType.HdDvd) { @@ -126,137 +54,61 @@ namespace MediaBrowser.Providers.MediaInfo } /// <summary> - /// Override this to return the date that should be compared to the last refresh date - /// to determine if this provider should be re-fetched. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>DateTime.</returns> - protected override DateTime CompareDate(BaseItem item) - { - return item.DateModified; - } - - /// <summary> - /// Gets the priority. + /// The null mount task result /// </summary> - /// <value>The priority.</value> - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.Last; } - } - - public override ItemUpdateType ItemUpdateType - { - get - { - return ItemUpdateType.ImageUpdate; - } - } + protected readonly Task<IIsoMount> NullMountTaskResult = Task.FromResult<IIsoMount>(null); /// <summary> - /// Fetches metadata and returns true or false indicating if any work that requires persistence was done + /// Mounts the iso if needed. /// </summary> /// <param name="item">The item.</param> - /// <param name="force">if set to <c>true</c> [force].</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.Boolean}.</returns> - public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) + /// <returns>Task{IIsoMount}.</returns> + protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken) { - item.ValidateImages(); - - var video = (Video)item; - - // Double check this here in case force was used - if (QualifiesForExtraction(video)) + if (item.VideoType == VideoType.Iso) { - try - { - await ExtractImage(video, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - // Swallow this so that we don't keep on trying over and over again - - Logger.ErrorException("Error extracting image for {0}", ex, item.Name); - } + return _isoManager.Mount(item.Path, cancellationToken); } - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; + return NullMountTaskResult; } - /// <summary> - /// Extracts the image. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task ExtractImage(Video item, CancellationToken cancellationToken) + public IEnumerable<ImageType> GetSupportedImages(IHasImages item) { - cancellationToken.ThrowIfCancellationRequested(); - - var path = GetVideoImagePath(item); - - if (!File.Exists(path)) - { - var semaphore = GetLock(path); - - // Acquire a lock - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - // Check again - if (!File.Exists(path)) - { - try - { - var parentPath = Path.GetDirectoryName(path); - - Directory.CreateDirectory(parentPath); - - await ExtractImageInternal(item, path, cancellationToken).ConfigureAwait(false); - } - finally - { - semaphore.Release(); - } - } - else - { - semaphore.Release(); - } - } + return new List<ImageType> { ImageType.Primary }; + } - // Image is already in the cache - item.SetImagePath(ImageType.Primary, path); + public Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken) + { + return GetVideoImage((Video)item, cancellationToken); } - /// <summary> - /// Extracts the image. - /// </summary> - /// <param name="video">The video.</param> - /// <param name="path">The path.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task ExtractImageInternal(Video video, string path, CancellationToken cancellationToken) + public async Task<DynamicImageResponse> GetVideoImage(Video item, CancellationToken cancellationToken) { - var isoMount = await MountIsoIfNeeded(video, cancellationToken).ConfigureAwait(false); + var isoMount = await MountIsoIfNeeded(item, cancellationToken).ConfigureAwait(false); try { // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in. // Always use 10 seconds for dvd because our duration could be out of whack - var imageOffset = video.VideoType != VideoType.Dvd && video.RunTimeTicks.HasValue && - video.RunTimeTicks.Value > 0 - ? TimeSpan.FromTicks(Convert.ToInt64(video.RunTimeTicks.Value * .1)) + var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue && + item.RunTimeTicks.Value > 0 + ? TimeSpan.FromTicks(Convert.ToInt64(item.RunTimeTicks.Value * .1)) : TimeSpan.FromSeconds(10); InputType type; - var inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type); + var inputPath = MediaEncoderHelpers.GetInputArgument(item.Path, item.LocationType == LocationType.Remote, item.VideoType, item.IsoType, isoMount, item.PlayableStreamFileNames, out type); - await _mediaEncoder.ExtractImage(inputPath, type, false, video.Video3DFormat, imageOffset, path, cancellationToken).ConfigureAwait(false); + var stream = await _mediaEncoder.ExtractImage(inputPath, type, false, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false); - video.SetImagePath(ImageType.Primary, path); + return new DynamicImageResponse + { + Format = ImageFormat.Jpg, + HasImage = true, + Stream = stream + }; } finally { @@ -267,63 +119,19 @@ namespace MediaBrowser.Providers.MediaInfo } } - /// <summary> - /// The null mount task result - /// </summary> - protected readonly Task<IIsoMount> NullMountTaskResult = Task.FromResult<IIsoMount>(null); - - /// <summary> - /// Mounts the iso if needed. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IIsoMount}.</returns> - protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken) - { - if (item.VideoType == VideoType.Iso) - { - return _isoManager.Mount(item.Path, cancellationToken); - } - - return NullMountTaskResult; - } - - /// <summary> - /// Gets the lock. - /// </summary> - /// <param name="filename">The filename.</param> - /// <returns>SemaphoreSlim.</returns> - private SemaphoreSlim GetLock(string filename) + public string Name { - return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); + get { return "Embedded Image"; } } - /// <summary> - /// Gets the video images data path. - /// </summary> - /// <value>The video images data path.</value> - public string VideoImagesPath + public bool Supports(IHasImages item) { - get + if (!_config.Configuration.EnableVideoImageExtraction) { - return Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "extracted-video-images"); + return false; } - } - /// <summary> - /// Gets the audio image path. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>System.String.</returns> - private string GetVideoImagePath(Video item) - { - var filename = item.Path + "_" + item.DateModified.Ticks + "_primary"; - - filename = filename.GetMD5() + ".jpg"; - - var prefix = filename.Substring(0, 1); - - return Path.Combine(VideoImagesPath, prefix, filename); + return item.LocationType == LocationType.FileSystem && item is Video; } } } diff --git a/MediaBrowser.Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Providers/Movies/MovieDbProvider.cs index 87e6be1f7..3adc682fc 100644 --- a/MediaBrowser.Providers/Movies/MovieDbProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbProvider.cs @@ -371,6 +371,11 @@ namespace MediaBrowser.Providers.Movies { var path = GetDataFilePath(item); + if (string.IsNullOrEmpty(path)) + { + return _cachedTask; + } + var fileInfo = _fileSystem.GetFileSystemInfo(path); if (fileInfo.Exists) diff --git a/MediaBrowser.Providers/Movies/MovieProviderFromXml.cs b/MediaBrowser.Providers/Movies/MovieProviderFromXml.cs index 67b62548e..7ae6dffc7 100644 --- a/MediaBrowser.Providers/Movies/MovieProviderFromXml.cs +++ b/MediaBrowser.Providers/Movies/MovieProviderFromXml.cs @@ -96,7 +96,7 @@ namespace MediaBrowser.Providers.Movies try { - await new MovieXmlParser(Logger, _itemRepo).FetchAsync(video, path, cancellationToken).ConfigureAwait(false); + new MovieXmlParser(Logger).FetchAsync(video, path, cancellationToken); } finally { diff --git a/MediaBrowser.Providers/Movies/MovieXmlParser.cs b/MediaBrowser.Providers/Movies/MovieXmlParser.cs index 61b73360c..64038e853 100644 --- a/MediaBrowser.Providers/Movies/MovieXmlParser.cs +++ b/MediaBrowser.Providers/Movies/MovieXmlParser.cs @@ -1,10 +1,8 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Logging; using System.Threading; -using System.Threading.Tasks; using System.Xml; namespace MediaBrowser.Providers.Movies @@ -14,28 +12,14 @@ namespace MediaBrowser.Providers.Movies /// </summary> public class MovieXmlParser : BaseItemXmlParser<Video> { - private readonly IItemRepository _itemRepo; - - private Task _chaptersTask = null; - - public MovieXmlParser(ILogger logger, IItemRepository itemRepo) + public MovieXmlParser(ILogger logger) : base(logger) { - _itemRepo = itemRepo; } - public async Task FetchAsync(Video item, string metadataFile, CancellationToken cancellationToken) + public void FetchAsync(Video item, string metadataFile, CancellationToken cancellationToken) { - _chaptersTask = null; - Fetch(item, metadataFile, cancellationToken); - - cancellationToken.ThrowIfCancellationRequested(); - - if (_chaptersTask != null) - { - await _chaptersTask.ConfigureAwait(false); - } } /// <summary> diff --git a/MediaBrowser.Providers/Movies/MovieXmlProvider.cs b/MediaBrowser.Providers/Movies/MovieXmlProvider.cs new file mode 100644 index 000000000..82250f2b5 --- /dev/null +++ b/MediaBrowser.Providers/Movies/MovieXmlProvider.cs @@ -0,0 +1,78 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Movies +{ + public class MovieXmlProvider : BaseXmlProvider, ILocalMetadataProvider<Movie> + { + private readonly ILogger _logger; + + public MovieXmlProvider(IFileSystem fileSystem, ILogger logger) + : base(fileSystem) + { + _logger = logger; + } + + public async Task<MetadataResult<Movie>> GetMetadata(string path, CancellationToken cancellationToken) + { + path = GetXmlFile(path).FullName; + + var result = new MetadataResult<Movie>(); + + await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + result.Item = new Movie(); + + new MovieXmlParser(_logger).Fetch(result.Item, path, cancellationToken); + result.HasMetadata = true; + } + catch (FileNotFoundException) + { + result.HasMetadata = false; + } + finally + { + XmlParsingResourcePool.Release(); + } + + return result; + } + + public string Name + { + get { return "Media Browser Xml"; } + } + + protected override FileInfo GetXmlFile(string path) + { + return GetXmlFileInfo(path, FileSystem); + } + + public static FileInfo GetXmlFileInfo(string path, IFileSystem _fileSystem) + { + var fileInfo = _fileSystem.GetFileSystemInfo(path); + + var directoryInfo = fileInfo as DirectoryInfo; + + if (directoryInfo == null) + { + directoryInfo = new DirectoryInfo(Path.GetDirectoryName(path)); + } + + var directoryPath = directoryInfo.FullName; + + var specificFile = Path.Combine(directoryPath, Path.GetFileNameWithoutExtension(path) + ".xml"); + + var file = new FileInfo(specificFile); + + return file.Exists ? file : new FileInfo(Path.Combine(directoryPath, "movie.xml")); + } + } +} diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 024e44cad..a0e2cc81b 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -112,7 +112,7 @@ namespace MediaBrowser.Providers.Music if (artist != null) { - id.ArtistMusicBrainzId = artist.GetProviderId(MetadataProviders.Musicbrainz); + id.ArtistProviderIds = artist.ProviderIds; id.AlbumArtist = id.AlbumArtist ?? artist.Name; } diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index 3f7c0ce96..f5cf49f74 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -37,7 +37,10 @@ namespace MediaBrowser.Providers.Music if (string.IsNullOrEmpty(releaseId)) { - var releaseResult = await GetReleaseResult(albumId.ArtistMusicBrainzId, albumId.AlbumArtist, albumId.Name, cancellationToken).ConfigureAwait(false); + string artistMusicBrainzId; + albumId.ArtistProviderIds.TryGetValue(MetadataProviders.Musicbrainz.ToString(), out artistMusicBrainzId); + + var releaseResult = await GetReleaseResult(artistMusicBrainzId, albumId.AlbumArtist, albumId.Name, cancellationToken).ConfigureAwait(false); result.Item = new MusicAlbum(); diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs new file mode 100644 index 000000000..6dd16819b --- /dev/null +++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs @@ -0,0 +1,53 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Providers.Manager; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Music +{ + class MusicVideoMetadataService : MetadataService<MusicVideo, ItemId> + { + private readonly ILibraryManager _libraryManager; + + public MusicVideoMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem) + { + _libraryManager = libraryManager; + } + + /// <summary> + /// Merges the specified source. + /// </summary> + /// <param name="source">The source.</param> + /// <param name="target">The target.</param> + /// <param name="lockedFields">The locked fields.</param> + /// <param name="replaceData">if set to <c>true</c> [replace data].</param> + /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param> + protected override void MergeData(MusicVideo source, MusicVideo target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + + if (replaceData || string.IsNullOrEmpty(target.Album)) + { + target.Album = source.Album; + } + + if (replaceData || string.IsNullOrEmpty(target.Artist)) + { + target.Artist = source.Artist; + } + } + + protected override Task SaveItem(MusicVideo item, ItemUpdateType reason, CancellationToken cancellationToken) + { + return _libraryManager.UpdateItem(item, reason, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Music/MusicVideoXmlProvider.cs b/MediaBrowser.Providers/Music/MusicVideoXmlProvider.cs new file mode 100644 index 000000000..dcfe0d89e --- /dev/null +++ b/MediaBrowser.Providers/Music/MusicVideoXmlProvider.cs @@ -0,0 +1,60 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; +using MediaBrowser.Providers.Movies; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Music +{ + class MusicVideoXmlProvider : BaseXmlProvider, ILocalMetadataProvider<MusicVideo> + { + private readonly ILogger _logger; + + public MusicVideoXmlProvider(IFileSystem fileSystem, ILogger logger) + : base(fileSystem) + { + _logger = logger; + } + + public async Task<MetadataResult<MusicVideo>> GetMetadata(string path, CancellationToken cancellationToken) + { + path = GetXmlFile(path).FullName; + + var result = new MetadataResult<MusicVideo>(); + + await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var item = new MusicVideo(); + + new MusicVideoXmlParser(_logger).Fetch(item, path, cancellationToken); + result.HasMetadata = true; + result.Item = item; + } + catch (FileNotFoundException) + { + result.HasMetadata = false; + } + finally + { + XmlParsingResourcePool.Release(); + } + + return result; + } + + public string Name + { + get { return "Media Browser Xml"; } + } + + protected override FileInfo GetXmlFile(string path) + { + return MovieXmlProvider.GetXmlFileInfo(path, FileSystem); + } + } +} diff --git a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs index c0a62fba2..f64f30a27 100644 --- a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs +++ b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs @@ -3,7 +3,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Providers.Manager; diff --git a/MediaBrowser.Providers/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Omdb/OmdbProvider.cs index 70dea5db4..bc1be2dd6 100644 --- a/MediaBrowser.Providers/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Omdb/OmdbProvider.cs @@ -2,6 +2,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Serialization; using System; @@ -26,13 +27,13 @@ namespace MediaBrowser.Providers.Omdb _httpClient = httpClient; } - public async Task Fetch(BaseItem item, CancellationToken cancellationToken) + public async Task<ItemUpdateType> Fetch(BaseItem item, CancellationToken cancellationToken) { var imdbId = item.GetProviderId(MetadataProviders.Imdb); if (string.IsNullOrEmpty(imdbId)) { - return; + return ItemUpdateType.Unspecified; } var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId; @@ -97,6 +98,8 @@ namespace MediaBrowser.Providers.Omdb ParseAdditionalMetadata(item, result); } + + return ItemUpdateType.MetadataDownload; } private void ParseAdditionalMetadata(BaseItem item, RootObject result) diff --git a/MediaBrowser.Providers/Omdb/OmdbSeriesProvider.cs b/MediaBrowser.Providers/Omdb/OmdbSeriesProvider.cs index 3659868c7..4ce2ad5e1 100644 --- a/MediaBrowser.Providers/Omdb/OmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Omdb/OmdbSeriesProvider.cs @@ -1,5 +1,8 @@ using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Serialization; using System.Threading; @@ -7,7 +10,8 @@ using System.Threading.Tasks; namespace MediaBrowser.Providers.Omdb { - public class OmdbSeriesProvider : ICustomMetadataProvider<Series> + public class OmdbSeriesProvider : ICustomMetadataProvider<Series>, + ICustomMetadataProvider<Movie>, ICustomMetadataProvider<Trailer> { private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; @@ -18,14 +22,30 @@ namespace MediaBrowser.Providers.Omdb _httpClient = httpClient; } - public Task FetchAsync(Series item, CancellationToken cancellationToken) + public string Name + { + get { return "OMDb"; } + } + + public Task<ItemUpdateType> FetchAsync(Series item, CancellationToken cancellationToken) { return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken); } - public string Name + public Task<ItemUpdateType> FetchAsync(Movie item, CancellationToken cancellationToken) { - get { return "OMDb"; } + return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken); + } + + private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.Unspecified); + public Task<ItemUpdateType> FetchAsync(Trailer item, CancellationToken cancellationToken) + { + if (item.IsLocalTrailer) + { + return _cachedTask; + } + + return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken); } } } diff --git a/MediaBrowser.Providers/ProviderUtils.cs b/MediaBrowser.Providers/ProviderUtils.cs index 543643474..61fe19a61 100644 --- a/MediaBrowser.Providers/ProviderUtils.cs +++ b/MediaBrowser.Providers/ProviderUtils.cs @@ -158,6 +158,7 @@ namespace MediaBrowser.Providers } MergeAlbumArtist(source, target, lockedFields, replaceData); + MergeBudget(source, target, lockedFields, replaceData); if (mergeMetadataSettings) { @@ -198,5 +199,24 @@ namespace MediaBrowser.Providers } } } + + private static void MergeBudget(BaseItem source, BaseItem target, List<MetadataFields> lockedFields, bool replaceData) + { + var sourceHasBudget = source as IHasBudget; + var targetHasBudget = target as IHasBudget; + + if (sourceHasBudget != null && targetHasBudget != null) + { + if (replaceData || !targetHasBudget.Budget.HasValue) + { + targetHasBudget.Budget = sourceHasBudget.Budget; + } + + if (replaceData || !targetHasBudget.Revenue.HasValue) + { + targetHasBudget.Revenue = sourceHasBudget.Revenue; + } + } + } } } diff --git a/MediaBrowser.Providers/Studios/StudioMetadataService.cs b/MediaBrowser.Providers/Studios/StudioMetadataService.cs index a42a882af..8d5184fc2 100644 --- a/MediaBrowser.Providers/Studios/StudioMetadataService.cs +++ b/MediaBrowser.Providers/Studios/StudioMetadataService.cs @@ -1,10 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Providers.Manager; diff --git a/MediaBrowser.Providers/TV/EpisodeImageFromMediaLocationProvider.cs b/MediaBrowser.Providers/TV/EpisodeImageFromMediaLocationProvider.cs deleted file mode 100644 index f2aaa0f06..000000000 --- a/MediaBrowser.Providers/TV/EpisodeImageFromMediaLocationProvider.cs +++ /dev/null @@ -1,163 +0,0 @@ -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -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.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.TV -{ - /// <summary> - /// Class EpisodeImageFromMediaLocationProvider - /// </summary> - public class EpisodeImageFromMediaLocationProvider : BaseMetadataProvider - { - public EpisodeImageFromMediaLocationProvider(ILogManager logManager, IServerConfigurationManager configurationManager) - : base(logManager, configurationManager) - { - } - - public override ItemUpdateType ItemUpdateType - { - get - { - return ItemUpdateType.ImageUpdate; - } - } - - /// <summary> - /// Supportses the specified item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - public override bool Supports(BaseItem item) - { - return item is Episode && item.LocationType == LocationType.FileSystem; - } - - /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.First; } - } - - /// <summary> - /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes - /// </summary> - /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value> - protected override bool RefreshOnFileSystemStampChange - { - get - { - return true; - } - } - - /// <summary> - /// Gets the filestamp extensions. - /// </summary> - /// <value>The filestamp extensions.</value> - protected override string[] FilestampExtensions - { - get - { - return BaseItem.SupportedImageExtensions; - } - } - - /// <summary> - /// Fetches metadata and returns true or false indicating if any work that requires persistence was done - /// </summary> - /// <param name="item">The item.</param> - /// <param name="force">if set to <c>true</c> [force].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.Boolean}.</returns> - public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var episode = (Episode)item; - - var episodeFileName = Path.GetFileName(episode.Path); - - var parent = item.ResolveArgs.Parent; - - ValidateImage(episode); - - cancellationToken.ThrowIfCancellationRequested(); - - SetPrimaryImagePath(episode, parent, item.MetaLocation, episodeFileName); - - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return TrueTaskResult; - } - - /// <summary> - /// Validates the primary image path still exists - /// </summary> - /// <param name="episode">The episode.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - private void ValidateImage(Episode episode) - { - var path = episode.PrimaryImagePath; - - if (string.IsNullOrEmpty(path)) - { - return; - } - - if (!File.Exists(path)) - { - episode.SetImagePath(ImageType.Primary, null); - } - } - - /// <summary> - /// Sets the primary image path. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="parent">The parent.</param> - /// <param name="metadataFolder">The metadata folder.</param> - /// <param name="episodeFileName">Name of the episode file.</param> - private void SetPrimaryImagePath(Episode item, Folder parent, string metadataFolder, string episodeFileName) - { - foreach (var extension in BaseItem.SupportedImageExtensions) - { - var path = Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, extension)); - - var file = parent.ResolveArgs.GetMetaFileByPath(path); - - if (file != null) - { - item.SetImagePath(ImageType.Primary, file.FullName); - return; - } - } - - var seasonFolder = Path.GetDirectoryName(item.Path); - - foreach (var extension in BaseItem.SupportedImageExtensions) - { - var imageFilename = Path.GetFileNameWithoutExtension(episodeFileName) + "-thumb" + extension; - - var path = Path.Combine(seasonFolder, imageFilename); - - var file = parent.ResolveArgs.GetMetaFileByPath(path); - - if (file != null) - { - item.SetImagePath(ImageType.Primary, file.FullName); - return; - } - } - } - } -} diff --git a/MediaBrowser.Providers/TV/EpisodeIndexNumberProvider.cs b/MediaBrowser.Providers/TV/EpisodeIndexNumberProvider.cs deleted file mode 100644 index 3e7597e0d..000000000 --- a/MediaBrowser.Providers/TV/EpisodeIndexNumberProvider.cs +++ /dev/null @@ -1,99 +0,0 @@ -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -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.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.TV -{ - /// <summary> - /// Making this a provider because of how slow it is - /// It only ever needs to run once - /// </summary> - public class EpisodeIndexNumberProvider : BaseMetadataProvider - { - /// <summary> - /// Initializes a new instance of the <see cref="BaseMetadataProvider" /> class. - /// </summary> - /// <param name="logManager">The log manager.</param> - /// <param name="configurationManager">The configuration manager.</param> - public EpisodeIndexNumberProvider(ILogManager logManager, IServerConfigurationManager configurationManager) - : base(logManager, configurationManager) - { - } - - protected override bool RefreshOnVersionChange - { - get - { - return true; - } - } - - protected override string ProviderVersion - { - get - { - return "2"; - } - } - - /// <summary> - /// Supportses the specified item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - public override bool Supports(BaseItem item) - { - if (item is Episode) - { - var locationType = item.LocationType; - return locationType != LocationType.Virtual && locationType != LocationType.Remote; - } - return false; - } - - /// <summary> - /// Fetches metadata and returns true or false indicating if any work that requires persistence was done - /// </summary> - /// <param name="item">The item.</param> - /// <param name="force">if set to <c>true</c> [force].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.Boolean}.</returns> - public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) - { - var episode = (Episode)item; - - episode.IndexNumber = TVUtils.GetEpisodeNumberFromFile(item.Path, item.Parent is Season); - episode.IndexNumberEnd = TVUtils.GetEndingEpisodeNumberFromFile(item.Path); - - if (!episode.ParentIndexNumber.HasValue) - { - var season = episode.Parent as Season; - - if (season != null) - { - episode.ParentIndexNumber = season.IndexNumber; - } - } - - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - - return TrueTaskResult; - } - - /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.First; } - } - } -} diff --git a/MediaBrowser.Providers/TV/EpisodeLocalImageProvider.cs b/MediaBrowser.Providers/TV/EpisodeLocalImageProvider.cs new file mode 100644 index 000000000..96e8f3158 --- /dev/null +++ b/MediaBrowser.Providers/TV/EpisodeLocalImageProvider.cs @@ -0,0 +1,59 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Providers.TV +{ + public class EpisodeLocalImageProvider : IImageFileProvider + { + public string Name + { + get { return "Local Images"; } + } + + public bool Supports(IHasImages item) + { + return item is Episode && item.LocationType == LocationType.FileSystem; + } + + public List<LocalImageInfo> GetImages(IHasImages item) + { + var parentPath = Path.GetDirectoryName(item.Path); + + var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path); + var thumbName = nameWithoutExtension + "-thumb"; + + return Directory.EnumerateFiles(parentPath, "*", SearchOption.AllDirectories) + .Where(i => + { + if (BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i) ?? string.Empty)) + { + var currentNameWithoutExtension = Path.GetFileNameWithoutExtension(i); + + if (string.Equals(nameWithoutExtension, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (string.Equals(thumbName, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + }) + .Select(i => new LocalImageInfo + { + Path = i, + Type = ImageType.Primary + }) + .ToList(); + } + } +} diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs new file mode 100644 index 000000000..2b6edaf08 --- /dev/null +++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs @@ -0,0 +1,137 @@ +using MediaBrowser.Common.IO; +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 MediaBrowser.Providers.Manager; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + public class EpisodeMetadataService : MetadataService<Episode, EpisodeId> + { + private readonly ILibraryManager _libraryManager; + + public EpisodeMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem) + { + _libraryManager = libraryManager; + } + + /// <summary> + /// Merges the specified source. + /// </summary> + /// <param name="source">The source.</param> + /// <param name="target">The target.</param> + /// <param name="lockedFields">The locked fields.</param> + /// <param name="replaceData">if set to <c>true</c> [replace data].</param> + /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param> + protected override void MergeData(Episode source, Episode target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + + if (replaceData || !target.AirsBeforeSeasonNumber.HasValue) + { + target.AirsBeforeSeasonNumber = source.AirsBeforeSeasonNumber; + } + + if (replaceData || !target.AirsAfterSeasonNumber.HasValue) + { + target.AirsAfterSeasonNumber = source.AirsAfterSeasonNumber; + } + + if (replaceData || !target.AirsBeforeEpisodeNumber.HasValue) + { + target.AirsBeforeEpisodeNumber = source.AirsBeforeEpisodeNumber; + } + + if (replaceData || !target.DvdSeasonNumber.HasValue) + { + target.DvdSeasonNumber = source.DvdSeasonNumber; + } + + if (replaceData || !target.DvdEpisodeNumber.HasValue) + { + target.DvdEpisodeNumber = source.DvdEpisodeNumber; + } + + if (replaceData || !target.AbsoluteEpisodeNumber.HasValue) + { + target.AbsoluteEpisodeNumber = source.AbsoluteEpisodeNumber; + } + + if (replaceData || !target.IndexNumberEnd.HasValue) + { + target.IndexNumberEnd = source.IndexNumberEnd; + } + } + + protected override Task SaveItem(Episode item, ItemUpdateType reason, CancellationToken cancellationToken) + { + return _libraryManager.UpdateItem(item, reason, cancellationToken); + } + + protected override EpisodeId GetId(Episode item) + { + var id = base.GetId(item); + + var series = item.Series; + + if (series != null) + { + id.SeriesProviderIds = series.ProviderIds; + } + + id.IndexNumberEnd = item.IndexNumberEnd; + + return id; + } + + protected override ItemUpdateType BeforeMetadataRefresh(Episode item) + { + var updateType = base.BeforeMetadataRefresh(item); + + var locationType = item.LocationType; + if (locationType == LocationType.FileSystem || locationType == LocationType.Offline) + { + var currentIndexNumber = item.IndexNumber; + var currentIndexNumberEnd = item.IndexNumberEnd; + var currentParentIndexNumber = item.ParentIndexNumber; + + item.IndexNumber = item.IndexNumber ?? TVUtils.GetEpisodeNumberFromFile(item.Path, item.Parent is Season); + item.IndexNumberEnd = item.IndexNumberEnd ?? TVUtils.GetEndingEpisodeNumberFromFile(item.Path); + + if (!item.ParentIndexNumber.HasValue) + { + var season = item.Season; + + if (season != null) + { + item.ParentIndexNumber = season.IndexNumber; + } + } + + if ((currentIndexNumber ?? -1) != (item.IndexNumber ?? -1)) + { + updateType = updateType | ItemUpdateType.MetadataImport; + } + + if ((currentIndexNumberEnd ?? -1) != (item.IndexNumberEnd ?? -1)) + { + updateType = updateType | ItemUpdateType.MetadataImport; + } + + if ((currentParentIndexNumber ?? -1) != (item.ParentIndexNumber ?? -1)) + { + updateType = updateType | ItemUpdateType.MetadataImport; + } + } + + return updateType; + } + } +} diff --git a/MediaBrowser.Providers/TV/EpisodeProviderFromXml.cs b/MediaBrowser.Providers/TV/EpisodeProviderFromXml.cs deleted file mode 100644 index 38921c008..000000000 --- a/MediaBrowser.Providers/TV/EpisodeProviderFromXml.cs +++ /dev/null @@ -1,103 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.TV -{ - /// <summary> - /// Class EpisodeProviderFromXml - /// </summary> - public class EpisodeProviderFromXml : BaseMetadataProvider - { - private readonly IItemRepository _itemRepo; - private readonly IFileSystem _fileSystem; - - public EpisodeProviderFromXml(ILogManager logManager, IServerConfigurationManager configurationManager, IItemRepository itemRepo, IFileSystem fileSystem) - : base(logManager, configurationManager) - { - _itemRepo = itemRepo; - _fileSystem = fileSystem; - } - - /// <summary> - /// Supportses the specified item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - public override bool Supports(BaseItem item) - { - return item is Episode && item.LocationType == LocationType.FileSystem; - } - - /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.First; } - } - - /// <summary> - /// Fetches metadata and returns true or false indicating if any work that requires persistence was done - /// </summary> - /// <param name="item">The item.</param> - /// <param name="force">if set to <c>true</c> [force].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.Boolean}.</returns> - public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var metadataFile = Path.Combine(item.MetaLocation, Path.ChangeExtension(Path.GetFileName(item.Path), ".xml")); - - var file = item.ResolveArgs.Parent.ResolveArgs.GetMetaFileByPath(metadataFile); - - if (file != null) - { - await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - await new EpisodeXmlParser(Logger, _itemRepo).FetchAsync((Episode)item, metadataFile, cancellationToken).ConfigureAwait(false); - } - finally - { - XmlParsingResourcePool.Release(); - } - } - - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - /// <summary> - /// Needses the refresh based on compare date. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="providerInfo">The provider info.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) - { - var metadataFile = Path.Combine(item.MetaLocation, Path.ChangeExtension(Path.GetFileName(item.Path), ".xml")); - - var file = item.ResolveArgs.Parent.ResolveArgs.GetMetaFileByPath(metadataFile); - - if (file == null) - { - return false; - } - - return _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; - } - } -} diff --git a/MediaBrowser.Providers/TV/EpisodeXmlParser.cs b/MediaBrowser.Providers/TV/EpisodeXmlParser.cs index d0bf7bcde..b35c18e09 100644 --- a/MediaBrowser.Providers/TV/EpisodeXmlParser.cs +++ b/MediaBrowser.Providers/TV/EpisodeXmlParser.cs @@ -1,13 +1,11 @@ -using System; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; +using System; using System.Globalization; using System.IO; using System.Threading; -using System.Threading.Tasks; using System.Xml; namespace MediaBrowser.Providers.TV @@ -17,28 +15,14 @@ namespace MediaBrowser.Providers.TV /// </summary> public class EpisodeXmlParser : BaseItemXmlParser<Episode> { - private readonly IItemRepository _itemRepo; - - private Task _chaptersTask = null; - - public EpisodeXmlParser(ILogger logger, IItemRepository itemRepo) + public EpisodeXmlParser(ILogger logger) : base(logger) { - _itemRepo = itemRepo; } - public async Task FetchAsync(Episode item, string metadataFile, CancellationToken cancellationToken) + public void FetchAsync(Episode item, string metadataFile, CancellationToken cancellationToken) { - _chaptersTask = null; - - Fetch(item, metadataFile, cancellationToken); - - cancellationToken.ThrowIfCancellationRequested(); - - if (_chaptersTask != null) - { - await _chaptersTask.ConfigureAwait(false); - } + Fetch(item, metadataFile, cancellationToken); } private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); diff --git a/MediaBrowser.Providers/TV/EpisodeXmlProvider.cs b/MediaBrowser.Providers/TV/EpisodeXmlProvider.cs new file mode 100644 index 000000000..b8d88f5e6 --- /dev/null +++ b/MediaBrowser.Providers/TV/EpisodeXmlProvider.cs @@ -0,0 +1,62 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + public class EpisodeXmlProvider : BaseXmlProvider, ILocalMetadataProvider<Episode> + { + private readonly ILogger _logger; + + public EpisodeXmlProvider(IFileSystem fileSystem, ILogger logger) + : base(fileSystem) + { + _logger = logger; + } + + public async Task<MetadataResult<Episode>> GetMetadata(string path, CancellationToken cancellationToken) + { + path = GetXmlFile(path).FullName; + + var result = new MetadataResult<Episode>(); + + await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + result.Item = new Episode(); + + new EpisodeXmlParser(_logger).Fetch(result.Item, path, cancellationToken); + result.HasMetadata = true; + } + catch (FileNotFoundException) + { + result.HasMetadata = false; + } + finally + { + XmlParsingResourcePool.Release(); + } + + return result; + } + + public string Name + { + get { return "Media Browser Xml"; } + } + + protected override FileInfo GetXmlFile(string path) + { + var metadataPath = Path.GetDirectoryName(path); + metadataPath = Path.Combine(metadataPath, "metadata"); + var metadataFile = Path.Combine(metadataPath, Path.ChangeExtension(Path.GetFileName(path), ".xml")); + + return new FileInfo(metadataFile); + } + } +} diff --git a/MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs index 85353bad5..6f988a2f6 100644 --- a/MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Net; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -6,6 +7,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -17,16 +19,18 @@ using System.Xml; namespace MediaBrowser.Providers.TV { - public class TvdbEpisodeImageProvider : IRemoteImageProvider + public class TvdbEpisodeImageProvider : IRemoteImageProvider, IHasChangeMonitor { private readonly IServerConfigurationManager _config; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; - public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient) + public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) { _config = config; _httpClient = httpClient; + _fileSystem = fileSystem; } public string Name @@ -65,7 +69,7 @@ namespace MediaBrowser.Providers.TV // Process images var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId); - var files = TvdbEpisodeProvider.Current.GetEpisodeXmlFiles(episode, seriesDataPath); + var files = TvdbEpisodeProvider.Current.GetEpisodeXmlFiles(episode.ParentIndexNumber, episode.IndexNumber, episode.IndexNumberEnd, seriesDataPath); var result = files.Select(i => GetImageInfo(i, cancellationToken)) .Where(i => i != null); @@ -186,5 +190,27 @@ namespace MediaBrowser.Providers.TV ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool }); } + + public bool HasChanged(IHasMetadata item, DateTime date) + { + if (!item.HasImage(ImageType.Primary)) + { + var episode = (Episode)item; + var series = episode.Series; + + var seriesId = series != null ? series.GetProviderId(MetadataProviders.Tvdb) : null; + + if (!string.IsNullOrEmpty(seriesId)) + { + // Process images + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId); + + var files = TvdbEpisodeProvider.Current.GetEpisodeXmlFiles(episode.ParentIndexNumber, episode.IndexNumber, episode.IndexNumberEnd, seriesDataPath); + + return files.Any(i => _fileSystem.GetLastWriteTimeUtc(i) > date); + } + } + return false; + } } } diff --git a/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs index f5e21bf69..5523b8ab3 100644 --- a/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs @@ -1,12 +1,10 @@ using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using System; using System.Collections.Generic; @@ -25,161 +23,88 @@ namespace MediaBrowser.Providers.TV /// <summary> /// Class RemoteEpisodeProvider /// </summary> - class TvdbEpisodeProvider : BaseMetadataProvider + class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode>, IHasChangeMonitor { - /// <summary> - /// The _provider manager - /// </summary> - private readonly IProviderManager _providerManager; - - /// <summary> - /// Gets the HTTP client. - /// </summary> - /// <value>The HTTP client.</value> - protected IHttpClient HttpClient { get; private set; } - private readonly IFileSystem _fileSystem; - internal static TvdbEpisodeProvider Current; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; - /// <summary> - /// Initializes a new instance of the <see cref="TvdbEpisodeProvider" /> class. - /// </summary> - /// <param name="httpClient">The HTTP client.</param> - /// <param name="logManager">The log manager.</param> - /// <param name="configurationManager">The configuration manager.</param> - /// <param name="providerManager">The provider manager.</param> - public TvdbEpisodeProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem) - : base(logManager, configurationManager) + public TvdbEpisodeProvider(IFileSystem fileSystem, IServerConfigurationManager config) { - HttpClient = httpClient; - _providerManager = providerManager; _fileSystem = fileSystem; + _config = config; Current = this; } - /// <summary> - /// Supportses the specified item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - public override bool Supports(BaseItem item) + public string Name { - return item is Episode; + get { return "TheTVDB"; } } - public override ItemUpdateType ItemUpdateType + public Task<MetadataResult<Episode>> GetMetadata(ItemId id, CancellationToken cancellationToken) { - get - { - return ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataDownload; - } - } + var episodeId = (EpisodeId)id; - /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.Third; } - } + string seriesTvdbId; + episodeId.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesTvdbId); - /// <summary> - /// Gets a value indicating whether [requires internet]. - /// </summary> - /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value> - public override bool RequiresInternet - { - get { return true; } - } + var result = new MetadataResult<Episode>(); - /// <summary> - /// Gets a value indicating whether [refresh on version change]. - /// </summary> - /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value> - protected override bool RefreshOnVersionChange - { - get + if (!string.IsNullOrEmpty(seriesTvdbId)) { - return true; - } - } + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesTvdbId); - /// <summary> - /// Gets the provider version. - /// </summary> - /// <value>The provider version.</value> - protected override string ProviderVersion - { - get - { - return "5"; - } - } - - /// <summary> - /// Needses the refresh internal. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="providerInfo">The provider info.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) - { - var locationType = item.LocationType; - - // Always use tvdb updates for non-file system episodes - if (locationType != LocationType.Remote && locationType != LocationType.Virtual) - { - // Don't proceed if there's local metadata - if (!ConfigurationManager.Configuration.EnableTvDbUpdates && HasLocalMeta(item)) + try { - return false; + result.Item = FetchEpisodeData(episodeId, seriesDataPath, cancellationToken); + result.HasMetadata = result.Item != null; + } + catch (FileNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. } } - return base.NeedsRefreshInternal(item, providerInfo); + return Task.FromResult(result); } - protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) + public bool HasChanged(IHasMetadata item, DateTime date) { var episode = (Episode)item; + var series = episode.Series; - var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; + var seriesId = series != null ? series.GetProviderId(MetadataProviders.Tvdb) : null; if (!string.IsNullOrEmpty(seriesId)) { // Process images - var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId); - var files = GetEpisodeXmlFiles(episode, seriesDataPath); + var files = GetEpisodeXmlFiles(episode.ParentIndexNumber, episode.IndexNumber, episode.IndexNumberEnd, seriesDataPath); - if (files.Count > 0) - { - return files.Select(i => _fileSystem.GetLastWriteTimeUtc(i)).Max() > providerInfo.LastRefreshed; - } + return files.Any(i => _fileSystem.GetLastWriteTimeUtc(i) > date); } - + return false; } /// <summary> /// Gets the episode XML files. /// </summary> - /// <param name="episode">The episode.</param> + /// <param name="seasonNumber">The season number.</param> + /// <param name="episodeNumber">The episode number.</param> + /// <param name="endingEpisodeNumber">The ending episode number.</param> /// <param name="seriesDataPath">The series data path.</param> /// <returns>List{FileInfo}.</returns> - internal List<FileInfo> GetEpisodeXmlFiles(Episode episode, string seriesDataPath) + internal List<FileInfo> GetEpisodeXmlFiles(int? seasonNumber, int? episodeNumber, int? endingEpisodeNumber, string seriesDataPath) { var files = new List<FileInfo>(); - if (episode.IndexNumber == null) + if (episodeNumber == null) { return files; } - var episodeNumber = episode.IndexNumber.Value; - var seasonNumber = episode.ParentIndexNumber; - if (seasonNumber == null) { return files; @@ -205,7 +130,7 @@ namespace MediaBrowser.Providers.TV } } - var end = episode.IndexNumberEnd ?? episodeNumber; + var end = endingEpisodeNumber ?? episodeNumber; episodeNumber++; while (episodeNumber <= end) @@ -236,72 +161,34 @@ namespace MediaBrowser.Providers.TV } /// <summary> - /// Fetches metadata and returns true or false indicating if any work that requires persistence was done - /// </summary> - /// <param name="item">The item.</param> - /// <param name="force">if set to <c>true</c> [force].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.Boolean}.</returns> - public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var status = ProviderRefreshStatus.Success; - - var episode = (Episode)item; - - var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; - - if (!string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); - - try - { - status = await FetchEpisodeData(episode, seriesDataPath, cancellationToken).ConfigureAwait(false); - } - catch (FileNotFoundException) - { - // Don't fail the provider because this will just keep on going and going. - } - } - - SetLastRefreshed(item, DateTime.UtcNow, providerInfo, status); - return true; - } - - - /// <summary> /// Fetches the episode data. /// </summary> - /// <param name="episode">The episode.</param> + /// <param name="id">The identifier.</param> /// <param name="seriesDataPath">The series data path.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{System.Boolean}.</returns> - private async Task<ProviderRefreshStatus> FetchEpisodeData(Episode episode, string seriesDataPath, CancellationToken cancellationToken) + private Episode FetchEpisodeData(EpisodeId id, string seriesDataPath, CancellationToken cancellationToken) { - var status = ProviderRefreshStatus.Success; - - if (episode.IndexNumber == null) + if (id.IndexNumber == null) { - return status; + return null; } - var episodeNumber = episode.IndexNumber.Value; - var seasonNumber = episode.ParentIndexNumber; + var episodeNumber = id.IndexNumber.Value; + var seasonNumber = id.ParentIndexNumber; if (seasonNumber == null) { - return status; + return null; } var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); var success = false; var usingAbsoluteData = false; - + var episode = new Episode(); try { - status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); + FetchMainEpisodeInfo(episode, file, cancellationToken); success = true; } @@ -318,11 +205,11 @@ namespace MediaBrowser.Providers.TV { file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); - status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); + FetchMainEpisodeInfo(episode, file, cancellationToken); usingAbsoluteData = true; } - var end = episode.IndexNumberEnd ?? episodeNumber; + var end = id.IndexNumberEnd ?? episodeNumber; episodeNumber++; while (episodeNumber <= end) @@ -348,12 +235,12 @@ namespace MediaBrowser.Providers.TV episodeNumber++; } - return status; + return success ? episode : null; } private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private async Task<ProviderRefreshStatus> FetchMainEpisodeInfo(Episode item, string xmlFile, CancellationToken cancellationToken) + private void FetchMainEpisodeInfo(Episode item, string xmlFile, CancellationToken cancellationToken) { var status = ProviderRefreshStatus.Success; @@ -506,7 +393,7 @@ namespace MediaBrowser.Providers.TV item.AirsBeforeSeasonNumber = rval; } } - + break; } @@ -534,7 +421,7 @@ namespace MediaBrowser.Providers.TV { var url = TVUtils.BannerUrl + val; - await _providerManager.SaveImage(item, url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); + //await _providerManager.SaveImage(item, url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); } catch (HttpException) { @@ -661,8 +548,6 @@ namespace MediaBrowser.Providers.TV } } } - - return status; } private void AddPeople(BaseItem item, string val, string personType) @@ -802,15 +687,5 @@ namespace MediaBrowser.Providers.TV } } } - - /// <summary> - /// Determines whether [has local meta] [the specified episode]. - /// </summary> - /// <param name="episode">The episode.</param> - /// <returns><c>true</c> if [has local meta] [the specified episode]; otherwise, <c>false</c>.</returns> - private bool HasLocalMeta(BaseItem episode) - { - return (episode.Parent.ResolveArgs.ContainsMetaFileByName(Path.GetFileNameWithoutExtension(episode.Path) + ".xml")); - } } } diff --git a/MediaBrowser.Providers/Users/UserMetadataService.cs b/MediaBrowser.Providers/Users/UserMetadataService.cs index 01c27c754..9f0b77dca 100644 --- a/MediaBrowser.Providers/Users/UserMetadataService.cs +++ b/MediaBrowser.Providers/Users/UserMetadataService.cs @@ -1,10 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Providers.Manager; |
