diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index d7b59c920e..c4981a7faa 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -66,7 +66,7 @@ namespace MediaBrowser.Api.Images [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } } - + /// /// Class UpdateItemImageIndex /// @@ -799,7 +799,12 @@ namespace MediaBrowser.Api.Images await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, null, CancellationToken.None).ConfigureAwait(false); - await entity.RefreshMetadata(CancellationToken.None, forceRefresh: true, forceSave: true, allowSlowProviders: false).ConfigureAwait(false); + await entity.RefreshMetadata(new MetadataRefreshOptions + { + ImageRefreshMode = MetadataRefreshMode.None, + ForceSave = true + + }, CancellationToken.None).ConfigureAwait(false); } } } diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs index c18954e500..b84a8f4f74 100644 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ b/MediaBrowser.Api/Images/RemoteImageService.cs @@ -9,13 +9,13 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using ServiceStack; +using ServiceStack.Text.Controller; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using ServiceStack.Text.Controller; namespace MediaBrowser.Api.Images { @@ -193,12 +193,7 @@ namespace MediaBrowser.Api.Images private List GetImageProviders(BaseItem item) { - return _providerManager.GetImageProviders(item).Select(i => new ImageProviderInfo - { - Name = i.Name, - Priority = i.Priority - - }).ToList(); + return _providerManager.GetImageProviderInfo(item).ToList(); } public object Get(GetRemoteImages request) @@ -229,7 +224,9 @@ namespace MediaBrowser.Api.Images var result = new RemoteImageResult { TotalRecordCount = imagesList.Count, - Providers = _providerManager.GetImageProviders(item).Select(i => i.Name).ToList() + Providers = images.Select(i => i.ProviderName) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList() }; if (request.StartIndex.HasValue) @@ -284,8 +281,13 @@ namespace MediaBrowser.Api.Images { await _providerManager.SaveImage(item, request.ImageUrl, null, request.Type, null, CancellationToken.None).ConfigureAwait(false); - await item.RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false) - .ConfigureAwait(false); + await item.RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = true, + ImageRefreshMode = MetadataRefreshMode.None, + MetadataRefreshMode = MetadataRefreshMode.None + + }, CancellationToken.None).ConfigureAwait(false); } /// diff --git a/MediaBrowser.Api/ItemRefreshService.cs b/MediaBrowser.Api/ItemRefreshService.cs index 1b8b49f98a..a0055f4e6b 100644 --- a/MediaBrowser.Api/ItemRefreshService.cs +++ b/MediaBrowser.Api/ItemRefreshService.cs @@ -2,6 +2,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using ServiceStack; using System; using System.Linq; @@ -131,7 +132,11 @@ namespace MediaBrowser.Api try { - await item.RefreshMetadata(cancellationToken, forceRefresh: request.Forced).ConfigureAwait(false); + await item.RefreshMetadata(new MetadataRefreshOptions + { + ReplaceAllMetadata = request.Forced, + + }, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -152,7 +157,11 @@ namespace MediaBrowser.Api try { - await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false); + await item.RefreshMetadata(new MetadataRefreshOptions + { + ReplaceAllMetadata = request.Forced, + + }, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -173,7 +182,11 @@ namespace MediaBrowser.Api try { - await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false); + await item.RefreshMetadata(new MetadataRefreshOptions + { + ReplaceAllMetadata = request.Forced, + + }, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -194,7 +207,11 @@ namespace MediaBrowser.Api try { - await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false); + await item.RefreshMetadata(new MetadataRefreshOptions + { + ReplaceAllMetadata = request.Forced, + + }, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -215,7 +232,11 @@ namespace MediaBrowser.Api try { - await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false); + await item.RefreshMetadata(new MetadataRefreshOptions + { + ReplaceAllMetadata = request.Forced, + + }, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -236,7 +257,11 @@ namespace MediaBrowser.Api try { - await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false); + await item.RefreshMetadata(new MetadataRefreshOptions + { + ReplaceAllMetadata = request.Forced, + + }, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -266,7 +291,11 @@ namespace MediaBrowser.Api try { - await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false); + await item.RefreshMetadata(new MetadataRefreshOptions + { + ReplaceAllMetadata = request.Forced, + + }, CancellationToken.None).ConfigureAwait(false); if (item.IsFolder) { @@ -301,7 +330,11 @@ namespace MediaBrowser.Api { foreach (var child in collectionFolder.Children.ToList()) { - await child.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false); + await child.RefreshMetadata(new MetadataRefreshOptions + { + ReplaceAllMetadata = request.Forced, + + }, CancellationToken.None).ConfigureAwait(false); if (child.IsFolder) { diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index 7759073798..8ea472da30 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -167,6 +167,17 @@ namespace MediaBrowser.Api.Library public bool RefreshLibrary { get; set; } } + [Route("/Library/Changes/Path", "POST")] + public class ReportChangedPath : IReturnVoid + { + /// + /// Gets or sets the name. + /// + /// The name. + [ApiMember(Name = "Path", Description = "The path that was changed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string Path { get; set; } + } + /// /// Class LibraryStructureService /// @@ -187,7 +198,7 @@ namespace MediaBrowser.Api.Library /// private readonly ILibraryManager _libraryManager; - private readonly IDirectoryWatchers _directoryWatchers; + private readonly ILibraryMonitor _libraryMonitor; private readonly IFileSystem _fileSystem; private readonly ILogger _logger; @@ -199,7 +210,7 @@ namespace MediaBrowser.Api.Library /// The user manager. /// The library manager. /// appPaths - public LibraryStructureService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IDirectoryWatchers directoryWatchers, IFileSystem fileSystem, ILogger logger) + public LibraryStructureService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IFileSystem fileSystem, ILogger logger) { if (appPaths == null) { @@ -209,11 +220,26 @@ namespace MediaBrowser.Api.Library _userManager = userManager; _appPaths = appPaths; _libraryManager = libraryManager; - _directoryWatchers = directoryWatchers; + _libraryMonitor = libraryMonitor; _fileSystem = fileSystem; _logger = logger; } + /// + /// Posts the specified request. + /// + /// The request. + /// Please supply a Path + public void Post(ReportChangedPath request) + { + if (string.IsNullOrEmpty(request.Path)) + { + throw new ArgumentException("Please supply a Path"); + } + + _libraryMonitor.ReportFileSystemChanged(request.Path); + } + /// /// Gets the specified request. /// @@ -270,8 +296,7 @@ namespace MediaBrowser.Api.Library throw new ArgumentException("There is already a media collection with the name " + name + "."); } - _directoryWatchers.Stop(); - _directoryWatchers.TemporarilyIgnore(virtualFolderPath); + _libraryMonitor.Stop(); try { @@ -294,10 +319,8 @@ namespace MediaBrowser.Api.Library // No need to start if scanning the library because it will handle it if (!request.RefreshLibrary) { - _directoryWatchers.Start(); + _libraryMonitor.Start(); } - - _directoryWatchers.RemoveTempIgnore(virtualFolderPath); } if (request.RefreshLibrary) @@ -348,9 +371,7 @@ namespace MediaBrowser.Api.Library throw new ArgumentException("There is already a media collection with the name " + newPath + "."); } - _directoryWatchers.Stop(); - _directoryWatchers.TemporarilyIgnore(currentPath); - _directoryWatchers.TemporarilyIgnore(newPath); + _libraryMonitor.Stop(); try { @@ -376,11 +397,8 @@ namespace MediaBrowser.Api.Library // No need to start if scanning the library because it will handle it if (!request.RefreshLibrary) { - _directoryWatchers.Start(); + _libraryMonitor.Start(); } - - _directoryWatchers.RemoveTempIgnore(currentPath); - _directoryWatchers.RemoveTempIgnore(newPath); } if (request.RefreshLibrary) @@ -420,8 +438,7 @@ namespace MediaBrowser.Api.Library throw new DirectoryNotFoundException("The media folder does not exist"); } - _directoryWatchers.Stop(); - _directoryWatchers.TemporarilyIgnore(path); + _libraryMonitor.Stop(); try { @@ -437,10 +454,8 @@ namespace MediaBrowser.Api.Library // No need to start if scanning the library because it will handle it if (!request.RefreshLibrary) { - _directoryWatchers.Start(); + _libraryMonitor.Start(); } - - _directoryWatchers.RemoveTempIgnore(path); } if (request.RefreshLibrary) @@ -460,7 +475,7 @@ namespace MediaBrowser.Api.Library throw new ArgumentNullException("request"); } - _directoryWatchers.Stop(); + _libraryMonitor.Stop(); try { @@ -485,7 +500,7 @@ namespace MediaBrowser.Api.Library // No need to start if scanning the library because it will handle it if (!request.RefreshLibrary) { - _directoryWatchers.Start(); + _libraryMonitor.Start(); } } @@ -506,7 +521,7 @@ namespace MediaBrowser.Api.Library throw new ArgumentNullException("request"); } - _directoryWatchers.Stop(); + _libraryMonitor.Stop(); try { @@ -531,7 +546,7 @@ namespace MediaBrowser.Api.Library // No need to start if scanning the library because it will handle it if (!request.RefreshLibrary) { - _directoryWatchers.Start(); + _libraryMonitor.Start(); } } diff --git a/MediaBrowser.Controller/Drawing/ImageExtensions.cs b/MediaBrowser.Controller/Drawing/ImageExtensions.cs index 9dc58b3d2f..c7e1968e7b 100644 --- a/MediaBrowser.Controller/Drawing/ImageExtensions.cs +++ b/MediaBrowser.Controller/Drawing/ImageExtensions.cs @@ -18,17 +18,17 @@ namespace MediaBrowser.Controller.Drawing /// The image. /// To stream. /// The quality. - public static void Save(this Image image, ImageFormat outputFormat, Stream toStream, int quality) + public static void Save(this Image image, System.Drawing.Imaging.ImageFormat outputFormat, Stream toStream, int quality) { // Use special save methods for jpeg and png that will result in a much higher quality image // All other formats use the generic Image.Save - if (ImageFormat.Jpeg.Equals(outputFormat)) + if (System.Drawing.Imaging.ImageFormat.Jpeg.Equals(outputFormat)) { SaveAsJpeg(image, toStream, quality); } - else if (ImageFormat.Png.Equals(outputFormat)) + else if (System.Drawing.Imaging.ImageFormat.Png.Equals(outputFormat)) { - image.Save(toStream, ImageFormat.Png); + image.Save(toStream, System.Drawing.Imaging.ImageFormat.Png); } else { diff --git a/MediaBrowser.Controller/Drawing/ImageFormat.cs b/MediaBrowser.Controller/Drawing/ImageFormat.cs new file mode 100644 index 0000000000..f785625567 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/ImageFormat.cs @@ -0,0 +1,11 @@ + +namespace MediaBrowser.Controller.Drawing +{ + public enum ImageFormat + { + Jpg, + Png, + Gif, + Bmp + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 7840fb3f07..06ebe89055 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Entities /// /// Class BaseItem /// - public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData + public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData, IHasMetadata { protected BaseItem() { @@ -364,11 +364,16 @@ namespace MediaBrowser.Controller.Entities } } + private string _forcedSortName; /// /// Gets or sets the name of the forced sort. /// /// The name of the forced sort. - public string ForcedSortName { get; set; } + public string ForcedSortName + { + get { return _forcedSortName; } + set { _forcedSortName = value; _sortName = null; } + } private string _sortName; /// @@ -767,25 +772,35 @@ namespace MediaBrowser.Controller.Entities }).ToList(); } + public Task RefreshMetadata(CancellationToken cancellationToken, bool resetResolveArgs = true) + { + return RefreshMetadata(new MetadataRefreshOptions { ResetResolveArgs = resetResolveArgs }, cancellationToken); + } + /// /// Overrides the base implementation to refresh metadata for local trailers /// + /// The options. /// The cancellation token. - /// if set to true [is new item]. - /// if set to true [force]. - /// if set to true [allow slow providers]. - /// if set to true [reset resolve args]. /// true if a provider reports we changed - public virtual async Task RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true) + public async Task RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken) { - if (resetResolveArgs) + if (options.ResetResolveArgs) { // Reload this ResetResolveArgs(); } + await ProviderManager.RefreshMetadata(this, options, cancellationToken).ConfigureAwait(false); + + return false; + } + + [Obsolete] + public virtual async Task RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false) + { // Refresh for the item - var itemRefreshTask = ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders); + var itemRefreshTask = ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh); cancellationToken.ThrowIfCancellationRequested(); @@ -800,15 +815,15 @@ namespace MediaBrowser.Controller.Entities var hasThemeMedia = this as IHasThemeMedia; if (hasThemeMedia != null) { - themeSongsChanged = await RefreshThemeSongs(hasThemeMedia, cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false); + themeSongsChanged = await RefreshThemeSongs(hasThemeMedia, cancellationToken, forceSave, forceRefresh).ConfigureAwait(false); - themeVideosChanged = await RefreshThemeVideos(hasThemeMedia, cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false); + themeVideosChanged = await RefreshThemeVideos(hasThemeMedia, cancellationToken, forceSave, forceRefresh).ConfigureAwait(false); } var hasTrailers = this as IHasTrailers; if (hasTrailers != null) { - localTrailersChanged = await RefreshLocalTrailers(hasTrailers, cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false); + localTrailersChanged = await RefreshLocalTrailers(hasTrailers, cancellationToken, forceSave, forceRefresh).ConfigureAwait(false); } } @@ -829,14 +844,20 @@ namespace MediaBrowser.Controller.Entities return changed; } - private async Task RefreshLocalTrailers(IHasTrailers item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true) + private async Task RefreshLocalTrailers(IHasTrailers item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false) { var newItems = LoadLocalTrailers().ToList(); var newItemIds = newItems.Select(i => i.Id).ToList(); var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds); - var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false)); + var tasks = newItems.Select(i => i.RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = forceSave, + ReplaceAllMetadata = forceRefresh, + ResetResolveArgs = false + + }, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -845,14 +866,20 @@ namespace MediaBrowser.Controller.Entities return itemsChanged || results.Contains(true); } - private async Task RefreshThemeVideos(IHasThemeMedia item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true) + private async Task RefreshThemeVideos(IHasThemeMedia item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false) { var newThemeVideos = LoadThemeVideos().ToList(); var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToList(); var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds); - var tasks = newThemeVideos.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false)); + var tasks = newThemeVideos.Select(i => i.RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = forceSave, + ReplaceAllMetadata = forceRefresh, + ResetResolveArgs = false + + }, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -864,14 +891,20 @@ namespace MediaBrowser.Controller.Entities /// /// Refreshes the theme songs. /// - private async Task RefreshThemeSongs(IHasThemeMedia item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true) + private async Task RefreshThemeSongs(IHasThemeMedia item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false) { var newThemeSongs = LoadThemeSongs().ToList(); var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToList(); var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds); - var tasks = newThemeSongs.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false)); + var tasks = newThemeSongs.Select(i => i.RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = forceSave, + ReplaceAllMetadata = forceRefresh, + ResetResolveArgs = false + + }, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -1456,7 +1489,13 @@ namespace MediaBrowser.Controller.Entities // Refresh metadata // Need to disable slow providers or the image might get re-downloaded - return RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false); + return RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = true, + ImageRefreshMode = MetadataRefreshMode.None, + MetadataRefreshMode = MetadataRefreshMode.None + + }, CancellationToken.None); } /// @@ -1482,8 +1521,10 @@ namespace MediaBrowser.Controller.Entities /// /// Validates that images within the item are still on the file system /// - public void ValidateImages() + public bool ValidateImages() { + var changed = false; + // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below var deletedKeys = Images .Where(image => !File.Exists(image.Value)) @@ -1494,14 +1535,28 @@ namespace MediaBrowser.Controller.Entities foreach (var key in deletedKeys) { Images.Remove(key); + changed = true; } + + if (ValidateBackdrops()) + { + changed = true; + } + if (ValidateScreenshots()) + { + changed = true; + } + + return changed; } /// /// Validates that backdrops within the item are still on the file system /// - public void ValidateBackdrops() + private bool ValidateBackdrops() { + var changed = false; + // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below var deletedImages = BackdropImagePaths .Where(path => !File.Exists(path)) @@ -1513,7 +1568,11 @@ namespace MediaBrowser.Controller.Entities BackdropImagePaths.Remove(path); RemoveImageSourceForPath(path); + + changed = true; } + + return changed; } /// @@ -1593,9 +1652,16 @@ namespace MediaBrowser.Controller.Entities /// /// Validates the screenshots. /// - public void ValidateScreenshots() + private bool ValidateScreenshots() { - var hasScreenshots = (IHasScreenshots)this; + var changed = false; + + var hasScreenshots = this as IHasScreenshots; + + if (hasScreenshots == null) + { + return changed; + } // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below var deletedImages = hasScreenshots.ScreenshotImagePaths @@ -1606,7 +1672,10 @@ namespace MediaBrowser.Controller.Entities foreach (var path in deletedImages) { hasScreenshots.ScreenshotImagePaths.Remove(path); + changed = true; } + + return changed; } /// @@ -1699,7 +1768,12 @@ namespace MediaBrowser.Controller.Entities FileSystem.SwapFiles(file1, file2); // Directory watchers should repeat this, but do a quick refresh first - return RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false); + return RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = true, + MetadataRefreshMode = MetadataRefreshMode.None + + }, CancellationToken.None); } public virtual bool IsPlayed(User user) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index a0fefeac77..a4257b2a5b 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -3,6 +3,7 @@ using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using MoreLinq; @@ -535,7 +536,13 @@ namespace MediaBrowser.Controller.Entities try { //refresh it - await child.RefreshMetadata(cancellationToken, forceSave: currentTuple.Item2, forceRefresh: forceRefreshMetadata, resetResolveArgs: false).ConfigureAwait(false); + await child.RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = currentTuple.Item2, + ReplaceAllMetadata = forceRefreshMetadata, + ResetResolveArgs = false + + }, cancellationToken).ConfigureAwait(false); } catch (IOException ex) { @@ -907,9 +914,9 @@ namespace MediaBrowser.Controller.Entities return item; } - public override async Task RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true) + public override async Task RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false) { - var changed = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false); + var changed = await base.RefreshMetadataDirect(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false); return (SupportsShortcutChildren && LocationType == LocationType.FileSystem && RefreshLinkedChildren()) || changed; } diff --git a/MediaBrowser.Controller/Entities/IHasImages.cs b/MediaBrowser.Controller/Entities/IHasImages.cs index a7cd76a66a..784337e3bb 100644 --- a/MediaBrowser.Controller/Entities/IHasImages.cs +++ b/MediaBrowser.Controller/Entities/IHasImages.cs @@ -1,5 +1,6 @@ using MediaBrowser.Model.Entities; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace MediaBrowser.Controller.Entities @@ -10,7 +11,7 @@ namespace MediaBrowser.Controller.Entities /// Gets the name. /// /// The name. - string Name { get; } + string Name { get; set; } /// /// Gets the path. @@ -24,6 +25,12 @@ namespace MediaBrowser.Controller.Entities /// The identifier. Guid Id { get; } + /// + /// Gets the type of the location. + /// + /// The type of the location. + LocationType LocationType { get; } + /// /// Gets the image path. /// @@ -81,6 +88,24 @@ namespace MediaBrowser.Controller.Entities /// /// System.String. string GetPreferredMetadataLanguage(); + + /// + /// Validates the images and returns true or false indicating if any were removed. + /// + bool ValidateImages(); + + /// + /// Gets or sets the backdrop image paths. + /// + /// The backdrop image paths. + List BackdropImagePaths { get; set; } + + /// + /// Determines whether [contains image with source URL] [the specified URL]. + /// + /// The URL. + /// true if [contains image with source URL] [the specified URL]; otherwise, false. + bool ContainsImageWithSourceUrl(string url); } public static class HasImagesExtensions diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs index 2276c707a7..70d154a958 100644 --- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs +++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs @@ -14,8 +14,10 @@ namespace MediaBrowser.Controller.Entities List ScreenshotImagePaths { get; set; } /// - /// Validates the screenshots. + /// Determines whether [contains image with source URL] [the specified URL]. /// - void ValidateScreenshots(); + /// The URL. + /// true if [contains image with source URL] [the specified URL]; otherwise, false. + bool ContainsImageWithSourceUrl(string url); } } diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 2b252a6c21..dbbe5ce018 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; @@ -108,13 +109,11 @@ namespace MediaBrowser.Controller.Entities.Movies /// The cancellation token. /// if set to true [is new item]. /// if set to true [force]. - /// if set to true [allow slow providers]. - /// The reset resolve args. /// Task{System.Boolean}. - public override async Task RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true) + public override async Task RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false) { // Kick off a task to refresh the main item - var result = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false); + var result = await base.RefreshMetadataDirect(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false); var specialFeaturesChanged = false; @@ -122,7 +121,7 @@ namespace MediaBrowser.Controller.Entities.Movies // In other words, it must be part of the Parent/Child tree if (LocationType == LocationType.FileSystem && Parent != null && !IsInMixedFolder) { - specialFeaturesChanged = await RefreshSpecialFeatures(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false); + specialFeaturesChanged = await RefreshSpecialFeatures(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false); } return specialFeaturesChanged || result; @@ -135,7 +134,13 @@ namespace MediaBrowser.Controller.Entities.Movies var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds); - var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false)); + var tasks = newItems.Select(i => i.RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = forceSave, + ReplaceAllMetadata = forceRefresh, + ResetResolveArgs = false + + }, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index 466e709dd0..c109e1d0cd 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -1,5 +1,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; using System; @@ -212,7 +213,12 @@ namespace MediaBrowser.Controller.Entities // Kick off a task to validate the media library Task.Run(() => ValidateMediaLibrary(new Progress(), CancellationToken.None)); - return RefreshMetadata(CancellationToken.None, forceSave: true, forceRefresh: true); + return RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = true, + ReplaceAllMetadata = true + + }, CancellationToken.None); } /// @@ -275,17 +281,13 @@ namespace MediaBrowser.Controller.Entities /// The cancellation token. /// if set to true [is new item]. /// if set to true [force]. - /// if set to true [allow slow providers]. /// true if a provider reports we changed - public override async Task RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true) + public override async Task RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false) { - if (resetResolveArgs) - { - // Reload this - ResetResolveArgs(); - } + // Reload this + ResetResolveArgs(); - var updateReason = await ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders).ConfigureAwait(false); + var updateReason = await ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh).ConfigureAwait(false); var changed = updateReason.HasValue; diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index e663c83538..9c94667669 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using System; @@ -164,13 +165,11 @@ namespace MediaBrowser.Controller.Entities /// The cancellation token. /// if set to true [is new item]. /// if set to true [force]. - /// if set to true [allow slow providers]. - /// The reset resolve args. /// true if a provider reports we changed - public override async Task RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true) + public override async Task RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false) { // Kick off a task to refresh the main item - var result = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false); + var result = await base.RefreshMetadataDirect(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false); var additionalPartsChanged = false; @@ -181,7 +180,7 @@ namespace MediaBrowser.Controller.Entities { try { - additionalPartsChanged = await RefreshAdditionalParts(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false); + additionalPartsChanged = await RefreshAdditionalParts(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false); } catch (IOException ex) { @@ -208,7 +207,12 @@ namespace MediaBrowser.Controller.Entities var itemsChanged = !AdditionalPartIds.SequenceEqual(newItemIds); - var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders)); + var tasks = newItems.Select(i => i.RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = forceSave, + ReplaceAllMetadata = forceRefresh + + }, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); diff --git a/MediaBrowser.Controller/IO/IDirectoryWatchers.cs b/MediaBrowser.Controller/IO/IDirectoryWatchers.cs deleted file mode 100644 index 9a43ee8acf..0000000000 --- a/MediaBrowser.Controller/IO/IDirectoryWatchers.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace MediaBrowser.Controller.IO -{ - public interface IDirectoryWatchers : IDisposable - { - /// - /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. - /// - /// The path. - void TemporarilyIgnore(string path); - - /// - /// Removes the temp ignore. - /// - /// The path. - void RemoveTempIgnore(string path); - - /// - /// Starts this instance. - /// - void Start(); - - /// - /// Stops this instance. - /// - void Stop(); - } -} \ No newline at end of file diff --git a/MediaBrowser.Controller/Library/ILibraryMonitor.cs b/MediaBrowser.Controller/Library/ILibraryMonitor.cs new file mode 100644 index 0000000000..918382f049 --- /dev/null +++ b/MediaBrowser.Controller/Library/ILibraryMonitor.cs @@ -0,0 +1,36 @@ +using System; + +namespace MediaBrowser.Controller.Library +{ + public interface ILibraryMonitor : IDisposable + { + /// + /// Starts this instance. + /// + void Start(); + + /// + /// Stops this instance. + /// + void Stop(); + + /// + /// Reports the file system change beginning. + /// + /// The path. + void ReportFileSystemChangeBeginning(string path); + + /// + /// Reports the file system change complete. + /// + /// The path. + /// if set to true [refresh path]. + void ReportFileSystemChangeComplete(string path, bool refreshPath); + + /// + /// Reports the file system changed. + /// + /// The path. + void ReportFileSystemChanged(string path); + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs b/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs index d9bceb6cad..c94a25a304 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using System.Threading; using System.Threading.Tasks; @@ -11,8 +12,6 @@ namespace MediaBrowser.Controller.LiveTv string MediaType { get; } - LocationType LocationType { get; } - RecordingInfo RecordingInfo { get; set; } string GetClientTypeName(); @@ -21,6 +20,6 @@ namespace MediaBrowser.Controller.LiveTv bool IsParentalAllowed(User user); - Task RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true); + Task RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/LiveTv/StreamResponseInfo.cs b/MediaBrowser.Controller/LiveTv/StreamResponseInfo.cs index c3b438c5ee..b133874d09 100644 --- a/MediaBrowser.Controller/LiveTv/StreamResponseInfo.cs +++ b/MediaBrowser.Controller/LiveTv/StreamResponseInfo.cs @@ -1,4 +1,5 @@ -using System.IO; +using MediaBrowser.Controller.Drawing; +using System.IO; namespace MediaBrowser.Controller.LiveTv { @@ -14,6 +15,6 @@ namespace MediaBrowser.Controller.LiveTv /// Gets or sets the type of the MIME. /// /// The type of the MIME. - public string MimeType { get; set; } + public ImageFormat Format { get; set; } } } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 56ac695a2d..ee8bb27619 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -69,6 +69,7 @@ Properties\SharedVersion.cs + @@ -143,8 +144,17 @@ + + + + + + + + + @@ -174,7 +184,7 @@ - + diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 3affe48e74..3a5cb4e870 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; @@ -112,22 +111,6 @@ namespace MediaBrowser.Controller.Persistence /// The cancellation token. /// Task. Task SaveMediaStreams(Guid id, IEnumerable streams, CancellationToken cancellationToken); - - /// - /// Gets the provider history. - /// - /// The item identifier. - /// IEnumerable{BaseProviderInfo}. - IEnumerable GetProviderHistory(Guid itemId); - - /// - /// Saves the provider history. - /// - /// The identifier. - /// The history. - /// The cancellation token. - /// Task. - Task SaveProviderHistory(Guid id, IEnumerable history, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Providers/IHasMetadata.cs b/MediaBrowser.Controller/Providers/IHasMetadata.cs new file mode 100644 index 0000000000..33c5184b8b --- /dev/null +++ b/MediaBrowser.Controller/Providers/IHasMetadata.cs @@ -0,0 +1,31 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Providers +{ + /// + /// Interface IHasMetadata + /// + public interface IHasMetadata : IHasImages, IHasProviderIds + { + /// + /// Gets the preferred metadata country code. + /// + /// System.String. + string GetPreferredMetadataCountryCode(); + + /// + /// Gets the locked fields. + /// + /// The locked fields. + List LockedFields { get; } + + /// + /// Gets or sets the date last saved. + /// + /// The date last saved. + DateTime DateLastSaved { get; set; } + } +} diff --git a/MediaBrowser.Controller/Providers/IImageProvider.cs b/MediaBrowser.Controller/Providers/IImageProvider.cs index ccf1998445..61f5579f4b 100644 --- a/MediaBrowser.Controller/Providers/IImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IImageProvider.cs @@ -1,9 +1,4 @@ using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; namespace MediaBrowser.Controller.Providers { @@ -26,26 +21,9 @@ namespace MediaBrowser.Controller.Providers bool Supports(IHasImages item); /// - /// Gets the images. + /// Gets the order. /// - /// The item. - /// Type of the image. - /// The cancellation token. - /// Task{IEnumerable{RemoteImageInfo}}. - Task> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken); - - /// - /// Gets the images. - /// - /// The item. - /// The cancellation token. - /// Task{IEnumerable{RemoteImageInfo}}. - Task> GetAllImages(IHasImages item, CancellationToken cancellationToken); - - /// - /// Gets the priority. - /// - /// The priority. - int Priority { get; } + /// The order. + int Order { get; } } } diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs new file mode 100644 index 0000000000..5c3ebd9acf --- /dev/null +++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs @@ -0,0 +1,66 @@ +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + /// + /// This is just a marker interface + /// + public interface ILocalImageProvider : IImageProvider + { + } + + public interface IImageFileProvider : ILocalImageProvider + { + List GetImages(IHasImages item); + } + + public class LocalImageInfo + { + public string Path { get; set; } + public ImageType Type { get; set; } + } + + public interface IDynamicImageProvider : ILocalImageProvider + { + /// + /// Gets the supported images. + /// + /// The item. + /// IEnumerable{ImageType}. + IEnumerable GetSupportedImages(IHasImages item); + + /// + /// Gets the image. + /// + /// The item. + /// The type. + /// The cancellation token. + /// Task{DynamicImageResponse}. + Task GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken); + } + + public class DynamicImageInfo + { + public string ImageId { get; set; } + public ImageType Type { get; set; } + } + + public class DynamicImageResponse + { + public string Path { get; set; } + public Stream Stream { get; set; } + public ImageFormat Format { get; set; } + public bool HasImage { get; set; } + + public void SetFormatFromMimeType(string mimeType) + { + + } + } +} diff --git a/MediaBrowser.Controller/Providers/IMetadataProvider.cs b/MediaBrowser.Controller/Providers/IMetadataProvider.cs new file mode 100644 index 0000000000..843ba263bc --- /dev/null +++ b/MediaBrowser.Controller/Providers/IMetadataProvider.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + /// + /// Marker interface + /// + public interface IMetadataProvider + { + /// + /// Gets the name. + /// + /// The name. + string Name { get; } + } + + public interface IMetadataProvider : IMetadataProvider + where TItemType : IHasMetadata + { + } + + public interface ILocalMetadataProvider : IMetadataProvider + { + /// + /// Determines whether [has local metadata] [the specified item]. + /// + /// The item. + /// true if [has local metadata] [the specified item]; otherwise, false. + bool HasLocalMetadata(IHasMetadata item); + } + + public interface IRemoteMetadataProvider : IMetadataProvider + { + } + + public interface IRemoteMetadataProvider : IMetadataProvider, IRemoteMetadataProvider + where TItemType : IHasMetadata + { + Task> GetMetadata(ItemId id, CancellationToken cancellationToken); + } + + public interface ILocalMetadataProvider : IMetadataProvider, ILocalMetadataProvider + where TItemType : IHasMetadata + { + Task> GetMetadata(string path, CancellationToken cancellationToken); + } + + public interface IHasChangeMonitor + { + /// + /// Determines whether the specified item has changed. + /// + /// The item. + /// The date. + /// true if the specified item has changed; otherwise, false. + bool HasChanged(IHasMetadata item, DateTime date); + } + + public class MetadataResult + where T : IHasMetadata + { + public bool HasMetadata { get; set; } + public T Item { get; set; } + } + +} diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs new file mode 100644 index 0000000000..c6cc2b716e --- /dev/null +++ b/MediaBrowser.Controller/Providers/IMetadataService.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + public interface IMetadataService + { + /// + /// Adds the parts. + /// + /// The providers. + /// The image providers. + void AddParts(IEnumerable providers, IEnumerable imageProviders); + + /// + /// Determines whether this instance can refresh the specified item. + /// + /// The item. + /// true if this instance can refresh the specified item; otherwise, false. + bool CanRefresh(IHasMetadata item); + + /// + /// Refreshes the metadata. + /// + /// The item. + /// The options. + /// The cancellation token. + /// Task. + Task RefreshMetadata(IHasMetadata item, MetadataRefreshOptions options, CancellationToken cancellationToken); + + /// + /// Gets the order. + /// + /// The order. + int Order { get; } + } +} diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 728030ecc9..dc57552c44 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -14,15 +14,23 @@ namespace MediaBrowser.Controller.Providers /// public interface IProviderManager { + /// + /// Refreshes the metadata. + /// + /// The item. + /// The options. + /// The cancellation token. + /// Task. + Task RefreshMetadata(IHasMetadata item, MetadataRefreshOptions options, CancellationToken cancellationToken); + /// /// Executes the metadata providers. /// /// The item. /// The cancellation token. /// if set to true [force]. - /// if set to true [allow slow providers]. /// Task{System.Boolean}. - Task ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false, bool allowSlowProviders = true); + Task ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false); /// /// Saves the image. @@ -54,7 +62,9 @@ namespace MediaBrowser.Controller.Providers /// /// The providers. /// The image providers. - void AddParts(IEnumerable providers, IEnumerable imageProviders); + /// The metadata services. + /// The metadata providers. + void AddParts(IEnumerable providers, IEnumerable imageProviders, IEnumerable metadataServices, IEnumerable metadataProviders); /// /// Gets the available remote images. @@ -70,7 +80,7 @@ namespace MediaBrowser.Controller.Providers /// Gets the image providers. /// /// The item. - /// IEnumerable{IImageProvider}. - IEnumerable GetImageProviders(BaseItem item); + /// IEnumerable{ImageProviderInfo}. + IEnumerable GetImageProviderInfo(BaseItem item); } } \ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IProviderRepository.cs b/MediaBrowser.Controller/Providers/IProviderRepository.cs new file mode 100644 index 0000000000..1c0ad2cd73 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IProviderRepository.cs @@ -0,0 +1,48 @@ +using MediaBrowser.Controller.Persistence; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + public interface IProviderRepository : IRepository + { + /// + /// Gets the provider history. + /// + /// The item identifier. + /// IEnumerable{BaseProviderInfo}. + IEnumerable GetProviderHistory(Guid itemId); + + /// + /// Saves the provider history. + /// + /// The identifier. + /// The history. + /// The cancellation token. + /// Task. + Task SaveProviderHistory(Guid id, IEnumerable history, CancellationToken cancellationToken); + + /// + /// Gets the metadata status. + /// + /// The item identifier. + /// MetadataStatus. + MetadataStatus GetMetadataStatus(Guid itemId); + + /// + /// Saves the metadata status. + /// + /// The status. + /// The cancellation token. + /// Task. + Task SaveMetadataStatus(MetadataStatus status, CancellationToken cancellationToken); + + /// + /// Initializes this instance. + /// + /// Task. + Task Initialize(); + } +} diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs new file mode 100644 index 0000000000..23fda2bfa0 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs @@ -0,0 +1,48 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + /// + /// Interface IImageProvider + /// + public interface IRemoteImageProvider : IImageProvider + { + /// + /// Gets the supported images. + /// + /// The item. + /// IEnumerable{ImageType}. + IEnumerable GetSupportedImages(IHasImages item); + + /// + /// Gets the images. + /// + /// The item. + /// Type of the image. + /// The cancellation token. + /// Task{IEnumerable{RemoteImageInfo}}. + Task> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken); + + /// + /// Gets the images. + /// + /// The item. + /// The cancellation token. + /// Task{IEnumerable{RemoteImageInfo}}. + Task> GetAllImages(IHasImages item, CancellationToken cancellationToken); + + /// + /// Gets the image response. + /// + /// The URL. + /// The cancellation token. + /// Task{HttpResponseInfo}. + Task GetImageResponse(string url, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Providers/ItemId.cs b/MediaBrowser.Controller/Providers/ItemId.cs new file mode 100644 index 0000000000..1116eb8b5d --- /dev/null +++ b/MediaBrowser.Controller/Providers/ItemId.cs @@ -0,0 +1,35 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Providers +{ + public class ItemId : IHasProviderIds + { + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } + /// + /// Gets or sets the metadata language. + /// + /// The metadata language. + public string MetadataLanguage { get; set; } + /// + /// Gets or sets the metadata country code. + /// + /// The metadata country code. + public string MetadataCountryCode { get; set; } + /// + /// Gets or sets the provider ids. + /// + /// The provider ids. + public Dictionary ProviderIds { get; set; } + + public ItemId() + { + ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs new file mode 100644 index 0000000000..d6e8a3afea --- /dev/null +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -0,0 +1,49 @@ +using System; + +namespace MediaBrowser.Controller.Providers +{ + public class MetadataRefreshOptions : ImageRefreshOptions + { + /// + /// When paired with MetadataRefreshMode=FullRefresh, all existing data will be overwritten with new data from the providers. + /// + public bool ReplaceAllMetadata { get; set; } + + public MetadataRefreshMode MetadataRefreshMode { get; set; } + + /// + /// TODO: deprecate. Keeping this for now, for api compatibility + /// + [Obsolete] + public bool ForceSave { get; set; } + + /// + /// TODO: deprecate. Keeping this for now, for api compatibility + /// + [Obsolete] + public bool ResetResolveArgs { get; set; } + } + + public class ImageRefreshOptions + { + public MetadataRefreshMode ImageRefreshMode { get; set; } + } + + public enum MetadataRefreshMode + { + /// + /// Providers will be executed based on default rules + /// + EnsureMetadata, + + /// + /// No providers will be executed + /// + None, + + /// + /// All providers will be executed to search for new metadata + /// + FullRefresh + } +} diff --git a/MediaBrowser.Controller/Providers/MetadataStatus.cs b/MediaBrowser.Controller/Providers/MetadataStatus.cs new file mode 100644 index 0000000000..834d8ec35f --- /dev/null +++ b/MediaBrowser.Controller/Providers/MetadataStatus.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Common.Extensions; + +namespace MediaBrowser.Controller.Providers +{ + public class MetadataStatus + { + /// + /// Gets or sets the item identifier. + /// + /// The item identifier. + public Guid ItemId { get; set; } + + /// + /// Gets or sets the date last metadata refresh. + /// + /// The date last metadata refresh. + public DateTime? DateLastMetadataRefresh { get; set; } + + /// + /// Gets or sets the date last images refresh. + /// + /// The date last images refresh. + public DateTime? DateLastImagesRefresh { get; set; } + + /// + /// Gets or sets the last result. + /// + /// The last result. + public ProviderRefreshStatus LastStatus { get; set; } + + /// + /// Gets or sets the last result error message. + /// + /// The last result error message. + public string LastErrorMessage { get; set; } + + /// + /// Gets or sets the providers refreshed. + /// + /// The providers refreshed. + public List MetadataProvidersRefreshed { get; set; } + public List ImageProvidersRefreshed { get; set; } + + public void AddStatus(ProviderRefreshStatus status, string errorMessage) + { + if (LastStatus != status) + { + IsDirty = true; + } + + if (string.IsNullOrEmpty(LastErrorMessage)) + { + LastErrorMessage = errorMessage; + } + if (LastStatus == ProviderRefreshStatus.Success) + { + LastStatus = status; + } + } + + public MetadataStatus() + { + LastStatus = ProviderRefreshStatus.Success; + + MetadataProvidersRefreshed = new List(); + ImageProvidersRefreshed = new List(); + } + + public bool IsDirty { get; private set; } + + public void SetDateLastMetadataRefresh(DateTime date) + { + if (date != (DateLastMetadataRefresh ?? DateTime.MinValue)) + { + IsDirty = true; + } + + DateLastMetadataRefresh = date; + } + + public void SetDateLastImagesRefresh(DateTime date) + { + if (date != (DateLastImagesRefresh ?? DateTime.MinValue)) + { + IsDirty = true; + } + + DateLastImagesRefresh = date; + } + + public void AddImageProvidersRefreshed(List providerIds) + { + var count = ImageProvidersRefreshed.Count; + + providerIds.AddRange(ImageProvidersRefreshed); + + ImageProvidersRefreshed = providerIds.Distinct().ToList(); + + if (ImageProvidersRefreshed.Count != count) + { + IsDirty = true; + } + } + + public void AddMetadataProvidersRefreshed(List providerIds) + { + var count = MetadataProvidersRefreshed.Count; + + providerIds.AddRange(MetadataProvidersRefreshed); + + MetadataProvidersRefreshed = providerIds.Distinct().ToList(); + + if (MetadataProvidersRefreshed.Count != count) + { + IsDirty = true; + } + } + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index f017fdf16f..923c5ab748 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -165,7 +165,7 @@ namespace MediaBrowser.Model.Configuration /// different directories and files. /// /// The file watcher delay. - public int FileWatcherDelay { get; set; } + public int RealtimeWatcherDelay { get; set; } /// /// Gets or sets a value indicating whether [enable dashboard response caching]. @@ -250,7 +250,7 @@ namespace MediaBrowser.Model.Configuration MaxResumePct = 90; MinResumeDurationSeconds = Convert.ToInt32(TimeSpan.FromMinutes(5).TotalSeconds); - FileWatcherDelay = 8; + RealtimeWatcherDelay = 20; RecentItemDays = 10; diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index a8c486d844..1a90e75d9e 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -116,6 +116,12 @@ namespace MediaBrowser.Model.Dto /// The overview. public string Overview { get; set; } + /// + /// Gets or sets the name of the TMDB collection. + /// + /// The name of the TMDB collection. + public string TmdbCollectionName { get; set; } + /// /// Gets or sets the taglines. /// diff --git a/MediaBrowser.Model/Entities/IHasProviderIds.cs b/MediaBrowser.Model/Entities/IHasProviderIds.cs index 1c54455da6..efb75412f5 100644 --- a/MediaBrowser.Model/Entities/IHasProviderIds.cs +++ b/MediaBrowser.Model/Entities/IHasProviderIds.cs @@ -20,6 +20,17 @@ namespace MediaBrowser.Model.Entities /// public static class ProviderIdsExtensions { + /// + /// Determines whether [has provider identifier] [the specified instance]. + /// + /// The instance. + /// The provider. + /// true if [has provider identifier] [the specified instance]; otherwise, false. + public static bool HasProviderId(this IHasProviderIds instance, MetadataProviders provider) + { + return !string.IsNullOrEmpty(instance.GetProviderId(provider.ToString())); + } + /// /// Gets a provider id /// diff --git a/MediaBrowser.Model/Providers/ImageProviderInfo.cs b/MediaBrowser.Model/Providers/ImageProviderInfo.cs index 325aa90cb7..1b8a2816a5 100644 --- a/MediaBrowser.Model/Providers/ImageProviderInfo.cs +++ b/MediaBrowser.Model/Providers/ImageProviderInfo.cs @@ -12,9 +12,9 @@ public string Name { get; set; } /// - /// Gets or sets the priority. + /// Gets or sets the order. /// - /// The priority. - public int Priority { get; set; } + /// The order. + public int Order { get; set; } } } diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 6f73e73f7a..406957daaa 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -150,6 +150,11 @@ namespace MediaBrowser.Model.Querying /// The tags /// Tags, + + /// + /// The TMDB collection name + /// + TmdbCollectionName, /// /// The trailer url of the item diff --git a/MediaBrowser.Providers/All/LocalImageProvider.cs b/MediaBrowser.Providers/All/LocalImageProvider.cs new file mode 100644 index 0000000000..88a68bc155 --- /dev/null +++ b/MediaBrowser.Providers/All/LocalImageProvider.cs @@ -0,0 +1,327 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Providers.All +{ + public class LocalImageProvider : IImageFileProvider + { + private readonly IFileSystem _fileSystem; + + public LocalImageProvider(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public string Name + { + get { return "Local Images"; } + } + + public int Order + { + get { return 0; } + } + + public bool Supports(IHasImages item) + { + var locationType = item.LocationType; + + if (locationType == LocationType.FileSystem) + { + // Episode has it's own provider + if (item is Episode) + { + return false; + } + + return true; + } + if (locationType == LocationType.Virtual) + { + var season = item as Season; + + if (season != null) + { + var series = season.Series; + + if (series != null && series.LocationType == LocationType.FileSystem) + { + return true; + } + } + } + + return false; + } + + private IEnumerable GetFiles(IHasImages item, bool includeDirectories) + { + if (item.LocationType != LocationType.FileSystem) + { + return new List(); + } + + var path = item.Path; + var fileInfo = _fileSystem.GetFileSystemInfo(path) as DirectoryInfo; + + if (fileInfo == null) + { + path = Path.GetDirectoryName(path); + } + + if (includeDirectories) + { + return Directory.EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly); + } + return Directory.EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly); + } + + public List GetImages(IHasImages item) + { + var files = GetFileDictionary(GetFiles(item, true)); + + var list = new List(); + + PopulateImages(item, list, files); + + return list; + } + + private void PopulateImages(IHasImages item, List images, Dictionary files) + { + var imagePrefix = string.Empty; + + var baseItem = item as BaseItem; + if (baseItem != null && baseItem.IsInMixedFolder) + { + imagePrefix = Path.GetFileNameWithoutExtension(item.Path) + "-"; + } + + PopulatePrimaryImages(item, images, files, imagePrefix); + PopulateBackdrops(item, images, files, imagePrefix); + PopulateScreenshots(images, files, imagePrefix); + + AddImage(files, images, imagePrefix + "logo", ImageType.Logo); + AddImage(files, images, imagePrefix + "clearart", ImageType.Art); + AddImage(files, images, imagePrefix + "disc", ImageType.Disc); + AddImage(files, images, imagePrefix + "cdart", ImageType.Disc); + AddImage(files, images, imagePrefix + "box", ImageType.Box); + AddImage(files, images, imagePrefix + "back", ImageType.BoxRear); + AddImage(files, images, imagePrefix + "boxrear", ImageType.BoxRear); + AddImage(files, images, imagePrefix + "menu", ImageType.Menu); + + // Banner + AddImage(files, images, imagePrefix + "banner", ImageType.Banner); + + // Thumb + AddImage(files, images, imagePrefix + "thumb", ImageType.Thumb); + AddImage(files, images, imagePrefix + "landscape", ImageType.Thumb); + + var season = item as Season; + + if (season != null) + { + PopulateSeasonImagesFromSeriesFolder(season, images); + } + } + + private void PopulatePrimaryImages(IHasImages item, List images, Dictionary files, string imagePrefix) + { + AddImage(files, images, imagePrefix + "folder", ImageType.Primary); + AddImage(files, images, imagePrefix + "cover", ImageType.Primary); + AddImage(files, images, imagePrefix + "poster", ImageType.Primary); + AddImage(files, images, imagePrefix + "default", ImageType.Primary); + + // Support plex/xbmc convention + if (item is Series) + { + AddImage(files, images, imagePrefix + "show", ImageType.Primary); + } + + // Support plex/xbmc convention + if (item is Movie || item is MusicVideo || item is AdultVideo) + { + AddImage(files, images, imagePrefix + "movie", ImageType.Primary); + } + + if (string.IsNullOrEmpty(item.Path)) + { + var name = Path.GetFileNameWithoutExtension(item.Path); + + if (!string.IsNullOrEmpty(name)) + { + AddImage(files, images, name, ImageType.Primary); + AddImage(files, images, name + "-poster", ImageType.Primary); + } + } + } + + private void PopulateBackdrops(IHasImages item, List images, Dictionary files, string imagePrefix) + { + PopulateBackdrops(images, files, imagePrefix, "backdrop", "backdrop", ImageType.Backdrop); + + if (string.IsNullOrEmpty(item.Path)) + { + var name = Path.GetFileNameWithoutExtension(item.Path); + + if (!string.IsNullOrEmpty(name)) + { + AddImage(files, images, imagePrefix + name + "-fanart", ImageType.Backdrop); + } + } + + PopulateBackdrops(images, files, imagePrefix, "fanart", "fanart-", ImageType.Backdrop); + PopulateBackdrops(images, files, imagePrefix, "background", "background-", ImageType.Backdrop); + PopulateBackdrops(images, files, imagePrefix, "art", "art-", ImageType.Backdrop); + + string extraFanartFolder; + if (files.TryGetValue("extrafanart", out extraFanartFolder)) + { + PopulateBackdropsFromExtraFanart(extraFanartFolder, images); + } + } + + private void PopulateBackdropsFromExtraFanart(string path, List images) + { + var imageFiles = Directory.EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly) + .Where(i => + { + var extension = Path.GetExtension(i); + + if (string.IsNullOrEmpty(extension)) + { + return false; + } + + return BaseItem.SupportedImageExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + }); + + images.AddRange(imageFiles.Select(i => new LocalImageInfo + { + Path = i, + Type = ImageType.Backdrop + })); + } + + private void PopulateScreenshots(List images, Dictionary files, string imagePrefix) + { + PopulateBackdrops(images, files, imagePrefix, "screenshot", "screenshot", ImageType.Screenshot); + } + + private void PopulateBackdrops(List images, Dictionary files, string imagePrefix, string firstFileName, string subsequentFileNamePrefix, ImageType type) + { + AddImage(files, images, imagePrefix + firstFileName, type); + + var unfound = 0; + for (var i = 1; i <= 20; i++) + { + // Screenshot Image + var found = AddImage(files, images, imagePrefix + subsequentFileNamePrefix + i, type); + + if (!found) + { + unfound++; + + if (unfound >= 3) + { + break; + } + } + } + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private void PopulateSeasonImagesFromSeriesFolder(Season season, List images) + { + var seasonNumber = season.IndexNumber; + + var series = season.Series; + if (!seasonNumber.HasValue || series.LocationType != LocationType.FileSystem) + { + return; + } + + var files = GetFileDictionary(GetFiles(series, false)); + + // Try using the season name + var prefix = season.Name.ToLower().Replace(" ", string.Empty); + + var filenamePrefixes = new List { prefix }; + + var seasonMarker = seasonNumber.Value == 0 + ? "-specials" + : seasonNumber.Value.ToString("00", _usCulture); + + // Get this one directly from the file system since we have to go up a level + if (!string.Equals(prefix, seasonMarker, StringComparison.OrdinalIgnoreCase)) + { + filenamePrefixes.Add("season" + seasonMarker); + } + + foreach (var filename in filenamePrefixes) + { + AddImage(files, images, filename + "-poster", ImageType.Primary); + AddImage(files, images, filename + "-fanart", ImageType.Backdrop); + AddImage(files, images, filename + "-banner", ImageType.Banner); + AddImage(files, images, filename + "-landscape", ImageType.Thumb); + } + } + + private Dictionary GetFileDictionary(IEnumerable paths) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var path in paths) + { + var filename = Path.GetFileName(path); + + if (!string.IsNullOrEmpty(filename)) + { + dict[filename] = path; + } + } + + return dict; + } + + private bool AddImage(Dictionary dict, List images, string name, ImageType type) + { + var image = GetImage(dict, name); + + if (image != null) + { + images.Add(new LocalImageInfo + { + Path = image, + Type = type + }); + + return true; + } + + return false; + } + + private string GetImage(Dictionary dict, string name) + { + return BaseItem.SupportedImageExtensions + .Select(i => + { + var filename = name + i; + string path; + + return dict.TryGetValue(filename, out path) ? path : null; + }) + .FirstOrDefault(i => i != null); + } + } +} diff --git a/MediaBrowser.Providers/BaseXmlProvider.cs b/MediaBrowser.Providers/BaseXmlProvider.cs new file mode 100644 index 0000000000..eab5bb574e --- /dev/null +++ b/MediaBrowser.Providers/BaseXmlProvider.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Providers; +using System; +using System.IO; +using System.Threading; + +namespace MediaBrowser.Providers +{ + public abstract class BaseXmlProvider: IHasChangeMonitor + { + protected static readonly SemaphoreSlim XmlParsingResourcePool = new SemaphoreSlim(4, 4); + + protected IFileSystem FileSystem; + + protected BaseXmlProvider(IFileSystem fileSystem) + { + FileSystem = fileSystem; + } + + protected abstract string GetXmlPath(string path); + + public bool HasChanged(IHasMetadata item, DateTime date) + { + var path = GetXmlPath(item.Path); + + return FileSystem.GetLastWriteTimeUtc(path) > date; + } + + public bool HasLocalMetadata(IHasMetadata item) + { + return File.Exists(GetXmlPath(item.Path)); + } + } +} diff --git a/MediaBrowser.Providers/CollectionFolderImageProvider.cs b/MediaBrowser.Providers/CollectionFolderImageProvider.cs index 6c36dbf7e4..12f13262d1 100644 --- a/MediaBrowser.Providers/CollectionFolderImageProvider.cs +++ b/MediaBrowser.Providers/CollectionFolderImageProvider.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using MediaBrowser.Common.IO; +using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -7,6 +6,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; +using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/MediaBrowser.Providers/ImagesByName/GameGenresManualImageProvider.cs b/MediaBrowser.Providers/GameGenres/GameGenreImageProvider.cs similarity index 81% rename from MediaBrowser.Providers/ImagesByName/GameGenresManualImageProvider.cs rename to MediaBrowser.Providers/GameGenres/GameGenreImageProvider.cs index 8207bb0428..0dbf114505 100644 --- a/MediaBrowser.Providers/ImagesByName/GameGenresManualImageProvider.cs +++ b/MediaBrowser.Providers/GameGenres/GameGenreImageProvider.cs @@ -5,15 +5,17 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Genres; +using MediaBrowser.Providers.ImagesByName; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.Providers.ImagesByName +namespace MediaBrowser.Providers.GameGenres { - public class GameGenresManualImageProvider : IImageProvider + public class GameGenreImageProvider : IRemoteImageProvider { private readonly IServerConfigurationManager _config; private readonly IHttpClient _httpClient; @@ -21,7 +23,7 @@ namespace MediaBrowser.Providers.ImagesByName private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1); - public GameGenresManualImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) + public GameGenreImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) { _config = config; _httpClient = httpClient; @@ -43,6 +45,15 @@ namespace MediaBrowser.Providers.ImagesByName return item is GameGenre; } + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary, + ImageType.Thumb + }; + } + public Task> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken); @@ -120,9 +131,19 @@ namespace MediaBrowser.Providers.ImagesByName return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken); } - public int Priority + public int Order { get { return 0; } } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = GenreImageProvider.ImageDownloadResourcePool + }); + } } } diff --git a/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs b/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs new file mode 100644 index 0000000000..389e2a2755 --- /dev/null +++ b/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs @@ -0,0 +1,42 @@ +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.GameGenres +{ + public class GameGenreMetadataService : MetadataService + { + private readonly ILibraryManager _libraryManager; + + public GameGenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo) + { + _libraryManager = libraryManager; + } + + /// + /// Merges the specified source. + /// + /// The source. + /// The target. + /// The locked fields. + /// if set to true [replace data]. + /// if set to true [merge metadata settings]. + protected override void MergeData(GameGenre source, GameGenre target, List lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + } + + protected override Task SaveItem(GameGenre item, ItemUpdateType reason, CancellationToken cancellationToken) + { + return _libraryManager.UpdateItem(item, reason, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/ImagesByName/GenresManualImageProvider.cs b/MediaBrowser.Providers/Genres/GenreImageProvider.cs similarity index 81% rename from MediaBrowser.Providers/ImagesByName/GenresManualImageProvider.cs rename to MediaBrowser.Providers/Genres/GenreImageProvider.cs index 469e133d05..189cc8cde6 100644 --- a/MediaBrowser.Providers/ImagesByName/GenresManualImageProvider.cs +++ b/MediaBrowser.Providers/Genres/GenreImageProvider.cs @@ -5,15 +5,16 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.ImagesByName; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.Providers.ImagesByName +namespace MediaBrowser.Providers.Genres { - public class GenresManualImageProvider : IImageProvider + public class GenreImageProvider : IRemoteImageProvider { private readonly IServerConfigurationManager _config; private readonly IHttpClient _httpClient; @@ -21,7 +22,9 @@ namespace MediaBrowser.Providers.ImagesByName private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1); - public GenresManualImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) + public static SemaphoreSlim ImageDownloadResourcePool = new SemaphoreSlim(5, 5); + + public GenreImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) { _config = config; _httpClient = httpClient; @@ -43,6 +46,15 @@ namespace MediaBrowser.Providers.ImagesByName return item is Genre; } + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary, + ImageType.Thumb + }; + } + public Task> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken); @@ -120,9 +132,19 @@ namespace MediaBrowser.Providers.ImagesByName return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken); } - public int Priority + public int Order { get { return 0; } } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = ImageDownloadResourcePool + }); + } } } diff --git a/MediaBrowser.Providers/Genres/GenreMetadataService.cs b/MediaBrowser.Providers/Genres/GenreMetadataService.cs new file mode 100644 index 0000000000..83253a190f --- /dev/null +++ b/MediaBrowser.Providers/Genres/GenreMetadataService.cs @@ -0,0 +1,42 @@ +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.Genres +{ + public class GenreMetadataService : MetadataService + { + private readonly ILibraryManager _libraryManager; + + public GenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo) + { + _libraryManager = libraryManager; + } + + /// + /// Merges the specified source. + /// + /// The source. + /// The target. + /// The locked fields. + /// if set to true [replace data]. + /// if set to true [merge metadata settings]. + protected override void MergeData(Genre source, Genre target, List lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + } + + protected override Task SaveItem(Genre item, ItemUpdateType reason, CancellationToken cancellationToken) + { + return _libraryManager.UpdateItem(item, reason, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs b/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs index 2146d927b3..bc58f31789 100644 --- a/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs +++ b/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs @@ -145,17 +145,6 @@ namespace MediaBrowser.Providers cancellationToken.ThrowIfCancellationRequested(); - // Make sure current backdrop paths still exist - item.ValidateBackdrops(); - - var hasScreenshots = item as IHasScreenshots; - if (hasScreenshots != null) - { - hasScreenshots.ValidateScreenshots(); - } - - cancellationToken.ThrowIfCancellationRequested(); - var args = GetResolveArgsContainingImages(item); PopulateBaseItemImages(item, args); diff --git a/MediaBrowser.Providers/ImagesByName/GameGenreImageProvider.cs b/MediaBrowser.Providers/ImagesByName/GameGenreImageProvider.cs deleted file mode 100644 index a0dbb83dc4..0000000000 --- a/MediaBrowser.Providers/ImagesByName/GameGenreImageProvider.cs +++ /dev/null @@ -1,160 +0,0 @@ -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.Model.Net; -using MediaBrowser.Model.Providers; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.ImagesByName -{ - public class GameGenreImageProvider : BaseMetadataProvider - { - private readonly IProviderManager _providerManager; - private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(5, 5); - - public GameGenreImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) - : base(logManager, configurationManager) - { - _providerManager = providerManager; - } - - public override bool Supports(BaseItem item) - { - return item is GameGenre; - } - - public override bool RequiresInternet - { - get - { - return true; - } - } - - public override ItemUpdateType ItemUpdateType - { - get - { - return ItemUpdateType.ImageUpdate; - } - } - - protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) - { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) - { - return false; - } - - // Try again periodically in case new images were added - if ((DateTime.UtcNow - providerInfo.LastRefreshed).TotalDays > 7) - { - return true; - } - - return base.NeedsRefreshInternal(item, providerInfo); - } - - protected override bool RefreshOnVersionChange - { - get - { - return true; - } - } - - protected override string ProviderVersion - { - get - { - return "8"; - } - } - - public override async Task FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) - { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) - { - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, GameGenresManualImageProvider.ProviderName).ConfigureAwait(false); - - await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false); - - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - private async Task DownloadImages(BaseItem item, List images, CancellationToken cancellationToken) - { - if (!item.LockedFields.Contains(MetadataFields.Images)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (!item.HasImage(ImageType.Primary)) - { - await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); - } - cancellationToken.ThrowIfCancellationRequested(); - - if (!item.HasImage(ImageType.Thumb)) - { - await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false); - } - } - - if (!item.LockedFields.Contains(MetadataFields.Backdrops)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.BackdropImagePaths.Count == 0) - { - foreach (var image in images.Where(i => i.Type == ImageType.Backdrop)) - { - await _providerManager.SaveImage(item, image.Url, _resourcePool, ImageType.Backdrop, null, cancellationToken) - .ConfigureAwait(false); - - break; - } - } - } - } - - - private async Task SaveImage(BaseItem item, IEnumerable images, ImageType type, CancellationToken cancellationToken) - { - foreach (var image in images.Where(i => i.Type == type)) - { - try - { - await _providerManager.SaveImage(item, image.Url, _resourcePool, type, null, cancellationToken).ConfigureAwait(false); - break; - } - catch (HttpException ex) - { - // Sometimes fanart has bad url's in their xml - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - continue; - } - break; - } - } - } - - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.Third; } - } - } -} diff --git a/MediaBrowser.Providers/ImagesByName/GenreImageProvider.cs b/MediaBrowser.Providers/ImagesByName/GenreImageProvider.cs deleted file mode 100644 index 5744ef5fad..0000000000 --- a/MediaBrowser.Providers/ImagesByName/GenreImageProvider.cs +++ /dev/null @@ -1,160 +0,0 @@ -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.Model.Net; -using MediaBrowser.Model.Providers; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.ImagesByName -{ - public class GenreImageProvider : BaseMetadataProvider - { - private readonly IProviderManager _providerManager; - private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(5, 5); - - public GenreImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) - : base(logManager, configurationManager) - { - _providerManager = providerManager; - } - - public override bool Supports(BaseItem item) - { - return item is Genre; - } - - public override bool RequiresInternet - { - get - { - return true; - } - } - - public override ItemUpdateType ItemUpdateType - { - get - { - return ItemUpdateType.ImageUpdate; - } - } - - protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) - { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) - { - return false; - } - - // Try again periodically in case new images were added - if ((DateTime.UtcNow - providerInfo.LastRefreshed).TotalDays > 7) - { - return true; - } - - return base.NeedsRefreshInternal(item, providerInfo); - } - - protected override bool RefreshOnVersionChange - { - get - { - return true; - } - } - - protected override string ProviderVersion - { - get - { - return "8"; - } - } - - public override async Task FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) - { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) - { - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, GenresManualImageProvider.ProviderName).ConfigureAwait(false); - - await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false); - - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - private async Task DownloadImages(BaseItem item, List images, CancellationToken cancellationToken) - { - if (!item.LockedFields.Contains(MetadataFields.Images)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (!item.HasImage(ImageType.Primary)) - { - await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); - } - cancellationToken.ThrowIfCancellationRequested(); - - if (!item.HasImage(ImageType.Thumb)) - { - await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false); - } - } - - if (!item.LockedFields.Contains(MetadataFields.Backdrops)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.BackdropImagePaths.Count == 0) - { - foreach (var image in images.Where(i => i.Type == ImageType.Backdrop)) - { - await _providerManager.SaveImage(item, image.Url, _resourcePool, ImageType.Backdrop, null, cancellationToken) - .ConfigureAwait(false); - - break; - } - } - } - } - - - private async Task SaveImage(BaseItem item, IEnumerable images, ImageType type, CancellationToken cancellationToken) - { - foreach (var image in images.Where(i => i.Type == type)) - { - try - { - await _providerManager.SaveImage(item, image.Url, _resourcePool, type, null, cancellationToken).ConfigureAwait(false); - break; - } - catch (HttpException ex) - { - // Sometimes fanart has bad url's in their xml - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - continue; - } - break; - } - } - } - - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.Third; } - } - } -} diff --git a/MediaBrowser.Providers/ImagesByName/MusicGenreImageProvider.cs b/MediaBrowser.Providers/ImagesByName/MusicGenreImageProvider.cs deleted file mode 100644 index 5b05a7b631..0000000000 --- a/MediaBrowser.Providers/ImagesByName/MusicGenreImageProvider.cs +++ /dev/null @@ -1,161 +0,0 @@ -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.ImagesByName -{ - public class MusicGenreImageProvider : BaseMetadataProvider - { - private readonly IProviderManager _providerManager; - private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(5, 5); - - public MusicGenreImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) - : base(logManager, configurationManager) - { - _providerManager = providerManager; - } - - public override bool Supports(BaseItem item) - { - return item is MusicGenre; - } - - public override bool RequiresInternet - { - get - { - return true; - } - } - - public override ItemUpdateType ItemUpdateType - { - get - { - return ItemUpdateType.ImageUpdate; - } - } - - protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) - { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) - { - return false; - } - - // Try again periodically in case new images were added - if ((DateTime.UtcNow - providerInfo.LastRefreshed).TotalDays > 7) - { - return true; - } - - return base.NeedsRefreshInternal(item, providerInfo); - } - - protected override bool RefreshOnVersionChange - { - get - { - return true; - } - } - - protected override string ProviderVersion - { - get - { - return "8"; - } - } - - public override async Task FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) - { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) - { - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, MusicGenresManualImageProvider.ProviderName).ConfigureAwait(false); - - await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false); - - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - private async Task DownloadImages(BaseItem item, List images, CancellationToken cancellationToken) - { - if (!item.LockedFields.Contains(MetadataFields.Images)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (!item.HasImage(ImageType.Primary)) - { - await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); - } - cancellationToken.ThrowIfCancellationRequested(); - - if (!item.HasImage(ImageType.Thumb)) - { - await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false); - } - } - - if (!item.LockedFields.Contains(MetadataFields.Backdrops)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.BackdropImagePaths.Count == 0) - { - foreach (var image in images.Where(i => i.Type == ImageType.Backdrop)) - { - await _providerManager.SaveImage(item, image.Url, _resourcePool, ImageType.Backdrop, null, cancellationToken) - .ConfigureAwait(false); - - break; - } - } - } - } - - - private async Task SaveImage(BaseItem item, IEnumerable images, ImageType type, CancellationToken cancellationToken) - { - foreach (var image in images.Where(i => i.Type == type)) - { - try - { - await _providerManager.SaveImage(item, image.Url, _resourcePool, type, null, cancellationToken).ConfigureAwait(false); - break; - } - catch (HttpException ex) - { - // Sometimes fanart has bad url's in their xml - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - continue; - } - break; - } - } - } - - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.Third; } - } - } -} diff --git a/MediaBrowser.Providers/ImagesByName/StudioImageProvider.cs b/MediaBrowser.Providers/ImagesByName/StudioImageProvider.cs deleted file mode 100644 index 3035b60143..0000000000 --- a/MediaBrowser.Providers/ImagesByName/StudioImageProvider.cs +++ /dev/null @@ -1,160 +0,0 @@ -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.Model.Net; -using MediaBrowser.Model.Providers; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.ImagesByName -{ - public class StudioImageProvider : BaseMetadataProvider - { - private readonly IProviderManager _providerManager; - private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(5, 5); - - public StudioImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) - : base(logManager, configurationManager) - { - _providerManager = providerManager; - } - - public override bool Supports(BaseItem item) - { - return item is Studio; - } - - public override bool RequiresInternet - { - get - { - return true; - } - } - - public override ItemUpdateType ItemUpdateType - { - get - { - return ItemUpdateType.ImageUpdate; - } - } - - protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) - { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) - { - return false; - } - - // Try again periodically in case new images were added - if ((DateTime.UtcNow - providerInfo.LastRefreshed).TotalDays > 7) - { - return true; - } - - return base.NeedsRefreshInternal(item, providerInfo); - } - - protected override bool RefreshOnVersionChange - { - get - { - return true; - } - } - - protected override string ProviderVersion - { - get - { - return "6"; - } - } - - public override async Task FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) - { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) - { - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, StudiosManualImageProvider.ProviderName).ConfigureAwait(false); - - await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false); - - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - private async Task DownloadImages(BaseItem item, List images, CancellationToken cancellationToken) - { - if (!item.LockedFields.Contains(MetadataFields.Images)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (!item.HasImage(ImageType.Primary)) - { - await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); - } - cancellationToken.ThrowIfCancellationRequested(); - - if (!item.HasImage(ImageType.Thumb)) - { - await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false); - } - } - - if (!item.LockedFields.Contains(MetadataFields.Backdrops)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.BackdropImagePaths.Count == 0) - { - foreach (var image in images.Where(i => i.Type == ImageType.Backdrop)) - { - await _providerManager.SaveImage(item, image.Url, _resourcePool, ImageType.Backdrop, null, cancellationToken) - .ConfigureAwait(false); - - break; - } - } - } - } - - - private async Task SaveImage(BaseItem item, IEnumerable images, ImageType type, CancellationToken cancellationToken) - { - foreach (var image in images.Where(i => i.Type == type)) - { - try - { - await _providerManager.SaveImage(item, image.Url, _resourcePool, type, null, cancellationToken).ConfigureAwait(false); - break; - } - catch (HttpException ex) - { - // Sometimes fanart has bad url's in their xml - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - continue; - } - break; - } - } - } - - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.Third; } - } - } -} diff --git a/MediaBrowser.Providers/LiveTv/ChannelMetadataService.cs b/MediaBrowser.Providers/LiveTv/ChannelMetadataService.cs new file mode 100644 index 0000000000..dd44ba7aae --- /dev/null +++ b/MediaBrowser.Providers/LiveTv/ChannelMetadataService.cs @@ -0,0 +1,37 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +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.LiveTv +{ + public class ChannelMetadataService : MetadataService + { + private readonly ILibraryManager _libraryManager; + + public ChannelMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo) + { + _libraryManager = libraryManager; + } + + /// + /// Merges the specified source. + /// + protected override void MergeData(LiveTvChannel source, LiveTvChannel target, List lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + } + + protected override Task SaveItem(LiveTvChannel item, ItemUpdateType reason, CancellationToken cancellationToken) + { + return _libraryManager.UpdateItem(item, reason, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs b/MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs deleted file mode 100644 index 8ee2553d03..0000000000 --- a/MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs +++ /dev/null @@ -1,91 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Logging; -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.LiveTv -{ - class ChannelProviderFromXml : BaseMetadataProvider - { - private readonly IFileSystem _fileSystem; - - public ChannelProviderFromXml(ILogManager logManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem) - : base(logManager, configurationManager) - { - _fileSystem = fileSystem; - } - - /// - /// Supportses the specified item. - /// - /// The item. - /// true if XXXX, false otherwise - public override bool Supports(BaseItem item) - { - return item is LiveTvChannel; - } - - /// - /// Gets the priority. - /// - /// The priority. - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.Second; } - } - - private const string XmlFileName = "channel.xml"; - protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) - { - var xml = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, XmlFileName)); - - if (xml == null) - { - return false; - } - - return _fileSystem.GetLastWriteTimeUtc(xml) > item.DateLastSaved; - } - - /// - /// Fetches metadata and returns true or false indicating if any work that requires persistence was done - /// - /// The item. - /// if set to true [force]. - /// The cancellation token. - /// Task{System.Boolean}. - public override async Task FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, XmlFileName)); - - if (metadataFile != null) - { - var path = metadataFile.FullName; - - await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - new BaseItemXmlParser(Logger).Fetch((LiveTvChannel)item, path, cancellationToken); - } - finally - { - XmlParsingResourcePool.Release(); - } - - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; - } - - return false; - } - } -} diff --git a/MediaBrowser.Providers/LiveTv/ChannelXmlProvider.cs b/MediaBrowser.Providers/LiveTv/ChannelXmlProvider.cs new file mode 100644 index 0000000000..544685f661 --- /dev/null +++ b/MediaBrowser.Providers/LiveTv/ChannelXmlProvider.cs @@ -0,0 +1,59 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.LiveTv +{ + public class ChannelXmlProvider : BaseXmlProvider, ILocalMetadataProvider + { + private readonly ILogger _logger; + + public ChannelXmlProvider(IFileSystem fileSystem, ILogger logger) + : base(fileSystem) + { + _logger = logger; + } + + public async Task> GetMetadata(string path, CancellationToken cancellationToken) + { + path = GetXmlPath(path); + + var result = new MetadataResult(); + + await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var item = new LiveTvChannel(); + + new BaseItemXmlParser(_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 string GetXmlPath(string path) + { + return Path.Combine(path, "channel.xml"); + } + } +} diff --git a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs new file mode 100644 index 0000000000..da032eb8f0 --- /dev/null +++ b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs @@ -0,0 +1,41 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +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.LiveTv +{ + public class ProgramMetadataService : MetadataService + { + private readonly ILibraryManager _libraryManager; + + public ProgramMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo) + { + _libraryManager = libraryManager; + } + + /// + /// Merges the specified source. + /// + /// The source. + /// The target. + /// The locked fields. + /// if set to true [replace data]. + protected override void MergeData(LiveTvProgram source, LiveTvProgram target, List lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + } + + protected override Task SaveItem(LiveTvProgram item, ItemUpdateType reason, CancellationToken cancellationToken) + { + return _libraryManager.UpdateItem(item, reason, cancellationToken); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs similarity index 95% rename from MediaBrowser.Server.Implementations/Providers/ImageSaver.cs rename to MediaBrowser.Providers/Manager/ImageSaver.cs index ec797b6889..2decba1610 100644 --- a/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; @@ -15,7 +15,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.Server.Implementations.Providers +namespace MediaBrowser.Providers.Manager { /// /// Class ImageSaver @@ -36,7 +36,7 @@ namespace MediaBrowser.Server.Implementations.Providers /// /// The _directory watchers /// - private readonly IDirectoryWatchers _directoryWatchers; + private readonly ILibraryMonitor _libraryMonitor; private readonly IFileSystem _fileSystem; private readonly ILogger _logger; @@ -44,11 +44,11 @@ namespace MediaBrowser.Server.Implementations.Providers /// Initializes a new instance of the class. /// /// The config. - /// The directory watchers. - public ImageSaver(IServerConfigurationManager config, IDirectoryWatchers directoryWatchers, IFileSystem fileSystem, ILogger logger) + /// The directory watchers. + public ImageSaver(IServerConfigurationManager config, ILibraryMonitor libraryMonitor, IFileSystem fileSystem, ILogger logger) { _config = config; - _directoryWatchers = directoryWatchers; + _libraryMonitor = libraryMonitor; _fileSystem = fileSystem; _logger = logger; _remoteImageCache = new FileSystemRepository(config.ApplicationPaths.DownloadedImagesDataPath); @@ -160,7 +160,7 @@ namespace MediaBrowser.Server.Implementations.Providers // Delete the current path if (!string.IsNullOrEmpty(currentPath) && !paths.Contains(currentPath, StringComparer.OrdinalIgnoreCase)) { - _directoryWatchers.TemporarilyIgnore(currentPath); + _libraryMonitor.ReportFileSystemChangeBeginning(currentPath); try { @@ -179,7 +179,7 @@ namespace MediaBrowser.Server.Implementations.Providers } finally { - _directoryWatchers.RemoveTempIgnore(currentPath); + _libraryMonitor.ReportFileSystemChangeComplete(currentPath, false); } } } @@ -197,8 +197,8 @@ namespace MediaBrowser.Server.Implementations.Providers var parentFolder = Path.GetDirectoryName(path); - _directoryWatchers.TemporarilyIgnore(path); - _directoryWatchers.TemporarilyIgnore(parentFolder); + _libraryMonitor.ReportFileSystemChangeBeginning(path); + _libraryMonitor.ReportFileSystemChangeBeginning(parentFolder); try { @@ -223,8 +223,8 @@ namespace MediaBrowser.Server.Implementations.Providers } finally { - _directoryWatchers.RemoveTempIgnore(path); - _directoryWatchers.RemoveTempIgnore(parentFolder); + _libraryMonitor.ReportFileSystemChangeComplete(path, false); + _libraryMonitor.ReportFileSystemChangeComplete(parentFolder, false); } } @@ -348,6 +348,9 @@ namespace MediaBrowser.Server.Implementations.Providers case ImageType.Art: filename = "clearart"; break; + case ImageType.BoxRear: + filename = "back"; + break; case ImageType.Disc: filename = item is MusicAlbum ? "cdart" : "disc"; break; diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs new file mode 100644 index 0000000000..e8bae1d2fa --- /dev/null +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -0,0 +1,435 @@ +using MediaBrowser.Common.Extensions; +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.Model.Net; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Manager +{ + public class ItemImageProvider + { + private readonly ILogger _logger; + private readonly IProviderManager _providerManager; + private readonly IServerConfigurationManager _config; + + public ItemImageProvider(ILogger logger, IProviderManager providerManager, IServerConfigurationManager config) + { + _logger = logger; + _providerManager = providerManager; + _config = config; + } + + public bool ValidateImages(IHasImages item, IEnumerable providers) + { + var hasChanges = item.ValidateImages(); + + foreach (var provider in providers.OfType()) + { + var images = provider.GetImages(item); + + if (MergeImages(item, images)) + { + hasChanges = true; + } + } + + return hasChanges; + } + + public async Task RefreshImages(IHasImages item, IEnumerable imageProviders, ImageRefreshOptions options, CancellationToken cancellationToken) + { + var result = new RefreshResult { UpdateType = ItemUpdateType.Unspecified }; + + var providers = GetImageProviders(item, imageProviders).ToList(); + + var providerIds = new List(); + + foreach (var provider in providers.OfType()) + { + await RefreshFromProvider(item, provider, options, result, cancellationToken).ConfigureAwait(false); + + providerIds.Add(provider.GetType().FullName.GetMD5()); + } + + foreach (var provider in providers.OfType()) + { + await RefreshFromProvider(item, provider, result, cancellationToken).ConfigureAwait(false); + + providerIds.Add(provider.GetType().FullName.GetMD5()); + } + + result.Providers = providerIds; + + return result; + } + + /// + /// Refreshes from provider. + /// + /// The item. + /// The provider. + /// The result. + /// The cancellation token. + /// Task. + private async Task RefreshFromProvider(IHasImages item, IDynamicImageProvider provider, RefreshResult result, CancellationToken cancellationToken) + { + _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); + + try + { + var images = provider.GetSupportedImages(item); + + foreach (var imageType in images) + { + if (!item.HasImage(imageType)) + { + var response = await provider.GetImage(item, imageType, cancellationToken).ConfigureAwait(false); + + if (response.HasImage) + { + var mimeType = "image/" + response.Format.ToString().ToLower(); + + await _providerManager.SaveImage((BaseItem)item, response.Stream, mimeType, imageType, null, Guid.NewGuid().ToString(), cancellationToken).ConfigureAwait(false); + + result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + } + } + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + result.ErrorMessage = ex.Message; + result.Status = ProviderRefreshStatus.CompletedWithErrors; + _logger.ErrorException("Error in {0}", ex, provider.Name); + } + } + + /// + /// Image types that are only one per item + /// + private readonly ImageType[] _singularImages = + { + ImageType.Primary, + ImageType.Art, + ImageType.Banner, + ImageType.Box, + ImageType.BoxRear, + ImageType.Disc, + ImageType.Logo, + ImageType.Menu, + ImageType.Thumb + }; + + /// + /// Determines if an item already contains the given images + /// + /// + /// + /// + private bool ContainsImages(IHasImages item, List images) + { + if (_singularImages.Any(i => images.Contains(i) && !item.HasImage(i))) + { + return false; + } + + if (images.Contains(ImageType.Backdrop) && item.BackdropImagePaths.Count < GetMaxBackdropCount(item)) + { + return false; + } + + if (images.Contains(ImageType.Screenshot)) + { + var hasScreenshots = item as IHasScreenshots; + if (hasScreenshots != null) + { + if (hasScreenshots.ScreenshotImagePaths.Count < GetMaxBackdropCount(item)) + { + return false; + } + } + } + + return true; + } + + /// + /// Refreshes from provider. + /// + /// The item. + /// The provider. + /// The options. + /// The result. + /// The cancellation token. + /// Task. + private async Task RefreshFromProvider(IHasImages item, IRemoteImageProvider provider, ImageRefreshOptions options, RefreshResult result, CancellationToken cancellationToken) + { + try + { + // TODO: Also factor in IsConfiguredToDownloadImage + if (ContainsImages(item, provider.GetSupportedImages(item).ToList())) + { + return; + } + + _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); + + var images = await provider.GetAllImages(item, cancellationToken).ConfigureAwait(false); + var list = images.ToList(); + + foreach (var type in _singularImages) + { + if (IsConfiguredToDownloadImage(item, type) && !item.HasImage(type)) + { + await DownloadImage(item, provider, result, list, type, cancellationToken).ConfigureAwait(false); + } + } + + await DownloadBackdrops(item, provider, result, list, cancellationToken).ConfigureAwait(false); + + var hasScreenshots = item as IHasScreenshots; + if (hasScreenshots != null) + { + await DownloadScreenshots(hasScreenshots, provider, result, list, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + result.ErrorMessage = ex.Message; + result.Status = ProviderRefreshStatus.CompletedWithErrors; + _logger.ErrorException("Error in {0}", ex, provider.Name); + } + } + + /// + /// Gets the image providers. + /// + /// The item. + /// The image providers. + /// IEnumerable{IImageProvider}. + private IEnumerable GetImageProviders(IHasImages item, IEnumerable imageProviders) + { + var providers = imageProviders.Where(i => + { + try + { + return i.Supports(item); + } + catch (Exception ex) + { + _logger.ErrorException("Error in ImageProvider.Supports", ex, i.Name); + + return false; + } + }); + + if (!_config.Configuration.EnableInternetProviders) + { + providers = providers.Where(i => !(i is IRemoteImageProvider)); + } + + return providers.OrderBy(i => i.Order); + } + + private bool MergeImages(IHasImages item, List images) + { + var changed = false; + + foreach (var type in _singularImages) + { + var image = images.FirstOrDefault(i => i.Type == type); + + if (image != null) + { + var oldPath = item.GetImagePath(type); + + item.SetImagePath(type, image.Path); + + if (!string.Equals(oldPath, image.Path, StringComparison.OrdinalIgnoreCase)) + { + changed = true; + } + } + } + + // The change reporting will only be accurate at the count level + // Improve this if/when needed + var backdrops = images.Where(i => i.Type == ImageType.Backdrop).ToList(); + if (backdrops.Count > 0) + { + var oldCount = item.BackdropImagePaths.Count; + + item.BackdropImagePaths = item.BackdropImagePaths + .Concat(backdrops.Select(i => i.Path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (oldCount != item.BackdropImagePaths.Count) + { + changed = true; + } + } + + var hasScreenshots = item as IHasScreenshots; + if (hasScreenshots != null) + { + var screenshots = images.Where(i => i.Type == ImageType.Screenshot).ToList(); + + if (screenshots.Count > 0) + { + var oldCount = hasScreenshots.ScreenshotImagePaths.Count; + + hasScreenshots.ScreenshotImagePaths = hasScreenshots.ScreenshotImagePaths + .Concat(screenshots.Select(i => i.Path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (oldCount != hasScreenshots.ScreenshotImagePaths.Count) + { + changed = true; + } + } + } + + return changed; + } + + private async Task DownloadImage(IHasImages item, IRemoteImageProvider provider, RefreshResult result, IEnumerable images, ImageType type, CancellationToken cancellationToken) + { + foreach (var image in images.Where(i => i.Type == type)) + { + var url = image.Url; + + try + { + var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false); + + await _providerManager.SaveImage((BaseItem)item, response.Content, response.ContentType, type, null, url, cancellationToken).ConfigureAwait(false); + + result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + break; + } + catch (HttpException ex) + { + // Sometimes providers send back bad url's. Just move onto the next image + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + continue; + } + break; + } + } + } + + private async Task DownloadBackdrops(IHasImages item, IRemoteImageProvider provider, RefreshResult result, IEnumerable images, CancellationToken cancellationToken) + { + const ImageType imageType = ImageType.Backdrop; + var maxCount = GetMaxBackdropCount(item); + + foreach (var image in images.Where(i => i.Type == imageType)) + { + if (item.BackdropImagePaths.Count >= maxCount) + { + break; + } + + var url = image.Url; + + if (item.ContainsImageWithSourceUrl(url)) + { + continue; + } + + try + { + var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false); + + await _providerManager.SaveImage((BaseItem)item, response.Content, response.ContentType, imageType, null, url, cancellationToken).ConfigureAwait(false); + result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + break; + } + catch (HttpException ex) + { + // Sometimes providers send back bad url's. Just move onto the next image + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + continue; + } + break; + } + } + } + + private async Task DownloadScreenshots(IHasScreenshots item, IRemoteImageProvider provider, RefreshResult result, IEnumerable images, CancellationToken cancellationToken) + { + const ImageType imageType = ImageType.Screenshot; + var maxCount = GetMaxScreenshotCount(item); + + foreach (var image in images.Where(i => i.Type == imageType)) + { + if (item.ScreenshotImagePaths.Count >= maxCount) + { + break; + } + + var url = image.Url; + + if (item.ContainsImageWithSourceUrl(url)) + { + continue; + } + + try + { + var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false); + + await _providerManager.SaveImage((BaseItem)item, response.Content, response.ContentType, imageType, null, url, cancellationToken).ConfigureAwait(false); + result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + break; + } + catch (HttpException ex) + { + // Sometimes providers send back bad url's. Just move onto the next image + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + continue; + } + break; + } + } + } + + private bool IsConfiguredToDownloadImage(IHasImages item, ImageType type) + { + return true; + } + + private int GetMaxBackdropCount(IHasImages item) + { + return 1; + } + + private int GetMaxScreenshotCount(IHasScreenshots item) + { + return 1; + } + } +} diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs new file mode 100644 index 0000000000..7916d7e86d --- /dev/null +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -0,0 +1,358 @@ +using MediaBrowser.Common.Extensions; +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 System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Manager +{ + public abstract class MetadataService : IMetadataService + where TItemType : IHasMetadata, new() + { + protected readonly IServerConfigurationManager ServerConfigurationManager; + protected readonly ILogger Logger; + protected readonly IProviderManager ProviderManager; + private readonly IProviderRepository _providerRepo; + + private IMetadataProvider[] _providers = { }; + + private IImageProvider[] _imageProviders = { }; + + protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo) + { + ServerConfigurationManager = serverConfigurationManager; + Logger = logger; + ProviderManager = providerManager; + _providerRepo = providerRepo; + } + + /// + /// Adds the parts. + /// + /// The providers. + /// The image providers. + public void AddParts(IEnumerable providers, IEnumerable imageProviders) + { + _providers = providers.OfType>() + .ToArray(); + + _imageProviders = imageProviders.OrderBy(i => i.Order).ToArray(); + } + + /// + /// Saves the provider result. + /// + /// The result. + /// Task. + protected Task SaveProviderResult(MetadataStatus result) + { + return _providerRepo.SaveMetadataStatus(result, CancellationToken.None); + } + + /// + /// Gets the last result. + /// + /// The item identifier. + /// ProviderResult. + protected MetadataStatus GetLastResult(Guid itemId) + { + return _providerRepo.GetMetadataStatus(itemId) ?? new MetadataStatus { ItemId = itemId }; + } + + public async Task RefreshMetadata(IHasMetadata item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + var itemOfType = (TItemType)item; + + var updateType = ItemUpdateType.Unspecified; + var lastResult = GetLastResult(item.Id); + var refreshResult = lastResult; + refreshResult.LastErrorMessage = string.Empty; + refreshResult.LastStatus = ProviderRefreshStatus.Success; + + var imageProviders = GetImageProviders(item).ToList(); + var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, ServerConfigurationManager); + var localImagesFailed = false; + + // Start by validating images + try + { + // Always validate images and check for new locally stored ones. + if (itemImageProvider.ValidateImages(item, imageProviders)) + { + updateType = updateType | ItemUpdateType.ImageUpdate; + } + } + catch (Exception ex) + { + localImagesFailed = true; + Logger.ErrorException("Error validating images for {0}", ex, item.Path ?? item.Name); + refreshResult.AddStatus(ProviderRefreshStatus.Failure, ex.Message); + } + + // Next run metadata providers + if (options.MetadataRefreshMode != MetadataRefreshMode.None) + { + var providers = GetProviders(item, lastResult.DateLastMetadataRefresh.HasValue, options).ToList(); + + if (providers.Count > 0) + { + var result = await RefreshWithProviders(itemOfType, options, providers, cancellationToken).ConfigureAwait(false); + + updateType = updateType | result.UpdateType; + refreshResult.AddStatus(result.Status, result.ErrorMessage); + refreshResult.SetDateLastMetadataRefresh(DateTime.UtcNow); + refreshResult.AddImageProvidersRefreshed(result.Providers); + } + } + + // Next run remote image providers, but only if local image providers didn't throw an exception + if (!localImagesFailed) + { + if ((options.ImageRefreshMode == MetadataRefreshMode.EnsureMetadata && !lastResult.DateLastImagesRefresh.HasValue) || + options.ImageRefreshMode == MetadataRefreshMode.FullRefresh) + { + var result = await itemImageProvider.RefreshImages(itemOfType, imageProviders, options, cancellationToken).ConfigureAwait(false); + + updateType = updateType | result.UpdateType; + refreshResult.AddStatus(result.Status, result.ErrorMessage); + refreshResult.SetDateLastImagesRefresh(DateTime.UtcNow); + refreshResult.AddImageProvidersRefreshed(result.Providers); + } + } + + var providersHadChanges = updateType > ItemUpdateType.Unspecified; + + if (options.ForceSave || providersHadChanges) + { + if (string.IsNullOrEmpty(item.Name)) + { + throw new InvalidOperationException("Item has no name"); + } + + // Save to database + await SaveItem(itemOfType, updateType, cancellationToken); + } + + if (providersHadChanges || refreshResult.IsDirty) + { + await SaveProviderResult(refreshResult).ConfigureAwait(false); + } + } + + /// + /// Gets the providers. + /// + /// The item. + /// if set to true [has refreshed metadata]. + /// The options. + /// IEnumerable{`0}. + protected virtual IEnumerable GetProviders(IHasMetadata item, bool hasRefreshedMetadata, MetadataRefreshOptions options) + { + // Get providers to refresh + var providers = _providers.Where(i => CanRefresh(i, item)).ToList(); + + // Run all if either of these flags are true + var runAllProviders = options.ReplaceAllMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || !hasRefreshedMetadata; + + if (!runAllProviders) + { + // Avoid implicitly captured closure + var currentItem = item; + + var providersWithChanges = providers.OfType() + .Where(i => i.HasChanged(currentItem, currentItem.DateLastSaved)) + .ToList(); + + // If local providers are the only ones with changes, then just run those + if (providersWithChanges.All(i => i is ILocalMetadataProvider)) + { + providers = providers.Where(i => i is ILocalMetadataProvider).ToList(); + } + } + + return providers; + } + + /// + /// Determines whether this instance can refresh the specified provider. + /// + /// The provider. + /// The item. + /// true if this instance can refresh the specified provider; otherwise, false. + protected bool CanRefresh(IMetadataProvider provider, IHasMetadata item) + { + if (!ServerConfigurationManager.Configuration.EnableInternetProviders && provider is IRemoteMetadataProvider) + { + return false; + } + + if (item.LocationType != LocationType.FileSystem && provider is ILocalMetadataProvider) + { + return false; + } + + return true; + } + + protected abstract Task SaveItem(TItemType item, ItemUpdateType reason, CancellationToken cancellationToken); + + protected virtual ItemId GetId(IHasMetadata item) + { + return new ItemId + { + MetadataCountryCode = item.GetPreferredMetadataCountryCode(), + MetadataLanguage = item.GetPreferredMetadataLanguage(), + Name = item.Name, + ProviderIds = item.ProviderIds + }; + } + + public bool CanRefresh(IHasMetadata item) + { + return item is TItemType; + } + + protected virtual async Task RefreshWithProviders(TItemType item, MetadataRefreshOptions options, List providers, CancellationToken cancellationToken) + { + var refreshResult = new RefreshResult + { + UpdateType = ItemUpdateType.Unspecified, + Providers = providers.Select(i => i.GetType().FullName.GetMD5()).ToList() + }; + + var temp = new TItemType(); + + // If replacing all metadata, run internet providers first + if (options.ReplaceAllMetadata) + { + await ExecuteRemoteProviders(item, temp, providers.OfType>(), refreshResult, cancellationToken).ConfigureAwait(false); + } + + var hasLocalMetadata = false; + + foreach (var provider in providers.OfType>()) + { + Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); + + try + { + var localItem = await provider.GetMetadata(item.Path, cancellationToken).ConfigureAwait(false); + + if (localItem.HasMetadata) + { + MergeData(localItem.Item, temp, new List(), false, true); + refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport; + + // Only one local provider allowed per item + hasLocalMetadata = true; + break; + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + // If a local provider fails, consider that a failure + refreshResult.Status = ProviderRefreshStatus.Failure; + refreshResult.ErrorMessage = ex.Message; + Logger.ErrorException("Error in {0}", ex, provider.Name); + + // If the local provider fails don't continue with remote providers because the user's saved metadata could be lost + return refreshResult; + } + } + + if (!options.ReplaceAllMetadata && !hasLocalMetadata) + { + await ExecuteRemoteProviders(item, temp, providers.OfType>(), refreshResult, cancellationToken).ConfigureAwait(false); + } + + MergeData(temp, item, item.LockedFields, true, true); + + return refreshResult; + } + + private async Task ExecuteRemoteProviders(TItemType item, TItemType temp, IEnumerable> providers, RefreshResult refreshResult, CancellationToken cancellationToken) + { + var id = GetId(item); + + foreach (var provider in providers) + { + Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); + + try + { + var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false); + + if (result.HasMetadata) + { + MergeData(result.Item, temp, new List(), false, 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); + } + } + } + + protected abstract void MergeData(TItemType source, TItemType target, List lockedFields, bool replaceData, bool mergeMetadataSettings); + + public virtual int Order + { + get + { + return 0; + } + } + + private IEnumerable GetImageProviders(IHasImages item) + { + var providers = _imageProviders.Where(i => + { + try + { + return i.Supports(item); + } + catch (Exception ex) + { + Logger.ErrorException("Error in ImageProvider.Supports", ex, i.Name); + + return false; + } + }); + + if (!ServerConfigurationManager.Configuration.EnableInternetProviders) + { + providers = providers.Where(i => !(i is IRemoteImageProvider)); + } + + return providers.OrderBy(i => i.Order); + } + } + + public class RefreshResult + { + public ItemUpdateType UpdateType { get; set; } + public ProviderRefreshStatus Status { get; set; } + public string ErrorMessage { get; set; } + public List Providers { get; set; } + } +} diff --git a/MediaBrowser.Server.Implementations/Providers/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs similarity index 83% rename from MediaBrowser.Server.Implementations/Providers/ProviderManager.cs rename to MediaBrowser.Providers/Manager/ProviderManager.cs index cbfd7d74d3..3696bd02f5 100644 --- a/MediaBrowser.Server.Implementations/Providers/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -2,7 +2,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -17,7 +16,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.Server.Implementations.Providers +namespace MediaBrowser.Providers.Manager { /// /// Class ProviderManager @@ -37,7 +36,7 @@ namespace MediaBrowser.Server.Implementations.Providers /// /// The _directory watchers /// - private readonly IDirectoryWatchers _directoryWatchers; + private readonly ILibraryMonitor _libraryMonitor; /// /// Gets or sets the configuration manager. @@ -51,26 +50,32 @@ namespace MediaBrowser.Server.Implementations.Providers /// The metadata providers enumerable. private BaseMetadataProvider[] MetadataProviders { get; set; } + private IRemoteImageProvider[] RemoteImageProviders { get; set; } private IImageProvider[] ImageProviders { get; set; } + private readonly IFileSystem _fileSystem; - private readonly IItemRepository _itemRepo; + private readonly IProviderRepository _providerRepo; + + private IMetadataService[] _metadataServices = { }; /// /// Initializes a new instance of the class. /// /// The HTTP client. /// The configuration manager. - /// The directory watchers. + /// The directory watchers. /// The log manager. - public ProviderManager(IHttpClient httpClient, IServerConfigurationManager configurationManager, IDirectoryWatchers directoryWatchers, ILogManager logManager, IFileSystem fileSystem, IItemRepository itemRepo) + /// The file system. + /// The provider repo. + public ProviderManager(IHttpClient httpClient, IServerConfigurationManager configurationManager, ILibraryMonitor libraryMonitor, ILogManager logManager, IFileSystem fileSystem, IProviderRepository providerRepo) { _logger = logManager.GetLogger("ProviderManager"); _httpClient = httpClient; ConfigurationManager = configurationManager; - _directoryWatchers = directoryWatchers; + _libraryMonitor = libraryMonitor; _fileSystem = fileSystem; - _itemRepo = itemRepo; + _providerRepo = providerRepo; } /// @@ -78,11 +83,34 @@ namespace MediaBrowser.Server.Implementations.Providers /// /// The providers. /// The image providers. - public void AddParts(IEnumerable providers, IEnumerable imageProviders) + /// The metadata services. + /// The metadata providers. + public void AddParts(IEnumerable providers, IEnumerable imageProviders, IEnumerable metadataServices, IEnumerable metadataProviders) { MetadataProviders = providers.OrderBy(e => e.Priority).ToArray(); - ImageProviders = imageProviders.OrderByDescending(i => i.Priority).ToArray(); + ImageProviders = imageProviders.OrderBy(i => i.Order).ToArray(); + RemoteImageProviders = ImageProviders.OfType().ToArray(); + + _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray(); + + var providerList = metadataProviders.ToList(); + foreach (var service in _metadataServices) + { + service.AddParts(providerList, ImageProviders); + } + } + + public Task RefreshMetadata(IHasMetadata item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + var service = _metadataServices.FirstOrDefault(i => i.CanRefresh(item)); + + if (service != null) + { + return service.RefreshMetadata(item, options, cancellationToken); + } + + return ((BaseItem)item).RefreshMetadataDirect(cancellationToken, options.ForceSave, options.ReplaceAllMetadata); } /// @@ -91,9 +119,9 @@ namespace MediaBrowser.Server.Implementations.Providers /// The item. /// The cancellation token. /// if set to true [force]. - /// if set to true [allow slow providers]. /// Task{System.Boolean}. - public async Task ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false, bool allowSlowProviders = true) + /// item + public async Task ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false) { if (item == null) { @@ -108,7 +136,7 @@ namespace MediaBrowser.Server.Implementations.Providers var providerHistories = item.DateLastSaved == default(DateTime) ? new List() : - _itemRepo.GetProviderHistory(item.Id).ToList(); + _providerRepo.GetProviderHistory(item.Id).ToList(); // Run the normal providers sequentially in order of priority foreach (var provider in MetadataProviders) @@ -126,12 +154,6 @@ namespace MediaBrowser.Server.Implementations.Providers continue; } - // Skip if is slow and we aren't allowing slow ones - if (provider.IsSlow && !allowSlowProviders) - { - continue; - } - // Put this check below the await because the needs refresh of the next tier of providers may depend on the previous ones running // This is the case for the fan art provider which depends on the movie and tv providers having run before them if (provider.RequiresInternet && item.DontFetchMeta && provider.EnforceDontFetchMetadata) @@ -179,7 +201,7 @@ namespace MediaBrowser.Server.Implementations.Providers if (result.HasValue || force) { - await _itemRepo.SaveProviderHistory(item.Id, providerHistories, cancellationToken); + await _providerRepo.SaveProviderHistory(item.Id, providerHistories, cancellationToken); } return result; @@ -293,7 +315,7 @@ namespace MediaBrowser.Server.Implementations.Providers } //Tell the watchers to ignore - _directoryWatchers.TemporarilyIgnore(path); + _libraryMonitor.ReportFileSystemChangeBeginning(path); if (dataToSave.CanSeek) { @@ -316,7 +338,7 @@ namespace MediaBrowser.Server.Implementations.Providers finally { //Remove the ignore - _directoryWatchers.RemoveTempIgnore(path); + _libraryMonitor.ReportFileSystemChangeComplete(path, false); } } @@ -358,7 +380,7 @@ namespace MediaBrowser.Server.Implementations.Providers /// Task. public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, string sourceUrl, CancellationToken cancellationToken) { - return new ImageSaver(ConfigurationManager, _directoryWatchers, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, sourceUrl, cancellationToken); + return new ImageSaver(ConfigurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, sourceUrl, cancellationToken); } /// @@ -371,7 +393,7 @@ namespace MediaBrowser.Server.Implementations.Providers /// Task{IEnumerable{RemoteImageInfo}}. public async Task> GetAvailableRemoteImages(BaseItem item, CancellationToken cancellationToken, string providerName = null, ImageType? type = null) { - var providers = GetImageProviders(item); + var providers = GetRemoteImageProviders(item); if (!string.IsNullOrEmpty(providerName)) { @@ -396,7 +418,7 @@ namespace MediaBrowser.Server.Implementations.Providers /// The preferred language. /// The type. /// Task{IEnumerable{RemoteImageInfo}}. - private async Task> GetImages(BaseItem item, CancellationToken cancellationToken, IImageProvider i, string preferredLanguage, ImageType? type = null) + private async Task> GetImages(BaseItem item, CancellationToken cancellationToken, IRemoteImageProvider i, string preferredLanguage, ImageType? type = null) { try { @@ -414,7 +436,7 @@ namespace MediaBrowser.Server.Implementations.Providers } catch (Exception ex) { - _logger.ErrorException("{0} failed in GetImages for type {1}", ex, i.GetType().Name, item.GetType().Name); + _logger.ErrorException("{0} failed in GetImageInfos for type {1}", ex, i.GetType().Name, item.GetType().Name); return new List(); } } @@ -430,14 +452,9 @@ namespace MediaBrowser.Server.Implementations.Providers return images; } - /// - /// Gets the supported image providers. - /// - /// The item. - /// IEnumerable{IImageProvider}. - public IEnumerable GetImageProviders(BaseItem item) + private IEnumerable GetRemoteImageProviders(BaseItem item) { - return ImageProviders.Where(i => + return RemoteImageProviders.Where(i => { try { @@ -448,6 +465,22 @@ namespace MediaBrowser.Server.Implementations.Providers _logger.ErrorException("{0} failed in Supports for type {1}", ex, i.GetType().Name, item.GetType().Name); return false; } + + }); + } + + /// + /// Gets the supported image providers. + /// + /// The item. + /// IEnumerable{IImageProvider}. + public IEnumerable GetImageProviderInfo(BaseItem item) + { + return GetRemoteImageProviders(item).Select(i => new ImageProviderInfo + { + Name = i.Name, + Order = i.Order + }); } } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index e686e7eda0..b44d3608e1 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -64,6 +64,17 @@ + + + + + + + + + + + @@ -72,14 +83,10 @@ - - - - - - + + + - @@ -88,8 +95,8 @@ - - + + @@ -98,8 +105,6 @@ - - @@ -118,7 +123,11 @@ + + + + @@ -133,8 +142,8 @@ - - + + @@ -145,7 +154,7 @@ - + @@ -157,7 +166,6 @@ - @@ -180,6 +188,7 @@ +