diff options
| author | Joshua M. Boniface <joshua@boniface.me> | 2021-08-18 02:46:59 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-08-18 02:46:59 -0400 |
| commit | 72d3f7020ad80ce1a53eeae8c5d57abeb22a4679 (patch) | |
| tree | dd43e663838cdc7d99a4af565523df58ae23c856 /MediaBrowser.Providers | |
| parent | 7aef0fce444e6d8e06386553ec7ea1401a01bbb1 (diff) | |
| parent | e5cbafdb6b47377052e0d638908ef96e30a997d6 (diff) | |
Merge branch 'master' into patch-2
Diffstat (limited to 'MediaBrowser.Providers')
145 files changed, 3602 insertions, 7664 deletions
diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index e5326da71c..88ce8d087d 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -59,9 +59,9 @@ namespace MediaBrowser.Providers.BoxSets } /// <inheritdoc /> - protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType currentUpdateType) + protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType updateType) { - var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType); + var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType); var libraryFolderIds = item.GetLibraryFolderIds(); @@ -69,10 +69,10 @@ namespace MediaBrowser.Providers.BoxSets if (itemLibraryFolderIds == null || !libraryFolderIds.SequenceEqual(itemLibraryFolderIds)) { item.LibraryFolderIds = libraryFolderIds; - updateType |= ItemUpdateType.MetadataImport; + updatedType |= ItemUpdateType.MetadataImport; } - return updateType; + return updatedType; } } } diff --git a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs index 46f368f72b..e0f3131fdb 100644 --- a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 - using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; diff --git a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs index 2e6cf45302..2e6cf45302 100644 --- a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs +++ b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 413d297cb5..fb1d4f4906 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -59,6 +58,16 @@ namespace MediaBrowser.Providers.Manager _logger = logger; } + private bool EnableExtraThumbsDuplication + { + get + { + var config = _config.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata"); + + return config.EnableExtraThumbsDuplication; + } + } + /// <summary> /// Saves the image. /// </summary> @@ -69,7 +78,7 @@ namespace MediaBrowser.Providers.Manager /// <param name="imageIndex">Index of the image.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">mimeType</exception> + /// <exception cref="ArgumentNullException">mimeType.</exception> public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken) { return SaveImage(item, source, mimeType, type, imageIndex, null, cancellationToken); @@ -93,10 +102,8 @@ namespace MediaBrowser.Providers.Manager { saveLocally = false; - var season = item as Season; - // If season is virtual under a physical series, save locally if using compatible convention - if (season != null && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible) + if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible) { var series = season.Series; @@ -129,7 +136,7 @@ namespace MediaBrowser.Providers.Manager var memoryStream = new MemoryStream(); await using (source.ConfigureAwait(false)) { - await source.CopyToAsync(memoryStream).ConfigureAwait(false); + await source.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); } source = memoryStream; @@ -165,7 +172,9 @@ namespace MediaBrowser.Providers.Manager SetImagePath(item, type, imageIndex, savedPaths[0]); // Delete the current path - if (currentImageIsLocalFile && !savedPaths.Contains(currentImagePath, StringComparer.OrdinalIgnoreCase)) + if (currentImageIsLocalFile + && !savedPaths.Contains(currentImagePath, StringComparer.OrdinalIgnoreCase) + && (saveLocally || currentImagePath.Contains(_config.ApplicationPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase))) { var currentPath = currentImagePath; @@ -254,7 +263,8 @@ namespace MediaBrowser.Providers.Manager _fileSystem.SetAttributes(path, false, false); - await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } @@ -312,7 +322,7 @@ namespace MediaBrowser.Providers.Manager /// <exception cref="ArgumentNullException"> /// imageIndex /// or - /// imageIndex + /// imageIndex. /// </exception> private ItemImageInfo GetCurrentImage(BaseItem item, ImageType type, int imageIndex) { @@ -328,7 +338,8 @@ namespace MediaBrowser.Providers.Manager /// <param name="path">The path.</param> /// <exception cref="ArgumentNullException">imageIndex /// or - /// imageIndex</exception> + /// imageIndex. + /// </exception> private void SetImagePath(BaseItem item, ImageType type, int? imageIndex, string path) { item.SetImagePath(type, imageIndex ?? 0, _fileSystem.GetFileInfo(path)); @@ -346,7 +357,7 @@ namespace MediaBrowser.Providers.Manager /// <exception cref="ArgumentNullException"> /// imageIndex /// or - /// imageIndex + /// imageIndex. /// </exception> private string GetStandardSavePath(BaseItem item, ImageType type, int? imageIndex, string mimeType, bool saveLocally) { @@ -500,7 +511,7 @@ namespace MediaBrowser.Providers.Manager /// <param name="imageIndex">Index of the image.</param> /// <param name="mimeType">Type of the MIME.</param> /// <returns>IEnumerable{System.String}.</returns> - /// <exception cref="ArgumentNullException">imageIndex</exception> + /// <exception cref="ArgumentNullException">imageIndex.</exception> private string[] GetCompatibleSavePaths(BaseItem item, ImageType type, int? imageIndex, string mimeType) { var season = item as Season; @@ -604,16 +615,6 @@ namespace MediaBrowser.Providers.Manager return new[] { GetStandardSavePath(item, type, imageIndex, mimeType, true) }; } - private bool EnableExtraThumbsDuplication - { - get - { - var config = _config.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata"); - - return config.EnableExtraThumbsDuplication; - } - } - /// <summary> /// Gets the save path for item in mixed folder. /// </summary> diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 9227b6d937..607fd127b2 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -1,10 +1,12 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1002, CS1591 using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -28,6 +30,22 @@ namespace MediaBrowser.Providers.Manager private readonly IProviderManager _providerManager; private readonly IFileSystem _fileSystem; + /// <summary> + /// Image types that are only one per item. + /// </summary> + private static readonly ImageType[] _singularImages = + { + ImageType.Primary, + ImageType.Art, + ImageType.Banner, + ImageType.Box, + ImageType.BoxRear, + ImageType.Disc, + ImageType.Logo, + ImageType.Menu, + ImageType.Thumb + }; + public ItemImageProvider(ILogger logger, IProviderManager providerManager, IFileSystem fileSystem) { _logger = logger; @@ -85,7 +103,7 @@ namespace MediaBrowser.Providers.Manager { if (provider is IRemoteImageProvider remoteProvider) { - await RefreshFromProvider(item, libraryOptions, remoteProvider, refreshOptions, typeOptions, backdropLimit, screenshotLimit, downloadedImages, result, cancellationToken).ConfigureAwait(false); + await RefreshFromProvider(item, remoteProvider, refreshOptions, typeOptions, backdropLimit, screenshotLimit, downloadedImages, result, cancellationToken).ConfigureAwait(false); continue; } @@ -175,22 +193,6 @@ namespace MediaBrowser.Providers.Manager } } - /// <summary> - /// Image types that are only one per item. - /// </summary> - private readonly ImageType[] _singularImages = - { - ImageType.Primary, - ImageType.Art, - ImageType.Banner, - ImageType.Box, - ImageType.BoxRear, - ImageType.Disc, - ImageType.Logo, - ImageType.Menu, - ImageType.Thumb - }; - private bool HasImage(BaseItem item, ImageType type) { return item.HasImage(type); @@ -207,9 +209,14 @@ namespace MediaBrowser.Providers.Manager /// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns> private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit, int screenshotLimit) { - if (_singularImages.Any(i => images.Contains(i) && !HasImage(item, i) && savedOptions.GetLimit(i) > 0)) + // Using .Any causes the creation of a DisplayClass aka. variable capture + for (var i = 0; i < _singularImages.Length; i++) { - return false; + var type = _singularImages[i]; + if (images.Contains(type) && !HasImage(item, type) && savedOptions.GetLimit(type) > 0) + { + return false; + } } if (images.Contains(ImageType.Backdrop) && item.GetImages(ImageType.Backdrop).Count() < backdropLimit) @@ -229,7 +236,6 @@ namespace MediaBrowser.Providers.Manager /// Refreshes from provider. /// </summary> /// <param name="item">The item.</param> - /// <param name="libraryOptions">The library options.</param> /// <param name="provider">The provider.</param> /// <param name="refreshOptions">The refresh options.</param> /// <param name="savedOptions">The saved options.</param> @@ -241,7 +247,6 @@ namespace MediaBrowser.Providers.Manager /// <returns>Task.</returns> private async Task RefreshFromProvider( BaseItem item, - LibraryOptions libraryOptions, IRemoteImageProvider provider, ImageRefreshOptions refreshOptions, TypeOptions savedOptions, @@ -289,7 +294,7 @@ namespace MediaBrowser.Providers.Manager if (!HasImage(item, imageType) || (refreshOptions.IsReplacingImage(imageType) && !downloadedImages.Contains(imageType))) { minWidth = savedOptions.GetMinWidth(imageType); - var downloaded = await DownloadImage(item, libraryOptions, provider, result, list, minWidth, imageType, cancellationToken).ConfigureAwait(false); + var downloaded = await DownloadImage(item, provider, result, list, minWidth, imageType, cancellationToken).ConfigureAwait(false); if (downloaded) { @@ -299,12 +304,12 @@ namespace MediaBrowser.Providers.Manager } minWidth = savedOptions.GetMinWidth(ImageType.Backdrop); - await DownloadBackdrops(item, libraryOptions, ImageType.Backdrop, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false); + await DownloadBackdrops(item, ImageType.Backdrop, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false); if (item is IHasScreenshots hasScreenshots) { minWidth = savedOptions.GetMinWidth(ImageType.Screenshot); - await DownloadBackdrops(item, libraryOptions, ImageType.Screenshot, screenshotLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false); + await DownloadBackdrops(item, ImageType.Screenshot, screenshotLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -328,7 +333,7 @@ namespace MediaBrowser.Providers.Manager var deleted = false; var deletedImages = new List<ItemImageInfo>(); - foreach (var image in item.GetImages(type).ToList()) + foreach (var image in item.GetImages(type)) { if (!image.IsLocalFile) { @@ -354,13 +359,14 @@ namespace MediaBrowser.Providers.Manager } } - public bool MergeImages(BaseItem item, List<LocalImageInfo> images) + public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images) { var changed = false; - foreach (var type in _singularImages) + for (var i = 0; i < _singularImages.Length; i++) { - var image = images.FirstOrDefault(i => i.Type == type); + var type = _singularImages[i]; + var image = GetFirstLocalImageInfoByType(images, type); if (image != null) { @@ -378,7 +384,6 @@ namespace MediaBrowser.Providers.Manager } else { - var newDateModified = _fileSystem.GetLastWriteTimeUtc(image.FileInfo); // If date changed then we need to reset saved image dimensions @@ -423,15 +428,29 @@ namespace MediaBrowser.Providers.Manager return changed; } - private bool UpdateMultiImages(BaseItem item, List<LocalImageInfo> images, ImageType type) + private static LocalImageInfo GetFirstLocalImageInfoByType(IReadOnlyList<LocalImageInfo> images, ImageType type) { - var changed = false; + var len = images.Count; + for (var i = 0; i < len; i++) + { + var image = images[i]; + if (image.Type == type) + { + return image; + } + } - var newImages = images.Where(i => i.Type == type).ToList(); + return null; + } - var newImageFileInfos = newImages - .Select(i => i.FileInfo) - .ToList(); + private bool UpdateMultiImages(BaseItem item, IReadOnlyList<LocalImageInfo> images, ImageType type) + { + var changed = false; + + var newImageFileInfos = images + .Where(i => i.Type == type) + .Select(i => i.FileInfo) + .ToList(); if (item.AddImages(type, newImageFileInfos)) { @@ -441,7 +460,8 @@ namespace MediaBrowser.Providers.Manager return changed; } - private async Task<bool> DownloadImage(BaseItem item, LibraryOptions libraryOptions, + private async Task<bool> DownloadImage( + BaseItem item, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, @@ -453,7 +473,7 @@ namespace MediaBrowser.Providers.Manager .Where(i => i.Type == type && !(i.Width.HasValue && i.Width.Value < minWidth)) .ToList(); - if (EnableImageStub(item, libraryOptions) && eligibleImages.Count > 0) + if (EnableImageStub(item) && eligibleImages.Count > 0) { SaveImageStub(item, type, eligibleImages.Select(i => i.Url)); result.UpdateType |= ItemUpdateType.ImageUpdate; @@ -467,7 +487,8 @@ namespace MediaBrowser.Providers.Manager try { using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await _providerManager.SaveImage( item, @@ -480,7 +501,7 @@ namespace MediaBrowser.Providers.Manager result.UpdateType |= ItemUpdateType.ImageUpdate; return true; } - catch (HttpException ex) + catch (HttpRequestException ex) { // Sometimes providers send back bad url's. Just move to the next image if (ex.StatusCode.HasValue @@ -496,7 +517,7 @@ namespace MediaBrowser.Providers.Manager return false; } - private bool EnableImageStub(BaseItem item, LibraryOptions libraryOptions) + private bool EnableImageStub(BaseItem item) { if (item is LiveTvProgram) { @@ -517,17 +538,8 @@ namespace MediaBrowser.Providers.Manager } } - if (libraryOptions.DownloadImagesInAdvance) - { - return false; - } - - // if (!item.IsSaveLocalMetadataEnabled()) - //{ - // return true; - //} - - return true; + // We always want to use prefetched images + return false; } private void SaveImageStub(BaseItem item, ImageType imageType, IEnumerable<string> urls) @@ -539,16 +551,18 @@ namespace MediaBrowser.Providers.Manager private void SaveImageStub(BaseItem item, ImageType imageType, IEnumerable<string> urls, int newIndex) { - var path = string.Join("|", urls.Take(1).ToArray()); + var path = string.Join('|', urls.Take(1)); - item.SetImage(new ItemImageInfo - { - Path = path, - Type = imageType - }, newIndex); + item.SetImage( + new ItemImageInfo + { + Path = path, + Type = imageType + }, + newIndex); } - private async Task DownloadBackdrops(BaseItem item, LibraryOptions libraryOptions, ImageType imageType, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken) + private async Task DownloadBackdrops(BaseItem item, ImageType imageType, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken) { foreach (var image in images.Where(i => i.Type == imageType)) { @@ -564,7 +578,7 @@ namespace MediaBrowser.Providers.Manager var url = image.Url; - if (EnableImageStub(item, libraryOptions)) + if (EnableImageStub(item)) { SaveImageStub(item, imageType, new[] { url }); result.UpdateType |= ItemUpdateType.ImageUpdate; @@ -592,7 +606,7 @@ namespace MediaBrowser.Providers.Manager } } - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await _providerManager.SaveImage( item, stream, @@ -602,7 +616,7 @@ namespace MediaBrowser.Providers.Manager cancellationToken).ConfigureAwait(false); result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; } - catch (HttpException ex) + catch (HttpRequestException ex) { // Sometimes providers send back bad urls. Just move onto the next image if (ex.StatusCode.HasValue diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index d0de584276..3a42eb4c19 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -21,12 +21,6 @@ namespace MediaBrowser.Providers.Manager where TItemType : BaseItem, IHasLookupInfo<TIdType>, new() where TIdType : ItemLookupInfo, new() { - protected readonly IServerConfigurationManager ServerConfigurationManager; - protected readonly ILogger<MetadataService<TItemType, TIdType>> Logger; - protected readonly IProviderManager ProviderManager; - protected readonly IFileSystem FileSystem; - protected readonly ILibraryManager LibraryManager; - protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) { ServerConfigurationManager = serverConfigurationManager; @@ -34,8 +28,31 @@ namespace MediaBrowser.Providers.Manager ProviderManager = providerManager; FileSystem = fileSystem; LibraryManager = libraryManager; + ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem); } + protected ItemImageProvider ImageProvider { get; } + + protected IServerConfigurationManager ServerConfigurationManager { get; } + + protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; } + + protected IProviderManager ProviderManager { get; } + + protected IFileSystem FileSystem { get; } + + protected ILibraryManager LibraryManager { get; } + + protected virtual bool EnableUpdatingPremiereDateFromChildren => false; + + protected virtual bool EnableUpdatingGenresFromChildren => false; + + protected virtual bool EnableUpdatingStudiosFromChildren => false; + + protected virtual bool EnableUpdatingOfficialRatingFromChildren => false; + + public virtual int Order => 0; + private FileSystemMetadata TryGetFile(string path, IDirectoryService directoryService) { try @@ -74,7 +91,6 @@ namespace MediaBrowser.Providers.Manager } } - var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem); var localImagesFailed = false; var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList(); @@ -83,7 +99,7 @@ namespace MediaBrowser.Providers.Manager try { // Always validate images and check for new locally stored ones. - if (itemImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService)) + if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService)) { updateType |= ItemUpdateType.ImageUpdate; } @@ -129,7 +145,7 @@ namespace MediaBrowser.Providers.Manager // await FindIdentities(id, cancellationToken).ConfigureAwait(false); id.IsAutomated = refreshOptions.IsAutomated; - var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, itemImageProvider, cancellationToken).ConfigureAwait(false); + var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false); updateType |= result.UpdateType; if (result.Failures > 0) @@ -146,7 +162,7 @@ namespace MediaBrowser.Providers.Manager if (providers.Count > 0) { - var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false); + var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false); updateType |= result.UpdateType; if (result.Failures > 0) @@ -187,7 +203,7 @@ namespace MediaBrowser.Providers.Manager } // Save to database - await SaveItemAsync(metadataResult, libraryOptions, updateType, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); } await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false); @@ -197,85 +213,37 @@ namespace MediaBrowser.Providers.Manager private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result) { - lookupInfo.ProviderIds = result.ProviderIds; - lookupInfo.Name = result.Name; - lookupInfo.Year = result.ProductionYear; + // Episode and Season do not support Identify, so the search results are the Series' + switch (lookupInfo) + { + case EpisodeInfo episodeInfo: + episodeInfo.SeriesProviderIds = result.ProviderIds; + episodeInfo.ProviderIds.Clear(); + break; + case SeasonInfo seasonInfo: + seasonInfo.SeriesProviderIds = result.ProviderIds; + seasonInfo.ProviderIds.Clear(); + break; + default: + lookupInfo.ProviderIds = result.ProviderIds; + lookupInfo.Name = result.Name; + lookupInfo.Year = result.ProductionYear; + break; + } } - protected async Task SaveItemAsync(MetadataResult<TItemType> result, LibraryOptions libraryOptions, ItemUpdateType reason, CancellationToken cancellationToken) + protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken) { if (result.Item.SupportsPeople && result.People != null) { var baseItem = result.Item; - LibraryManager.UpdatePeople(baseItem, result.People); - await SavePeopleMetadataAsync(result.People, libraryOptions, cancellationToken).ConfigureAwait(false); + await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false); } await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); } - private async Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken) - { - foreach (var person in people) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl)) - { - var updateType = ItemUpdateType.MetadataDownload; - - var saveEntity = false; - var personEntity = LibraryManager.GetPerson(person.Name); - foreach (var id in person.ProviderIds) - { - if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)) - { - personEntity.SetProviderId(id.Key, id.Value); - saveEntity = true; - } - } - - if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary)) - { - await AddPersonImageAsync(personEntity, libraryOptions, person.ImageUrl, cancellationToken).ConfigureAwait(false); - - saveEntity = true; - updateType |= ItemUpdateType.ImageUpdate; - } - - if (saveEntity) - { - await personEntity.UpdateToRepositoryAsync(updateType, cancellationToken).ConfigureAwait(false); - } - } - } - } - - private async Task AddPersonImageAsync(Person personEntity, LibraryOptions libraryOptions, string imageUrl, CancellationToken cancellationToken) - { - if (libraryOptions.DownloadImagesInAdvance) - { - try - { - await ProviderManager.SaveImage(personEntity, imageUrl, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); - return; - } - catch (Exception ex) - { - Logger.LogError(ex, "Error in AddPersonImage"); - } - } - - personEntity.SetImage( - new ItemImageInfo - { - Path = imageUrl, - Type = ImageType.Primary - }, - 0); - } - protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { item.AfterMetadataRefresh(); @@ -283,7 +251,7 @@ namespace MediaBrowser.Providers.Manager } /// <summary> - /// Befores the save. + /// Before the save. /// </summary> /// <param name="item">The item.</param> /// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param> @@ -329,8 +297,7 @@ namespace MediaBrowser.Providers.Manager return true; } - var folder = item as Folder; - if (folder != null) + if (item is Folder folder) { return folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks; } @@ -341,13 +308,12 @@ namespace MediaBrowser.Providers.Manager protected virtual IList<BaseItem> GetChildrenForMetadataUpdates(TItemType item) { - var folder = item as Folder; - if (folder != null) + if (item is Folder folder) { return folder.GetRecursiveChildren(); } - return new List<BaseItem>(); + return Array.Empty<BaseItem>(); } protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) @@ -385,8 +351,7 @@ namespace MediaBrowser.Providers.Manager private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IList<BaseItem> children) { - var folder = item as Folder; - if (folder != null && folder.SupportsCumulativeRunTimeTicks) + if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks) { long ticks = 0; @@ -442,14 +407,6 @@ namespace MediaBrowser.Providers.Manager return updateType; } - protected virtual bool EnableUpdatingPremiereDateFromChildren => false; - - protected virtual bool EnableUpdatingGenresFromChildren => false; - - protected virtual bool EnableUpdatingStudiosFromChildren => false; - - protected virtual bool EnableUpdatingOfficialRatingFromChildren => false; - private ItemUpdateType UpdatePremiereDate(TItemType item, IList<BaseItem> children) { var updateType = ItemUpdateType.None; @@ -482,7 +439,7 @@ namespace MediaBrowser.Providers.Manager if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue) || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1)) { - updateType = updateType | ItemUpdateType.MetadataEdit; + updateType |= ItemUpdateType.MetadataEdit; } return updateType; @@ -502,7 +459,7 @@ namespace MediaBrowser.Providers.Manager if (currentList.Length != item.Genres.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Genres.OrderBy(i => i), StringComparer.OrdinalIgnoreCase)) { - updateType = updateType | ItemUpdateType.MetadataEdit; + updateType |= ItemUpdateType.MetadataEdit; } } @@ -523,7 +480,7 @@ namespace MediaBrowser.Providers.Manager if (currentList.Length != item.Studios.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Studios.OrderBy(i => i), StringComparer.OrdinalIgnoreCase)) { - updateType = updateType | ItemUpdateType.MetadataEdit; + updateType |= ItemUpdateType.MetadataEdit; } } @@ -538,7 +495,7 @@ namespace MediaBrowser.Providers.Manager { if (item.UpdateRatingToItems(children)) { - updateType = updateType | ItemUpdateType.MetadataEdit; + updateType |= ItemUpdateType.MetadataEdit; } } @@ -548,6 +505,11 @@ namespace MediaBrowser.Providers.Manager /// <summary> /// Gets the providers. /// </summary> + /// <param name="item">A media item.</param> + /// <param name="libraryOptions">The LibraryOptions to use.</param> + /// <param name="options">The MetadataRefreshOptions to use.</param> + /// <param name="isFirstRefresh">Specifies first refresh mode.</param> + /// <param name="requiresRefresh">Specifies refresh mode.</param> /// <returns>IEnumerable{`0}.</returns> protected IEnumerable<IMetadataProvider> GetProviders(BaseItem item, LibraryOptions libraryOptions, MetadataRefreshOptions options, bool isFirstRefresh, bool requiresRefresh) { @@ -622,7 +584,7 @@ namespace MediaBrowser.Providers.Manager protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options) { // Get providers to refresh - var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)).ToList(); + var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)); var dateLastImageRefresh = item.DateLastRefreshed; @@ -634,15 +596,13 @@ namespace MediaBrowser.Providers.Manager providers = providers .Where(i => { - var hasFileChangeMonitor = i as IHasItemChangeMonitor; - if (hasFileChangeMonitor != null) + if (i is IHasItemChangeMonitor hasFileChangeMonitor) { return HasChanged(item, hasFileChangeMonitor, options.DirectoryService); } return false; - }) - .ToList(); + }); } return providers; @@ -658,10 +618,11 @@ namespace MediaBrowser.Providers.Manager return type == typeof(TItemType); } - protected virtual async Task<RefreshResult> RefreshWithProviders(MetadataResult<TItemType> metadata, + protected virtual async Task<RefreshResult> RefreshWithProviders( + MetadataResult<TItemType> metadata, TIdType id, MetadataRefreshOptions options, - List<IMetadataProvider> providers, + ICollection<IMetadataProvider> providers, ItemImageProvider imageService, CancellationToken cancellationToken) { @@ -694,7 +655,7 @@ namespace MediaBrowser.Providers.Manager var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken) .ConfigureAwait(false); - refreshResult.UpdateType = refreshResult.UpdateType | remoteResult.UpdateType; + refreshResult.UpdateType |= remoteResult.UpdateType; refreshResult.ErrorMessage = remoteResult.ErrorMessage; refreshResult.Failures += remoteResult.Failures; } @@ -714,9 +675,15 @@ namespace MediaBrowser.Providers.Manager if (localItem.HasMetadata) { + foreach (var remoteImage in localItem.RemoteImages) + { + await ProviderManager.SaveImage(item, remoteImage.url, remoteImage.type, null, cancellationToken).ConfigureAwait(false); + refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; + } + if (imageService.MergeImages(item, localItem.Images)) { - refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.ImageUpdate; + refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; } if (localItem.UserDataList != null) @@ -725,7 +692,7 @@ namespace MediaBrowser.Providers.Manager } MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true); - refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport; + refreshResult.UpdateType |= ItemUpdateType.MetadataImport; // Only one local provider allowed per item if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item)) @@ -757,7 +724,7 @@ namespace MediaBrowser.Providers.Manager var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken) .ConfigureAwait(false); - refreshResult.UpdateType = refreshResult.UpdateType | remoteResult.UpdateType; + refreshResult.UpdateType |= remoteResult.UpdateType; refreshResult.ErrorMessage = remoteResult.ErrorMessage; refreshResult.Failures += remoteResult.Failures; } @@ -773,7 +740,7 @@ namespace MediaBrowser.Providers.Manager else { // TODO: If the new metadata from above has some blank data, this can cause old data to get filled into those empty fields - MergeData(metadata, temp, new MetadataField[] { }, false, false); + MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false); MergeData(temp, metadata, item.LockedFields, true, false); } } @@ -807,7 +774,7 @@ namespace MediaBrowser.Providers.Manager try { - refreshResult.UpdateType = refreshResult.UpdateType | await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false); + refreshResult.UpdateType |= await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -853,7 +820,7 @@ namespace MediaBrowser.Providers.Manager MergeData(result, temp, Array.Empty<MetadataField>(), false, false); MergeNewData(temp.Item, id); - refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload; + refreshResult.UpdateType |= ItemUpdateType.MetadataDownload; } else { @@ -875,16 +842,6 @@ namespace MediaBrowser.Providers.Manager return refreshResult; } - private string NormalizeLanguage(string language) - { - if (string.IsNullOrWhiteSpace(language)) - { - return "en"; - } - - return language; - } - private void MergeNewData(TItemType source, TIdType lookupInfo) { // Copy new provider id's that may have been obtained @@ -900,24 +857,23 @@ namespace MediaBrowser.Providers.Manager } } - protected abstract void MergeData(MetadataResult<TItemType> source, + protected abstract void MergeData( + MetadataResult<TItemType> source, MetadataResult<TItemType> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings); - public virtual int Order => 0; - private bool HasChanged(BaseItem item, IHasItemChangeMonitor changeMonitor, IDirectoryService directoryService) { try { var hasChanged = changeMonitor.HasChanged(item, directoryService); - // if (hasChanged) - //{ - // logger.LogDebug("{0} reports change to {1}", changeMonitor.GetType().Name, item.Path ?? item.Name); - //} + if (hasChanged) + { + Logger.LogDebug("{0} reports change to {1}", changeMonitor.GetType().Name, item.Path ?? item.Name); + } return hasChanged; } @@ -928,13 +884,4 @@ namespace MediaBrowser.Providers.Manager } } } - - public class RefreshResult - { - public ItemUpdateType UpdateType { get; set; } - - public string ErrorMessage { get; set; } - - public int Failures { get; set; } - } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 155e8cb8ab..2dfaa372c5 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -9,11 +9,11 @@ using System.Net.Http; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; using MediaBrowser.Common.Net; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; +using MediaBrowser.Controller.BaseItemManager; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -25,7 +25,6 @@ using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; using Priority_Queue; @@ -52,6 +51,7 @@ namespace MediaBrowser.Providers.Manager private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subtitleManager; private readonly IServerConfigurationManager _configurationManager; + private readonly IBaseItemManager _baseItemManager; private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>(); private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue = @@ -59,8 +59,8 @@ namespace MediaBrowser.Providers.Manager private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>(); private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>(); - private IEnumerable<IMetadataSaver> _savers; - private IExternalId[] _externalIds; + private IMetadataSaver[] _savers = Array.Empty<IMetadataSaver>(); + private IExternalId[] _externalIds = Array.Empty<IExternalId>(); private bool _isProcessingRefreshQueue; private bool _disposed; @@ -75,6 +75,7 @@ namespace MediaBrowser.Providers.Manager /// <param name="fileSystem">The filesystem.</param> /// <param name="appPaths">The server application paths.</param> /// <param name="libraryManager">The library manager.</param> + /// <param name="baseItemManager">The BaseItem manager.</param> public ProviderManager( IHttpClientFactory httpClientFactory, ISubtitleManager subtitleManager, @@ -83,7 +84,8 @@ namespace MediaBrowser.Providers.Manager ILogger<ProviderManager> logger, IFileSystem fileSystem, IServerApplicationPaths appPaths, - ILibraryManager libraryManager) + ILibraryManager libraryManager, + IBaseItemManager baseItemManager) { _logger = logger; _httpClientFactory = httpClientFactory; @@ -93,6 +95,7 @@ namespace MediaBrowser.Providers.Manager _appPaths = appPaths; _libraryManager = libraryManager; _subtitleManager = subtitleManager; + _baseItemManager = baseItemManager; } /// <inheritdoc/> @@ -121,7 +124,7 @@ namespace MediaBrowser.Providers.Manager _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray(); _savers = metadataSavers - .Where(i => !(i is IConfigurableProvider configurable) || configurable.IsEnabled) + .Where(i => i is not IConfigurableProvider configurable || configurable.IsEnabled) .ToArray(); } @@ -156,10 +159,15 @@ namespace MediaBrowser.Providers.Manager /// <inheritdoc/> public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); - var contentType = response.Content.Headers.ContentType.MediaType; + if (response.StatusCode != HttpStatusCode.OK) + { + throw new HttpRequestException("Invalid image received.", null, response.StatusCode); + } + + var contentType = response.Content.Headers.ContentType?.MediaType; // Workaround for tvheadend channel icons // TODO: Isolate this hack into the tvh plugin @@ -174,13 +182,10 @@ namespace MediaBrowser.Providers.Manager // thetvdb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons... if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase)) { - throw new HttpException("Invalid image received.") - { - StatusCode = HttpStatusCode.NotFound - }; + throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound); } - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await SaveImage( item, stream, @@ -236,6 +241,7 @@ namespace MediaBrowser.Providers.Manager languages.Add(preferredLanguage); } + // TODO include [query.IncludeAllLanguages] as an argument to the providers var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -391,7 +397,7 @@ namespace MediaBrowser.Providers.Manager if (provider is IRemoteMetadataProvider) { - if (!forceEnableInternetMetadata && !item.IsMetadataFetcherEnabled(libraryOptions, provider.Name)) + if (!forceEnableInternetMetadata && !_baseItemManager.IsMetadataFetcherEnabled(item, libraryOptions, provider.Name)) { return false; } @@ -435,7 +441,7 @@ namespace MediaBrowser.Providers.Manager if (provider is IRemoteImageProvider || provider is IDynamicImageProvider) { - if (!item.IsImageFetcherEnabled(libraryOptions, provider.Name)) + if (!_baseItemManager.IsImageFetcherEnabled(item, libraryOptions, provider.Name)) { return false; } @@ -863,14 +869,14 @@ namespace MediaBrowser.Providers.Manager } } } - catch (Exception) +#pragma warning disable CA1031 // do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // do not catch general exception types { - // Logged at lower levels + _logger.LogError(ex, "Provider {ProviderName} failed to retrieve search results", provider.Name); } } - // _logger.LogDebug("Returning search results {0}", _json.SerializeToString(resultList)); - return resultList; } @@ -905,8 +911,7 @@ namespace MediaBrowser.Providers.Manager return provider.GetImageResponse(url, cancellationToken); } - /// <inheritdoc/> - public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item) + private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item) { return _externalIds.Where(i => { @@ -955,13 +960,11 @@ namespace MediaBrowser.Providers.Manager public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item) { return GetExternalIds(item) - .Select(i => new ExternalIdInfo - { - Name = i.ProviderName, - Key = i.Key, - Type = i.Type, - UrlFormatString = i.UrlFormatString - }); + .Select(i => new ExternalIdInfo( + name: i.ProviderName, + key: i.Key, + type: i.Type, + urlFormatString: i.UrlFormatString)); } /// <inheritdoc/> @@ -1018,26 +1021,26 @@ namespace MediaBrowser.Providers.Manager // TODO: Need to hunt down the conditions for this happening _activeRefreshes.AddOrUpdate( id, - (_) => throw new Exception( + (_) => throw new InvalidOperationException( string.Format( CultureInfo.InvariantCulture, "Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture))), - (_, __) => progress); + (_, _) => progress); RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress))); } /// <inheritdoc/> - public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority) + public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority) { if (_disposed) { return; } - _refreshQueue.Enqueue(new Tuple<Guid, MetadataRefreshOptions>(id, options), (int)priority); + _refreshQueue.Enqueue(new Tuple<Guid, MetadataRefreshOptions>(itemId, options), (int)priority); lock (_refreshQueueLock) { @@ -1070,17 +1073,16 @@ namespace MediaBrowser.Providers.Manager try { var item = libraryManager.GetItemById(refreshItem.Item1); - if (item != null) + if (item == null) { - // Try to throttle this a little bit. - await Task.Delay(100, cancellationToken).ConfigureAwait(false); + continue; + } - var task = item is MusicArtist artist - ? RefreshArtist(artist, refreshItem.Item2, cancellationToken) - : RefreshItem(item, refreshItem.Item2, cancellationToken); + var task = item is MusicArtist artist + ? RefreshArtist(artist, refreshItem.Item2, cancellationToken) + : RefreshItem(item, refreshItem.Item2, cancellationToken); - await task.ConfigureAwait(false); - } + await task.ConfigureAwait(false); } catch (OperationCanceledException) { @@ -1109,7 +1111,7 @@ namespace MediaBrowser.Providers.Manager await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false); break; case Folder folder: - await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false); + await folder.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false); break; } } @@ -1120,7 +1122,7 @@ namespace MediaBrowser.Providers.Manager { await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); - await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false); + await child.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false); } } @@ -1142,7 +1144,7 @@ namespace MediaBrowser.Providers.Manager .Select(i => i.MusicArtist) .Where(i => i != null); - var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true)); + var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), options, true, cancellationToken)); await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false); @@ -1162,6 +1164,29 @@ namespace MediaBrowser.Providers.Manager return RefreshItem(item, options, cancellationToken); } + /// <summary> + /// Runs multiple metadata refreshes concurrently. + /// </summary> + /// <param name="action">The action to run.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns> + public async Task RunMetadataRefresh(Func<Task> action, CancellationToken cancellationToken) + { + // create a variable for this since it is possible MetadataRefreshThrottler could change due to a config update during a scan + var metadataRefreshThrottler = _baseItemManager.MetadataRefreshThrottler; + + await metadataRefreshThrottler.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await action().ConfigureAwait(false); + } + finally + { + metadataRefreshThrottler.Release(); + } + } + /// <inheritdoc/> public void Dispose() { diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs index a4fd6ca84e..aceba22158 100644 --- a/MediaBrowser.Providers/Manager/ProviderUtils.cs +++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Diacritics.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -26,19 +26,19 @@ namespace MediaBrowser.Providers.Manager if (source == null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentException("Item cannot be null.", nameof(sourceResult)); } if (target == null) { - throw new ArgumentNullException(nameof(target)); + throw new ArgumentException("Item cannot be null.", nameof(targetResult)); } if (!lockedFields.Contains(MetadataField.Name)) { if (replaceData || string.IsNullOrEmpty(target.Name)) { - // Safeguard against incoming data having an emtpy name + // Safeguard against incoming data having an empty name if (!string.IsNullOrWhiteSpace(source.Name)) { target.Name = source.Name; @@ -48,14 +48,14 @@ namespace MediaBrowser.Providers.Manager if (replaceData || string.IsNullOrEmpty(target.OriginalTitle)) { - // Safeguard against incoming data having an emtpy name + // Safeguard against incoming data having an empty name if (!string.IsNullOrWhiteSpace(source.OriginalTitle)) { target.OriginalTitle = source.OriginalTitle; } } - if (replaceData || !target.CommunityRating.HasValue || (source.CommunityRating.HasValue && string.Equals(sourceResult.Provider, "The Open Movie Database", StringComparison.OrdinalIgnoreCase))) + if (replaceData || !target.CommunityRating.HasValue) { target.CommunityRating = source.CommunityRating; } diff --git a/MediaBrowser.Providers/Manager/RefreshResult.cs b/MediaBrowser.Providers/Manager/RefreshResult.cs new file mode 100644 index 0000000000..72fc61e424 --- /dev/null +++ b/MediaBrowser.Providers/Manager/RefreshResult.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Library; + +namespace MediaBrowser.Providers.Manager +{ + public class RefreshResult + { + public ItemUpdateType UpdateType { get; set; } + + public string ErrorMessage { get; set; } + + public int Failures { get; set; } + } +} diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 85966b3bf5..3d866cdc2b 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -16,33 +16,32 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" /> - <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> - <PackageReference Include="PlaylistsNET" Version="1.1.2" /> - <PackageReference Include="TvDbSharper" Version="3.2.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" /> + <PackageReference Include="PlaylistsNET" Version="1.1.3" /> + <PackageReference Include="TMDbLib" Version="1.8.1" /> </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <AnalysisMode Condition=" '$(Configuration)' == 'Debug'">AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + <Nullable>disable</Nullable> </PropertyGroup> <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <None Remove="Plugins\AudioDb\Configuration\config.html" /> <EmbeddedResource Include="Plugins\AudioDb\Configuration\config.html" /> diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index f69ec9744a..12125cbb95 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1002, CS1591 using System; using System.Collections.Generic; @@ -34,6 +34,10 @@ namespace MediaBrowser.Providers.MediaInfo _fileSystem = fileSystem; } + public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images"); + + public string Name => "Image Extractor"; + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> { ImageType.Primary }; @@ -97,11 +101,11 @@ namespace MediaBrowser.Providers.MediaInfo if (item.GetType() == typeof(Audio)) { - var albumArtist = item.AlbumArtists.FirstOrDefault(); - - if (!string.IsNullOrWhiteSpace(item.Album) && !string.IsNullOrWhiteSpace(albumArtist)) + if (item.AlbumArtists.Count > 0 + && !string.IsNullOrWhiteSpace(item.Album) + && !string.IsNullOrWhiteSpace(item.AlbumArtists[0])) { - filename = (item.Album + "-" + albumArtist).GetMD5().ToString("N", CultureInfo.InvariantCulture); + filename = (item.Album + "-" + item.AlbumArtists[0]).GetMD5().ToString("N", CultureInfo.InvariantCulture); } else { @@ -121,10 +125,6 @@ namespace MediaBrowser.Providers.MediaInfo return Path.Join(AudioImagesPath, prefix, filename); } - public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images"); - - public string Name => "Image Extractor"; - public bool Supports(BaseItem item) { if (item.IsShortcut) @@ -137,9 +137,7 @@ namespace MediaBrowser.Providers.MediaInfo return false; } - var audio = item as Audio; - - return audio != null; + return item is Audio; } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs index 77f03580ab..cf271e7db1 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs @@ -37,7 +37,9 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; } - public async Task<ItemUpdateType> Probe<T>(T item, MetadataRefreshOptions options, + public async Task<ItemUpdateType> Probe<T>( + T item, + MetadataRefreshOptions options, CancellationToken cancellationToken) where T : Audio { @@ -52,19 +54,21 @@ namespace MediaBrowser.Providers.MediaInfo protocol = _mediaSourceManager.GetPathProtocol(path); } - var result = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest - { - MediaType = DlnaProfileType.Audio, - MediaSource = new MediaSourceInfo + var result = await _mediaEncoder.GetMediaInfo( + new MediaInfoRequest { - Path = path, - Protocol = protocol - } - }, cancellationToken).ConfigureAwait(false); + MediaType = DlnaProfileType.Audio, + MediaSource = new MediaSourceInfo + { + Path = path, + Protocol = protocol + } + }, + cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - Fetch(item, cancellationToken, result); + Fetch(item, result, cancellationToken); } return ItemUpdateType.MetadataImport; @@ -74,10 +78,9 @@ namespace MediaBrowser.Providers.MediaInfo /// Fetches the specified audio. /// </summary> /// <param name="audio">The audio.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="mediaInfo">The media information.</param> - /// <returns>Task.</returns> - protected void Fetch(Audio audio, CancellationToken cancellationToken, Model.MediaInfo.MediaInfo mediaInfo) + /// <param name="cancellationToken">The cancellation token.</param> + protected void Fetch(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, CancellationToken cancellationToken) { var mediaStreams = mediaInfo.MediaStreams; @@ -108,6 +111,11 @@ namespace MediaBrowser.Providers.MediaInfo audio.Name = data.Name; } + if (!string.IsNullOrEmpty(data.ForcedSortName)) + { + audio.ForcedSortName = data.ForcedSortName; + } + if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { var people = new List<PersonInfo>(); diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index 9926275ae7..4fff57273c 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -5,8 +5,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -20,9 +18,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo @@ -50,9 +46,43 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; + private readonly SubtitleResolver _subtitleResolver; + + private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None); + + public FFProbeProvider( + ILogger<FFProbeProvider> logger, + IMediaSourceManager mediaSourceManager, + IMediaEncoder mediaEncoder, + IItemRepository itemRepo, + IBlurayExaminer blurayExaminer, + ILocalizationManager localization, + IEncodingManager encodingManager, + IServerConfigurationManager config, + ISubtitleManager subtitleManager, + IChapterManager chapterManager, + ILibraryManager libraryManager) + { + _logger = logger; + _mediaEncoder = mediaEncoder; + _itemRepo = itemRepo; + _blurayExaminer = blurayExaminer; + _localization = localization; + _encodingManager = encodingManager; + _config = config; + _subtitleManager = subtitleManager; + _chapterManager = chapterManager; + _libraryManager = libraryManager; + _mediaSourceManager = mediaSourceManager; + + _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager); + } public string Name => "ffprobe"; + // Run last + public int Order => 100; + public bool HasChanged(BaseItem item, IDirectoryService directoryService) { var video = item as Video; @@ -117,45 +147,9 @@ namespace MediaBrowser.Providers.MediaInfo return FetchAudioInfo(item, options, cancellationToken); } - private SubtitleResolver _subtitleResolver; - - public FFProbeProvider( - ILogger<FFProbeProvider> logger, - IMediaSourceManager mediaSourceManager, - IMediaEncoder mediaEncoder, - IItemRepository itemRepo, - IBlurayExaminer blurayExaminer, - ILocalizationManager localization, - IEncodingManager encodingManager, - IServerConfigurationManager config, - ISubtitleManager subtitleManager, - IChapterManager chapterManager, - ILibraryManager libraryManager) - { - _logger = logger; - _mediaEncoder = mediaEncoder; - _itemRepo = itemRepo; - _blurayExaminer = blurayExaminer; - _localization = localization; - _encodingManager = encodingManager; - _config = config; - _subtitleManager = subtitleManager; - _chapterManager = chapterManager; - _libraryManager = libraryManager; - _mediaSourceManager = mediaSourceManager; - - _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager); - } - - private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None); public Task<ItemUpdateType> FetchVideoInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) where T : Video { - if (item.VideoType == VideoType.Iso) - { - return _cachedTask; - } - if (item.IsPlaceHolder) { return _cachedTask; @@ -209,7 +203,7 @@ namespace MediaBrowser.Providers.MediaInfo { item.ShortcutPath = File.ReadAllLines(item.Path) .Select(NormalizeStrmLine) - .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith("#", StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#')); } public Task<ItemUpdateType> FetchAudioInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) @@ -234,8 +228,5 @@ namespace MediaBrowser.Providers.MediaInfo return prober.Probe(item, options, cancellationToken); } - - // Run last - public int Order => 100; } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 53a6bb6195..1f17d8cd4c 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1068, CS1591 using System; using System.Collections.Generic; @@ -111,12 +111,9 @@ namespace MediaBrowser.Providers.MediaInfo } } - if (streamFileNames == null) - { - streamFileNames = Array.Empty<string>(); - } + streamFileNames ??= Array.Empty<string>(); - mediaInfoResult = await GetMediaInfo(item, streamFileNames, cancellationToken).ConfigureAwait(false); + mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } @@ -128,7 +125,6 @@ namespace MediaBrowser.Providers.MediaInfo private Task<Model.MediaInfo.MediaInfo> GetMediaInfo( Video item, - string[] streamFileNames, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -145,14 +141,14 @@ namespace MediaBrowser.Providers.MediaInfo return _mediaEncoder.GetMediaInfo( new MediaInfoRequest { - PlayableStreamFileNames = streamFileNames, ExtractChapters = true, MediaType = DlnaProfileType.Video, MediaSource = new MediaSourceInfo { Path = path, Protocol = protocol, - VideoType = item.VideoType + VideoType = item.VideoType, + IsoType = item.IsoType } }, cancellationToken); @@ -396,6 +392,12 @@ namespace MediaBrowser.Providers.MediaInfo } } + if (video is MusicVideo musicVideo) + { + musicVideo.Album = data.Album; + musicVideo.Artists = data.Artists; + } + if (data.ProductionYear.HasValue) { if (!video.ProductionYear.HasValue || isFullRefresh) @@ -438,6 +440,11 @@ namespace MediaBrowser.Providers.MediaInfo video.Name = data.Name; } } + + if (!string.IsNullOrWhiteSpace(data.ForcedSortName)) + { + video.ForcedSortName = data.ForcedSortName; + } } // If we don't have a ProductionYear try and get it from PremiereDate @@ -539,17 +546,18 @@ namespace MediaBrowser.Providers.MediaInfo if (enableSubtitleDownloading && enabled) { - var downloadedLanguages = await new SubtitleDownloader(_logger, - _subtitleManager) - .DownloadSubtitles(video, - currentStreams.Concat(externalSubtitleStreams).ToList(), - skipIfEmbeddedSubtitlesPresent, - skipIfAudioTrackMatches, - requirePerfectMatch, - subtitleDownloadLanguages, - libraryOptions.DisabledSubtitleFetchers, - libraryOptions.SubtitleFetcherOrder, - cancellationToken).ConfigureAwait(false); + var downloadedLanguages = await new SubtitleDownloader( + _logger, + _subtitleManager).DownloadSubtitles( + video, + currentStreams.Concat(externalSubtitleStreams).ToList(), + skipIfEmbeddedSubtitlesPresent, + skipIfAudioTrackMatches, + requirePerfectMatch, + subtitleDownloadLanguages, + libraryOptions.DisabledSubtitleFetchers, + libraryOptions.SubtitleFetcherOrder, + cancellationToken).ConfigureAwait(false); // Rescan if (downloadedLanguages.Count > 0) @@ -620,7 +628,7 @@ namespace MediaBrowser.Providers.MediaInfo item.RunTimeTicks = GetRuntime(primaryTitle); } - return _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null, titleNumber) + return _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, titleNumber) .Select(Path.GetFileName) .ToArray(); } diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs index acddb73d0b..aa0743bd02 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs @@ -1,7 +1,8 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1002, CS1591 using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -42,8 +43,16 @@ namespace MediaBrowser.Providers.MediaInfo foreach (var lang in languages) { - var downloaded = await DownloadSubtitles(video, mediaStreams, skipIfEmbeddedSubtitlesPresent, - skipIfAudioTrackMatches, requirePerfectMatch, lang, disabledSubtitleFetchers, subtitleFetcherOrder, cancellationToken).ConfigureAwait(false); + var downloaded = await DownloadSubtitles( + video, + mediaStreams, + skipIfEmbeddedSubtitlesPresent, + skipIfAudioTrackMatches, + requirePerfectMatch, + lang, + disabledSubtitleFetchers, + subtitleFetcherOrder, + cancellationToken).ConfigureAwait(false); if (downloaded) { @@ -54,7 +63,8 @@ namespace MediaBrowser.Providers.MediaInfo return downloadedLanguages; } - public Task<bool> DownloadSubtitles(Video video, + public Task<bool> DownloadSubtitles( + Video video, List<MediaStream> mediaStreams, bool skipIfEmbeddedSubtitlesPresent, bool skipIfAudioTrackMatches, @@ -90,11 +100,21 @@ namespace MediaBrowser.Providers.MediaInfo return Task.FromResult(false); } - return DownloadSubtitles(video, mediaStreams, skipIfEmbeddedSubtitlesPresent, skipIfAudioTrackMatches, - requirePerfectMatch, lang, disabledSubtitleFetchers, subtitleFetcherOrder, mediaType, cancellationToken); + return DownloadSubtitles( + video, + mediaStreams, + skipIfEmbeddedSubtitlesPresent, + skipIfAudioTrackMatches, + requirePerfectMatch, + lang, + disabledSubtitleFetchers, + subtitleFetcherOrder, + mediaType, + cancellationToken); } - private async Task<bool> DownloadSubtitles(Video video, + private async Task<bool> DownloadSubtitles( + Video video, List<MediaStream> mediaStreams, bool skipIfEmbeddedSubtitlesPresent, bool skipIfAudioTrackMatches, @@ -153,9 +173,7 @@ namespace MediaBrowser.Providers.MediaInfo SubtitleFetcherOrder = subtitleFetcherOrder }; - var episode = video as Episode; - - if (episode != null) + if (video is Episode episode) { request.IndexNumberEnd = episode.IndexNumberEnd; request.SeriesName = episode.SeriesName; diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index 43659b68cc..b3d0659290 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -1,9 +1,9 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1002, CS1591 using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; -using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -15,17 +15,6 @@ namespace MediaBrowser.Providers.MediaInfo { private readonly ILocalizationManager _localization; - private static readonly HashSet<string> SubtitleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) - { - ".srt", - ".ssa", - ".ass", - ".sub", - ".smi", - ".sami", - ".vtt" - }; - public SubtitleResolver(ILocalizationManager localization) { _localization = localization; @@ -66,134 +55,142 @@ namespace MediaBrowser.Providers.MediaInfo return streams; } - public List<string> GetExternalSubtitleFiles(Video video, - IDirectoryService directoryService, - bool clearCache) + public IEnumerable<string> GetExternalSubtitleFiles( + Video video, + IDirectoryService directoryService, + bool clearCache) { - var list = new List<string>(); - if (!video.IsFileProtocol) { - return list; + yield break; } var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache); foreach (var stream in streams) { - list.Add(stream.Path); + yield return stream.Path; } - - return list; } - private void AddExternalSubtitleStreams(List<MediaStream> streams, string folder, + public void AddExternalSubtitleStreams( + List<MediaStream> streams, string videoPath, int startIndex, - IDirectoryService directoryService, - bool clearCache) + IReadOnlyList<string> files) { - var files = directoryService.GetFilePaths(folder, clearCache).OrderBy(i => i).ToArray(); - - AddExternalSubtitleStreams(streams, videoPath, startIndex, files); - } + var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath); - public void AddExternalSubtitleStreams(List<MediaStream> streams, - string videoPath, - int startIndex, - string[] files) - { - var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(videoPath); - videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoFileNameWithoutExtension); - - foreach (var fullName in files) + for (var i = 0; i < files.Count; i++) { - var extension = Path.GetExtension(fullName); - - if (!SubtitleExtensions.Contains(extension)) + var fullName = files[i]; + var extension = Path.GetExtension(fullName.AsSpan()); + if (!IsSubtitleExtension(extension)) { continue; } - var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullName); - fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fileNameWithoutExtension); + var fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fullName); - if (!string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase) && - !fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var codec = Path.GetExtension(fullName).ToLowerInvariant().TrimStart('.'); + MediaStream mediaStream; - if (string.Equals(codec, "txt", StringComparison.OrdinalIgnoreCase)) + // The subtitle filename must either be equal to the video filename or start with the video filename followed by a dot + if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { - codec = "srt"; - } - - // If the subtitle file matches the video file name - if (string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) - { - streams.Add(new MediaStream + mediaStream = new MediaStream { Index = startIndex++, Type = MediaStreamType.Subtitle, IsExternal = true, - Path = fullName, - Codec = codec - }); + Path = fullName + }; } - else if (fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase)) + else if (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length + && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.' + && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { - var isForced = fullName.IndexOf(".forced.", StringComparison.OrdinalIgnoreCase) != -1 || - fullName.IndexOf(".foreign.", StringComparison.OrdinalIgnoreCase) != -1; + var isForced = fullName.Contains(".forced.", StringComparison.OrdinalIgnoreCase) + || fullName.Contains(".foreign.", StringComparison.OrdinalIgnoreCase); - var isDefault = fullName.IndexOf(".default.", StringComparison.OrdinalIgnoreCase) != -1; + var isDefault = fullName.Contains(".default.", StringComparison.OrdinalIgnoreCase); // Support xbmc naming conventions - 300.spanish.srt - var language = fileNameWithoutExtension - .Replace(".forced", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(".foreign", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(".default", string.Empty, StringComparison.OrdinalIgnoreCase) - .Split('.') - .LastOrDefault(); + var languageSpan = fileNameWithoutExtension; + while (languageSpan.Length > 0) + { + var lastDot = languageSpan.LastIndexOf('.'); + var currentSlice = languageSpan[lastDot..]; + if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase) + || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase) + || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase)) + { + languageSpan = languageSpan[..lastDot]; + continue; + } + + languageSpan = languageSpan[(lastDot + 1)..]; + break; + } // Try to translate to three character code // Be flexible and check against both the full and three character versions + var language = languageSpan.ToString(); var culture = _localization.FindLanguageInfo(language); - if (culture != null) - { - language = culture.ThreeLetterISOLanguageName; - } + language = culture == null ? language : culture.ThreeLetterISOLanguageName; - streams.Add(new MediaStream + mediaStream = new MediaStream { Index = startIndex++, Type = MediaStreamType.Subtitle, IsExternal = true, Path = fullName, - Codec = codec, Language = language, IsForced = isForced, IsDefault = isDefault - }); + }; } + else + { + continue; + } + + mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant(); + + streams.Add(mediaStream); } } - private string NormalizeFilenameForSubtitleComparison(string filename) + private static bool IsSubtitleExtension(ReadOnlySpan<char> extension) { - // Try to account for sloppy file naming - filename = filename.Replace("_", string.Empty); - filename = filename.Replace(" ", string.Empty); + return extension.Equals(".srt", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".ssa", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".ass", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".sub", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".vtt", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".smi", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".sami", StringComparison.OrdinalIgnoreCase); + } - // can't normalize this due to languages such as pt-br - // filename = filename.Replace("-", string.Empty); + private static ReadOnlySpan<char> NormalizeFilenameForSubtitleComparison(string filename) + { + // Try to account for sloppy file naming + filename = filename.Replace("_", string.Empty, StringComparison.Ordinal); + filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal); + return Path.GetFileNameWithoutExtension(filename.AsSpan()); + } - // filename = filename.Replace(".", string.Empty); + private void AddExternalSubtitleStreams( + List<MediaStream> streams, + string folder, + string videoPath, + int startIndex, + IDirectoryService directoryService, + bool clearCache) + { + var files = directoryService.GetFilePaths(folder, clearCache, true); - return filename; + AddExternalSubtitleStreams(streams, videoPath, startIndex, files); } } } diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 91ab7b4acb..9804ec3bb4 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -12,11 +12,10 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Globalization; namespace MediaBrowser.Providers.MediaInfo { @@ -25,29 +24,37 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; - private readonly IMediaSourceManager _mediaSourceManager; private readonly ILogger<SubtitleScheduledTask> _logger; - private readonly IJsonSerializer _json; private readonly ILocalizationManager _localization; public SubtitleScheduledTask( ILibraryManager libraryManager, - IJsonSerializer json, IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger<SubtitleScheduledTask> logger, - IMediaSourceManager mediaSourceManager, ILocalizationManager localization) { _libraryManager = libraryManager; _config = config; _subtitleManager = subtitleManager; _logger = logger; - _mediaSourceManager = mediaSourceManager; - _json = json; _localization = localization; } + public string Name => _localization.GetLocalizedString("TaskDownloadMissingSubtitles"); + + public string Description => _localization.GetLocalizedString("TaskDownloadMissingSubtitlesDescription"); + + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + public string Key => "DownloadSubtitles"; + + public bool IsHidden => false; + + public bool IsEnabled => true; + + public bool IsLogged => true; + private SubtitleOptions GetOptions() { return _config.GetConfiguration<SubtitleOptions>("subtitles"); @@ -66,23 +73,23 @@ namespace MediaBrowser.Providers.MediaInfo var libraryOptions = _libraryManager.GetLibraryOptions(library); string[] subtitleDownloadLanguages; - bool SkipIfEmbeddedSubtitlesPresent; - bool SkipIfAudioTrackMatches; - bool RequirePerfectMatch; + bool skipIfEmbeddedSubtitlesPresent; + bool skipIfAudioTrackMatches; + bool requirePerfectMatch; if (libraryOptions.SubtitleDownloadLanguages == null) { subtitleDownloadLanguages = options.DownloadLanguages; - SkipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; - SkipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; - RequirePerfectMatch = options.RequirePerfectMatch; + skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; + requirePerfectMatch = options.RequirePerfectMatch; } else { subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - SkipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - SkipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - RequirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; } foreach (var lang in subtitleDownloadLanguages) @@ -98,12 +105,12 @@ namespace MediaBrowser.Providers.MediaInfo Recursive = true }; - if (SkipIfAudioTrackMatches) + if (skipIfAudioTrackMatches) { query.HasNoAudioTrackWithLanguage = lang; } - if (SkipIfEmbeddedSubtitlesPresent) + if (skipIfEmbeddedSubtitlesPresent) { // Exclude if it already has any subtitles of the same language query.HasNoSubtitleTrackWithLanguage = lang; @@ -160,36 +167,37 @@ namespace MediaBrowser.Providers.MediaInfo var libraryOptions = _libraryManager.GetLibraryOptions(video); string[] subtitleDownloadLanguages; - bool SkipIfEmbeddedSubtitlesPresent; - bool SkipIfAudioTrackMatches; - bool RequirePerfectMatch; + bool skipIfEmbeddedSubtitlesPresent; + bool skipIfAudioTrackMatches; + bool requirePerfectMatch; if (libraryOptions.SubtitleDownloadLanguages == null) { subtitleDownloadLanguages = options.DownloadLanguages; - SkipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; - SkipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; - RequirePerfectMatch = options.RequirePerfectMatch; + skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; + requirePerfectMatch = options.RequirePerfectMatch; } else { subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - SkipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - SkipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - RequirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; } - var downloadedLanguages = await new SubtitleDownloader(_logger, - _subtitleManager) - .DownloadSubtitles(video, - mediaStreams, - SkipIfEmbeddedSubtitlesPresent, - SkipIfAudioTrackMatches, - RequirePerfectMatch, - subtitleDownloadLanguages, - libraryOptions.DisabledSubtitleFetchers, - libraryOptions.SubtitleFetcherOrder, - cancellationToken).ConfigureAwait(false); + var downloadedLanguages = await new SubtitleDownloader( + _logger, + _subtitleManager).DownloadSubtitles( + video, + mediaStreams, + skipIfEmbeddedSubtitlesPresent, + skipIfAudioTrackMatches, + requirePerfectMatch, + subtitleDownloadLanguages, + libraryOptions.DisabledSubtitleFetchers, + libraryOptions.SubtitleFetcherOrder, + cancellationToken).ConfigureAwait(false); // Rescan if (downloadedLanguages.Count > 0) @@ -203,25 +211,11 @@ namespace MediaBrowser.Providers.MediaInfo public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new[] { - + return new[] + { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } - - public string Name => _localization.GetLocalizedString("TaskDownloadMissingSubtitles"); - - public string Description => _localization.GetLocalizedString("TaskDownloadMissingSubtitlesDescription"); - - public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); - - public string Key => "DownloadSubtitles"; - - public bool IsHidden => false; - - public bool IsEnabled => true; - - public bool IsLogged => true; } } diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index e23854d900..30af6710ab 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -29,6 +30,11 @@ namespace MediaBrowser.Providers.MediaInfo _fileSystem = fileSystem; } + public string Name => "Screen Grabber"; + + // Make sure this comes after internet image providers + public int Order => 100; + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> { ImageType.Primary }; @@ -45,7 +51,7 @@ namespace MediaBrowser.Providers.MediaInfo } // No support for this - if (video.VideoType == VideoType.Iso || video.VideoType == VideoType.Dvd || video.VideoType == VideoType.BluRay) + if (video.VideoType == VideoType.Dvd) { return Task.FromResult(new DynamicImageResponse { HasImage = false }); } @@ -64,11 +70,7 @@ namespace MediaBrowser.Providers.MediaInfo { var protocol = item.PathProtocol ?? MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument( - _fileSystem, - item.Path, - null, - item.GetPlayableStreamFileNames()); + var inputPath = item.Path; var mediaStreams = item.GetMediaStreams(); @@ -102,7 +104,14 @@ namespace MediaBrowser.Providers.MediaInfo } } - extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, protocol, imageStream, videoIndex, cancellationToken).ConfigureAwait(false); + MediaSourceInfo mediaSource = new MediaSourceInfo + { + VideoType = item.VideoType, + IsoType = item.IsoType, + Protocol = item.PathProtocol.Value, + }; + + extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, videoIndex, cancellationToken).ConfigureAwait(false); } else { @@ -114,8 +123,14 @@ namespace MediaBrowser.Providers.MediaInfo : TimeSpan.FromSeconds(10); var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); + var mediaSource = new MediaSourceInfo + { + VideoType = item.VideoType, + IsoType = item.IsoType, + Protocol = item.PathProtocol.Value, + }; - extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, protocol, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false); + extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false); } return new DynamicImageResponse @@ -127,8 +142,6 @@ namespace MediaBrowser.Providers.MediaInfo }; } - public string Name => "Screen Grabber"; - public bool Supports(BaseItem item) { if (item.IsShortcut) @@ -141,16 +154,12 @@ namespace MediaBrowser.Providers.MediaInfo return false; } - var video = item as Video; - - if (video != null && !video.IsPlaceHolder && video.IsCompleteMedia) + if (item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia) { return true; } return false; } - // Make sure this comes after internet image providers - public int Order => 100; } } diff --git a/MediaBrowser.Providers/Movies/MovieExternalIds.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs index 14080841c2..a8d74aa0b5 100644 --- a/MediaBrowser.Providers/Movies/MovieExternalIds.cs +++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs @@ -36,22 +36,4 @@ namespace MediaBrowser.Providers.Movies return item is Movie || item is MusicVideo || item is Series || item is Episode || item is Trailer; } } - - public class ImdbPersonExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "IMDb"; - - /// <inheritdoc /> - public string Key => MetadataProvider.Imdb.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Person; - - /// <inheritdoc /> - public string UrlFormatString => "https://www.imdb.com/name/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Person; - } } diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs new file mode 100644 index 0000000000..8151ab4715 --- /dev/null +++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Movies +{ + public class ImdbPersonExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "IMDb"; + + /// <inheritdoc /> + public string Key => MetadataProvider.Imdb.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Person; + + /// <inheritdoc /> + public string UrlFormatString => "https://www.imdb.com/name/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Person; + } +} diff --git a/MediaBrowser.Providers/Music/Extensions.cs b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs index dddfd02e4f..dddfd02e4f 100644 --- a/MediaBrowser.Providers/Music/Extensions.cs +++ b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs diff --git a/MediaBrowser.Providers/Music/MusicExternalIds.cs b/MediaBrowser.Providers/Music/ImvdbId.cs index a1726b9965..a1726b9965 100644 --- a/MediaBrowser.Providers/Music/MusicExternalIds.cs +++ b/MediaBrowser.Providers/Music/ImvdbId.cs diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs index 5cc0a527ea..067d585cba 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs @@ -10,7 +10,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; using PlaylistsNET.Content; @@ -23,16 +22,17 @@ namespace MediaBrowser.Providers.Playlists IHasItemChangeMonitor { private readonly ILogger<PlaylistItemsProvider> _logger; - private IFileSystem _fileSystem; - public PlaylistItemsProvider(IFileSystem fileSystem, ILogger<PlaylistItemsProvider> logger) + public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger) { - _fileSystem = fileSystem; _logger = logger; } public string Name => "Playlist Reader"; + // Run last + public int Order => 100; + public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken) { var path = item.Path; @@ -163,7 +163,5 @@ namespace MediaBrowser.Providers.Playlists return false; } - // Run last - public int Order => 100; } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs new file mode 100644 index 0000000000..138cfef19a --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class AudioDbAlbumExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "TheAudioDb"; + + /// <inheritdoc /> + public string Key => MetadataProvider.AudioDbAlbum.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => null; + + /// <inheritdoc /> + public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is MusicAlbum; + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs index c9dac9ecd8..36d8eeb401 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs @@ -1,16 +1,19 @@ #pragma warning disable CS1591 using System.Collections.Generic; +using System.IO; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.AudioDb { @@ -18,13 +21,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { private readonly IServerConfigurationManager _config; private readonly IHttpClientFactory _httpClientFactory; - private readonly IJsonSerializer _json; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IJsonSerializer json) + public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory) { _config = config; _httpClientFactory = httpClientFactory; - _json = json; } /// <inheritdoc /> @@ -55,7 +57,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id); - var obj = _json.DeserializeFromFile<AudioDbAlbumProvider.RootObject>(path); + await using FileStream jsonStream = File.OpenRead(path); + var obj = await JsonSerializer.DeserializeAsync<AudioDbAlbumProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (obj != null && obj.album != null && obj.album.Count > 0) { @@ -96,12 +99,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return httpClient.GetAsync(url, cancellationToken); } /// <inheritdoc /> public bool Supports(BaseItem item) - => Plugin.Instance.Configuration.Enable && item is MusicAlbum; + => item is MusicAlbum; } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index 321144edf9..9f2f7fc11e 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1002, CS1591, SA1300 using System; using System.Collections.Generic; @@ -6,17 +6,19 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Music; namespace MediaBrowser.Providers.Plugins.AudioDb @@ -26,16 +28,17 @@ namespace MediaBrowser.Providers.Plugins.AudioDb private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; private readonly IHttpClientFactory _httpClientFactory; - private readonly IJsonSerializer _json; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; +#pragma warning disable SA1401, CA2211 public static AudioDbAlbumProvider Current; +#pragma warning restore SA1401, CA2211 - public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json) + public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory) { _config = config; _fileSystem = fileSystem; _httpClientFactory = httpClientFactory; - _json = json; Current = this; } @@ -55,13 +58,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<MusicAlbum>(); - - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return result; - } - var id = info.GetReleaseGroupId(); if (!string.IsNullOrWhiteSpace(id)) @@ -70,7 +66,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = GetAlbumInfoPath(_config.ApplicationPaths, id); - var obj = _json.DeserializeFromFile<RootObject>(path); + await using FileStream jsonStream = File.OpenRead(path); + var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (obj != null && obj.album != null && obj.album.Count > 0) { @@ -173,9 +170,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb Directory.CreateDirectory(Path.GetDirectoryName(path)); - using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true); await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } @@ -200,6 +198,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb return Path.Combine(dataPath, "album.json"); } + /// <inheritdoc /> + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + +#pragma warning disable CA1034, CA2227 public class Album { public string idAlbum { get; set; } @@ -283,11 +288,5 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { public List<Album> album { get; set; } } - - /// <inheritdoc /> - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs new file mode 100644 index 0000000000..8aceb48c0c --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class AudioDbArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "TheAudioDb"; + + /// <inheritdoc /> + public string Key => MetadataProvider.AudioDbArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; + + /// <inheritdoc /> + public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is MusicArtist; + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs index 6512668681..aa61a56f66 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs @@ -1,16 +1,19 @@ #pragma warning disable CS1591 using System.Collections.Generic; +using System.IO; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.AudioDb { @@ -18,12 +21,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { private readonly IServerConfigurationManager _config; private readonly IHttpClientFactory _httpClientFactory; - private readonly IJsonSerializer _json; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - public AudioDbArtistImageProvider(IServerConfigurationManager config, IJsonSerializer json, IHttpClientFactory httpClientFactory) + public AudioDbArtistImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory) { _config = config; - _json = json; _httpClientFactory = httpClientFactory; } @@ -57,7 +59,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id); - var obj = _json.DeserializeFromFile<AudioDbArtistProvider.RootObject>(path); + await using FileStream jsonStream = File.OpenRead(path); + var obj = await JsonSerializer.DeserializeAsync<AudioDbArtistProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (obj != null && obj.artists != null && obj.artists.Count > 0) { @@ -137,12 +140,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return httpClient.GetAsync(url, cancellationToken); } /// <inheritdoc /> public bool Supports(BaseItem item) - => Plugin.Instance.Configuration.Enable && item is MusicArtist; + => item is MusicArtist; } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 708426500b..2857c6c13a 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -1,46 +1,47 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1034, CS1591, CA1002, SA1028, SA1300 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Music; namespace MediaBrowser.Providers.Plugins.AudioDb { public class AudioDbArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IHasOrder { + private const string ApiKey = "195003"; + public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey; + private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; private readonly IHttpClientFactory _httpClientFactory; - private readonly IJsonSerializer _json; - - public static AudioDbArtistProvider Current; - - private const string ApiKey = "195003"; - public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json) + public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory) { _config = config; _fileSystem = fileSystem; _httpClientFactory = httpClientFactory; - _json = json; Current = this; } + public static AudioDbArtistProvider Current { get; private set; } + /// <inheritdoc /> public string Name => "TheAudioDB"; @@ -56,13 +57,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<MusicArtist>(); - - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return result; - } - var id = info.GetMusicBrainzArtistId(); if (!string.IsNullOrWhiteSpace(id)) @@ -71,7 +65,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = GetArtistInfoPath(_config.ApplicationPaths, id); - var obj = _json.DeserializeFromFile<RootObject>(path); + await using FileStream jsonStream = File.OpenRead(path); + var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (obj != null && obj.artists != null && obj.artists.Count > 0) { @@ -154,12 +149,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); - using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); Directory.CreateDirectory(Path.GetDirectoryName(path)); - await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true); await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } @@ -187,6 +183,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb return Path.Combine(dataPath, "artist.json"); } + /// <inheritdoc /> + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + public class Artist { public string idArtist { get; set; } @@ -272,15 +274,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string strLocked { get; set; } } +#pragma warning disable CA2227 public class RootObject { public List<Artist> artists { get; set; } } - - /// <inheritdoc /> - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs new file mode 100644 index 0000000000..014481da24 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class AudioDbOtherAlbumExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "TheAudioDb"; + + /// <inheritdoc /> + public string Key => MetadataProvider.AudioDbAlbum.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Album; + + /// <inheritdoc /> + public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs new file mode 100644 index 0000000000..7875391043 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class AudioDbOtherArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "TheAudioDb"; + + /// <inheritdoc /> + public string Key => MetadataProvider.AudioDbArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; + + /// <inheritdoc /> + public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs index 9657a290fc..664474dcdd 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs @@ -6,8 +6,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { public class PluginConfiguration : BasePluginConfiguration { - public bool Enable { get; set; } - public bool ReplaceAlbumName { get; set; } } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html index 82f26a8f26..eab252005f 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -9,10 +9,6 @@ <div class="content-primary"> <form class="configForm"> <label class="checkboxContainer"> - <input is="emby-checkbox" type="checkbox" id="enable" /> - <span>Enable this provider for metadata searches on artists and albums.</span> - </label> - <label class="checkboxContainer"> <input is="emby-checkbox" type="checkbox" id="replaceAlbumName" /> <span>When an album is found during a metadata search, replace the name with the value on the server.</span> </label> @@ -32,9 +28,8 @@ .addEventListener('pageshow', function () { Dashboard.showLoadingMsg(); ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - document.querySelector('#enable').checked = config.Enable; document.querySelector('#replaceAlbumName').checked = config.ReplaceAlbumName; - + Dashboard.hideLoadingMsg(); }); }); @@ -42,14 +37,13 @@ document.querySelector('.configForm') .addEventListener('submit', function (e) { Dashboard.showLoadingMsg(); - + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - config.Enable = document.querySelector('#enable').checked; config.ReplaceAlbumName = document.querySelector('#replaceAlbumName').checked; - + ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); - + e.preventDefault(); return false; }); diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs b/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs deleted file mode 100644 index 1cc1f0fa18..0000000000 --- a/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs +++ /dev/null @@ -1,81 +0,0 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; - -namespace MediaBrowser.Providers.Plugins.AudioDb -{ - public class AudioDbAlbumExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "TheAudioDb"; - - /// <inheritdoc /> - public string Key => MetadataProvider.AudioDbAlbum.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => null; - - /// <inheritdoc /> - public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is MusicAlbum; - } - - public class AudioDbOtherAlbumExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "TheAudioDb"; - - /// <inheritdoc /> - public string Key => MetadataProvider.AudioDbAlbum.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Album; - - /// <inheritdoc /> - public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio; - } - - public class AudioDbArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "TheAudioDb"; - - /// <inheritdoc /> - public string Key => MetadataProvider.AudioDbArtist.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; - - /// <inheritdoc /> - public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is MusicArtist; - } - - public class AudioDbOtherArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "TheAudioDb"; - - /// <inheritdoc /> - public string Key => MetadataProvider.AudioDbArtist.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; - - /// <inheritdoc /> - public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } -} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs index 54054d0153..ba0d7b5697 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using System; using System.Collections.Generic; @@ -11,6 +11,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages { + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + public static Plugin Instance { get; private set; } public override Guid Id => new Guid("a629c0da-fac5-4c7e-931a-7174223f14c8"); @@ -22,12 +28,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb // TODO remove when plugin removed from server. public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml"; - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) - { - Instance = this; - } - public IEnumerable<PluginPageInfo> GetPages() { yield return new PluginPageInfo diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs index 980da9a010..0cec9e359c 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -43,8 +43,6 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz } } - public bool Enable { get; set; } - public bool ReplaceArtistName { get; set; } } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 1945e6cb49..6f1296bb77 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -17,10 +17,6 @@ <div class="fieldDescription">Span of time between requests in milliseconds. The official server is limited to one request every two seconds.</div> </div> <label class="checkboxContainer"> - <input is="emby-checkbox" type="checkbox" id="enable" /> - <span>Enable this provider for metadata searches on artists and albums.</span> - </label> - <label class="checkboxContainer"> <input is="emby-checkbox" type="checkbox" id="replaceArtistName" /> <span>When an artist is found during a metadata search, replace the artist name with the value on the server.</span> </label> @@ -46,7 +42,7 @@ bubbles: true, cancelable: false })); - + var rateLimit = document.querySelector('#rateLimit'); rateLimit.value = config.RateLimit; rateLimit.dispatchEvent(new Event('change', { @@ -54,26 +50,24 @@ cancelable: false })); - document.querySelector('#enable').checked = config.Enable; document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName; Dashboard.hideLoadingMsg(); }); }); - + document.querySelector('.musicBrainzConfigForm') .addEventListener('submit', function (e) { Dashboard.showLoadingMsg(); - + ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { config.Server = document.querySelector('#server').value; config.RateLimit = document.querySelector('#rateLimit').value; - config.Enable = document.querySelector('#enable').checked; config.ReplaceArtistName = document.querySelector('#replaceArtistName').checked; - + ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); - + e.preventDefault(); return false; }); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs deleted file mode 100644 index 5600c389c0..0000000000 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs +++ /dev/null @@ -1,119 +0,0 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.MusicBrainz; - -namespace MediaBrowser.Providers.Music -{ - public class MusicBrainzReleaseGroupExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; - - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; - - /// <inheritdoc /> - public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } - - public class MusicBrainzAlbumArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; - - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; - - /// <inheritdoc /> - public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio; - } - - public class MusicBrainzAlbumExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; - - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzAlbum.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Album; - - /// <inheritdoc /> - public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } - - public class MusicBrainzArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; - - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzArtist.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; - - /// <inheritdoc /> - public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is MusicArtist; - } - - public class MusicBrainzOtherArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; - - /// <inheritdoc /> - - public string Key => MetadataProvider.MusicBrainzArtist.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; - - /// <inheritdoc /> - public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } - - public class MusicBrainzTrackId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; - - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzTrack.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Track; - - /// <inheritdoc /> - public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio; - } -} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs new file mode 100644 index 0000000000..1b37e2a60d --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Plugins.MusicBrainz; + +namespace MediaBrowser.Providers.Music +{ + public class MusicBrainzAlbumArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs new file mode 100644 index 0000000000..ef095111a8 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Plugins.MusicBrainz; + +namespace MediaBrowser.Providers.Music +{ + public class MusicBrainzAlbumExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzAlbum.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Album; + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 7f10e6922b..c97affdbf2 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591, SA1401 using System; using System.Collections.Generic; @@ -8,12 +8,12 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -23,40 +23,36 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Music { - public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder + public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable { /// <summary> - /// The Jellyfin user-agent is unrestricted but source IP must not exceed - /// one request per second, therefore we rate limit to avoid throttling. - /// Be prudent, use a value slightly above the minimun required. - /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting - /// </summary> - private readonly long _musicBrainzQueryIntervalMs; - - /// <summary> /// For each single MB lookup/search, this is the maximum number of /// attempts that shall be made whilst receiving a 503 Server /// Unavailable (indicating throttled) response. /// </summary> private const uint MusicBrainzQueryAttempts = 5u; - internal static MusicBrainzAlbumProvider Current; + /// <summary> + /// The Jellyfin user-agent is unrestricted but source IP must not exceed + /// one request per second, therefore we rate limit to avoid throttling. + /// Be prudent, use a value slightly above the minimun required. + /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting. + /// </summary> + private readonly long _musicBrainzQueryIntervalMs; private readonly IHttpClientFactory _httpClientFactory; - private readonly IApplicationHost _appHost; private readonly ILogger<MusicBrainzAlbumProvider> _logger; private readonly string _musicBrainzBaseUrl; + private SemaphoreSlim _apiRequestLock = new SemaphoreSlim(1, 1); private Stopwatch _stopWatchMusicBrainz = new Stopwatch(); public MusicBrainzAlbumProvider( IHttpClientFactory httpClientFactory, - IApplicationHost appHost, ILogger<MusicBrainzAlbumProvider> logger) { _httpClientFactory = httpClientFactory; - _appHost = appHost; _logger = logger; _musicBrainzBaseUrl = Plugin.Instance.Configuration.Server; @@ -68,6 +64,8 @@ namespace MediaBrowser.Providers.Music Current = this; } + internal static MusicBrainzAlbumProvider Current { get; private set; } + /// <inheritdoc /> public string Name => "MusicBrainz"; @@ -77,12 +75,6 @@ namespace MediaBrowser.Providers.Music /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) { - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return Enumerable.Empty<RemoteSearchResult>(); - } - var releaseId = searchInfo.GetReleaseId(); var releaseGroupId = searchInfo.GetReleaseGroupId(); @@ -111,7 +103,7 @@ namespace MediaBrowser.Providers.Music else { // I'm sure there is a better way but for now it resolves search for 12" Mixes - var queryName = searchInfo.Name.Replace("\"", string.Empty); + var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal); url = string.Format( CultureInfo.InvariantCulture, @@ -124,7 +116,7 @@ namespace MediaBrowser.Providers.Music if (!string.IsNullOrWhiteSpace(url)) { using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return GetResultsFromResponse(stream); } @@ -133,72 +125,62 @@ namespace MediaBrowser.Providers.Music private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream) { - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + using var oReader = new StreamReader(stream, Encoding.UTF8); + var settings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; + + using var reader = XmlReader.Create(oReader, settings); + var results = ReleaseResult.Parse(reader); + + return results.Select(i => { - var settings = new XmlReaderSettings() + var result = new RemoteSearchResult { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true + Name = i.Title, + ProductionYear = i.Year }; - using (var reader = XmlReader.Create(oReader, settings)) + if (i.Artists.Count > 0) { - var results = ReleaseResult.Parse(reader); - - return results.Select(i => + result.AlbumArtist = new RemoteSearchResult { - var result = new RemoteSearchResult - { - Name = i.Title, - ProductionYear = i.Year - }; - - if (i.Artists.Count > 0) - { - result.AlbumArtist = new RemoteSearchResult - { - SearchProviderName = Name, - Name = i.Artists[0].Item1 - }; + SearchProviderName = Name, + Name = i.Artists[0].Item1 + }; - result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2); - } - - if (!string.IsNullOrWhiteSpace(i.ReleaseId)) - { - result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId); - } + result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2); + } - if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId)) - { - result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId); - } + if (!string.IsNullOrWhiteSpace(i.ReleaseId)) + { + result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId); + } - return result; - }); + if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId)) + { + result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId); } - } + + return result; + }); } /// <inheritdoc /> - public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo id, CancellationToken cancellationToken) + public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken) { - var releaseId = id.GetReleaseId(); - var releaseGroupId = id.GetReleaseGroupId(); + var releaseId = info.GetReleaseId(); + var releaseGroupId = info.GetReleaseGroupId(); var result = new MetadataResult<MusicAlbum> { Item = new MusicAlbum() }; - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return result; - } - // If we have a release group Id but not a release Id... if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId)) { @@ -208,9 +190,9 @@ namespace MediaBrowser.Providers.Music if (string.IsNullOrWhiteSpace(releaseId)) { - var artistMusicBrainzId = id.GetMusicBrainzArtistId(); + var artistMusicBrainzId = info.GetMusicBrainzArtistId(); - var releaseResult = await GetReleaseResult(artistMusicBrainzId, id.GetAlbumArtist(), id.Name, cancellationToken).ConfigureAwait(false); + var releaseResult = await GetReleaseResult(artistMusicBrainzId, info.GetAlbumArtist(), info.Name, cancellationToken).ConfigureAwait(false); if (releaseResult != null) { @@ -276,12 +258,14 @@ namespace MediaBrowser.Providers.Music private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken) { - var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/release/?query=\"{0}\" AND arid:{1}", + var url = string.Format( + CultureInfo.InvariantCulture, + "/ws/2/release/?query=\"{0}\" AND arid:{1}", WebUtility.UrlEncode(albumName), artistId); using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var oReader = new StreamReader(stream, Encoding.UTF8); var settings = new XmlReaderSettings { @@ -304,7 +288,7 @@ namespace MediaBrowser.Providers.Music WebUtility.UrlEncode(artistName)); using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var oReader = new StreamReader(stream, Encoding.UTF8); var settings = new XmlReaderSettings() { @@ -318,184 +302,7 @@ namespace MediaBrowser.Providers.Music return ReleaseResult.Parse(reader).FirstOrDefault(); } - private class ReleaseResult - { - public string ReleaseId; - public string ReleaseGroupId; - public string Title; - public string Overview; - public int? Year; - - public List<ValueTuple<string, string>> Artists = new List<ValueTuple<string, string>>(); - - public static IEnumerable<ReleaseResult> Parse(XmlReader reader) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "release-list": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subReader = reader.ReadSubtree()) - { - return ParseReleaseList(subReader).ToList(); - } - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } - - return Enumerable.Empty<ReleaseResult>(); - } - - private static IEnumerable<ReleaseResult> ParseReleaseList(XmlReader reader) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "release": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - var releaseId = reader.GetAttribute("id"); - - using (var subReader = reader.ReadSubtree()) - { - var release = ParseRelease(subReader, releaseId); - if (release != null) - { - yield return release; - } - } - - break; - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } - } - - private static ReleaseResult ParseRelease(XmlReader reader, string releaseId) - { - var result = new ReleaseResult - { - ReleaseId = releaseId - }; - - reader.MoveToContent(); - reader.Read(); - - // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "title": - { - result.Title = reader.ReadElementContentAsString(); - break; - } - case "date": - { - var val = reader.ReadElementContentAsString(); - if (DateTime.TryParse(val, out var date)) - { - result.Year = date.Year; - } - - break; - } - case "annotation": - { - result.Overview = reader.ReadElementContentAsString(); - break; - } - case "release-group": - { - result.ReleaseGroupId = reader.GetAttribute("id"); - reader.Skip(); - break; - } - case "artist-credit": - { - using (var subReader = reader.ReadSubtree()) - { - var artist = ParseArtistCredit(subReader); - - if (!string.IsNullOrEmpty(artist.Item1)) - { - result.Artists.Add(artist); - } - } - - break; - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } - - return result; - } - } - - private static ValueTuple<string, string> ParseArtistCredit(XmlReader reader) + private static (string, string) ParseArtistCredit(XmlReader reader) { reader.MoveToContent(); reader.Read(); @@ -510,18 +317,16 @@ namespace MediaBrowser.Providers.Music switch (reader.Name) { case "name-credit": - { - using (var subReader = reader.ReadSubtree()) - { - return ParseArtistNameCredit(subReader); - } - } + { + using var subReader = reader.ReadSubtree(); + return ParseArtistNameCredit(subReader); + } default: - { - reader.Skip(); - break; - } + { + reader.Skip(); + break; + } } } else @@ -530,7 +335,7 @@ namespace MediaBrowser.Providers.Music } } - return new ValueTuple<string, string>(); + return default; } private static (string, string) ParseArtistNameCredit(XmlReader reader) @@ -550,10 +355,8 @@ namespace MediaBrowser.Providers.Music case "artist": { var id = reader.GetAttribute("id"); - using (var subReader = reader.ReadSubtree()) - { - return ParseArtistArtistCredit(subReader, id); - } + using var subReader = reader.ReadSubtree(); + return ParseArtistArtistCredit(subReader, id); } default: @@ -615,7 +418,7 @@ namespace MediaBrowser.Providers.Music var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture); using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var oReader = new StreamReader(stream, Encoding.UTF8); var settings = new XmlReaderSettings { @@ -642,7 +445,7 @@ namespace MediaBrowser.Providers.Music var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture); using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var oReader = new StreamReader(stream, Encoding.UTF8); var settings = new XmlReaderSettings { @@ -652,47 +455,43 @@ namespace MediaBrowser.Providers.Music IgnoreComments = true }; - using (var reader = XmlReader.Create(oReader, settings)) - { - reader.MoveToContent(); - reader.Read(); + using var reader = XmlReader.Create(oReader, settings); + reader.MoveToContent(); + reader.Read(); - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) { - if (reader.NodeType == XmlNodeType.Element) + switch (reader.Name) { - switch (reader.Name) + case "release-group-list": { - case "release-group-list": + if (reader.IsEmptyElement) { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subReader = reader.ReadSubtree()) - { - return GetFirstReleaseGroupId(subReader); - } + reader.Read(); + continue; } - default: - { - reader.Skip(); - break; - } + using var subReader = reader.ReadSubtree(); + return GetFirstReleaseGroupId(subReader); + } + + default: + { + reader.Skip(); + break; } - } - else - { - reader.Read(); } } - - return null; + else + { + reader.Read(); + } } + + return null; } private string GetFirstReleaseGroupId(XmlReader reader) @@ -734,50 +533,57 @@ namespace MediaBrowser.Providers.Music /// A number of retries shall be made in order to try and satisfy the request before /// giving up and returning null. /// </summary> + /// <param name="url">Address of MusicBrainz server.</param> + /// <param name="cancellationToken">CancellationToken to use for method.</param> + /// <returns>Returns response from MusicBrainz service.</returns> internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken) { - using var options = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url); - - // MusicBrainz request a contact email address is supplied, as comment, in user agent field: - // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent - options.Headers.UserAgent.ParseAdd(string.Format( - CultureInfo.InvariantCulture, - "{0} ( {1} )", - _appHost.ApplicationUserAgent, - _appHost.ApplicationUserAgentAddress)); - - HttpResponseMessage response; - var attempts = 0u; + await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false); - do + try { - attempts++; + HttpResponseMessage response; + var attempts = 0u; + var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url; - if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs) + do { - // MusicBrainz is extremely adamant about limiting to one request per second - var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; - await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); - } + attempts++; - // Write time since last request to debug log as evidence we're meeting rate limit - // requirement, before resetting stopwatch back to zero. - _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); - _stopWatchMusicBrainz.Restart(); + if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs) + { + // MusicBrainz is extremely adamant about limiting to one request per second. + var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; + await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); + } - response = await _httpClientFactory.CreateClient().SendAsync(options).ConfigureAwait(false); + // Write time since last request to debug log as evidence we're meeting rate limit + // requirement, before resetting stopwatch back to zero. + _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); + _stopWatchMusicBrainz.Restart(); - // We retry a finite number of times, and only whilst MB is indicating 503 (throttling) - } - while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); + using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + response = await _httpClientFactory + .CreateClient(NamedClient.MusicBrainz) + .SendAsync(request, cancellationToken) + .ConfigureAwait(false); + + // We retry a finite number of times, and only whilst MB is indicating 503 (throttling). + } + while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); + + // Log error if unable to query MB database due to throttling. + if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) + { + _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl); + } - // Log error if unable to query MB database due to throttling - if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) + return response; + } + finally { - _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.RequestUri); + _apiRequestLock.Release(); } - - return response; } /// <inheritdoc /> @@ -785,5 +591,195 @@ namespace MediaBrowser.Providers.Music { throw new NotImplementedException(); } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _apiRequestLock?.Dispose(); + } + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private class ReleaseResult + { + public string ReleaseId; + public string ReleaseGroupId; + public string Title; + public string Overview; + public int? Year; + + public List<ValueTuple<string, string>> Artists = new List<ValueTuple<string, string>>(); + + public static IEnumerable<ReleaseResult> Parse(XmlReader reader) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "release-list": + { + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + + using var subReader = reader.ReadSubtree(); + return ParseReleaseList(subReader).ToList(); + } + + default: + { + reader.Skip(); + break; + } + } + } + else + { + reader.Read(); + } + } + + return Enumerable.Empty<ReleaseResult>(); + } + + private static IEnumerable<ReleaseResult> ParseReleaseList(XmlReader reader) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "release": + { + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + + var releaseId = reader.GetAttribute("id"); + + using var subReader = reader.ReadSubtree(); + var release = ParseRelease(subReader, releaseId); + if (release != null) + { + yield return release; + } + + break; + } + + default: + { + reader.Skip(); + break; + } + } + } + else + { + reader.Read(); + } + } + } + + private static ReleaseResult ParseRelease(XmlReader reader, string releaseId) + { + var result = new ReleaseResult + { + ReleaseId = releaseId + }; + + reader.MoveToContent(); + reader.Read(); + + // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "title": + { + result.Title = reader.ReadElementContentAsString(); + break; + } + + case "date": + { + var val = reader.ReadElementContentAsString(); + if (DateTime.TryParse(val, out var date)) + { + result.Year = date.Year; + } + + break; + } + + case "annotation": + { + result.Overview = reader.ReadElementContentAsString(); + break; + } + + case "release-group": + { + result.ReleaseGroupId = reader.GetAttribute("id"); + reader.Skip(); + break; + } + + case "artist-credit": + { + using var subReader = reader.ReadSubtree(); + var artist = ParseArtistCredit(subReader); + + if (!string.IsNullOrEmpty(artist.Item1)) + { + result.Artists.Add(artist); + } + + break; + } + + default: + { + reader.Skip(); + break; + } + } + } + else + { + reader.Read(); + } + } + + return result; + } + } } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs new file mode 100644 index 0000000000..d654e1372f --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Plugins.MusicBrainz; + +namespace MediaBrowser.Providers.Music +{ + public class MusicBrainzArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is MusicArtist; + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 781b716406..7cff5f5952 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -11,8 +11,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; +using Diacritics.Extensions; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; @@ -22,15 +22,11 @@ namespace MediaBrowser.Providers.Music { public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo> { + public string Name => "MusicBrainz"; + /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) { - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return Enumerable.Empty<RemoteSearchResult>(); - } - var musicBrainzId = searchInfo.GetMusicBrainzArtistId(); if (!string.IsNullOrWhiteSpace(musicBrainzId)) @@ -38,7 +34,7 @@ namespace MediaBrowser.Providers.Music var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture); using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return GetResultsFromResponse(stream); } else @@ -49,7 +45,7 @@ namespace MediaBrowser.Providers.Music var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch)); using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - await using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + await using (var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) { var results = GetResultsFromResponse(stream).ToList(); @@ -59,13 +55,13 @@ namespace MediaBrowser.Providers.Music } } - if (HasDiacritics(searchInfo.Name)) + if (searchInfo.Name.HasDiacritics()) { // Try again using the search with accent characters url url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch)); using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return GetResultsFromResponse(stream); } } @@ -75,58 +71,52 @@ namespace MediaBrowser.Providers.Music private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream) { - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + using var oReader = new StreamReader(stream, Encoding.UTF8); + var settings = new XmlReaderSettings() { - var settings = new XmlReaderSettings() - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; - using (var reader = XmlReader.Create(oReader, settings)) - { - reader.MoveToContent(); - reader.Read(); + using var reader = XmlReader.Create(oReader, settings); + reader.MoveToContent(); + reader.Read(); - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) { - if (reader.NodeType == XmlNodeType.Element) + case "artist-list": { - switch (reader.Name) + if (reader.IsEmptyElement) { - case "artist-list": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subReader = reader.ReadSubtree()) - { - return ParseArtistList(subReader).ToList(); - } - } - - default: - { - reader.Skip(); - break; - } + reader.Read(); + continue; } + + using var subReader = reader.ReadSubtree(); + return ParseArtistList(subReader).ToList(); } - else + + default: { - reader.Read(); + reader.Skip(); + break; } } - - return Enumerable.Empty<RemoteSearchResult>(); + } + else + { + reader.Read(); } } + + return Enumerable.Empty<RemoteSearchResult>(); } private IEnumerable<RemoteSearchResult> ParseArtistList(XmlReader reader) @@ -151,13 +141,11 @@ namespace MediaBrowser.Providers.Music var mbzId = reader.GetAttribute("id"); - using (var subReader = reader.ReadSubtree()) + using var subReader = reader.ReadSubtree(); + var artist = ParseArtist(subReader, mbzId); + if (artist != null) { - var artist = ParseArtist(subReader, mbzId); - if (artist != null) - { - yield return artist; - } + yield return artist; } break; @@ -198,6 +186,7 @@ namespace MediaBrowser.Providers.Music result.Name = reader.ReadElementContentAsString(); break; } + case "annotation": { result.Overview = reader.ReadElementContentAsString(); @@ -228,24 +217,19 @@ namespace MediaBrowser.Providers.Music return result; } - public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo id, CancellationToken cancellationToken) + /// <inheritdoc /> + public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<MusicArtist> { Item = new MusicArtist() }; - // TODO maybe remove when artist metadata can be disabled - if (!Plugin.Instance.Configuration.Enable) - { - return result; - } - - var musicBrainzId = id.GetMusicBrainzArtistId(); + var musicBrainzId = info.GetMusicBrainzArtistId(); if (string.IsNullOrWhiteSpace(musicBrainzId)) { - var searchResults = await GetSearchResults(id, cancellationToken).ConfigureAwait(false); + var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); var singleResult = searchResults.FirstOrDefault(); @@ -271,16 +255,6 @@ namespace MediaBrowser.Providers.Music } /// <summary> - /// Determines whether the specified text has diacritics. - /// </summary> - /// <param name="text">The text.</param> - /// <returns><c>true</c> if the specified text has diacritics; otherwise, <c>false</c>.</returns> - private bool HasDiacritics(string text) - { - return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal); - } - - /// <summary> /// Encodes an URL. /// </summary> /// <param name="name">The name.</param> @@ -290,8 +264,6 @@ namespace MediaBrowser.Providers.Music return WebUtility.UrlEncode(name); } - public string Name => "MusicBrainz"; - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs new file mode 100644 index 0000000000..f889a34b5c --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Plugins.MusicBrainz; + +namespace MediaBrowser.Providers.Music +{ + public class MusicBrainzOtherArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs new file mode 100644 index 0000000000..53783d2c0c --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Plugins.MusicBrainz; + +namespace MediaBrowser.Providers.Music +{ + public class MusicBrainzReleaseGroupExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs new file mode 100644 index 0000000000..627f8f098d --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Plugins.MusicBrainz; + +namespace MediaBrowser.Providers.Music +{ + public class MusicBrainzTrackId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzTrack.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Track; + + /// <inheritdoc /> + public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs index 90266e4409..69b69be428 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using System; using System.Collections.Generic; @@ -11,6 +11,16 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz { public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages { + public const string DefaultServer = "https://musicbrainz.org"; + + public const long DefaultRateLimit = 2000u; + + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + public static Plugin Instance { get; private set; } public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"); @@ -19,19 +29,9 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz public override string Description => "Get artist and album metadata from any MusicBrainz server."; - public const string DefaultServer = "https://musicbrainz.org"; - - public const long DefaultRateLimit = 2000u; - // TODO remove when plugin removed from server. public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml"; - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) - { - Instance = this; - } - public IEnumerable<PluginPageInfo> GetPages() { yield return new PluginPageInfo diff --git a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs new file mode 100644 index 0000000000..268538815e --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs @@ -0,0 +1,46 @@ +#nullable enable + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Providers.Plugins.Omdb +{ + /// <summary> + /// Converts a string <c>N/A</c> to <c>string.Empty</c>. + /// </summary> + public class JsonOmdbNotAvailableInt32Converter : JsonConverter<int?> + { + /// <inheritdoc /> + public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var str = reader.GetString(); + if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var converter = TypeDescriptor.GetConverter(typeToConvert); + return (int?)converter.ConvertFromString(str); + } + + return JsonSerializer.Deserialize<int>(ref reader, options); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + writer.WriteNumberValue(value.Value); + } + else + { + writer.WriteNullValue(); + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs new file mode 100644 index 0000000000..c19589d45b --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs @@ -0,0 +1,43 @@ +#nullable enable + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Providers.Plugins.Omdb +{ + /// <summary> + /// Converts a string <c>N/A</c> to <c>string.Empty</c>. + /// </summary> + public class JsonOmdbNotAvailableStringConverter : JsonConverter<string?> + { + /// <inheritdoc /> + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType == JsonTokenType.String) + { + // GetString can't return null here because we already handled it above + var str = reader.GetString()!; + if (str.Equals("N/A", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return str; + } + + return JsonSerializer.Deserialize<string?>(ref reader, options); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs index bfc840ea59..24ef80a35d 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs @@ -12,13 +12,11 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder { - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; private readonly OmdbItemProvider _itemProvider; private readonly IFileSystem _fileSystem; @@ -26,19 +24,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb private readonly IApplicationHost _appHost; public OmdbEpisodeProvider( - IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClientFactory httpClientFactory, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager) { - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; - _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClientFactory, libraryManager, fileSystem, configurationManager); + _itemProvider = new OmdbItemProvider(_appHost, httpClientFactory, libraryManager, fileSystem, configurationManager); } // After TheTvDb @@ -69,7 +65,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { if (info.IndexNumber.HasValue && info.ParentIndexNumber.HasValue) { - result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager) + result.HasMetadata = await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager) .FetchEpisodeData(result, info.IndexNumber.Value, info.ParentIndexNumber.Value, info.GetProviderId(MetadataProvider.Imdb), seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs index c18725e0aa..df67aff31c 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs @@ -1,11 +1,12 @@ #pragma warning disable CS1591 using System.Collections.Generic; -using System.Net.Http; using System.Globalization; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -14,27 +15,30 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; - private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IApplicationHost _appHost; - public OmdbImageProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager) + public OmdbImageProvider(IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager) { - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; } + public string Name => "The Open Movie Database"; + + // After other internet providers, because they're better + // But before fallback providers like screengrab + public int Order => 90; + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -49,7 +53,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var list = new List<RemoteImageInfo>(); - var provider = new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager); + var provider = new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager); if (!string.IsNullOrWhiteSpace(imdbId)) { @@ -82,18 +86,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } - public string Name => "The Open Movie Database"; - public bool Supports(BaseItem item) { return item is Movie || item is Trailer || item is Episode; } - - // After other internet providers, because they're better - // But before fallback providers like screengrab - public int Order => 90; } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 102ad82e17..02e696de51 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591, SA1300 using System; using System.Collections.Generic; @@ -6,9 +6,12 @@ using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -18,36 +21,39 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbItemProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder { - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IApplicationHost _appHost; + private readonly JsonSerializerOptions _jsonOptions; public OmdbItemProvider( - IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClientFactory httpClientFactory, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager) { - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; + + _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options); + _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter()); + _jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter()); } + public string Name => "The Open Movie Database"; + // After primary option public int Order => 2; @@ -129,13 +135,13 @@ namespace MediaBrowser.Providers.Plugins.Omdb var url = OmdbProvider.GetOmdbUrl(urlQuery); - using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var resultList = new List<SearchResult>(); if (isSearch) { - var searchResultList = await _jsonSerializer.DeserializeFromStreamAsync<SearchResultList>(stream).ConfigureAwait(false); + var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (searchResultList != null && searchResultList.Search != null) { resultList.AddRange(searchResultList.Search); @@ -143,7 +149,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb } else { - var result = await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false); + var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase)) { resultList.Add(result); @@ -198,8 +204,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb return GetSearchResults(searchInfo, "movie", cancellationToken); } - public string Name => "The Open Movie Database"; - public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<Series> @@ -220,7 +224,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb result.Item.SetProviderId(MetadataProvider.Imdb, imdbId); result.HasMetadata = true; - await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); + await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); } return result; @@ -252,7 +256,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb result.Item.SetProviderId(MetadataProvider.Imdb, imdbId); result.HasMetadata = true; - await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); + await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); } return result; @@ -262,22 +266,22 @@ namespace MediaBrowser.Providers.Plugins.Omdb { var results = await GetSearchResultsInternal(info, "movie", false, cancellationToken).ConfigureAwait(false); var first = results.FirstOrDefault(); - return first == null ? null : first.GetProviderId(MetadataProvider.Imdb); + return first?.GetProviderId(MetadataProvider.Imdb); } private async Task<string> GetSeriesImdbId(SeriesInfo info, CancellationToken cancellationToken) { var results = await GetSearchResultsInternal(info, "series", false, cancellationToken).ConfigureAwait(false); var first = results.FirstOrDefault(); - return first == null ? null : first.GetProviderId(MetadataProvider.Imdb); + return first?.GetProviderId(MetadataProvider.Imdb); } public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } - class SearchResult + private class SearchResult { public string Title { get; set; } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index c45149c3a7..1ae712e9e2 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS159, SA1300 using System; using System.Collections.Generic; @@ -6,37 +6,55 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; -using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.Omdb { + /// <summary>Provider for OMDB service.</summary> public class OmdbProvider { - private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IHttpClientFactory _httpClientFactory; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IApplicationHost _appHost; - - public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager) + private readonly JsonSerializerOptions _jsonOptions; + + /// <summary>Initializes a new instance of the <see cref="OmdbProvider"/> class.</summary> + /// <param name="httpClientFactory">HttpClientFactory to use for calls to OMDB service.</param> + /// <param name="fileSystem">IFileSystem to use for store OMDB data.</param> + /// <param name="appHost">IApplicationHost to use.</param> + /// <param name="configurationManager">IServerConfigurationManager to use.</param> + public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager) { - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; + + _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options); + _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter()); + _jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter()); } + /// <summary>Fetches data from OMDB service.</summary> + /// <param name="itemResult">Metadata about media item.</param> + /// <param name="imdbId">IMDB ID for media.</param> + /// <param name="language">Media language.</param> + /// <param name="country">Country of origin.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <typeparam name="T">The first generic type parameter.</typeparam> + /// <returns>Returns a Task object that can be awaited.</returns> public async Task Fetch<T>(MetadataResult<T> itemResult, string imdbId, string language, string country, CancellationToken cancellationToken) where T : BaseItem { @@ -49,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var result = await GetRootObject(imdbId, cancellationToken).ConfigureAwait(false); - // Only take the name and rating if the user's language is set to english, since Omdb has no localization + // Only take the name and rating if the user's language is set to English, since Omdb has no localization if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport) { item.Name = result.Title; @@ -101,6 +119,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb ParseAdditionalMetadata(itemResult, result); } + /// <summary>Gets data about an episode.</summary> + /// <param name="itemResult">Metadata about episode.</param> + /// <param name="episodeNumber">Episode number.</param> + /// <param name="seasonNumber">Season number.</param> + /// <param name="episodeImdbId">Episode ID.</param> + /// <param name="seriesImdbId">Season ID.</param> + /// <param name="language">Episode language.</param> + /// <param name="country">Country of origin.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <typeparam name="T">The first generic type parameter.</typeparam> + /// <returns>Whether operation was successful.</returns> public async Task<bool> FetchEpisodeData<T>(MetadataResult<T> itemResult, int episodeNumber, int seasonNumber, string episodeImdbId, string seriesImdbId, string language, string country, CancellationToken cancellationToken) where T : BaseItem { @@ -113,7 +142,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(false); - if (seasonResult == null) + if (seasonResult?.Episodes == null) { return false; } @@ -150,7 +179,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb return false; } - // Only take the name and rating if the user's language is set to english, since Omdb has no localization + // Only take the name and rating if the user's language is set to English, since Omdb has no localization if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport) { item.Name = result.Title; @@ -207,44 +236,20 @@ namespace MediaBrowser.Providers.Plugins.Omdb internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken) { var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false); - - string resultString; - - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - using (var reader = new StreamReader(stream, new UTF8Encoding(false))) - { - resultString = reader.ReadToEnd(); - resultString = resultString.Replace("\"N/A\"", "\"\""); - } - } - - var result = _jsonSerializer.DeserializeFromString<RootObject>(resultString); - return result; + await using var stream = File.OpenRead(path); + return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); } internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken) { var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false); - - string resultString; - - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - using (var reader = new StreamReader(stream, new UTF8Encoding(false))) - { - resultString = reader.ReadToEnd(); - resultString = resultString.Replace("\"N/A\"", "\"\""); - } - } - - var result = _jsonSerializer.DeserializeFromString<SeasonRootObject>(resultString); - return result; + await using var stream = File.OpenRead(path); + return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); } internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds) { - if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string id) && !string.IsNullOrEmpty(id)) + if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string id)) { // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. if (!string.IsNullOrWhiteSpace(id)) @@ -256,6 +261,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb return false; } + /// <summary>Gets OMDB URL.</summary> + /// <param name="query">Appends query string to URL.</param> + /// <returns>OMDB URL with optional query string.</returns> public static string GetOmdbUrl(string query) { const string Url = "https://www.omdbapi.com?apikey=2c9d9507"; @@ -289,6 +297,10 @@ namespace MediaBrowser.Providers.Plugins.Omdb return path; } } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + } var url = GetOmdbUrl( string.Format( @@ -296,11 +308,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb "i={0}&plot=short&tomatoes=true&r=json", imdbParam)); - using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - _jsonSerializer.SerializeToFile(rootObject, path); + var rootObject = await GetDeserializedOmdbResponse<RootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); + await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); return path; } @@ -326,6 +336,10 @@ namespace MediaBrowser.Providers.Plugins.Omdb return path; } } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + } var url = GetOmdbUrl( string.Format( @@ -334,15 +348,32 @@ namespace MediaBrowser.Providers.Plugins.Omdb imdbParam, seasonId)); - using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - _jsonSerializer.SerializeToFile(rootObject, path); + var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); + await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); return path; } + /// <summary>Gets response from OMDB service as type T.</summary> + /// <param name="httpClient">HttpClient instance to use for service call.</param> + /// <param name="url">Http URL to use for service call.</param> + /// <param name="cancellationToken">CancellationToken to use for service call.</param> + /// <typeparam name="T">The first generic type parameter.</typeparam> + /// <returns>OMDB service response as type T.</returns> + public async Task<T> GetDeserializedOmdbResponse<T>(HttpClient httpClient, string url, CancellationToken cancellationToken) + { + using var response = await GetOmdbResponse(httpClient, url, cancellationToken).ConfigureAwait(false); + await using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + + return await JsonSerializer.DeserializeAsync<T>(content, _jsonOptions, cancellationToken).ConfigureAwait(false); + } + + /// <summary>Gets response from OMDB service.</summary> + /// <param name="httpClient">HttpClient instance to use for service call.</param> + /// <param name="url">Http URL to use for service call.</param> + /// <param name="cancellationToken">CancellationToken to use for service call.</param> + /// <returns>OMDB service response as HttpResponseMessage.</returns> public static Task<HttpResponseMessage> GetOmdbResponse(HttpClient httpClient, string url, CancellationToken cancellationToken) { return httpClient.GetAsync(url, cancellationToken); @@ -384,13 +415,13 @@ namespace MediaBrowser.Providers.Plugins.Omdb var isConfiguredForEnglish = IsConfiguredForEnglish(item) || _configurationManager.Configuration.EnableNewOmdbSupport; // Grab series genres because IMDb data is better than TVDB. Leave movies alone - // But only do it if english is the preferred language because this data will not be localized + // But only do it if English is the preferred language because this data will not be localized if (isConfiguredForEnglish && !string.IsNullOrWhiteSpace(result.Genre)) { item.Genres = Array.Empty<string>(); foreach (var genre in result.Genre - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(i => i.Trim()) .Where(i => !string.IsNullOrWhiteSpace(i))) { @@ -400,7 +431,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb if (isConfiguredForEnglish) { - // Omdb is currently english only, so for other languages skip this and let secondary providers fill it in + // Omdb is currently English only, so for other languages skip this and let secondary providers fill it in item.Overview = result.Plot; } @@ -424,7 +455,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { var person = new PersonInfo { - Name = result.Director.Trim(), + Name = result.Writer.Trim(), Type = PersonType.Writer }; @@ -454,7 +485,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { var lang = item.GetPreferredMetadataLanguage(); - // The data isn't localized and so can only be used for english users + // The data isn't localized and so can only be used for English users return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase); } @@ -464,7 +495,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb public string seriesID { get; set; } - public int Season { get; set; } + public int? Season { get; set; } public int? totalSeasons { get; set; } @@ -525,7 +556,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb public string Response { get; set; } - public int Episode { get; set; } + public int? Episode { get; set; } public float? GetRottenTomatoScore() { @@ -546,10 +577,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } +#pragma warning disable CA1034 + /// <summary>Describes OMDB rating.</summary> public class OmdbRating { + /// <summary>Gets or sets rating source.</summary> public string Source { get; set; } + /// <summary>Gets or sets rating value.</summary> public string Value { get; set; } } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs index 41ca561643..047df4f33d 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using System; using System.Collections.Generic; @@ -11,6 +11,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb { public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages { + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + public static Plugin Instance { get; private set; } public override Guid Id => new Guid("a628c0da-fac5-4c7e-9d1a-7134223f14c8"); @@ -22,12 +28,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb // TODO remove when plugin removed from server. public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml"; - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) - { - Instance = this; - } - public IEnumerable<PluginPageInfo> GetPages() { yield return new PluginPageInfo diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs deleted file mode 100644 index 690a52c4d3..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs +++ /dev/null @@ -1,10 +0,0 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Model.Plugins; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - public class PluginConfiguration : BasePluginConfiguration - { - } -} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs deleted file mode 100644 index e7079ed3cd..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs +++ /dev/null @@ -1,29 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Serialization; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - public class Plugin : BasePlugin<PluginConfiguration> - { - public static Plugin Instance { get; private set; } - - public override Guid Id => new Guid("a677c0da-fac5-4cde-941a-7134223f14c8"); - - public override string Name => "TheTVDB"; - - public override string Description => "Get metadata for movies and other video content from TheTVDB."; - - // TODO remove when plugin removed from server. - public override string ConfigurationFileName => "Jellyfin.Plugin.TheTvdb.xml"; - - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) - { - Instance = this; - } - } -} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs deleted file mode 100644 index f22d484abc..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs +++ /dev/null @@ -1,314 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using Microsoft.Extensions.Caching.Memory; -using TvDbSharper; -using TvDbSharper.Dto; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - public class TvdbClientManager - { - private const string DefaultLanguage = "en"; - - private readonly IMemoryCache _cache; - private readonly TvDbClient _tvDbClient; - private DateTime _tokenCreatedAt; - - public TvdbClientManager(IMemoryCache memoryCache) - { - _cache = memoryCache; - _tvDbClient = new TvDbClient(); - } - - private TvDbClient TvDbClient - { - get - { - if (string.IsNullOrEmpty(_tvDbClient.Authentication.Token)) - { - _tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult(); - _tokenCreatedAt = DateTime.Now; - } - - // Refresh if necessary - if (_tokenCreatedAt < DateTime.Now.Subtract(TimeSpan.FromHours(20))) - { - try - { - _tvDbClient.Authentication.RefreshTokenAsync().GetAwaiter().GetResult(); - } - catch - { - _tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult(); - } - - _tokenCreatedAt = DateTime.Now; - } - - return _tvDbClient; - } - } - - public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByNameAsync(string name, string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("series", name, language); - return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByNameAsync(name, cancellationToken)); - } - - public Task<TvDbResponse<Series>> GetSeriesByIdAsync(int tvdbId, string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("series", tvdbId, language); - return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetAsync(tvdbId, cancellationToken)); - } - - public Task<TvDbResponse<EpisodeRecord>> GetEpisodesAsync(int episodeTvdbId, string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("episode", episodeTvdbId, language); - return TryGetValue(cacheKey, language, () => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken)); - } - - public async Task<List<EpisodeRecord>> GetAllEpisodesAsync(int tvdbId, string language, - CancellationToken cancellationToken) - { - // Traverse all episode pages and join them together - var episodes = new List<EpisodeRecord>(); - var episodePage = await GetEpisodesPageAsync(tvdbId, new EpisodeQuery(), language, cancellationToken) - .ConfigureAwait(false); - episodes.AddRange(episodePage.Data); - if (!episodePage.Links.Next.HasValue || !episodePage.Links.Last.HasValue) - { - return episodes; - } - - int next = episodePage.Links.Next.Value; - int last = episodePage.Links.Last.Value; - - for (var page = next; page <= last; ++page) - { - episodePage = await GetEpisodesPageAsync(tvdbId, page, new EpisodeQuery(), language, cancellationToken) - .ConfigureAwait(false); - episodes.AddRange(episodePage.Data); - } - - return episodes; - } - - public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync( - string imdbId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("series", imdbId, language); - return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByImdbIdAsync(imdbId, cancellationToken)); - } - - public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByZap2ItIdAsync( - string zap2ItId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("series", zap2ItId, language); - return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByZap2ItIdAsync(zap2ItId, cancellationToken)); - } - - public Task<TvDbResponse<Actor[]>> GetActorsAsync( - int tvdbId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("actors", tvdbId, language); - return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetActorsAsync(tvdbId, cancellationToken)); - } - - public Task<TvDbResponse<Image[]>> GetImagesAsync( - int tvdbId, - ImagesQuery imageQuery, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("images", tvdbId, language, imageQuery); - return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesAsync(tvdbId, imageQuery, cancellationToken)); - } - - public Task<TvDbResponse<Language[]>> GetLanguagesAsync(CancellationToken cancellationToken) - { - return TryGetValue("languages", null, () => TvDbClient.Languages.GetAllAsync(cancellationToken)); - } - - public Task<TvDbResponse<EpisodesSummary>> GetSeriesEpisodeSummaryAsync( - int tvdbId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("seriesepisodesummary", tvdbId, language); - return TryGetValue(cacheKey, language, - () => TvDbClient.Series.GetEpisodesSummaryAsync(tvdbId, cancellationToken)); - } - - public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync( - int tvdbId, - int page, - EpisodeQuery episodeQuery, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey(language, tvdbId, episodeQuery); - - return TryGetValue(cacheKey, language, - () => TvDbClient.Series.GetEpisodesAsync(tvdbId, page, episodeQuery, cancellationToken)); - } - - public Task<string> GetEpisodeTvdbId( - EpisodeInfo searchInfo, - string language, - CancellationToken cancellationToken) - { - searchInfo.SeriesProviderIds.TryGetValue(nameof(MetadataProvider.Tvdb), - out var seriesTvdbId); - - var episodeQuery = new EpisodeQuery(); - - // Prefer SxE over premiere date as it is more robust - if (searchInfo.IndexNumber.HasValue && searchInfo.ParentIndexNumber.HasValue) - { - switch (searchInfo.SeriesDisplayOrder) - { - case "dvd": - episodeQuery.DvdEpisode = searchInfo.IndexNumber.Value; - episodeQuery.DvdSeason = searchInfo.ParentIndexNumber.Value; - break; - case "absolute": - episodeQuery.AbsoluteNumber = searchInfo.IndexNumber.Value; - break; - default: - // aired order - episodeQuery.AiredEpisode = searchInfo.IndexNumber.Value; - episodeQuery.AiredSeason = searchInfo.ParentIndexNumber.Value; - break; - } - } - else if (searchInfo.PremiereDate.HasValue) - { - // tvdb expects yyyy-mm-dd format - episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); - } - - return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), episodeQuery, language, cancellationToken); - } - - public async Task<string> GetEpisodeTvdbId( - int seriesTvdbId, - EpisodeQuery episodeQuery, - string language, - CancellationToken cancellationToken) - { - var episodePage = - await GetEpisodesPageAsync(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken) - .ConfigureAwait(false); - return episodePage.Data.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture); - } - - public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync( - int tvdbId, - EpisodeQuery episodeQuery, - string language, - CancellationToken cancellationToken) - { - return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken); - } - - public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken) - { - var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId); - var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false); - - if (imagesSummary.Data.Fanart > 0) - { - yield return KeyType.Fanart; - } - - if (imagesSummary.Data.Series > 0) - { - yield return KeyType.Series; - } - - if (imagesSummary.Data.Poster > 0) - { - yield return KeyType.Poster; - } - } - - public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken) - { - var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId); - var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false); - - if (imagesSummary.Data.Season > 0) - { - yield return KeyType.Season; - } - - if (imagesSummary.Data.Fanart > 0) - { - yield return KeyType.Fanart; - } - - // TODO seasonwide is not supported in TvDbSharper - } - - private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory) - { - if (_cache.TryGetValue(key, out T cachedValue)) - { - return cachedValue; - } - - _tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage; - var result = await resultFactory.Invoke().ConfigureAwait(false); - _cache.Set(key, result, TimeSpan.FromHours(1)); - return result; - } - - private static string GenerateKey(params object[] objects) - { - var key = string.Empty; - - foreach (var obj in objects) - { - var objType = obj.GetType(); - if (objType.IsPrimitive || objType == typeof(string)) - { - key += obj + ";"; - } - else - { - foreach (PropertyInfo propertyInfo in objType.GetProperties()) - { - var currentValue = propertyInfo.GetValue(obj, null); - if (currentValue == null) - { - continue; - } - - key += propertyInfo.Name + "=" + currentValue + ";"; - } - } - } - - return key; - } - } -} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs deleted file mode 100644 index 4e7c0e5a60..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs +++ /dev/null @@ -1,122 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - public class TvdbEpisodeImageProvider : IRemoteImageProvider - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger<TvdbEpisodeImageProvider> _logger; - private readonly TvdbClientManager _tvdbClientManager; - - public TvdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - _tvdbClientManager = tvdbClientManager; - } - - public string Name => "TheTVDB"; - - public bool Supports(BaseItem item) - { - return item is Episode; - } - - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - return new List<ImageType> - { - ImageType.Primary - }; - } - - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var episode = (Episode)item; - var series = episode.Series; - var imageResult = new List<RemoteImageInfo>(); - var language = item.GetPreferredMetadataLanguage(); - if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) - { - // Process images - try - { - var episodeInfo = new EpisodeInfo - { - IndexNumber = episode.IndexNumber.Value, - ParentIndexNumber = episode.ParentIndexNumber.Value, - SeriesProviderIds = series.ProviderIds, - SeriesDisplayOrder = series.DisplayOrder - }; - string episodeTvdbId = await _tvdbClientManager - .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(episodeTvdbId)) - { - _logger.LogError( - "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", - episodeInfo.ParentIndexNumber, - episodeInfo.IndexNumber, - series.GetProviderId(MetadataProvider.Tvdb)); - return imageResult; - } - - var episodeResult = - await _tvdbClientManager - .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId, CultureInfo.InvariantCulture), language, cancellationToken) - .ConfigureAwait(false); - - var image = GetImageInfo(episodeResult.Data); - if (image != null) - { - imageResult.Add(image); - } - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}", series.GetProviderId(MetadataProvider.Tvdb)); - } - } - - return imageResult; - } - - private RemoteImageInfo GetImageInfo(EpisodeRecord episode) - { - if (string.IsNullOrEmpty(episode.Filename)) - { - return null; - } - - return new RemoteImageInfo - { - Width = Convert.ToInt32(episode.ThumbWidth, CultureInfo.InvariantCulture), - Height = Convert.ToInt32(episode.ThumbHeight, CultureInfo.InvariantCulture), - ProviderName = Name, - Url = TvdbUtils.BannerUrl + episode.Filename, - Type = ImageType.Primary - }; - } - - public int Order => 0; - - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs deleted file mode 100644 index 90436c7c99..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs +++ /dev/null @@ -1,252 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - /// <summary> - /// Class RemoteEpisodeProvider. - /// </summary> - public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger<TvdbEpisodeProvider> _logger; - private readonly TvdbClientManager _tvdbClientManager; - - public TvdbEpisodeProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - _tvdbClientManager = tvdbClientManager; - } - - public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) - { - var list = new List<RemoteSearchResult>(); - - // Either an episode number or date must be provided; and the dictionary of provider ids must be valid - if ((searchInfo.IndexNumber == null && searchInfo.PremiereDate == null) - || !TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) - { - return list; - } - - var metadataResult = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false); - - if (!metadataResult.HasMetadata) - { - return list; - } - - var item = metadataResult.Item; - - list.Add(new RemoteSearchResult - { - IndexNumber = item.IndexNumber, - Name = item.Name, - ParentIndexNumber = item.ParentIndexNumber, - PremiereDate = item.PremiereDate, - ProductionYear = item.ProductionYear, - ProviderIds = item.ProviderIds, - SearchProviderName = Name, - IndexNumberEnd = item.IndexNumberEnd - }); - - return list; - } - - public string Name => "TheTVDB"; - - public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo searchInfo, CancellationToken cancellationToken) - { - var result = new MetadataResult<Episode> - { - QueriedById = true - }; - - if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) && - (searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue)) - { - result = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false); - } - else - { - _logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name); - } - - return result; - } - - private async Task<MetadataResult<Episode>> GetEpisode(EpisodeInfo searchInfo, CancellationToken cancellationToken) - { - var result = new MetadataResult<Episode> - { - QueriedById = true - }; - - string seriesTvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb); - string episodeTvdbId = null; - try - { - episodeTvdbId = await _tvdbClientManager - .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) - .ConfigureAwait(false); - if (string.IsNullOrEmpty(episodeTvdbId)) - { - _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", - searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId); - return result; - } - - var episodeResult = await _tvdbClientManager.GetEpisodesAsync( - Convert.ToInt32(episodeTvdbId), searchInfo.MetadataLanguage, - cancellationToken).ConfigureAwait(false); - - result = MapEpisodeToResult(searchInfo, episodeResult.Data); - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to retrieve episode with id {EpisodeTvDbId}, series id {SeriesTvdbId}", episodeTvdbId, seriesTvdbId); - } - - return result; - } - - private static MetadataResult<Episode> MapEpisodeToResult(EpisodeInfo id, EpisodeRecord episode) - { - var result = new MetadataResult<Episode> - { - HasMetadata = true, - Item = new Episode - { - IndexNumber = id.IndexNumber, - ParentIndexNumber = id.ParentIndexNumber, - IndexNumberEnd = id.IndexNumberEnd, - AirsBeforeEpisodeNumber = episode.AirsBeforeEpisode, - AirsAfterSeasonNumber = episode.AirsAfterSeason, - AirsBeforeSeasonNumber = episode.AirsBeforeSeason, - Name = episode.EpisodeName, - Overview = episode.Overview, - CommunityRating = (float?)episode.SiteRating, - } - }; - result.ResetPeople(); - - var item = result.Item; - item.SetProviderId(MetadataProvider.Tvdb, episode.Id.ToString()); - item.SetProviderId(MetadataProvider.Imdb, episode.ImdbId); - - if (string.Equals(id.SeriesDisplayOrder, "dvd", StringComparison.OrdinalIgnoreCase)) - { - item.IndexNumber = Convert.ToInt32(episode.DvdEpisodeNumber ?? episode.AiredEpisodeNumber); - item.ParentIndexNumber = episode.DvdSeason ?? episode.AiredSeason; - } - else if (episode.AiredEpisodeNumber.HasValue) - { - item.IndexNumber = episode.AiredEpisodeNumber; - } - else if (episode.AiredSeason.HasValue) - { - item.ParentIndexNumber = episode.AiredSeason; - } - - if (DateTime.TryParse(episode.FirstAired, out var date)) - { - // dates from tvdb are UTC but without offset or Z - item.PremiereDate = date; - item.ProductionYear = date.Year; - } - - foreach (var director in episode.Directors) - { - result.AddPerson(new PersonInfo - { - Name = director, - Type = PersonType.Director - }); - } - - // GuestStars is a weird list of names and roles - // Example: - // 1: Some Actor (Role1 - // 2: Role2 - // 3: Role3) - // 4: Another Actor (Role1 - // ... - for (var i = 0; i < episode.GuestStars.Length; ++i) - { - var currentActor = episode.GuestStars[i]; - var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal); - - if (roleStartIndex == -1) - { - result.AddPerson(new PersonInfo - { - Type = PersonType.GuestStar, - Name = currentActor, - Role = string.Empty - }); - continue; - } - - var roles = new List<string> { currentActor.Substring(roleStartIndex + 1) }; - - // Fetch all roles - for (var j = i + 1; j < episode.GuestStars.Length; ++j) - { - var currentRole = episode.GuestStars[j]; - var roleEndIndex = currentRole.IndexOf(')', StringComparison.Ordinal); - - if (roleEndIndex == -1) - { - roles.Add(currentRole); - continue; - } - - roles.Add(currentRole.TrimEnd(')')); - // Update the outer index (keep in mind it adds 1 after the iteration) - i = j; - break; - } - - result.AddPerson(new PersonInfo - { - Type = PersonType.GuestStar, - Name = currentActor.Substring(0, roleStartIndex).Trim(), - Role = string.Join(", ", roles) - }); - } - - foreach (var writer in episode.Writers) - { - result.AddPerson(new PersonInfo - { - Name = writer, - Type = PersonType.Writer - }); - } - - result.ResultLanguage = episode.Language.EpisodeName; - return result; - } - - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - - public int Order => 0; - } -} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs deleted file mode 100644 index 388a4e3e72..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs +++ /dev/null @@ -1,112 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using Microsoft.Extensions.Logging; -using TvDbSharper; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger<TvdbPersonImageProvider> _logger; - private readonly ILibraryManager _libraryManager; - private readonly TvdbClientManager _tvdbClientManager; - - public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClientFactory httpClientFactory, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager) - { - _libraryManager = libraryManager; - _httpClientFactory = httpClientFactory; - _logger = logger; - _tvdbClientManager = tvdbClientManager; - } - - /// <inheritdoc /> - public string Name => "TheTVDB"; - - /// <inheritdoc /> - public int Order => 1; - - /// <inheritdoc /> - public bool Supports(BaseItem item) => item is Person; - - /// <inheritdoc /> - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - yield return ImageType.Primary; - } - - /// <inheritdoc /> - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Series).Name }, - PersonIds = new[] { item.Id }, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } - }).Cast<Series>() - .Where(i => TvdbSeriesProvider.IsValidSeries(i.ProviderIds)) - .ToList(); - - var infos = (await Task.WhenAll(seriesWithPerson.Select(async i => - await GetImageFromSeriesData(i, item.Name, cancellationToken).ConfigureAwait(false))) - .ConfigureAwait(false)) - .Where(i => i != null) - .Take(1); - - return infos; - } - - private async Task<RemoteImageInfo> GetImageFromSeriesData(Series series, string personName, CancellationToken cancellationToken) - { - var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProvider.Tvdb)); - - try - { - var actorsResult = await _tvdbClientManager - .GetActorsAsync(tvdbId, series.GetPreferredMetadataLanguage(), cancellationToken) - .ConfigureAwait(false); - var actor = actorsResult.Data.FirstOrDefault(a => - string.Equals(a.Name, personName, StringComparison.OrdinalIgnoreCase) && - !string.IsNullOrEmpty(a.Image)); - if (actor == null) - { - return null; - } - - return new RemoteImageInfo - { - Url = TvdbUtils.BannerUrl + actor.Image, - Type = ImageType.Primary, - ProviderName = Name - }; - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to retrieve actor {ActorName} from series {SeriesTvdbId}", personName, tvdbId); - return null; - } - } - - /// <inheritdoc /> - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs deleted file mode 100644 index ff8c3455fa..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs +++ /dev/null @@ -1,154 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; -using RatingType = MediaBrowser.Model.Dto.RatingType; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger<TvdbSeasonImageProvider> _logger; - private readonly TvdbClientManager _tvdbClientManager; - - public TvdbSeasonImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - _tvdbClientManager = tvdbClientManager; - } - - public string Name => ProviderName; - - public static string ProviderName => "TheTVDB"; - - public bool Supports(BaseItem item) - { - return item is Season; - } - - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - return new List<ImageType> - { - ImageType.Primary, - ImageType.Banner, - ImageType.Backdrop - }; - } - - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var season = (Season)item; - var series = season.Series; - - if (series == null || !season.IndexNumber.HasValue || !TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) - { - return Array.Empty<RemoteImageInfo>(); - } - - var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProvider.Tvdb)); - var seasonNumber = season.IndexNumber.Value; - var language = item.GetPreferredMetadataLanguage(); - var remoteImages = new List<RemoteImageInfo>(); - - var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false); - await foreach (var keyType in keyTypes) - { - var imageQuery = new ImagesQuery - { - KeyType = keyType, - SubKey = seasonNumber.ToString() - }; - try - { - var imageResults = await _tvdbClientManager - .GetImagesAsync(tvdbId, imageQuery, language, cancellationToken).ConfigureAwait(false); - remoteImages.AddRange(GetImages(imageResults.Data, language)); - } - catch (TvDbServerException) - { - _logger.LogDebug("No images of type {KeyType} found for series {TvdbId}", keyType, tvdbId); - } - } - - return remoteImages; - } - - private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage) - { - var list = new List<RemoteImageInfo>(); - // any languages with null ids are ignored - var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data.Where(x => x.Id.HasValue); - foreach (Image image in images) - { - var imageInfo = new RemoteImageInfo - { - RatingType = RatingType.Score, - CommunityRating = (double?)image.RatingsInfo.Average, - VoteCount = image.RatingsInfo.Count, - Url = TvdbUtils.BannerUrl + image.FileName, - ProviderName = ProviderName, - Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation, - ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail - }; - - var resolution = image.Resolution.Split('x'); - if (resolution.Length == 2) - { - imageInfo.Width = Convert.ToInt32(resolution[0]); - imageInfo.Height = Convert.ToInt32(resolution[1]); - } - - imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType); - list.Add(imageInfo); - } - - var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); - return list.OrderByDescending(i => - { - if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - public int Order => 0; - - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs deleted file mode 100644 index d287828eb5..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs +++ /dev/null @@ -1,152 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; -using RatingType = MediaBrowser.Model.Dto.RatingType; -using Series = MediaBrowser.Controller.Entities.TV.Series; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger<TvdbSeriesImageProvider> _logger; - private readonly TvdbClientManager _tvdbClientManager; - - public TvdbSeriesImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - _tvdbClientManager = tvdbClientManager; - } - - public string Name => ProviderName; - - public static string ProviderName => "TheTVDB"; - - public bool Supports(BaseItem item) - { - return item is Series; - } - - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - return new List<ImageType> - { - ImageType.Primary, - ImageType.Banner, - ImageType.Backdrop - }; - } - - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) - { - if (!TvdbSeriesProvider.IsValidSeries(item.ProviderIds)) - { - return Array.Empty<RemoteImageInfo>(); - } - - var language = item.GetPreferredMetadataLanguage(); - var remoteImages = new List<RemoteImageInfo>(); - var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb)); - var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken) - .ConfigureAwait(false); - await foreach (KeyType keyType in allowedKeyTypes) - { - var imageQuery = new ImagesQuery - { - KeyType = keyType - }; - try - { - var imageResults = - await _tvdbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken) - .ConfigureAwait(false); - - remoteImages.AddRange(GetImages(imageResults.Data, language)); - } - catch (TvDbServerException) - { - _logger.LogDebug("No images of type {KeyType} exist for series {TvDbId}", keyType, - tvdbId); - } - } - - return remoteImages; - } - - private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage) - { - var list = new List<RemoteImageInfo>(); - var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; - - foreach (Image image in images) - { - var imageInfo = new RemoteImageInfo - { - RatingType = RatingType.Score, - CommunityRating = (double?)image.RatingsInfo.Average, - VoteCount = image.RatingsInfo.Count, - Url = TvdbUtils.BannerUrl + image.FileName, - ProviderName = Name, - Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation, - ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail - }; - - var resolution = image.Resolution.Split('x'); - if (resolution.Length == 2) - { - imageInfo.Width = Convert.ToInt32(resolution[0]); - imageInfo.Height = Convert.ToInt32(resolution[1]); - } - - imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType); - list.Add(imageInfo); - } - - var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); - return list.OrderByDescending(i => - { - if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - public int Order => 0; - - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs deleted file mode 100644 index c6dd8a5f3d..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ /dev/null @@ -1,416 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Providers; -using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; -using Series = MediaBrowser.Controller.Entities.TV.Series; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder - { - internal static TvdbSeriesProvider Current { get; private set; } - - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger<TvdbSeriesProvider> _logger; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localizationManager; - private readonly TvdbClientManager _tvdbClientManager; - - public TvdbSeriesProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - _libraryManager = libraryManager; - _localizationManager = localizationManager; - Current = this; - _tvdbClientManager = tvdbClientManager; - } - - public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) - { - if (IsValidSeries(searchInfo.ProviderIds)) - { - var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false); - - if (metadata.HasMetadata) - { - return new List<RemoteSearchResult> - { - new RemoteSearchResult - { - Name = metadata.Item.Name, - PremiereDate = metadata.Item.PremiereDate, - ProductionYear = metadata.Item.ProductionYear, - ProviderIds = metadata.Item.ProviderIds, - SearchProviderName = Name - } - }; - } - } - - return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - } - - public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken) - { - var result = new MetadataResult<Series> - { - QueriedById = true - }; - - if (!IsValidSeries(itemId.ProviderIds)) - { - result.QueriedById = false; - await Identify(itemId).ConfigureAwait(false); - } - - cancellationToken.ThrowIfCancellationRequested(); - - if (IsValidSeries(itemId.ProviderIds)) - { - result.Item = new Series(); - result.HasMetadata = true; - - await FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken) - .ConfigureAwait(false); - } - - return result; - } - - private async Task FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken) - { - var series = result.Item; - - if (seriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var tvdbId) && !string.IsNullOrEmpty(tvdbId)) - { - series.SetProviderId(MetadataProvider.Tvdb, tvdbId); - } - - if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out var imdbId) && !string.IsNullOrEmpty(imdbId)) - { - series.SetProviderId(MetadataProvider.Imdb, imdbId); - tvdbId = await GetSeriesByRemoteId(imdbId, MetadataProvider.Imdb.ToString(), metadataLanguage, - cancellationToken).ConfigureAwait(false); - } - - if (seriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out var zap2It) && !string.IsNullOrEmpty(zap2It)) - { - series.SetProviderId(MetadataProvider.Zap2It, zap2It); - tvdbId = await GetSeriesByRemoteId(zap2It, MetadataProvider.Zap2It.ToString(), metadataLanguage, - cancellationToken).ConfigureAwait(false); - } - - try - { - var seriesResult = - await _tvdbClientManager - .GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken) - .ConfigureAwait(false); - MapSeriesToResult(result, seriesResult.Data, metadataLanguage); - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to retrieve series with id {TvdbId}", tvdbId); - return; - } - - cancellationToken.ThrowIfCancellationRequested(); - - result.ResetPeople(); - - try - { - var actorsResult = await _tvdbClientManager - .GetActorsAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken).ConfigureAwait(false); - MapActorsToResult(result, actorsResult.Data); - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to retrieve actors for series {TvdbId}", tvdbId); - } - } - - private async Task<string> GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken) - { - TvDbResponse<SeriesSearchResult[]> result = null; - - try - { - if (string.Equals(idType, MetadataProvider.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase)) - { - result = await _tvdbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken) - .ConfigureAwait(false); - } - else - { - result = await _tvdbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken) - .ConfigureAwait(false); - } - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to retrieve series with remote id {RemoteId}", id); - } - - return result?.Data.First().Id.ToString(); - } - - /// <summary> - /// Check whether a dictionary of provider IDs includes an entry for a valid TV metadata provider. - /// </summary> - /// <param name="seriesProviderIds">The dictionary to check.</param> - /// <returns>True, if the dictionary contains a valid TV provider ID, otherwise false.</returns> - internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds) - { - return seriesProviderIds.ContainsKey(MetadataProvider.Tvdb.ToString()) || - seriesProviderIds.ContainsKey(MetadataProvider.Imdb.ToString()) || - seriesProviderIds.ContainsKey(MetadataProvider.Zap2It.ToString()); - } - - /// <summary> - /// Finds the series. - /// </summary> - /// <param name="name">The name.</param> - /// <param name="year">The year.</param> - /// <param name="language">The language.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.String}.</returns> - private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken) - { - var results = await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false); - - if (results.Count == 0) - { - var parsedName = _libraryManager.ParseName(name); - var nameWithoutYear = parsedName.Name; - - if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase)) - { - results = await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false); - } - } - - return results.Where(i => - { - if (year.HasValue && i.ProductionYear.HasValue) - { - // Allow one year tolerance - return Math.Abs(year.Value - i.ProductionYear.Value) <= 1; - } - - return true; - }); - } - - private async Task<List<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken) - { - var comparableName = GetComparableName(name); - var list = new List<Tuple<List<string>, RemoteSearchResult>>(); - TvDbResponse<SeriesSearchResult[]> result; - try - { - result = await _tvdbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken) - .ConfigureAwait(false); - } - catch (TvDbServerException e) - { - _logger.LogError(e, "No series results found for {Name}", comparableName); - return new List<RemoteSearchResult>(); - } - - foreach (var seriesSearchResult in result.Data) - { - var tvdbTitles = new List<string> - { - GetComparableName(seriesSearchResult.SeriesName) - }; - tvdbTitles.AddRange(seriesSearchResult.Aliases.Select(GetComparableName)); - - DateTime.TryParse(seriesSearchResult.FirstAired, out var firstAired); - var remoteSearchResult = new RemoteSearchResult - { - Name = tvdbTitles.FirstOrDefault(), - ProductionYear = firstAired.Year, - SearchProviderName = Name - }; - - if (!string.IsNullOrEmpty(seriesSearchResult.Banner)) - { - // Results from their Search endpoints already include the /banners/ part in the url, because reasons... - remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner; - } - - try - { - var seriesSesult = - await _tvdbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken) - .ConfigureAwait(false); - remoteSearchResult.SetProviderId(MetadataProvider.Imdb, seriesSesult.Data.ImdbId); - remoteSearchResult.SetProviderId(MetadataProvider.Zap2It, seriesSesult.Data.Zap2itId); - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Unable to retrieve series with id {TvdbId}", seriesSearchResult.Id); - } - - remoteSearchResult.SetProviderId(MetadataProvider.Tvdb, seriesSearchResult.Id.ToString()); - list.Add(new Tuple<List<string>, RemoteSearchResult>(tvdbTitles, remoteSearchResult)); - } - - return list - .OrderBy(i => i.Item1.Contains(comparableName, StringComparer.OrdinalIgnoreCase) ? 0 : 1) - .ThenBy(i => list.IndexOf(i)) - .Select(i => i.Item2) - .ToList(); - } - - /// <summary> - /// Gets the name of the comparable. - /// </summary> - /// <param name="name">The name.</param> - /// <returns>System.String.</returns> - private string GetComparableName(string name) - { - name = name.ToLowerInvariant(); - name = name.Normalize(NormalizationForm.FormKD); - name = name.Replace(", the", string.Empty).Replace("the ", " ").Replace(" the ", " "); - name = name.Replace("&", " and " ); - name = Regex.Replace(name, @"[\p{Lm}\p{Mn}]", string.Empty); // Remove diacritics, etc - name = Regex.Replace(name, @"[\W\p{Pc}]+", " "); // Replace sequences of non-word characters and _ with " " - return name.Trim(); - } - - private void MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage) - { - Series series = result.Item; - series.SetProviderId(MetadataProvider.Tvdb, tvdbSeries.Id.ToString()); - series.Name = tvdbSeries.SeriesName; - series.Overview = (tvdbSeries.Overview ?? string.Empty).Trim(); - result.ResultLanguage = metadataLanguage; - series.AirDays = TVUtils.GetAirDays(tvdbSeries.AirsDayOfWeek); - series.AirTime = tvdbSeries.AirsTime; - series.CommunityRating = (float?)tvdbSeries.SiteRating; - series.SetProviderId(MetadataProvider.Imdb, tvdbSeries.ImdbId); - series.SetProviderId(MetadataProvider.Zap2It, tvdbSeries.Zap2itId); - if (Enum.TryParse(tvdbSeries.Status, true, out SeriesStatus seriesStatus)) - { - series.Status = seriesStatus; - } - - if (DateTime.TryParse(tvdbSeries.FirstAired, out var date)) - { - // dates from tvdb are UTC but without offset or Z - series.PremiereDate = date; - series.ProductionYear = date.Year; - } - - if (!string.IsNullOrEmpty(tvdbSeries.Runtime) && double.TryParse(tvdbSeries.Runtime, out double runtime)) - { - series.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; - } - - foreach (var genre in tvdbSeries.Genre) - { - series.AddGenre(genre); - } - - if (!string.IsNullOrEmpty(tvdbSeries.Network)) - { - series.AddStudio(tvdbSeries.Network); - } - - if (result.Item.Status.HasValue && result.Item.Status.Value == SeriesStatus.Ended) - { - try - { - var episodeSummary = _tvdbClientManager - .GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).Result.Data; - var maxSeasonNumber = episodeSummary.AiredSeasons.Select(s => Convert.ToInt32(s)).Max(); - var episodeQuery = new EpisodeQuery - { - AiredSeason = maxSeasonNumber - }; - var episodesPage = - _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data; - result.Item.EndDate = episodesPage.Select(e => - { - DateTime.TryParse(e.FirstAired, out var firstAired); - return firstAired; - }).Max(); - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to find series end date for series {TvdbId}", tvdbSeries.Id); - } - } - } - - private static void MapActorsToResult(MetadataResult<Series> result, IEnumerable<Actor> actors) - { - foreach (Actor actor in actors) - { - var personInfo = new PersonInfo - { - Type = PersonType.Actor, - Name = (actor.Name ?? string.Empty).Trim(), - Role = actor.Role, - SortOrder = actor.SortOrder - }; - - if (!string.IsNullOrEmpty(actor.Image)) - { - personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image; - } - - if (!string.IsNullOrWhiteSpace(personInfo.Name)) - { - result.AddPerson(personInfo); - } - } - } - - public string Name => "TheTVDB"; - - public async Task Identify(SeriesInfo info) - { - if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProvider.Tvdb))) - { - return; - } - - var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None) - .ConfigureAwait(false); - - var entry = srch.FirstOrDefault(); - - if (entry != null) - { - var id = entry.GetProviderId(MetadataProvider.Tvdb); - info.SetProviderId(MetadataProvider.Tvdb, id); - } - } - - public int Order => 0; - - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs deleted file mode 100644 index 37a8d04a6f..0000000000 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs +++ /dev/null @@ -1,39 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Providers.Plugins.TheTvdb -{ - public static class TvdbUtils - { - public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K"; - public const string TvdbBaseUrl = "https://www.thetvdb.com/"; - public const string TvdbImageBaseUrl = "https://www.thetvdb.com"; - public const string BannerUrl = TvdbImageBaseUrl + "/banners/"; - - public static ImageType GetImageTypeFromKeyType(string keyType) - { - switch (keyType.ToLowerInvariant()) - { - case "poster": - case "season": return ImageType.Primary; - case "series": - case "seasonwide": return ImageType.Banner; - case "fanart": return ImageType.Backdrop; - default: throw new ArgumentException($"Invalid or unknown keytype: {keyType}", nameof(keyType)); - } - } - - public static string NormalizeLanguage(string language) - { - if (string.IsNullOrWhiteSpace(language)) - { - return null; - } - - // pt-br is just pt to tvdb - return language.Split('-')[0].ToLowerInvariant(); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index f434440283..5ad61c567f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -2,34 +2,36 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets { public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory) + public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; } - public string Name => ProviderName; + public string Name => TmdbUtils.ProviderName; - public static string ProviderName => TmdbUtils.ProviderName; + public int Order => 0; public bool Supports(BaseItem item) { @@ -47,115 +49,64 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - var tmdbId = item.GetProviderId(MetadataProvider.Tmdb); + var tmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (!string.IsNullOrEmpty(tmdbId)) + if (tmdbId <= 0) { - var language = item.GetPreferredMetadataLanguage(); - - var mainResult = await TmdbBoxSetProvider.Current.GetMovieDbResult(tmdbId, null, cancellationToken).ConfigureAwait(false); + return Enumerable.Empty<RemoteImageInfo>(); + } - if (mainResult != null) - { - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var language = item.GetPreferredMetadataLanguage(); - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here + var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, null, null, cancellationToken).ConfigureAwait(false); - return GetImages(mainResult, language, tmdbImageUrl); - } + if (collection?.Images == null) + { + return Enumerable.Empty<RemoteImageInfo>(); } - return new List<RemoteImageInfo>(); - } + var remoteImages = new List<RemoteImageInfo>(); - private IEnumerable<RemoteImageInfo> GetImages(CollectionResult obj, string language, string baseUrl) - { - var list = new List<RemoteImageInfo>(); - - var images = obj.Images ?? new CollectionImages(); - - list.AddRange(GetPosters(images).Select(i => new RemoteImageInfo - { - Url = baseUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - })); - - list.AddRange(GetBackdrops(images).Select(i => new RemoteImageInfo + for (var i = 0; i < collection.Images.Posters.Count; i++) { - Url = baseUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - ProviderName = Name, - Type = ImageType.Backdrop, - RatingType = RatingType.Score - })); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (!isLanguageEn) + var poster = collection.Images.Posters[i]; + remoteImages.Add(new RemoteImageInfo { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } + Url = _tmdbClientManager.GetPosterUrl(poster.FilePath), + CommunityRating = poster.VoteAverage, + VoteCount = poster.VoteCount, + Width = poster.Width, + Height = poster.Height, + Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language), + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + }); + } - if (string.IsNullOrEmpty(i.Language)) + for (var i = 0; i < collection.Images.Backdrops.Count; i++) + { + var backdrop = collection.Images.Backdrops[i]; + remoteImages.Add(new RemoteImageInfo { - return isLanguageEn ? 3 : 2; - } - - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - /// <summary> - /// Gets the posters. - /// </summary> - /// <param name="images">The images.</param> - /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns> - private IEnumerable<Poster> GetPosters(CollectionImages images) - { - return images.Posters ?? new List<Poster>(); - } - - /// <summary> - /// Gets the backdrops. - /// </summary> - /// <param name="images">The images.</param> - /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns> - private IEnumerable<Backdrop> GetBackdrops(CollectionImages images) - { - var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() : - images.Backdrops; + Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath), + CommunityRating = backdrop.VoteAverage, + VoteCount = backdrop.VoteCount, + Width = backdrop.Width, + Height = backdrop.Height, + ProviderName = Name, + Type = ImageType.Backdrop, + RatingType = RatingType.Score + }); + } - return eligibleBackdrops.OrderByDescending(i => i.Vote_Average) - .ThenByDescending(i => i.Vote_Count); + return remoteImages.OrderByLanguageDescending(language); } - public int Order => 0; - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index 4da2c042f6..5dd1f0b73a 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -3,275 +3,128 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets { public class TmdbBoxSetProvider : IRemoteMetadataProvider<BoxSet, BoxSetInfo> { - private const string GetCollectionInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/collection/{0}?api_key={1}&append_to_response=images"; - - internal static TmdbBoxSetProvider Current; - - private readonly ILogger<TmdbBoxSetProvider> _logger; - private readonly IJsonSerializer _json; - private readonly IServerConfigurationManager _config; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; private readonly ILibraryManager _libraryManager; - public TmdbBoxSetProvider( - ILogger<TmdbBoxSetProvider> logger, - IJsonSerializer json, - IServerConfigurationManager config, - IFileSystem fileSystem, - ILocalizationManager localization, - IHttpClientFactory httpClientFactory, - ILibraryManager libraryManager) + public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager, ILibraryManager libraryManager) { - _logger = logger; - _json = json; - _config = config; - _fileSystem = fileSystem; - _localization = localization; _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; _libraryManager = libraryManager; - Current = this; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + public string Name => TmdbUtils.ProviderName; public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken) { - var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); + var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + var language = searchInfo.MetadataLanguage; - if (!string.IsNullOrEmpty(tmdbId)) + if (tmdbId > 0) { - await EnsureInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, searchInfo.MetadataLanguage); - var info = _json.DeserializeFromFile<CollectionResult>(dataFilePath); - - var images = (info.Images ?? new CollectionImages()).Posters ?? new List<Poster>(); - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false); - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); + if (collection == null) + { + return Enumerable.Empty<RemoteSearchResult>(); + } var result = new RemoteSearchResult { - Name = info.Name, - SearchProviderName = Name, - ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path) + Name = collection.Name, + SearchProviderName = Name }; - result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture)); - - return new[] { result }; - } - - return await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - } - - public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken) - { - var tmdbId = id.GetProviderId(MetadataProvider.Tmdb); - - // We don't already have an Id, need to fetch it - if (string.IsNullOrEmpty(tmdbId)) - { - var searchResults = await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false); - - var searchResult = searchResults.FirstOrDefault(); - - if (searchResult != null) + if (collection.Images != null) { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); + result.ImageUrl = _tmdbClientManager.GetPosterUrl(collection.PosterPath); } - } - var result = new MetadataResult<BoxSet>(); + result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture)); - if (!string.IsNullOrEmpty(tmdbId)) - { - var mainResult = await GetMovieDbResult(tmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (mainResult != null) - { - result.HasMetadata = true; - result.Item = GetItem(mainResult); - } - } - - return result; - } - - internal async Task<CollectionResult> GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); + return new[] { result }; } - await EnsureInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, language); + var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(dataFilePath)) + var collections = new List<RemoteSearchResult>(); + for (var i = 0; i < collectionSearchResults.Count; i++) { - return _json.DeserializeFromFile<CollectionResult>(dataFilePath); - } - - return null; - } - - private BoxSet GetItem(CollectionResult obj) - { - var item = new BoxSet - { - Name = obj.Name, - Overview = obj.Overview - }; - - item.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture)); - - return item; - } - - private async Task DownloadInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var mainResult = await FetchMainResult(tmdbId, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + var collection = new RemoteSearchResult + { + Name = collectionSearchResults[i].Name, + SearchProviderName = Name + }; + collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture)); - if (mainResult == null) - { - return; + collections.Add(collection); } - var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - - _json.SerializeToFile(mainResult, dataFilePath); + return collections; } - private async Task<CollectionResult> FetchMainResult(string id, string language, CancellationToken cancellationToken) + public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo info, CancellationToken cancellationToken) { - var url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) + var tmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + var language = info.MetadataLanguage; + // We don't already have an Id, need to fetch it + if (tmdbId <= 0) { - url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); + // ParseName is required here. + // Caller provides the filename with extension stripped and NOT the parsed filename + var parsedName = _libraryManager.ParseName(info.Name); + var cleanedName = TmdbUtils.CleanName(parsedName.Name); + var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, cancellationToken).ConfigureAwait(false); - // Get images in english and with no language - url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); + if (searchResults != null && searchResults.Count > 0) + { + tmdbId = searchResults[0].Id; + } } - cancellationToken.ThrowIfCancellationRequested(); + var result = new MetadataResult<BoxSet>(); - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) + if (tmdbId > 0) { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage); - await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(stream).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); + var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false); - if (mainResult != null && string.IsNullOrEmpty(mainResult.Name)) - { - if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) + if (collection != null) { - url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en"; - - if (!string.IsNullOrEmpty(language)) + var item = new BoxSet { - // Get images in english and with no language - url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); - } + Name = collection.Name, + Overview = collection.Overview + }; - using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - await using var langStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(langStream).ConfigureAwait(false); - } - } - - return mainResult; - } + item.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture)); - internal Task EnsureInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var path = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; + result.HasMetadata = true; + result.Item = item; } } - return DownloadInfo(tmdbId, preferredMetadataLanguage, cancellationToken); - } - - public string Name => TmdbUtils.ProviderName; - - private static string GetDataFilePath(IApplicationPaths appPaths, string tmdbId, string preferredLanguage) - { - var path = GetDataPath(appPaths, tmdbId); - - var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage ?? string.Empty); - - return Path.Combine(path, filename); - } - - private static string GetDataPath(IApplicationPaths appPaths, string tmdbId) - { - var dataPath = GetCollectionsDataPath(appPaths); - - return Path.Combine(dataPath, tmdbId); - } - - private static string GetCollectionsDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "tmdb-collections"); - - return dataPath; + return result; } public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs deleted file mode 100644 index 0a8994d540..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections -{ - public class CollectionImages - { - public List<Backdrop> Backdrops { get; set; } - - public List<Poster> Posters { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs deleted file mode 100644 index c6b851c237..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections -{ - public class CollectionResult - { - public int Id { get; set; } - - public string Name { get; set; } - - public string Overview { get; set; } - - public string Poster_Path { get; set; } - - public string Backdrop_Path { get; set; } - - public List<Part> Parts { get; set; } - - public CollectionImages Images { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs deleted file mode 100644 index a48124b3e1..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections -{ - public class Part - { - public string Title { get; set; } - - public int Id { get; set; } - - public string Release_Date { get; set; } - - public string Poster_Path { get; set; } - - public string Backdrop_Path { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs deleted file mode 100644 index 5b7627f6e8..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Backdrop - { - public double Aspect_Ratio { get; set; } - - public string File_Path { get; set; } - - public int Height { get; set; } - - public string Iso_639_1 { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - - public int Width { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs deleted file mode 100644 index 339ecb6285..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Crew - { - public int Id { get; set; } - - public string Credit_Id { get; set; } - - public string Name { get; set; } - - public string Department { get; set; } - - public string Job { get; set; } - - public string Profile_Path { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs deleted file mode 100644 index aac4420e8b..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class ExternalIds - { - public string Imdb_Id { get; set; } - - public object Freebase_Id { get; set; } - - public string Freebase_Mid { get; set; } - - public int? Tvdb_Id { get; set; } - - public int? Tvrage_Id { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs deleted file mode 100644 index 9ba1c15c65..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Genre - { - public int Id { get; set; } - - public string Name { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs deleted file mode 100644 index 0538cf174d..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Images - { - public List<Backdrop> Backdrops { get; set; } - - public List<Poster> Posters { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs deleted file mode 100644 index fff86931be..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Keyword - { - public int Id { get; set; } - - public string Name { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs deleted file mode 100644 index 235ecb5682..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Keywords - { - public List<Keyword> Results { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs deleted file mode 100644 index 4f61e978be..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Poster - { - public double Aspect_Ratio { get; set; } - - public string File_Path { get; set; } - - public int Height { get; set; } - - public string Iso_639_1 { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - - public int Width { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs deleted file mode 100644 index 0a1f8843eb..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Profile - { - public string File_Path { get; set; } - - public int Width { get; set; } - - public int Height { get; set; } - - public object Iso_639_1 { get; set; } - - public double Aspect_Ratio { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs deleted file mode 100644 index 61de819b93..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Still - { - public double Aspect_Ratio { get; set; } - - public string File_Path { get; set; } - - public int Height { get; set; } - - public string Id { get; set; } - - public string Iso_639_1 { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - - public int Width { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs deleted file mode 100644 index 59ab18b7bf..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class StillImages - { - public List<Still> Stills { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs deleted file mode 100644 index ebd5c7acee..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Video - { - public string Id { get; set; } - - public string Iso_639_1 { get; set; } - - public string Iso_3166_1 { get; set; } - - public string Key { get; set; } - - public string Name { get; set; } - - public string Site { get; set; } - - public string Size { get; set; } - - public string Type { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs deleted file mode 100644 index 241dcab4de..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Videos - { - public List<Video> Results { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs deleted file mode 100644 index e8745be140..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs +++ /dev/null @@ -1,15 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class BelongsToCollection - { - public int Id { get; set; } - - public string Name { get; set; } - - public string Poster_Path { get; set; } - - public string Backdrop_Path { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs deleted file mode 100644 index 937cfb8f6b..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class Cast - { - public int Id { get; set; } - - public string Name { get; set; } - - public string Character { get; set; } - - public int Order { get; set; } - - public int Cast_Id { get; set; } - - public string Profile_Path { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs deleted file mode 100644 index 37547640f5..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class Casts - { - public List<Cast> Cast { get; set; } - - public List<Crew> Crew { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs deleted file mode 100644 index edd656a461..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs +++ /dev/null @@ -1,15 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class Country - { - public string Iso_3166_1 { get; set; } - - public string Certification { get; set; } - - public DateTime Release_Date { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs deleted file mode 100644 index 704ebcd5a1..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs +++ /dev/null @@ -1,80 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class MovieResult - { - public bool Adult { get; set; } - - public string Backdrop_Path { get; set; } - - public BelongsToCollection Belongs_To_Collection { get; set; } - - public long Budget { get; set; } - - public List<Genre> Genres { get; set; } - - public string Homepage { get; set; } - - public int Id { get; set; } - - public string Imdb_Id { get; set; } - - public string Original_Title { get; set; } - - public string Original_Name { get; set; } - - public string Overview { get; set; } - - public double Popularity { get; set; } - - public string Poster_Path { get; set; } - - public List<ProductionCompany> Production_Companies { get; set; } - - public List<ProductionCountry> Production_Countries { get; set; } - - public string Release_Date { get; set; } - - public long Revenue { get; set; } - - public int Runtime { get; set; } - - public List<SpokenLanguage> Spoken_Languages { get; set; } - - public string Status { get; set; } - - public string Tagline { get; set; } - - public string Title { get; set; } - - public string Name { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - - public Casts Casts { get; set; } - - public Releases Releases { get; set; } - - public Images Images { get; set; } - - public Keywords Keywords { get; set; } - - public Trailers Trailers { get; set; } - - public string GetOriginalTitle() - { - return Original_Name ?? Original_Title; - } - - public string GetTitle() - { - return Name ?? Title ?? GetOriginalTitle(); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs deleted file mode 100644 index 2788731b2e..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class ProductionCompany - { - public string Name { get; set; } - - public int Id { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs deleted file mode 100644 index 1b6f2cc67a..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class ProductionCountry - { - public string Iso_3166_1 { get; set; } - - public string Name { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs deleted file mode 100644 index 276fbaaf55..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class Releases - { - public List<Country> Countries { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs deleted file mode 100644 index 67231d219d..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class SpokenLanguage - { - public string Iso_639_1 { get; set; } - - public string Name { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs deleted file mode 100644 index 166860f517..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class Trailers - { - public List<Youtube> Youtube { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs deleted file mode 100644 index 6885b7dab5..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies -{ - public class Youtube - { - public string Name { get; set; } - - public string Size { get; set; } - - public string Source { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs deleted file mode 100644 index 3ea12334e8..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs +++ /dev/null @@ -1,12 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People -{ - public class PersonImages - { - public List<Profile> Profiles { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs deleted file mode 100644 index 460ced49a0..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs +++ /dev/null @@ -1,38 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People -{ - public class PersonResult - { - public bool Adult { get; set; } - - public List<string> Also_Known_As { get; set; } - - public string Biography { get; set; } - - public string Birthday { get; set; } - - public string Deathday { get; set; } - - public string Homepage { get; set; } - - public int Id { get; set; } - - public string Imdb_Id { get; set; } - - public string Name { get; set; } - - public string Place_Of_Birth { get; set; } - - public double Popularity { get; set; } - - public string Profile_Path { get; set; } - - public PersonImages Images { get; set; } - - public ExternalIds External_Ids { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs deleted file mode 100644 index 87c2a723d1..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search -{ - public class ExternalIdLookupResult - { - public List<TvResult> Tv_Results { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs deleted file mode 100644 index 401c75c319..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs +++ /dev/null @@ -1,78 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search -{ - public class MovieResult - { - /// <summary> - /// Gets or sets a value indicating whether this <see cref="MovieResult" /> is adult. - /// </summary> - /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value> - public bool Adult { get; set; } - - /// <summary> - /// Gets or sets the backdrop_path. - /// </summary> - /// <value>The backdrop_path.</value> - public string Backdrop_Path { get; set; } - - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - public int Id { get; set; } - - /// <summary> - /// Gets or sets the original_title. - /// </summary> - /// <value>The original_title.</value> - public string Original_Title { get; set; } - - /// <summary> - /// Gets or sets the original_name. - /// </summary> - /// <value>The original_name.</value> - public string Original_Name { get; set; } - - /// <summary> - /// Gets or sets the release_date. - /// </summary> - /// <value>The release_date.</value> - public string Release_Date { get; set; } - - /// <summary> - /// Gets or sets the poster_path. - /// </summary> - /// <value>The poster_path.</value> - public string Poster_Path { get; set; } - - /// <summary> - /// Gets or sets the popularity. - /// </summary> - /// <value>The popularity.</value> - public double Popularity { get; set; } - - /// <summary> - /// Gets or sets the title. - /// </summary> - /// <value>The title.</value> - public string Title { get; set; } - - /// <summary> - /// Gets or sets the vote_average. - /// </summary> - /// <value>The vote_average.</value> - public double Vote_Average { get; set; } - - /// <summary> - /// For collection search results. - /// </summary> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the vote_count. - /// </summary> - /// <value>The vote_count.</value> - public int Vote_Count { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs deleted file mode 100644 index 4cff45ca64..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search -{ - public class PersonSearchResult - { - /// <summary> - /// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult. - /// </summary> - /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value> - public bool Adult { get; set; } - - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - public int Id { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the profile_ path. - /// </summary> - /// <value>The profile_ path.</value> - public string Profile_Path { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs deleted file mode 100644 index 3b9257b623..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs +++ /dev/null @@ -1,33 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search -{ - public class TmdbSearchResult<T> - { - /// <summary> - /// Gets or sets the page. - /// </summary> - /// <value>The page.</value> - public int Page { get; set; } - - /// <summary> - /// Gets or sets the results. - /// </summary> - /// <value>The results.</value> - public List<T> Results { get; set; } - - /// <summary> - /// Gets or sets the total_pages. - /// </summary> - /// <value>The total_pages.</value> - public int Total_Pages { get; set; } - - /// <summary> - /// Gets or sets the total_results. - /// </summary> - /// <value>The total_results.</value> - public int Total_Results { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs deleted file mode 100644 index b2bb068b58..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs +++ /dev/null @@ -1,25 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search -{ - public class TvResult - { - public string Backdrop_Path { get; set; } - - public string First_Air_Date { get; set; } - - public int Id { get; set; } - - public string Original_Name { get; set; } - - public string Poster_Path { get; set; } - - public double Popularity { get; set; } - - public string Name { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs deleted file mode 100644 index 4ce26c65ee..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class Cast - { - public string Character { get; set; } - - public string Credit_Id { get; set; } - - public int Id { get; set; } - - public string Name { get; set; } - - public string Profile_Path { get; set; } - - public int Order { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs deleted file mode 100644 index aef4e28632..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class ContentRating - { - public string Iso_3166_1 { get; set; } - - public string Rating { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs deleted file mode 100644 index ae1b5668d3..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class ContentRatings - { - public List<ContentRating> Results { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs deleted file mode 100644 index ba36632e04..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class CreatedBy - { - public int Id { get; set; } - - public string Name { get; set; } - - public string Profile_Path { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs deleted file mode 100644 index 47205d8751..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class Credits - { - public List<Cast> Cast { get; set; } - - public List<Crew> Crew { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs deleted file mode 100644 index 53e3c26959..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class Episode - { - public string Air_Date { get; set; } - - public int Episode_Number { get; set; } - - public int Id { get; set; } - - public string Name { get; set; } - - public string Overview { get; set; } - - public string Still_Path { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs deleted file mode 100644 index 9707e4bf49..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class EpisodeCredits - { - public List<Cast> Cast { get; set; } - - public List<Crew> Crew { get; set; } - - public List<GuestStar> Guest_Stars { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs deleted file mode 100644 index 4458bad367..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs +++ /dev/null @@ -1,38 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class EpisodeResult - { - public DateTime Air_Date { get; set; } - - public int Episode_Number { get; set; } - - public string Name { get; set; } - - public string Overview { get; set; } - - public int Id { get; set; } - - public object Production_Code { get; set; } - - public int Season_Number { get; set; } - - public string Still_Path { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - - public StillImages Images { get; set; } - - public ExternalIds External_Ids { get; set; } - - public EpisodeCredits Credits { get; set; } - - public Tmdb.Models.General.Videos Videos { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs deleted file mode 100644 index 8f39886418..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class GuestStar - { - public int Id { get; set; } - - public string Name { get; set; } - - public string Credit_Id { get; set; } - - public string Character { get; set; } - - public int Order { get; set; } - - public string Profile_Path { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs deleted file mode 100644 index 3dc310d330..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class Network - { - public int Id { get; set; } - - public string Name { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs deleted file mode 100644 index 9cbd283a95..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class Season - { - public string Air_Date { get; set; } - - public int Episode_Count { get; set; } - - public int Id { get; set; } - - public string Poster_Path { get; set; } - - public int Season_Number { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs deleted file mode 100644 index f364d4921b..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs +++ /dev/null @@ -1,12 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class SeasonImages - { - public List<Poster> Posters { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs deleted file mode 100644 index e98048eacc..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs +++ /dev/null @@ -1,33 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class SeasonResult - { - public DateTime Air_Date { get; set; } - - public List<Episode> Episodes { get; set; } - - public string Name { get; set; } - - public string Overview { get; set; } - - public int Id { get; set; } - - public string Poster_Path { get; set; } - - public int Season_Number { get; set; } - - public Credits Credits { get; set; } - - public SeasonImages Images { get; set; } - - public ExternalIds External_Ids { get; set; } - - public General.Videos Videos { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs deleted file mode 100644 index 331cd59fa6..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs +++ /dev/null @@ -1,71 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV -{ - public class SeriesResult - { - public string Backdrop_Path { get; set; } - - public List<CreatedBy> Created_By { get; set; } - - public List<int> Episode_Run_Time { get; set; } - - public DateTime First_Air_Date { get; set; } - - public List<Genre> Genres { get; set; } - - public string Homepage { get; set; } - - public int Id { get; set; } - - public bool In_Production { get; set; } - - public List<string> Languages { get; set; } - - public DateTime Last_Air_Date { get; set; } - - public string Name { get; set; } - - public List<Network> Networks { get; set; } - - public int Number_Of_Episodes { get; set; } - - public int Number_Of_Seasons { get; set; } - - public string Original_Name { get; set; } - - public List<string> Origin_Country { get; set; } - - public string Overview { get; set; } - - public string Popularity { get; set; } - - public string Poster_Path { get; set; } - - public List<Season> Seasons { get; set; } - - public string Status { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - - public Credits Credits { get; set; } - - public Images Images { get; set; } - - public Keywords Keywords { get; set; } - - public ExternalIds External_Ids { get; set; } - - public General.Videos Videos { get; set; } - - public ContentRatings Content_Ratings { get; set; } - - public string ResultLanguage { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs deleted file mode 100644 index 01a887eed5..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs +++ /dev/null @@ -1,310 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Movies -{ - public class GenericTmdbMovieInfo<T> - where T : BaseItem, new() - { - private readonly ILogger _logger; - private readonly IJsonSerializer _jsonSerializer; - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _fileSystem; - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public GenericTmdbMovieInfo(ILogger logger, IJsonSerializer jsonSerializer, ILibraryManager libraryManager, IFileSystem fileSystem) - { - _logger = logger; - _jsonSerializer = jsonSerializer; - _libraryManager = libraryManager; - _fileSystem = fileSystem; - } - - public async Task<MetadataResult<T>> GetMetadata(ItemLookupInfo itemId, CancellationToken cancellationToken) - { - var tmdbId = itemId.GetProviderId(MetadataProvider.Tmdb); - var imdbId = itemId.GetProviderId(MetadataProvider.Imdb); - - // Don't search for music video id's because it is very easy to misidentify. - if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId) && typeof(T) != typeof(MusicVideo)) - { - var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(itemId, cancellationToken).ConfigureAwait(false); - - var searchResult = searchResults.FirstOrDefault(); - - if (searchResult != null) - { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); - } - } - - if (!string.IsNullOrEmpty(tmdbId) || !string.IsNullOrEmpty(imdbId)) - { - cancellationToken.ThrowIfCancellationRequested(); - - return await FetchMovieData(tmdbId, imdbId, itemId.MetadataLanguage, itemId.MetadataCountryCode, cancellationToken).ConfigureAwait(false); - } - - return new MetadataResult<T>(); - } - - /// <summary> - /// Fetches the movie data. - /// </summary> - /// <param name="tmdbId">The TMDB identifier.</param> - /// <param name="imdbId">The imdb identifier.</param> - /// <param name="language">The language.</param> - /// <param name="preferredCountryCode">The preferred country code.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{`0}.</returns> - private async Task<MetadataResult<T>> FetchMovieData(string tmdbId, string imdbId, string language, string preferredCountryCode, CancellationToken cancellationToken) - { - var item = new MetadataResult<T> - { - Item = new T() - }; - - string dataFilePath = null; - MovieResult movieInfo = null; - - // Id could be ImdbId or TmdbId - if (string.IsNullOrEmpty(tmdbId)) - { - movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false); - if (movieInfo != null) - { - tmdbId = movieInfo.Id.ToString(_usCulture); - - dataFilePath = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language); - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(movieInfo, dataFilePath); - } - } - - if (!string.IsNullOrWhiteSpace(tmdbId)) - { - await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - - dataFilePath = dataFilePath ?? TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language); - movieInfo = movieInfo ?? _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath); - - var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - ProcessMainInfo(item, settings, preferredCountryCode, movieInfo); - item.HasMetadata = true; - } - - return item; - } - - /// <summary> - /// Processes the main info. - /// </summary> - /// <param name="resultItem">The result item.</param> - /// <param name="settings">The settings.</param> - /// <param name="preferredCountryCode">The preferred country code.</param> - /// <param name="movieData">The movie data.</param> - private void ProcessMainInfo(MetadataResult<T> resultItem, TmdbSettingsResult settings, string preferredCountryCode, MovieResult movieData) - { - var movie = resultItem.Item; - - movie.Name = movieData.GetTitle() ?? movie.Name; - - movie.OriginalTitle = movieData.GetOriginalTitle(); - - movie.Overview = string.IsNullOrWhiteSpace(movieData.Overview) ? null : WebUtility.HtmlDecode(movieData.Overview); - movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null; - - // movie.HomePageUrl = movieData.homepage; - - if (!string.IsNullOrEmpty(movieData.Tagline)) - { - movie.Tagline = movieData.Tagline; - } - - if (movieData.Production_Countries != null) - { - movie.ProductionLocations = movieData - .Production_Countries - .Select(i => i.Name) - .ToArray(); - } - - movie.SetProviderId(MetadataProvider.Tmdb, movieData.Id.ToString(_usCulture)); - movie.SetProviderId(MetadataProvider.Imdb, movieData.Imdb_Id); - - if (movieData.Belongs_To_Collection != null) - { - movie.SetProviderId(MetadataProvider.TmdbCollection, - movieData.Belongs_To_Collection.Id.ToString(CultureInfo.InvariantCulture)); - - if (movie is Movie movieItem) - { - movieItem.CollectionName = movieData.Belongs_To_Collection.Name; - } - } - - string voteAvg = movieData.Vote_Average.ToString(CultureInfo.InvariantCulture); - - if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var rating)) - { - movie.CommunityRating = rating; - } - - // movie.VoteCount = movieData.vote_count; - - if (movieData.Releases != null && movieData.Releases.Countries != null) - { - var releases = movieData.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList(); - - var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); - var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); - - if (ourRelease != null) - { - var ratingPrefix = string.Equals(preferredCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? "" : preferredCountryCode + "-"; - var newRating = ratingPrefix + ourRelease.Certification; - - newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase); - - movie.OfficialRating = newRating; - } - else if (usRelease != null) - { - movie.OfficialRating = usRelease.Certification; - } - } - - if (!string.IsNullOrWhiteSpace(movieData.Release_Date)) - { - // These dates are always in this exact format - if (DateTime.TryParse(movieData.Release_Date, _usCulture, DateTimeStyles.None, out var r)) - { - movie.PremiereDate = r.ToUniversalTime(); - movie.ProductionYear = movie.PremiereDate.Value.Year; - } - } - - // studios - if (movieData.Production_Companies != null) - { - movie.SetStudios(movieData.Production_Companies.Select(c => c.Name)); - } - - // genres - // Movies get this from imdb - var genres = movieData.Genres ?? new List<Tmdb.Models.General.Genre>(); - - foreach (var genre in genres.Select(g => g.Name)) - { - movie.AddGenre(genre); - } - - resultItem.ResetPeople(); - var tmdbImageUrl = settings.images.GetImageUrl("original"); - - // Actors, Directors, Writers - all in People - // actors come from cast - if (movieData.Casts != null && movieData.Casts.Cast != null) - { - foreach (var actor in movieData.Casts.Cast.OrderBy(a => a.Order)) - { - var personInfo = new PersonInfo - { - Name = actor.Name.Trim(), - Role = actor.Character, - Type = PersonType.Actor, - SortOrder = actor.Order - }; - - if (!string.IsNullOrWhiteSpace(actor.Profile_Path)) - { - personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path; - } - - if (actor.Id > 0) - { - personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); - } - - resultItem.AddPerson(personInfo); - } - } - - // and the rest from crew - if (movieData.Casts?.Crew != null) - { - var keepTypes = new[] - { - PersonType.Director, - PersonType.Writer, - PersonType.Producer - }; - - foreach (var person in movieData.Casts.Crew) - { - // Normalize this - var type = TmdbUtils.MapCrewToPersonType(person); - - if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) && - !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - var personInfo = new PersonInfo - { - Name = person.Name.Trim(), - Role = person.Job, - Type = type - }; - - if (!string.IsNullOrWhiteSpace(person.Profile_Path)) - { - personInfo.ImageUrl = tmdbImageUrl + person.Profile_Path; - } - - if (person.Id > 0) - { - personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); - } - - resultItem.AddPerson(personInfo); - } - } - - // if (movieData.keywords != null && movieData.keywords.keywords != null) - //{ - // movie.Keywords = movieData.keywords.keywords.Select(i => i.name).ToList(); - //} - - if (movieData.Trailers != null && movieData.Trailers.Youtube != null) - { - movie.RemoteTrailers = movieData.Trailers.Youtube.Select(i => new MediaUrl - { - Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", i.Source), - Name = i.Name - - }).ToArray(); - } - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs deleted file mode 100644 index 60d7a599ea..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs +++ /dev/null @@ -1,210 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Movies -{ - public class TmdbImageProvider : IRemoteImageProvider, IHasOrder - { - private readonly IJsonSerializer _jsonSerializer; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IFileSystem _fileSystem; - - public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem) - { - _jsonSerializer = jsonSerializer; - _httpClientFactory = httpClientFactory; - _fileSystem = fileSystem; - } - - public string Name => ProviderName; - - public static string ProviderName => TmdbUtils.ProviderName; - - public bool Supports(BaseItem item) - { - return item is Movie || item is MusicVideo || item is Trailer; - } - - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - return new List<ImageType> - { - ImageType.Primary, - ImageType.Backdrop - }; - } - - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var list = new List<RemoteImageInfo>(); - - var language = item.GetPreferredMetadataLanguage(); - - var results = await FetchImages(item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false); - - if (results == null) - { - return list; - } - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - var supportedImages = GetSupportedImages(item).ToList(); - - if (supportedImages.Contains(ImageType.Primary)) - { - list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - })); - } - - if (supportedImages.Contains(ImageType.Backdrop)) - { - list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - ProviderName = Name, - Type = ImageType.Backdrop, - RatingType = RatingType.Score - })); - } - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - /// <summary> - /// Gets the posters. - /// </summary> - /// <param name="images">The images.</param> - /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns> - private IEnumerable<Poster> GetPosters(Images images) - { - return images.Posters ?? new List<Poster>(); - } - - /// <summary> - /// Gets the backdrops. - /// </summary> - /// <param name="images">The images.</param> - /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns> - private IEnumerable<Backdrop> GetBackdrops(Images images) - { - var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() : - images.Backdrops; - - return eligibleBackdrops.OrderByDescending(i => i.Vote_Average) - .ThenByDescending(i => i.Vote_Count); - } - - /// <summary> - /// Fetches the images. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="language">The language.</param> - /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{MovieImages}.</returns> - private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, CancellationToken cancellationToken) - { - var tmdbId = item.GetProviderId(MetadataProvider.Tmdb); - - if (string.IsNullOrWhiteSpace(tmdbId)) - { - var imdbId = item.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrWhiteSpace(imdbId)) - { - var movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false); - if (movieInfo != null) - { - tmdbId = movieInfo.Id.ToString(CultureInfo.InvariantCulture); - } - } - } - - if (string.IsNullOrWhiteSpace(tmdbId)) - { - return null; - } - - await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - - var path = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language); - - if (!string.IsNullOrEmpty(path)) - { - var fileInfo = _fileSystem.GetFileInfo(path); - - if (fileInfo.Exists) - { - return jsonSerializer.DeserializeFromFile<MovieResult>(path).Images; - } - } - - return null; - } - - public int Order => 0; - - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs index 9610e40585..f1a1b65d82 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs @@ -1,4 +1,3 @@ -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Providers; @@ -33,7 +32,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies return true; } - return item is Movie || item is MusicVideo || item is Trailer; + return item is Movie; } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs new file mode 100644 index 0000000000..f34d689c1a --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs @@ -0,0 +1,129 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Providers; +using TMDbLib.Objects.Find; + +namespace MediaBrowser.Providers.Plugins.Tmdb.Movies +{ + public class TmdbMovieImageProvider : IRemoteImageProvider, IHasOrder + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; + + public TmdbMovieImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) + { + _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; + } + + public int Order => 0; + + public string Name => TmdbUtils.ProviderName; + + public bool Supports(BaseItem item) + { + return item is Movie || item is Trailer; + } + + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) + { + return new List<ImageType> + { + ImageType.Primary, + ImageType.Backdrop + }; + } + + public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) + { + var language = item.GetPreferredMetadataLanguage(); + + var movieTmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + if (movieTmdbId <= 0) + { + var movieImdbId = item.GetProviderId(MetadataProvider.Imdb); + if (string.IsNullOrEmpty(movieImdbId)) + { + return Enumerable.Empty<RemoteImageInfo>(); + } + + var movieResult = await _tmdbClientManager.FindByExternalIdAsync(movieImdbId, FindExternalSource.Imdb, language, cancellationToken).ConfigureAwait(false); + if (movieResult?.MovieResults != null && movieResult.MovieResults.Count > 0) + { + movieTmdbId = movieResult.MovieResults[0].Id; + } + } + + if (movieTmdbId <= 0) + { + return Enumerable.Empty<RemoteImageInfo>(); + } + + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here + var movie = await _tmdbClientManager + .GetMovieAsync(movieTmdbId, null, null, cancellationToken) + .ConfigureAwait(false); + + if (movie?.Images == null) + { + return Enumerable.Empty<RemoteImageInfo>(); + } + + var remoteImages = new List<RemoteImageInfo>(); + + for (var i = 0; i < movie.Images.Posters.Count; i++) + { + var poster = movie.Images.Posters[i]; + remoteImages.Add(new RemoteImageInfo + { + Url = _tmdbClientManager.GetPosterUrl(poster.FilePath), + CommunityRating = poster.VoteAverage, + VoteCount = poster.VoteCount, + Width = poster.Width, + Height = poster.Height, + Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language), + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + }); + } + + for (var i = 0; i < movie.Images.Backdrops.Count; i++) + { + var backdrop = movie.Images.Backdrops[i]; + remoteImages.Add(new RemoteImageInfo + { + Url = _tmdbClientManager.GetPosterUrl(backdrop.FilePath), + CommunityRating = backdrop.VoteAverage, + VoteCount = backdrop.VoteCount, + Width = backdrop.Width, + Height = backdrop.Height, + ProviderName = Name, + Type = ImageType.Backdrop, + RatingType = RatingType.Score + }); + } + + return remoteImages.OrderByLanguageDescending(language); + } + + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index d8918bb6b9..54f8d450ac 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -3,25 +3,19 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Net; +using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies; -using Microsoft.Extensions.Logging; +using TMDbLib.Objects.Find; +using TMDbLib.Objects.Search; namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { @@ -30,369 +24,315 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// </summary> public class TmdbMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder { - private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}"; - private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers"; - - internal static TmdbMovieProvider Current { get; private set; } - - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _configurationManager; - private readonly ILogger<TmdbMovieProvider> _logger; private readonly ILibraryManager _libraryManager; - private readonly IApplicationHost _appHost; - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly TmdbClientManager _tmdbClientManager; public TmdbMovieProvider( - IJsonSerializer jsonSerializer, - IHttpClientFactory httpClientFactory, - IFileSystem fileSystem, - IServerConfigurationManager configurationManager, - ILogger<TmdbMovieProvider> logger, ILibraryManager libraryManager, - IApplicationHost appHost) + TmdbClientManager tmdbClientManager, + IHttpClientFactory httpClientFactory) { - _jsonSerializer = jsonSerializer; - _httpClientFactory = httpClientFactory; - _fileSystem = fileSystem; - _configurationManager = configurationManager; - _logger = logger; _libraryManager = libraryManager; - _appHost = appHost; - Current = this; + _tmdbClientManager = tmdbClientManager; + _httpClientFactory = httpClientFactory; } - public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken) - { - return GetMovieSearchResults(searchInfo, cancellationToken); - } + public string Name => TmdbUtils.ProviderName; - public async Task<IEnumerable<RemoteSearchResult>> GetMovieSearchResults(ItemLookupInfo searchInfo, CancellationToken cancellationToken) - { - var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); + /// <inheritdoc /> + public int Order => 1; - if (!string.IsNullOrEmpty(tmdbId)) + public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken) + { + if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var id)) { - cancellationToken.ThrowIfCancellationRequested(); - - await EnsureMovieInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage); - - var obj = _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath); - - var tmdbSettings = await GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); + var movie = await _tmdbClientManager + .GetMovieAsync( + int.Parse(id, CultureInfo.InvariantCulture), + searchInfo.MetadataLanguage, + TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), + cancellationToken) + .ConfigureAwait(false); var remoteResult = new RemoteSearchResult { - Name = obj.GetTitle(), + Name = movie.Title ?? movie.OriginalTitle, SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path + ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath), + Overview = movie.Overview }; - if (!string.IsNullOrWhiteSpace(obj.Release_Date)) + if (movie.ReleaseDate != null) { - // These dates are always in this exact format - if (DateTime.TryParse(obj.Release_Date, _usCulture, DateTimeStyles.None, out var r)) - { - remoteResult.PremiereDate = r.ToUniversalTime(); - remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; - } + var releaseDate = movie.ReleaseDate.Value.ToUniversalTime(); + remoteResult.PremiereDate = releaseDate; + remoteResult.ProductionYear = releaseDate.Year; } - remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture)); + remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture)); - if (!string.IsNullOrWhiteSpace(obj.Imdb_Id)) + if (!string.IsNullOrWhiteSpace(movie.ImdbId)) { - remoteResult.SetProviderId(MetadataProvider.Imdb, obj.Imdb_Id); + remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId); } return new[] { remoteResult }; } - return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - } - - public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken) - { - return GetItemMetadata<Movie>(info, cancellationToken); - } - - public Task<MetadataResult<T>> GetItemMetadata<T>(ItemLookupInfo id, CancellationToken cancellationToken) - where T : BaseItem, new() - { - var movieDb = new GenericTmdbMovieInfo<T>(_logger, _jsonSerializer, _libraryManager, _fileSystem); - - return movieDb.GetMetadata(id, cancellationToken); - } - - public string Name => TmdbUtils.ProviderName; - - /// <summary> - /// The _TMDB settings task. - /// </summary> - private TmdbSettingsResult _tmdbSettings; - - /// <summary> - /// Gets the TMDB settings. - /// </summary> - /// <returns>Task{TmdbSettingsResult}.</returns> - internal async Task<TmdbSettingsResult> GetTmdbSettings(CancellationToken cancellationToken) - { - if (_tmdbSettings != null) + IReadOnlyList<SearchMovie> movieResults; + if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out id)) { - return _tmdbSettings; + var result = await _tmdbClientManager.FindByExternalIdAsync( + id, + FindExternalSource.Imdb, + TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), + cancellationToken).ConfigureAwait(false); + movieResults = result.MovieResults; } - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey)); - foreach (var header in TmdbUtils.AcceptHeaders) + else if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out id)) { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + var result = await _tmdbClientManager.FindByExternalIdAsync( + id, + FindExternalSource.TvDb, + TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), + cancellationToken).ConfigureAwait(false); + movieResults = result.MovieResults; + } + else + { + movieResults = await _tmdbClientManager + .SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); } - using var response = await GetMovieDbResponse(requestMessage).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(stream).ConfigureAwait(false); - return _tmdbSettings; - } - - /// <summary> - /// Gets the movie data path. - /// </summary> - /// <param name="appPaths">The app paths.</param> - /// <param name="tmdbId">The TMDB id.</param> - /// <returns>System.String.</returns> - internal static string GetMovieDataPath(IApplicationPaths appPaths, string tmdbId) - { - var dataPath = GetMoviesDataPath(appPaths); + var len = movieResults.Count; + var remoteSearchResults = new RemoteSearchResult[len]; + for (var i = 0; i < len; i++) + { + var movieResult = movieResults[i]; + var remoteSearchResult = new RemoteSearchResult + { + Name = movieResult.Title ?? movieResult.OriginalTitle, + ImageUrl = _tmdbClientManager.GetPosterUrl(movieResult.PosterPath), + Overview = movieResult.Overview, + SearchProviderName = Name + }; - return Path.Combine(dataPath, tmdbId); - } + var releaseDate = movieResult.ReleaseDate?.ToUniversalTime(); + remoteSearchResult.PremiereDate = releaseDate; + remoteSearchResult.ProductionYear = releaseDate?.Year; - internal static string GetMoviesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "tmdb-movies2"); + remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, movieResult.Id.ToString(CultureInfo.InvariantCulture)); + remoteSearchResults[i] = remoteSearchResult; + } - return dataPath; + return remoteSearchResults; } - /// <summary> - /// Downloads the movie info. - /// </summary> - /// <param name="id">The id.</param> - /// <param name="preferredMetadataLanguage">The preferred metadata language.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - internal async Task DownloadMovieInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken) + public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken) { - var mainResult = await FetchMainResult(id, true, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); + var imdbId = info.GetProviderId(MetadataProvider.Imdb); - if (mainResult == null) + if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId)) { - return; - } - - var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage); + // ParseName is required here. + // Caller provides the filename with extension stripped and NOT the parsed filename + var parsedName = _libraryManager.ParseName(info.Name); + var cleanedName = TmdbUtils.CleanName(parsedName.Name); + var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal Task EnsureMovieInfo(string tmdbId, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); + if (searchResults.Count > 0) + { + tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); + } } - var path = GetDataFilePath(tmdbId, language); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) + if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId)) { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) + var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + if (movieResultFromImdbId?.MovieResults.Count > 0) { - return Task.CompletedTask; + tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture); } } - return DownloadMovieInfo(tmdbId, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, string preferredLanguage) - { if (string.IsNullOrEmpty(tmdbId)) { - throw new ArgumentNullException(nameof(tmdbId)); + return new MetadataResult<Movie>(); } - var path = GetMovieDataPath(_configurationManager.ApplicationPaths, tmdbId); + var movieResult = await _tmdbClientManager + .GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(preferredLanguage)) + if (movieResult == null) { - preferredLanguage = "alllang"; + return new MetadataResult<Movie>(); } - var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage); - - return Path.Combine(path, filename); - } + var movie = new Movie + { + Name = movieResult.Title ?? movieResult.OriginalTitle, + OriginalTitle = movieResult.OriginalTitle, + Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture), + Tagline = movieResult.Tagline, + ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray() + }; + var metadataResult = new MetadataResult<Movie> + { + HasMetadata = true, + ResultLanguage = info.MetadataLanguage, + Item = movie + }; + + movie.SetProviderId(MetadataProvider.Tmdb, tmdbId); + movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId); + if (movieResult.BelongsToCollection != null) + { + movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture)); + movie.CollectionName = movieResult.BelongsToCollection.Name; + } - public static string GetImageLanguagesParam(string preferredLanguage) - { - var languages = new List<string>(); + movie.CommunityRating = Convert.ToSingle(movieResult.VoteAverage); - if (!string.IsNullOrEmpty(preferredLanguage)) + if (movieResult.Releases?.Countries != null) { - preferredLanguage = NormalizeLanguage(preferredLanguage); + var releases = movieResult.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList(); - languages.Add(preferredLanguage); + var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)); + var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); - if (preferredLanguage.Length == 5) // like en-US + if (ourRelease != null) { - // Currenty, TMDB supports 2-letter language codes only - // They are planning to change this in the future, thus we're - // supplying both codes if we're having a 5-letter code. - languages.Add(preferredLanguage.Substring(0, 2)); + movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification); + } + else if (usRelease != null) + { + movie.OfficialRating = usRelease.Certification; } } - languages.Add("null"); + movie.PremiereDate = movieResult.ReleaseDate; + movie.ProductionYear = movieResult.ReleaseDate?.Year; - if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase)) + if (movieResult.ProductionCompanies != null) { - languages.Add("en"); + movie.SetStudios(movieResult.ProductionCompanies.Select(c => c.Name)); } - return string.Join(",", languages); - } + var genres = movieResult.Genres; - public static string NormalizeLanguage(string language) - { - if (!string.IsNullOrEmpty(language)) + foreach (var genre in genres.Select(g => g.Name)) { - // They require this to be uppercase - // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api. - // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab - var parts = language.Split('-'); + movie.AddGenre(genre); + } - if (parts.Length == 2) + if (movieResult.Keywords?.Keywords != null) + { + for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++) { - language = parts[0] + "-" + parts[1].ToUpperInvariant(); + movie.AddTag(movieResult.Keywords.Keywords[i].Name); } } - return language; - } - - public static string AdjustImageLanguage(string imageLanguage, string requestLanguage) - { - if (!string.IsNullOrEmpty(imageLanguage) - && !string.IsNullOrEmpty(requestLanguage) - && requestLanguage.Length > 2 - && imageLanguage.Length == 2 - && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase)) + if (movieResult.Credits?.Cast != null) { - return requestLanguage; - } - - return imageLanguage; - } + // TODO configurable + foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers)) + { + var personInfo = new PersonInfo + { + Name = actor.Name.Trim(), + Role = actor.Character, + Type = PersonType.Actor, + SortOrder = actor.Order + }; - /// <summary> - /// Fetches the main result. - /// </summary> - /// <param name="id">The id.</param> - /// <param name="isTmdbId">if set to <c>true</c> [is TMDB identifier].</param> - /// <param name="language">The language.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{CompleteMovieData}.</returns> - internal async Task<MovieResult> FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken) - { - var url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey); + if (!string.IsNullOrWhiteSpace(actor.ProfilePath)) + { + personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath); + } - if (!string.IsNullOrEmpty(language)) - { - url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language)); + if (actor.Id > 0) + { + personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); + } - // Get images in english and with no language - url += "&include_image_language=" + GetImageLanguagesParam(language); + metadataResult.AddPerson(personInfo); + } } - cancellationToken.ThrowIfCancellationRequested(); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) + if (movieResult.Credits?.Crew != null) { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } + var keepTypes = new[] + { + PersonType.Director, + PersonType.Writer, + PersonType.Producer + }; - using var mainResponse = await GetMovieDbResponse(requestMessage).ConfigureAwait(false); - if (mainResponse.StatusCode == HttpStatusCode.NotFound) - { - return null; - } + foreach (var person in movieResult.Credits.Crew) + { + // Normalize this + var type = TmdbUtils.MapCrewToPersonType(person); - await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false); + if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) && + !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + continue; + } - cancellationToken.ThrowIfCancellationRequested(); + var personInfo = new PersonInfo + { + Name = person.Name.Trim(), + Role = person.Job, + Type = type + }; - // If the language preference isn't english, then have the overview fallback to english if it's blank - if (mainResult != null && - string.IsNullOrEmpty(mainResult.Overview) && - !string.IsNullOrEmpty(language) && - !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English..."); + if (!string.IsNullOrWhiteSpace(person.ProfilePath)) + { + personInfo.ImageUrl = _tmdbClientManager.GetPosterUrl(person.ProfilePath); + } - url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en"; + if (person.Id > 0) + { + personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); + } - if (!string.IsNullOrEmpty(language)) - { - // Get images in english and with no language - url += "&include_image_language=" + GetImageLanguagesParam(language); + metadataResult.AddPerson(personInfo); } + } - using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) + if (movieResult.Videos?.Results != null) + { + var trailers = new List<MediaUrl>(); + for (var i = 0; i < movieResult.Videos.Results.Count; i++) { - langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } + var video = movieResult.Videos.Results[0]; + if (!TmdbUtils.IsTrailerType(video)) + { + continue; + } - using var langResponse = await GetMovieDbResponse(langRequestMessage).ConfigureAwait(false); + trailers.Add(new MediaUrl + { + Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", video.Key), + Name = video.Name + }); + } - await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var langResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false); - mainResult.Overview = langResult.Overview; + movie.RemoteTrailers = trailers; } - return mainResult; - } - - /// <summary> - /// Gets the movie db response. - /// </summary> - internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message) - { - message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent); - return _httpClientFactory.CreateClient().SendAsync(message); + return metadataResult; } /// <inheritdoc /> - public int Order => 1; - - /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs deleted file mode 100644 index 2a6c6d0350..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs +++ /dev/null @@ -1,297 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Search; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Movies -{ - public class TmdbSearch - { - private const string SearchUrl = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; - private const string SearchUrlTvWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/tv?api_key={1}&query={0}&language={2}&first_air_date_year={3}"; - private const string SearchUrlMovieWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/movie?api_key={1}&query={0}&language={2}&primary_release_year={3}"; - - private static readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled); - private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled); - private static readonly Regex _cleanStopWords = new Regex( - @"\b( # Start at word boundary - 19[0-9]{2}|20[0-9]{2}| # 1900-2099 - S[0-9]{2}| # Season - E[0-9]{2}| # Episode - (2160|1080|720|576|480)[ip]?| # Resolution - [xh]?264| # Encoding - (web|dvd|bd|hdtv|hd)rip| # *Rip - web|hdtv|mp4|bluray|ktr|dl|single|imageset|internal|doku|dubbed|retail|xxx|flac - ).* # Match rest of string", - RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase); - - private readonly ILogger _logger; - private readonly IJsonSerializer _json; - private readonly ILibraryManager _libraryManager; - - public TmdbSearch(ILogger logger, IJsonSerializer json, ILibraryManager libraryManager) - { - _logger = logger; - _json = json; - _libraryManager = libraryManager; - } - - public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo idInfo, CancellationToken cancellationToken) - { - return GetSearchResults(idInfo, "tv", cancellationToken); - } - - public Task<IEnumerable<RemoteSearchResult>> GetMovieSearchResults(ItemLookupInfo idInfo, CancellationToken cancellationToken) - { - return GetSearchResults(idInfo, "movie", cancellationToken); - } - - public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo idInfo, CancellationToken cancellationToken) - { - return GetSearchResults(idInfo, "collection", cancellationToken); - } - - private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ItemLookupInfo idInfo, string searchType, CancellationToken cancellationToken) - { - var name = idInfo.Name; - var year = idInfo.Year; - - if (string.IsNullOrWhiteSpace(name)) - { - return new List<RemoteSearchResult>(); - } - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - // ParseName is required here. - // Caller provides the filename with extension stripped and NOT the parsed filename - var parsedName = _libraryManager.ParseName(name); - var yearInName = parsedName.Year; - name = parsedName.Name; - year ??= yearInName; - - var language = idInfo.MetadataLanguage.ToLowerInvariant(); - - // Replace sequences of non-word characters with space - // TMDB expects a space separated list of words make sure that is the case - name = _cleanNonWord.Replace(name, " ").Trim(); - - _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name, year); - var results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); - - if (results.Count == 0) - { - // try in english if wasn't before - if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - results = await GetSearchResults(name, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false); - } - } - - // TODO: retrying alternatives should be done outside the search - // provider so that the retry logic can be common for all search - // providers - if (results.Count == 0) - { - var name2 = parsedName.Name; - - // Remove things enclosed in []{}() etc - name2 = _cleanEnclosed.Replace(name2, string.Empty); - - // Replace sequences of non-word characters with space - name2 = _cleanNonWord.Replace(name2, " "); - - // Clean based on common stop words / tokens - name2 = _cleanStopWords.Replace(name2, string.Empty); - - // Trim whitespace - name2 = name2.Trim(); - - // Search again if the new name is different - if (!string.Equals(name2, name, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(name2)) - { - _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year); - results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); - - if (results.Count == 0 && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - // one more time, in english - results = await GetSearchResults(name2, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false); - } - } - } - - return results.Where(i => - { - if (year.HasValue && i.ProductionYear.HasValue) - { - // Allow one year tolerance - return Math.Abs(year.Value - i.ProductionYear.Value) <= 1; - } - - return true; - }); - } - - private Task<List<RemoteSearchResult>> GetSearchResults(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken) - { - switch (type) - { - case "tv": - return GetSearchResultsTv(name, year, language, baseImageUrl, cancellationToken); - default: - return GetSearchResultsGeneric(name, type, year, language, baseImageUrl, cancellationToken); - } - } - - private async Task<List<RemoteSearchResult>> GetSearchResultsGeneric(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("String can't be null or empty.", nameof(name)); - } - - string url3; - if (year != null && string.Equals(type, "movie", StringComparison.OrdinalIgnoreCase)) - { - url3 = string.Format( - CultureInfo.InvariantCulture, - SearchUrlMovieWithYear, - WebUtility.UrlEncode(name), - TmdbUtils.ApiKey, - language, - year); - } - else - { - url3 = string.Format( - CultureInfo.InvariantCulture, - SearchUrl, - WebUtility.UrlEncode(name), - TmdbUtils.ApiKey, - language, - type); - } - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(stream).ConfigureAwait(false); - - var results = searchResults.Results ?? new List<MovieResult>(); - - return results - .Select(i => - { - var remoteResult = new RemoteSearchResult {SearchProviderName = TmdbMovieProvider.Current.Name, Name = i.Title ?? i.Name ?? i.Original_Title, ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path}; - - if (!string.IsNullOrWhiteSpace(i.Release_Date)) - { - // These dates are always in this exact format - if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r)) - { - remoteResult.PremiereDate = r.ToUniversalTime(); - remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; - } - } - - remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture)); - - return remoteResult; - }) - .ToList(); - } - - private async Task<List<RemoteSearchResult>> GetSearchResultsTv(string name, int? year, string language, string baseImageUrl, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("String can't be null or empty.", nameof(name)); - } - - string url3; - if (year == null) - { - url3 = string.Format( - CultureInfo.InvariantCulture, - SearchUrl, - WebUtility.UrlEncode(name), - TmdbUtils.ApiKey, - language, - "tv"); - } - else - { - url3 = string.Format( - CultureInfo.InvariantCulture, - SearchUrlTvWithYear, - WebUtility.UrlEncode(name), - TmdbUtils.ApiKey, - language, - year); - } - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(stream).ConfigureAwait(false); - - var results = searchResults.Results ?? new List<TvResult>(); - - return results - .Select(i => - { - var remoteResult = new RemoteSearchResult - { - SearchProviderName = TmdbMovieProvider.Current.Name, - Name = i.Name ?? i.Original_Name, - ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path - }; - - if (!string.IsNullOrWhiteSpace(i.First_Air_Date)) - { - // These dates are always in this exact format - if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r)) - { - remoteResult.PremiereDate = r.ToUniversalTime(); - remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; - } - } - - remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture)); - - return remoteResult; - }) - .ToList(); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs deleted file mode 100644 index 128258ab36..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Movies -{ - internal class TmdbImageSettings - { - public List<string> backdrop_sizes { get; set; } - - public string secure_base_url { get; set; } - - public List<string> poster_sizes { get; set; } - - public List<string> profile_sizes { get; set; } - - public string GetImageUrl(string image) - { - return secure_base_url + image; - } - } - - internal class TmdbSettingsResult - { - public TmdbImageSettings images { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs deleted file mode 100644 index 73e49ba5bf..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs +++ /dev/null @@ -1,34 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Music -{ - public class TmdbMusicVideoProvider : IRemoteMetadataProvider<MusicVideo, MusicVideoInfo> - { - public Task<MetadataResult<MusicVideo>> GetMetadata(MusicVideoInfo info, CancellationToken cancellationToken) - { - return TmdbMovieProvider.Current.GetItemMetadata<MusicVideo>(info, cancellationToken); - } - - public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MusicVideoInfo searchInfo, CancellationToken cancellationToken) - { - return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>()); - } - - public string Name => TmdbMovieProvider.Current.Name; - - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index f31a7faea2..e4c908a62c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -1,39 +1,36 @@ #pragma warning disable CS1591 -using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Models.People; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; namespace MediaBrowser.Providers.Plugins.Tmdb.People { public class TmdbPersonImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IServerConfigurationManager _config; - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory) + public TmdbPersonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - _config = config; - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; } - public string Name => ProviderName; + /// <inheritdoc /> + public string Name => TmdbUtils.ProviderName; - public static string ProviderName => TmdbUtils.ProviderName; + /// <inheritdoc /> + public int Order => 0; public bool Supports(BaseItem item) { @@ -51,85 +48,41 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var person = (Person)item; - var id = person.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(id)) + if (!person.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId)) { - await TmdbPersonProvider.Current.EnsurePersonInfo(id, cancellationToken).ConfigureAwait(false); - - var dataFilePath = TmdbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id); - - var result = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath); - - var images = result.Images ?? new PersonImages(); - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - return GetImages(images, item.GetPreferredMetadataLanguage(), tmdbImageUrl); + return Enumerable.Empty<RemoteImageInfo>(); } - return new List<RemoteImageInfo>(); - } + var language = item.GetPreferredMetadataLanguage(); + var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), language, cancellationToken).ConfigureAwait(false); + if (personResult?.Images?.Profiles == null) + { + return Enumerable.Empty<RemoteImageInfo>(); + } - private IEnumerable<RemoteImageInfo> GetImages(PersonImages images, string preferredLanguage, string baseImageUrl) - { - var list = new List<RemoteImageInfo>(); + var remoteImages = new RemoteImageInfo[personResult.Images.Profiles.Count]; - if (images.Profiles != null) + for (var i = 0; i < personResult.Images.Profiles.Count; i++) { - list.AddRange(images.Profiles.Select(i => new RemoteImageInfo + var image = personResult.Images.Profiles[i]; + remoteImages[i] = new RemoteImageInfo { ProviderName = Name, Type = ImageType.Primary, - Width = i.Width, - Height = i.Height, - Language = GetLanguage(i), - Url = baseImageUrl + i.File_Path - })); + Width = image.Width, + Height = image.Height, + Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language), + Url = _tmdbClientManager.GetProfileUrl(image.FilePath) + }; } - var language = preferredLanguage; - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - private string GetLanguage(Profile profile) - { - return profile.Iso_639_1?.ToString(); + return remoteImages.OrderByLanguageDescending(language); } - public int Order => 0; - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index e9fb5c7034..dac1183889 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -3,194 +3,123 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Models.People; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Search; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.People { public class TmdbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo> { - const string DataFileName = "info.json"; - - internal static TmdbPersonProvider Current { get; private set; } - - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _configurationManager; private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger<TmdbPersonProvider> _logger; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbPersonProvider( - IFileSystem fileSystem, - IServerConfigurationManager configurationManager, - IJsonSerializer jsonSerializer, - IHttpClientFactory httpClientFactory, - ILogger<TmdbPersonProvider> logger) + public TmdbPersonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - _fileSystem = fileSystem; - _configurationManager = configurationManager; - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; - _logger = logger; - Current = this; + _tmdbClientManager = tmdbClientManager; } public string Name => TmdbUtils.ProviderName; public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) { - var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - if (!string.IsNullOrEmpty(tmdbId)) + if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId)) { - await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId); - var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath); + var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - var images = (info.Images ?? new PersonImages()).Profiles ?? new List<Profile>(); - - var result = new RemoteSearchResult + if (personResult != null) { - Name = info.Name, - - SearchProviderName = Name, - - ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path) - }; - - result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture)); - result.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id); - - return new[] { result }; - } + var result = new RemoteSearchResult + { + Name = personResult.Name, + SearchProviderName = Name, + Overview = personResult.Biography + }; - if (searchInfo.IsAutomated) - { - // Don't hammer moviedb searching by name - return new List<RemoteSearchResult>(); - } + if (personResult.Images?.Profiles != null && personResult.Images.Profiles.Count > 0) + { + result.ImageUrl = _tmdbClientManager.GetProfileUrl(personResult.Images.Profiles[0].FilePath); + } - var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey); + result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture)); + if (!string.IsNullOrEmpty(personResult.ExternalIds.ImdbId)) + { + result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId); + } - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + return new[] { result }; + } } - var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - - var result2 = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(stream).ConfigureAwait(false) - ?? new TmdbSearchResult<PersonSearchResult>(); - - return result2.Results.Select(i => GetSearchResult(i, tmdbImageUrl)); - } + var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false); - private RemoteSearchResult GetSearchResult(PersonSearchResult i, string baseImageUrl) - { - var result = new RemoteSearchResult + var remoteSearchResults = new List<RemoteSearchResult>(); + for (var i = 0; i < personSearchResult.Count; i++) { - SearchProviderName = Name, - - Name = i.Name, - - ImageUrl = string.IsNullOrEmpty(i.Profile_Path) ? null : baseImageUrl + i.Profile_Path - }; + var person = personSearchResult[i]; + var remoteSearchResult = new RemoteSearchResult + { + SearchProviderName = Name, + Name = person.Name, + ImageUrl = _tmdbClientManager.GetProfileUrl(person.ProfilePath) + }; - result.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture)); + remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); + remoteSearchResults.Add(remoteSearchResult); + } - return result; + return remoteSearchResults; } - public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo id, CancellationToken cancellationToken) + public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo info, CancellationToken cancellationToken) { - var tmdbId = id.GetProviderId(MetadataProvider.Tmdb); + var personTmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); // We don't already have an Id, need to fetch it - if (string.IsNullOrEmpty(tmdbId)) + if (personTmdbId <= 0) { - tmdbId = await GetTmdbId(id, cancellationToken).ConfigureAwait(false); + var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false); + if (personSearchResults.Count > 0) + { + personTmdbId = personSearchResults[0].Id; + } } var result = new MetadataResult<Person>(); - if (!string.IsNullOrEmpty(tmdbId)) + if (personTmdbId > 0) { - try - { - await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - return result; - } - - throw; - } + var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); - var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId); - - var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath); - - var item = new Person(); result.HasMetadata = true; - // Take name from incoming info, don't rename the person - // TODO: This should go in PersonMetadataService, not each person provider - item.Name = id.Name; - - // item.HomePageUrl = info.homepage; - - if (!string.IsNullOrWhiteSpace(info.Place_Of_Birth)) - { - item.ProductionLocations = new string[] { info.Place_Of_Birth }; - } - - item.Overview = info.Biography; - - if (DateTime.TryParseExact(info.Birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out var date)) + var item = new Person { - item.PremiereDate = date.ToUniversalTime(); - } + // Take name from incoming info, don't rename the person + // TODO: This should go in PersonMetadataService, not each person provider + Name = info.Name, + HomePageUrl = person.Homepage, + Overview = person.Biography, + PremiereDate = person.Birthday?.ToUniversalTime(), + EndDate = person.Deathday?.ToUniversalTime() + }; - if (DateTime.TryParseExact(info.Deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date)) + if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth)) { - item.EndDate = date.ToUniversalTime(); + item.ProductionLocations = new[] { person.PlaceOfBirth }; } - item.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture)); + item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); - if (!string.IsNullOrEmpty(info.Imdb_Id)) + if (!string.IsNullOrEmpty(person.ImdbId)) { - item.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id); + item.SetProviderId(MetadataProvider.Imdb, person.ImdbId); } result.HasMetadata = true; @@ -200,66 +129,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return result; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - /// <summary> - /// Gets the TMDB id. - /// </summary> - /// <param name="info">The information.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.String}.</returns> - private async Task<string> GetTmdbId(PersonLookupInfo info, CancellationToken cancellationToken) - { - var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); - - return results.Select(i => i.GetProviderId(MetadataProvider.Tmdb)).FirstOrDefault(); - } - - internal async Task EnsurePersonInfo(string id, CancellationToken cancellationToken) - { - var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, id); - - var fileInfo = _fileSystem.GetFileSystemInfo(dataFilePath); - - if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return; - } - - var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false); - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - await using var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); - await response.Content.CopyToAsync(fs).ConfigureAwait(false); - } - - private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId) - { - var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1); - - return Path.Join(GetPersonsDataPath(appPaths), letter, tmdbId); - } - - internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId) - { - return Path.Combine(GetPersonDataPath(appPaths, tmdbId), DataFileName); - } - - private static string GetPersonsDataPath(IApplicationPaths appPaths) - { - return Path.Combine(appPaths.CachePath, "tmdb-people"); - } - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index eebecdac67..ba18c542fe 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -2,33 +2,36 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { - public class TmdbEpisodeImageProvider : - TmdbEpisodeProviderBase, - IRemoteImageProvider, - IHasOrder + public class TmdbEpisodeImageProvider : IRemoteImageProvider, IHasOrder { - public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) - : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory) - { } + private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; + + public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) + { + _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; + } + + // After TheTvDb + public int Order => 1; + + public string Name => TmdbUtils.ProviderName; public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { @@ -43,13 +46,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var episode = (Controller.Entities.TV.Episode)item; var series = episode.Series; - var seriesId = series != null ? series.GetProviderId(MetadataProvider.Tmdb) : null; - - var list = new List<RemoteImageInfo>(); + var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (string.IsNullOrEmpty(seriesId)) + if (seriesTmdbId <= 0) { - return list; + return Enumerable.Empty<RemoteImageInfo>(); } var seasonNumber = episode.ParentIndexNumber; @@ -57,77 +58,51 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (!seasonNumber.HasValue || !episodeNumber.HasValue) { - return list; + return Enumerable.Empty<RemoteImageInfo>(); } var language = item.GetPreferredMetadataLanguage(); - var response = await GetEpisodeInfo(seriesId, seasonNumber.Value, episodeNumber.Value, - language, cancellationToken).ConfigureAwait(false); - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here + var episodeResult = await _tmdbClientManager + .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken) + .ConfigureAwait(false); - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - list.AddRange(GetPosters(response.Images).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - })); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => + var stills = episodeResult?.Images?.Stills; + if (stills == null) { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } + return Enumerable.Empty<RemoteImageInfo>(); + } - if (string.IsNullOrEmpty(i.Language)) + var remoteImages = new RemoteImageInfo[stills.Count]; + for (var i = 0; i < stills.Count; i++) + { + var image = stills[i]; + remoteImages[i] = new RemoteImageInfo { - return isLanguageEn ? 3 : 2; - } + Url = _tmdbClientManager.GetStillUrl(image.FilePath), + CommunityRating = image.VoteAverage, + VoteCount = image.VoteCount, + Width = image.Width, + Height = image.Height, + Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language), + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + }; + } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - private IEnumerable<Still> GetPosters(StillImages images) - { - return images.Stills ?? new List<Still>(); + return remoteImages.OrderByLanguageDescending(language); } public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return GetResponse(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } - public string Name => TmdbUtils.ProviderName; - public bool Supports(BaseItem item) { return item is Controller.Entities.TV.Episode; } - - // After TheTvDb - public int Order => 1; } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 90e3cea932..8ec8f64641 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -4,50 +4,54 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { - public class TmdbEpisodeProvider : - TmdbEpisodeProviderBase, - IRemoteMetadataProvider<Episode, EpisodeInfo>, - IHasOrder + public class TmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder { - public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) - : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory) - { } + private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) + public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - var list = new List<RemoteSearchResult>(); + _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; + } + + // After TheTvDb + public int Order => 1; + + public string Name => TmdbUtils.ProviderName; + public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { // The search query must either provide an episode number or date if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue) { - return list; + return Enumerable.Empty<RemoteSearchResult>(); } - var metadataResult = await GetMetadata(searchInfo, cancellationToken); + var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false); - if (metadataResult.HasMetadata) + if (!metadataResult.HasMetadata) { - var item = metadataResult.Item; + return Enumerable.Empty<RemoteSearchResult>(); + } + + var item = metadataResult.Item; - list.Add(new RemoteSearchResult + return new[] + { + new RemoteSearchResult { IndexNumber = item.IndexNumber, Name = item.Name, @@ -57,27 +61,26 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV ProviderIds = item.ProviderIds, SearchProviderName = Name, IndexNumberEnd = item.IndexNumberEnd - }); - } - - return list; + } + }; } public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) { - var result = new MetadataResult<Episode>(); + var metadataResult = new MetadataResult<Episode>(); // Allowing this will dramatically increase scan times if (info.IsMissingEpisode) { - return result; + return metadataResult; } - info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string seriesTmdbId); + info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string tmdbId); - if (string.IsNullOrEmpty(seriesTmdbId)) + var seriesTmdbId = Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture); + if (seriesTmdbId <= 0) { - return result; + return metadataResult; } var seasonNumber = info.ParentIndexNumber; @@ -85,130 +88,127 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (!seasonNumber.HasValue || !episodeNumber.HasValue) { - return result; + return metadataResult; } - try + var episodeResult = await _tmdbClientManager + .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); + + if (episodeResult == null) { - var response = await GetEpisodeInfo(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + return metadataResult; + } - result.HasMetadata = true; - result.QueriedById = true; + metadataResult.HasMetadata = true; + metadataResult.QueriedById = true; - if (!string.IsNullOrEmpty(response.Overview)) - { - // if overview is non-empty, we can assume that localized data was returned - result.ResultLanguage = info.MetadataLanguage; - } + if (!string.IsNullOrEmpty(episodeResult.Overview)) + { + // if overview is non-empty, we can assume that localized data was returned + metadataResult.ResultLanguage = info.MetadataLanguage; + } - var item = new Episode(); - result.Item = item; + var item = new Episode + { + IndexNumber = info.IndexNumber, + ParentIndexNumber = info.ParentIndexNumber, + IndexNumberEnd = info.IndexNumberEnd, + Name = episodeResult.Name, + PremiereDate = episodeResult.AirDate, + ProductionYear = episodeResult.AirDate?.Year, + Overview = episodeResult.Overview, + CommunityRating = Convert.ToSingle(episodeResult.VoteAverage) + }; + + var externalIds = episodeResult.ExternalIds; + if (!string.IsNullOrEmpty(externalIds?.TvdbId)) + { + item.SetProviderId(MetadataProvider.Tvdb, externalIds.TvdbId); + } - item.Name = info.Name; - item.IndexNumber = info.IndexNumber; - item.ParentIndexNumber = info.ParentIndexNumber; - item.IndexNumberEnd = info.IndexNumberEnd; + if (!string.IsNullOrEmpty(externalIds?.ImdbId)) + { + item.SetProviderId(MetadataProvider.Imdb, externalIds.ImdbId); + } + + if (!string.IsNullOrEmpty(externalIds?.TvrageId)) + { + item.SetProviderId(MetadataProvider.TvRage, externalIds.TvrageId); + } - if (response.External_Ids.Tvdb_Id > 0) + if (episodeResult.Videos?.Results != null) + { + foreach (var video in episodeResult.Videos.Results) { - item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture)); + if (TmdbUtils.IsTrailerType(video)) + { + item.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key); + } } + } - item.PremiereDate = response.Air_Date; - item.ProductionYear = result.Item.PremiereDate.Value.Year; - - item.Name = response.Name; - item.Overview = response.Overview; - - item.CommunityRating = (float)response.Vote_Average; + var credits = episodeResult.Credits; - if (response.Videos?.Results != null) + if (credits?.Cast != null) + { + foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers)) { - foreach (var video in response.Videos.Results) + metadataResult.AddPerson(new PersonInfo { - if (video.Type.Equals("trailer", System.StringComparison.OrdinalIgnoreCase) - || video.Type.Equals("clip", System.StringComparison.OrdinalIgnoreCase)) - { - if (video.Site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase)) - { - var videoUrl = string.Format(CultureInfo.InvariantCulture, "http://www.youtube.com/watch?v={0}", video.Key); - item.AddTrailerUrl(videoUrl); - } - } - } + Name = actor.Name.Trim(), + Role = actor.Character, + Type = PersonType.Actor, + SortOrder = actor.Order + }); } + } - result.ResetPeople(); - - var credits = response.Credits; - if (credits != null) + if (credits?.GuestStars != null) + { + foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers)) { - // Actors, Directors, Writers - all in People - // actors come from cast - if (credits.Cast != null) + metadataResult.AddPerson(new PersonInfo { - foreach (var actor in credits.Cast.OrderBy(a => a.Order)) - { - result.AddPerson(new PersonInfo { Name = actor.Name.Trim(), Role = actor.Character, Type = PersonType.Actor, SortOrder = actor.Order }); - } - } + Name = guest.Name.Trim(), + Role = guest.Character, + Type = PersonType.GuestStar, + SortOrder = guest.Order + }); + } + } + + // and the rest from crew + if (credits?.Crew != null) + { + foreach (var person in credits.Crew) + { + // Normalize this + var type = TmdbUtils.MapCrewToPersonType(person); - // guest stars - if (credits.Guest_Stars != null) + if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase) + && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - foreach (var guest in credits.Guest_Stars.OrderBy(a => a.Order)) - { - result.AddPerson(new PersonInfo { Name = guest.Name.Trim(), Role = guest.Character, Type = PersonType.GuestStar, SortOrder = guest.Order }); - } + continue; } - // and the rest from crew - if (credits.Crew != null) + metadataResult.AddPerson(new PersonInfo { - var keepTypes = new[] - { - PersonType.Director, - PersonType.Writer, - PersonType.Producer - }; - - foreach (var person in credits.Crew) - { - // Normalize this - var type = TmdbUtils.MapCrewToPersonType(person); - - if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) && - !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - result.AddPerson(new PersonInfo { Name = person.Name.Trim(), Role = person.Job, Type = type }); - } - } + Name = person.Name.Trim(), + Role = person.Job, + Type = type + }); } } - catch (HttpException ex) - { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - return result; - } - throw; - } + metadataResult.Item = item; - return result; + return metadataResult; } public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return GetResponse(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } - - // After TheTvDb - public int Order => 1; - - public string Name => TmdbUtils.ProviderName; } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs deleted file mode 100644 index 5705885b46..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs +++ /dev/null @@ -1,144 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.TV; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.Plugins.Tmdb.TV -{ - public abstract class TmdbEpisodeProviderBase - { - private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos"; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerConfigurationManager _configurationManager; - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; - private readonly ILogger<TmdbEpisodeProviderBase> _logger; - - protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) - { - _httpClientFactory = httpClientFactory; - _configurationManager = configurationManager; - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - _localization = localization; - _logger = loggerFactory.CreateLogger<TmdbEpisodeProviderBase>(); - } - - protected ILogger Logger => _logger; - - protected async Task<EpisodeResult> GetEpisodeInfo(string seriesTmdbId, int season, int episodeNumber, string preferredMetadataLanguage, - CancellationToken cancellationToken) - { - await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken) - .ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage); - - return _jsonSerializer.DeserializeFromFile<EpisodeResult>(dataFilePath); - } - - internal Task EnsureEpisodeInfo(string tmdbId, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - var path = GetDataFilePath(tmdbId, seasonNumber, episodeNumber, language); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } - } - - return DownloadEpisodeInfo(tmdbId, seasonNumber, episodeNumber, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, int seasonNumber, int episodeNumber, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - if (string.IsNullOrEmpty(preferredLanguage)) - { - throw new ArgumentNullException(nameof(preferredLanguage)); - } - - var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - - var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-episode-{1}-{2}.json", - seasonNumber.ToString(CultureInfo.InvariantCulture), - episodeNumber.ToString(CultureInfo.InvariantCulture), - preferredLanguage); - - return Path.Combine(path, filename); - } - - internal async Task DownloadEpisodeInfo(string id, int seasonNumber, int episodeNumber, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var mainResult = await FetchMainResult(EpisodeUrlPattern, id, seasonNumber, episodeNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(id, seasonNumber, episodeNumber, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal async Task<EpisodeResult> FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) - { - var url = string.Format(urlPattern, id, seasonNumber.ToString(CultureInfo.InvariantCulture), episodeNumber, TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += string.Format(CultureInfo.InvariantCulture, "&language={0}", language); - } - - var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language); - // Get images in english and with no language - url += "&include_image_language=" + includeImageLanguageParam; - - cancellationToken.ThrowIfCancellationRequested(); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(stream).ConfigureAwait(false); - } - - protected Task<HttpResponseMessage> GetResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index 787514d05d..0d23c7872f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -2,128 +2,86 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { public class TmdbSeasonImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory) + public TmdbSeasonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; } public int Order => 1; - public string Name => ProviderName; - - public static string ProviderName => TmdbUtils.ProviderName; + public string Name => TmdbUtils.ProviderName; public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var season = (Season)item; - var series = season.Series; - - var seriesId = series?.GetProviderId(MetadataProvider.Tmdb); - - if (string.IsNullOrEmpty(seriesId)) - { - return Enumerable.Empty<RemoteImageInfo>(); - } + var series = season?.Series; - var seasonNumber = season.IndexNumber; + var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (!seasonNumber.HasValue) + if (seriesTmdbId <= 0 || season?.IndexNumber == null) { return Enumerable.Empty<RemoteImageInfo>(); } var language = item.GetPreferredMetadataLanguage(); - var results = await FetchImages(season, seriesId, language, cancellationToken).ConfigureAwait(false); + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here + var seasonResult = await _tmdbClientManager + .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, null, null, cancellationToken) + .ConfigureAwait(false); - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - var list = results.Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - }); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => + var posters = seasonResult?.Images?.Posters; + if (posters == null) { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - private async Task<List<Poster>> FetchImages(Season item, string tmdbId, string language, CancellationToken cancellationToken) - { - await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, item.IndexNumber.GetValueOrDefault(), language, cancellationToken).ConfigureAwait(false); - - var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language); + return Enumerable.Empty<RemoteImageInfo>(); + } - if (!string.IsNullOrEmpty(path)) + var remoteImages = new RemoteImageInfo[posters.Count]; + for (var i = 0; i < posters.Count; i++) { - if (File.Exists(path)) + var image = posters[i]; + remoteImages[i] = new RemoteImageInfo { - return _jsonSerializer.DeserializeFromFile<Models.TV.SeasonResult>(path).Images.Posters; - } + Url = _tmdbClientManager.GetPosterUrl(image.FilePath), + CommunityRating = image.VoteAverage, + VoteCount = image.VoteCount, + Width = image.Width, + Height = image.Height, + Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language), + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + }; } - return null; + return remoteImages.OrderByLanguageDescending(language); } public IEnumerable<ImageType> GetSupportedImages(BaseItem item) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index e59504cc6d..66e30115dc 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -3,50 +3,32 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Net; +using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.TV; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; -using Season = MediaBrowser.Controller.Entities.TV.Season; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo> { - private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos"; private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerConfigurationManager _configurationManager; - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; - private readonly ILogger<TmdbSeasonProvider> _logger; + private readonly TmdbClientManager _tmdbClientManager; - internal static TmdbSeasonProvider Current { get; private set; } - - public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger) + public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; - _configurationManager = configurationManager; - _fileSystem = fileSystem; - _localization = localization; - _jsonSerializer = jsonSerializer; - _logger = logger; - Current = this; + _tmdbClientManager = tmdbClientManager; } + public string Name => TmdbUtils.ProviderName; + public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<Season>(); @@ -55,172 +37,85 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var seasonNumber = info.IndexNumber; - if (!string.IsNullOrWhiteSpace(seriesTmdbId) && seasonNumber.HasValue) + if (string.IsNullOrWhiteSpace(seriesTmdbId) || !seasonNumber.HasValue) { - try - { - var seasonInfo = await GetSeasonInfo(seriesTmdbId, seasonNumber.Value, info.MetadataLanguage, cancellationToken) - .ConfigureAwait(false); - - result.HasMetadata = true; - result.Item = new Season(); - - // Don't use moviedb season names for now until if/when we have field-level configuration - // result.Item.Name = seasonInfo.name; - - result.Item.Name = info.Name; - - result.Item.IndexNumber = seasonNumber; - - result.Item.Overview = seasonInfo.Overview; - - if (seasonInfo.External_Ids.Tvdb_Id > 0) - { - result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture)); - } - - var credits = seasonInfo.Credits; - if (credits != null) - { - // Actors, Directors, Writers - all in People - // actors come from cast - if (credits.Cast != null) - { - // foreach (var actor in credits.cast.OrderBy(a => a.order)) result.Item.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order }); - } - - // and the rest from crew - if (credits.Crew != null) - { - // foreach (var person in credits.crew) result.Item.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department }); - } - } - - result.Item.PremiereDate = seasonInfo.Air_Date; - result.Item.ProductionYear = result.Item.PremiereDate.Value.Year; - } - catch (HttpException ex) - { - _logger.LogError(ex, "No metadata found for {0}", seasonNumber.Value); - - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - return result; - } - - throw; - } + return result; } - return result; - } - - public string Name => TmdbUtils.ProviderName; - - public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken) - { - return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>()); - } - - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - - private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage, - CancellationToken cancellationToken) - { - await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken) - .ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(seriesTmdbId, season, preferredMetadataLanguage); + var seasonResult = await _tmdbClientManager + .GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); - return _jsonSerializer.DeserializeFromFile<SeasonResult>(dataFilePath); - } - - internal Task EnsureSeasonInfo(string tmdbId, int seasonNumber, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) + if (seasonResult == null) { - throw new ArgumentNullException(nameof(tmdbId)); + return result; } - if (string.IsNullOrEmpty(language)) + result.HasMetadata = true; + result.Item = new Season { - throw new ArgumentNullException(nameof(language)); - } - - var path = GetDataFilePath(tmdbId, seasonNumber, language); + IndexNumber = seasonNumber, + Overview = seasonResult.Overview + }; - var fileInfo = _fileSystem.GetFileSystemInfo(path); + if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId)) + { + result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId); + } - if (fileInfo.Exists) + // TODO why was this disabled? + var credits = seasonResult.Credits; + if (credits?.Cast != null) { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) + var cast = credits.Cast.OrderBy(c => c.Order).Take(TmdbUtils.MaxCastMembers).ToList(); + for (var i = 0; i < cast.Count; i++) { - return Task.CompletedTask; + result.AddPerson(new PersonInfo + { + Name = cast[i].Name.Trim(), + Role = cast[i].Character, + Type = PersonType.Actor, + SortOrder = cast[i].Order + }); } } - return DownloadSeasonInfo(tmdbId, seasonNumber, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, int seasonNumber, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) + if (credits?.Crew != null) { - throw new ArgumentNullException(nameof(tmdbId)); - } + foreach (var person in credits.Crew) + { + // Normalize this + var type = TmdbUtils.MapCrewToPersonType(person); - if (string.IsNullOrEmpty(preferredLanguage)) - { - throw new ArgumentNullException(nameof(preferredLanguage)); - } + if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase) + && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + continue; + } - var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); + result.AddPerson(new PersonInfo + { + Name = person.Name.Trim(), + Role = person.Job, + Type = type + }); + } + } - var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-{1}.json", - seasonNumber.ToString(CultureInfo.InvariantCulture), - preferredLanguage); + result.Item.PremiereDate = seasonResult.AirDate; + result.Item.ProductionYear = seasonResult.AirDate?.Year; - return Path.Combine(path, filename); + return result; } - internal async Task DownloadSeasonInfo(string id, int seasonNumber, string preferredMetadataLanguage, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken) { - var mainResult = await FetchMainResult(id, seasonNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(id, seasonNumber, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); + return Task.FromResult(Enumerable.Empty<RemoteSearchResult>()); } - internal async Task<SeasonResult> FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - var url = string.Format(GetTvInfo3, id, seasonNumber.ToString(CultureInfo.InvariantCulture), TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); - } - - var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language); - // Get images in english and with no language - url += "&include_image_language=" + includeImageLanguageParam; - - cancellationToken.ThrowIfCancellationRequested(); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(stream).ConfigureAwait(false); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index f11eeb15b3..326c116b3b 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -2,40 +2,37 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Models.TV; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { public class TmdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; - private readonly IFileSystem _fileSystem; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem) + public TmdbSeriesImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; - _fileSystem = fileSystem; + _tmdbClientManager = tmdbClientManager; } - public string Name => ProviderName; + public string Name => TmdbUtils.ProviderName; - public static string ProviderName => TmdbUtils.ProviderName; + // After tvdb and fanart + public int Order => 2; public bool Supports(BaseItem item) { @@ -53,136 +50,69 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - var list = new List<RemoteImageInfo>(); - - var results = await FetchImages(item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false); + var tmdbId = item.GetProviderId(MetadataProvider.Tmdb); - if (results == null) + if (string.IsNullOrEmpty(tmdbId)) { - return list; + return Enumerable.Empty<RemoteImageInfo>(); } - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - var language = item.GetPreferredMetadataLanguage(); - list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - })); - - list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - ProviderName = Name, - Type = ImageType.Backdrop, - RatingType = RatingType.Score - })); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - /// <summary> - /// Gets the posters. - /// </summary> - /// <param name="images">The images.</param> - private IEnumerable<Poster> GetPosters(Images images) - { - return images.Posters ?? new List<Poster>(); - } - - /// <summary> - /// Gets the backdrops. - /// </summary> - /// <param name="images">The images.</param> - private IEnumerable<Backdrop> GetBackdrops(Images images) - { - var eligibleBackdrops = images.Backdrops ?? new List<Backdrop>(); + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here + var series = await _tmdbClientManager + .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), null, null, cancellationToken) + .ConfigureAwait(false); - return eligibleBackdrops.OrderByDescending(i => i.Vote_Average) - .ThenByDescending(i => i.Vote_Count); - } - - /// <summary> - /// Fetches the images. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="language">The language.</param> - /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{MovieImages}.</returns> - private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, - CancellationToken cancellationToken) - { - var tmdbId = item.GetProviderId(MetadataProvider.Tmdb); - - if (string.IsNullOrEmpty(tmdbId)) + if (series?.Images == null) { - return null; + return Enumerable.Empty<RemoteImageInfo>(); } - await TmdbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + var posters = series.Images.Posters; + var backdrops = series.Images.Backdrops; - var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language); + var remoteImages = new RemoteImageInfo[posters.Count + backdrops.Count]; - if (!string.IsNullOrEmpty(path)) + for (var i = 0; i < posters.Count; i++) { - var fileInfo = _fileSystem.GetFileInfo(path); + var poster = posters[i]; + remoteImages[i] = new RemoteImageInfo + { + Url = _tmdbClientManager.GetPosterUrl(poster.FilePath), + CommunityRating = poster.VoteAverage, + VoteCount = poster.VoteCount, + Width = poster.Width, + Height = poster.Height, + Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language), + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + }; + } - if (fileInfo.Exists) + for (var i = 0; i < backdrops.Count; i++) + { + var backdrop = series.Images.Backdrops[i]; + remoteImages[posters.Count + i] = new RemoteImageInfo { - return jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images; - } + Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath), + CommunityRating = backdrop.VoteAverage, + VoteCount = backdrop.VoteCount, + Width = backdrop.Width, + Height = backdrop.Height, + ProviderName = Name, + Type = ImageType.Backdrop, + RatingType = RatingType.Score + }; } - return null; + return remoteImages.OrderByLanguageDescending(language); } - // After tvdb and fanart - public int Order => 2; - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 0eded32330..da76345b5b 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -3,282 +3,296 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Search; -using MediaBrowser.Providers.Plugins.Tmdb.Models.TV; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; +using TMDbLib.Objects.Find; +using TMDbLib.Objects.Search; +using TMDbLib.Objects.TvShows; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder { - private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings"; - - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _configurationManager; - private readonly ILogger<TmdbSeriesProvider> _logger; - private readonly ILocalizationManager _localization; private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - internal static TmdbSeriesProvider Current { get; private set; } + private readonly TmdbClientManager _tmdbClientManager; public TmdbSeriesProvider( - IJsonSerializer jsonSerializer, - IFileSystem fileSystem, - IServerConfigurationManager configurationManager, - ILogger<TmdbSeriesProvider> logger, - ILocalizationManager localization, + ILibraryManager libraryManager, IHttpClientFactory httpClientFactory, - ILibraryManager libraryManager) + TmdbClientManager tmdbClientManager) { - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - _configurationManager = configurationManager; - _logger = logger; - _localization = localization; - _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; - Current = this; + _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; } public string Name => TmdbUtils.ProviderName; + // After TheTVDB + public int Order => 1; + public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) { - var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); - - if (!string.IsNullOrEmpty(tmdbId)) + if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)) { - cancellationToken.ThrowIfCancellationRequested(); + var series = await _tmdbClientManager + .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); - await EnsureSeriesInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage); + if (series != null) + { + var remoteResult = MapTvShowToRemoteSearchResult(series); - var obj = _jsonSerializer.DeserializeFromFile<SeriesResult>(dataFilePath); + return new[] { remoteResult }; + } + } - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); + if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out var imdbId)) + { + var findResult = await _tmdbClientManager + .FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); - var remoteResult = new RemoteSearchResult + var tvResults = findResult?.TvResults; + if (tvResults != null) { - Name = obj.Name, - SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path - }; + var imdbIdResults = new RemoteSearchResult[tvResults.Count]; + for (var i = 0; i < tvResults.Count; i++) + { + var remoteResult = MapSearchTvToRemoteSearchResult(tvResults[i]); + remoteResult.SetProviderId(MetadataProvider.Imdb, imdbId); + imdbIdResults[i] = remoteResult; + } - remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture)); - remoteResult.SetProviderId(MetadataProvider.Imdb, obj.External_Ids.Imdb_Id); + return imdbIdResults; + } + } + + if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)) + { + var findResult = await _tmdbClientManager + .FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); - if (obj.External_Ids.Tvdb_Id > 0) + var tvResults = findResult?.TvResults; + if (tvResults != null) { - remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.ToString(_usCulture)); + var tvIdResults = new RemoteSearchResult[tvResults.Count]; + for (var i = 0; i < tvResults.Count; i++) + { + var remoteResult = MapSearchTvToRemoteSearchResult(tvResults[i]); + remoteResult.SetProviderId(MetadataProvider.Tvdb, tvdbId); + tvIdResults[i] = remoteResult; + } + + return tvIdResults; } + } + + var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken: cancellationToken) + .ConfigureAwait(false); - return new[] { remoteResult }; + var remoteResults = new RemoteSearchResult[tvSearchResults.Count]; + for (var i = 0; i < tvSearchResults.Count; i++) + { + remoteResults[i] = MapSearchTvToRemoteSearchResult(tvSearchResults[i]); } - var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb); + return remoteResults; + } - if (!string.IsNullOrEmpty(imdbId)) + private RemoteSearchResult MapTvShowToRemoteSearchResult(TvShow series) + { + var remoteResult = new RemoteSearchResult { - var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false); + Name = series.Name ?? series.OriginalName, + SearchProviderName = Name, + ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath), + Overview = series.Overview + }; + + remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture)); + if (series.ExternalIds != null) + { + if (!string.IsNullOrEmpty(series.ExternalIds.ImdbId)) + { + remoteResult.SetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId); + } - if (searchResult != null) + if (!string.IsNullOrEmpty(series.ExternalIds.TvdbId)) { - return new[] { searchResult }; + remoteResult.SetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId); } } - var tvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb); + remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); + + return remoteResult; + } - if (!string.IsNullOrEmpty(tvdbId)) + private RemoteSearchResult MapSearchTvToRemoteSearchResult(SearchTv series) + { + var remoteResult = new RemoteSearchResult { - var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false); + Name = series.Name ?? series.OriginalName, + SearchProviderName = Name, + ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath), + Overview = series.Overview + }; - if (searchResult != null) - { - return new[] { searchResult }; - } - } + remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture)); + remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); - return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); + return remoteResult; } public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) { - var result = new MetadataResult<Series>(); - result.QueriedById = true; + var result = new MetadataResult<Series> + { + QueriedById = true + }; var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); - if (string.IsNullOrEmpty(tmdbId)) + if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId)) { - var imdbId = info.GetProviderId(MetadataProvider.Imdb); - - if (!string.IsNullOrEmpty(imdbId)) + var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + if (searchResult?.TvResults.Count > 0) { - var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false); - - if (searchResult != null) - { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); - } + tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); } } - if (string.IsNullOrEmpty(tmdbId)) + if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)) { - var tvdbId = info.GetProviderId(MetadataProvider.Tvdb); - - if (!string.IsNullOrEmpty(tvdbId)) + var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + if (searchResult?.TvResults.Count > 0) { - var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false); - - if (searchResult != null) - { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); - } + tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); } } if (string.IsNullOrEmpty(tmdbId)) { result.QueriedById = false; - var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false); + // ParseName is required here. + // Caller provides the filename with extension stripped and NOT the parsed filename + var parsedName = _libraryManager.ParseName(info.Name); + var cleanedName = TmdbUtils.CleanName(parsedName.Name); + var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false); - var searchResult = searchResults.FirstOrDefault(); - - if (searchResult != null) + if (searchResults.Count > 0) { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); + tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); } } - if (!string.IsNullOrEmpty(tmdbId)) + if (string.IsNullOrEmpty(tmdbId)) { - cancellationToken.ThrowIfCancellationRequested(); - - result = await FetchMovieData(tmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); - - result.HasMetadata = result.Item != null; + return result; } - return result; - } + cancellationToken.ThrowIfCancellationRequested(); - private async Task<MetadataResult<Series>> FetchMovieData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken) - { - SeriesResult seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false); + var tvShow = await _tmdbClientManager + .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); - if (seriesInfo == null) + result = new MetadataResult<Series> { - return null; - } - - tmdbId = seriesInfo.Id.ToString(_usCulture); - - string dataFilePath = GetDataFilePath(tmdbId, language); - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath); - - await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode), + ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage + }; - var result = new MetadataResult<Series>(); - result.Item = new Series(); - result.ResultLanguage = seriesInfo.ResultLanguage; - - var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + foreach (var person in GetPersons(tvShow)) + { + result.AddPerson(person); + } - ProcessMainInfo(result, seriesInfo, preferredCountryCode, settings); + result.HasMetadata = result.Item != null; return result; } - private void ProcessMainInfo(MetadataResult<Series> seriesResult, SeriesResult seriesInfo, string preferredCountryCode, TmdbSettingsResult settings) + private static Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode) { - var series = seriesResult.Item; + var series = new Series + { + Name = seriesResult.Name, + OriginalTitle = seriesResult.OriginalName + }; + + series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(CultureInfo.InvariantCulture)); - series.Name = seriesInfo.Name; - series.OriginalTitle = seriesInfo.Original_Name; - series.SetProviderId(MetadataProvider.Tmdb, seriesInfo.Id.ToString(_usCulture)); + series.CommunityRating = Convert.ToSingle(seriesResult.VoteAverage); - string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture); + series.Overview = seriesResult.Overview; - if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating)) + if (seriesResult.Networks != null) { - series.CommunityRating = rating; + series.Studios = seriesResult.Networks.Select(i => i.Name).ToArray(); } - series.Overview = seriesInfo.Overview; - - if (seriesInfo.Networks != null) + if (seriesResult.Genres != null) { - series.Studios = seriesInfo.Networks.Select(i => i.Name).ToArray(); + series.Genres = seriesResult.Genres.Select(i => i.Name).ToArray(); } - if (seriesInfo.Genres != null) + if (seriesResult.Keywords?.Results != null) { - series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray(); + for (var i = 0; i < seriesResult.Keywords.Results.Count; i++) + { + series.AddTag(seriesResult.Keywords.Results[i].Name); + } } - series.HomePageUrl = seriesInfo.Homepage; + series.HomePageUrl = seriesResult.Homepage; - series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); + series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); - if (string.Equals(seriesInfo.Status, "Ended", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(seriesResult.Status, "Ended", StringComparison.OrdinalIgnoreCase)) { series.Status = SeriesStatus.Ended; - series.EndDate = seriesInfo.Last_Air_Date; + series.EndDate = seriesResult.LastAirDate; } else { series.Status = SeriesStatus.Continuing; } - series.PremiereDate = seriesInfo.First_Air_Date; + series.PremiereDate = seriesResult.FirstAirDate; - var ids = seriesInfo.External_Ids; + var ids = seriesResult.ExternalIds; if (ids != null) { - if (!string.IsNullOrWhiteSpace(ids.Imdb_Id)) + if (!string.IsNullOrWhiteSpace(ids.ImdbId)) { - series.SetProviderId(MetadataProvider.Imdb, ids.Imdb_Id); + series.SetProviderId(MetadataProvider.Imdb, ids.ImdbId); } - if (ids.Tvrage_Id > 0) + if (!string.IsNullOrEmpty(ids.TvrageId)) { - series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.Value.ToString(_usCulture)); + series.SetProviderId(MetadataProvider.TvRage, ids.TvrageId); } - if (ids.Tvdb_Id > 0) + if (!string.IsNullOrEmpty(ids.TvdbId)) { - series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.Value.ToString(_usCulture)); + series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId); } } - var contentRatings = (seriesInfo.Content_Ratings ?? new ContentRatings()).Results ?? new List<ContentRating>(); + var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>(); var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); @@ -286,7 +300,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (ourRelease != null) { - series.OfficialRating = ourRelease.Rating; + series.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Rating); } else if (usRelease != null) { @@ -297,261 +311,77 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV series.OfficialRating = minimumRelease.Rating; } - if (seriesInfo.Videos != null && seriesInfo.Videos.Results != null) + if (seriesResult.Videos?.Results != null) { - foreach (var video in seriesInfo.Videos.Results) + foreach (var video in seriesResult.Videos.Results) { - if ((video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase) - || video.Type.Equals("clip", StringComparison.OrdinalIgnoreCase)) - && video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)) + if (TmdbUtils.IsTrailerType(video)) { - series.AddTrailerUrl($"http://www.youtube.com/watch?v={video.Key}"); + series.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key); } } } - seriesResult.ResetPeople(); - var tmdbImageUrl = settings.images.GetImageUrl("original"); + return series; + } - if (seriesInfo.Credits != null) + private IEnumerable<PersonInfo> GetPersons(TvShow seriesResult) + { + if (seriesResult.Credits?.Cast != null) { - if (seriesInfo.Credits.Cast != null) + foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers)) { - foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order)) + var personInfo = new PersonInfo { - var personInfo = new PersonInfo - { - Name = actor.Name.Trim(), - Role = actor.Character, - Type = PersonType.Actor, - SortOrder = actor.Order - }; - - if (!string.IsNullOrWhiteSpace(actor.Profile_Path)) - { - personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path; - } - - if (actor.Id > 0) - { - personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); - } - - seriesResult.AddPerson(personInfo); - } - } - - if (seriesInfo.Credits.Crew != null) - { - var keepTypes = new[] - { - PersonType.Director, - PersonType.Writer, - PersonType.Producer + Name = actor.Name.Trim(), + Role = actor.Character, + Type = PersonType.Actor, + SortOrder = actor.Order, + ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath) }; - foreach (var person in seriesInfo.Credits.Crew) + if (actor.Id > 0) { - // Normalize this - var type = TmdbUtils.MapCrewToPersonType(person); - - if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) - && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - seriesResult.AddPerson(new PersonInfo - { - Name = person.Name.Trim(), - Role = person.Job, - Type = type - }); + personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); } - } - } - } - - internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId) - { - var dataPath = GetSeriesDataPath(appPaths); - return Path.Combine(dataPath, tmdbId); - } - - internal static string GetSeriesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "tmdb-tv"); - - return dataPath; - } - - internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - SeriesResult mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (mainResult == null) - { - return; - } - - var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal async Task<SeriesResult> FetchMainResult(string id, string language, CancellationToken cancellationToken) - { - var url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += "&language=" + TmdbMovieProvider.NormalizeLanguage(language) - + "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); // Get images in english and with no language - } - - cancellationToken.ThrowIfCancellationRequested(); - - using var mainRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage); - await using var mainStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(mainStream).ConfigureAwait(false); - - if (!string.IsNullOrEmpty(language)) - { - mainResult.ResultLanguage = language; - } - - cancellationToken.ThrowIfCancellationRequested(); - - // If the language preference isn't english, then have the overview fallback to english if it's blank - if (mainResult != null && - string.IsNullOrEmpty(mainResult.Overview) && - !string.IsNullOrEmpty(language) && - !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogInformation("MovieDbSeriesProvider couldn't find meta for language {Language}. Trying English...", language); - - url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en"; - - if (!string.IsNullOrEmpty(language)) - { - // Get images in english and with no language - url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); - } - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + yield return personInfo; } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(stream).ConfigureAwait(false); - - mainResult.Overview = englishResult.Overview; - mainResult.ResultLanguage = "en"; } - return mainResult; - } - - internal Task EnsureSeriesInfo(string tmdbId, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - var path = GetDataFilePath(tmdbId, language); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) + if (seriesResult.Credits?.Crew != null) { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) + var keepTypes = new[] { - return Task.CompletedTask; - } - } - - return DownloadSeriesInfo(tmdbId, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - - var filename = string.Format(CultureInfo.InvariantCulture, "series-{0}.json", preferredLanguage ?? string.Empty); - - return Path.Combine(path, filename); - } - - private async Task<RemoteSearchResult> FindByExternalId(string id, string externalSource, CancellationToken cancellationToken) - { - var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}", - id, - TmdbUtils.ApiKey, - externalSource); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - - var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(stream).ConfigureAwait(false); - - if (result != null && result.Tv_Results != null) - { - var tv = result.Tv_Results.FirstOrDefault(); + PersonType.Director, + PersonType.Writer, + PersonType.Producer + }; - if (tv != null) + foreach (var person in seriesResult.Credits.Crew) { - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); + // Normalize this + var type = TmdbUtils.MapCrewToPersonType(person); - var remoteResult = new RemoteSearchResult + if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) + && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - Name = tv.Name, - SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path) - ? null - : tmdbImageUrl + tv.Poster_Path - }; - - remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture)); + continue; + } - return remoteResult; + yield return new PersonInfo + { + Name = person.Name.Trim(), + Role = person.Job, + Type = type + }; } } - - return null; } - // After TheTVDB - public int Order => 1; - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs new file mode 100644 index 0000000000..4de4bf4db6 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -0,0 +1,556 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using TMDbLib.Client; +using TMDbLib.Objects.Collections; +using TMDbLib.Objects.Find; +using TMDbLib.Objects.General; +using TMDbLib.Objects.Movies; +using TMDbLib.Objects.People; +using TMDbLib.Objects.Search; +using TMDbLib.Objects.TvShows; + +namespace MediaBrowser.Providers.Plugins.Tmdb +{ + /// <summary> + /// Manager class for abstracting the TMDb API client library. + /// </summary> + public class TmdbClientManager : IDisposable + { + private const int CacheDurationInHours = 1; + + private readonly IMemoryCache _memoryCache; + private readonly TMDbClient _tmDbClient; + + /// <summary> + /// Initializes a new instance of the <see cref="TmdbClientManager"/> class. + /// </summary> + /// <param name="memoryCache">An instance of <see cref="IMemoryCache"/>.</param> + public TmdbClientManager(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + _tmDbClient = new TMDbClient(TmdbUtils.ApiKey); + // Not really interested in NotFoundException + _tmDbClient.ThrowApiExceptions = false; + } + + /// <summary> + /// Gets a movie from the TMDb API based on its TMDb id. + /// </summary> + /// <param name="tmdbId">The movie's TMDb id.</param> + /// <param name="language">The movie's language.</param> + /// <param name="imageLanguages">A comma-separated list of image languages.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb movie or null if not found.</returns> + public async Task<Movie> GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out Movie movie)) + { + return movie; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + movie = await _tmDbClient.GetMovieAsync( + tmdbId, + TmdbUtils.NormalizeLanguage(language), + imageLanguages, + MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos, + cancellationToken).ConfigureAwait(false); + + if (movie != null) + { + _memoryCache.Set(key, movie, TimeSpan.FromHours(CacheDurationInHours)); + } + + return movie; + } + + /// <summary> + /// Gets a collection from the TMDb API based on its TMDb id. + /// </summary> + /// <param name="tmdbId">The collection's TMDb id.</param> + /// <param name="language">The collection's language.</param> + /// <param name="imageLanguages">A comma-separated list of image languages.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb collection or null if not found.</returns> + public async Task<Collection> GetCollectionAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out Collection collection)) + { + return collection; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + collection = await _tmDbClient.GetCollectionAsync( + tmdbId, + TmdbUtils.NormalizeLanguage(language), + imageLanguages, + CollectionMethods.Images, + cancellationToken).ConfigureAwait(false); + + if (collection != null) + { + _memoryCache.Set(key, collection, TimeSpan.FromHours(CacheDurationInHours)); + } + + return collection; + } + + /// <summary> + /// Gets a tv show from the TMDb API based on its TMDb id. + /// </summary> + /// <param name="tmdbId">The tv show's TMDb id.</param> + /// <param name="language">The tv show's language.</param> + /// <param name="imageLanguages">A comma-separated list of image languages.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb tv show information or null if not found.</returns> + public async Task<TvShow> GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out TvShow series)) + { + return series; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + series = await _tmDbClient.GetTvShowAsync( + tmdbId, + language: TmdbUtils.NormalizeLanguage(language), + includeImageLanguage: imageLanguages, + extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (series != null) + { + _memoryCache.Set(key, series, TimeSpan.FromHours(CacheDurationInHours)); + } + + return series; + } + + /// <summary> + /// Gets a tv show episode group from the TMDb API based on the show id and the display order. + /// </summary> + /// <param name="tvShowId">The tv show's TMDb id.</param> + /// <param name="displayOrder">The display order.</param> + /// <param name="language">The tv show's language.</param> + /// <param name="imageLanguages">A comma-separated list of image languages.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb tv show episode group information or null if not found.</returns> + private async Task<TvGroupCollection> GetSeriesGroupAsync(int tvShowId, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken) + { + TvGroupType? groupType = + string.Equals(displayOrder, "absolute", StringComparison.Ordinal) ? TvGroupType.Absolute : + string.Equals(displayOrder, "dvd", StringComparison.Ordinal) ? TvGroupType.DVD : + null; + + if (groupType == null) + { + return null; + } + + var key = $"group-{tvShowId.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; + if (_memoryCache.TryGetValue(key, out TvGroupCollection group)) + { + return group; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var series = await GetSeriesAsync(tvShowId, language, imageLanguages, cancellationToken).ConfigureAwait(false); + var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id; + + if (episodeGroupId == null) + { + return null; + } + + group = await _tmDbClient.GetTvEpisodeGroupsAsync( + episodeGroupId, + language: TmdbUtils.NormalizeLanguage(language), + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (group != null) + { + _memoryCache.Set(key, group, TimeSpan.FromHours(CacheDurationInHours)); + } + + return group; + } + + /// <summary> + /// Gets a tv season from the TMDb API based on the tv show's TMDb id. + /// </summary> + /// <param name="tvShowId">The tv season's TMDb id.</param> + /// <param name="seasonNumber">The season number.</param> + /// <param name="language">The tv season's language.</param> + /// <param name="imageLanguages">A comma-separated list of image languages.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb tv season information or null if not found.</returns> + public async Task<TvSeason> GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out TvSeason season)) + { + return season; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + season = await _tmDbClient.GetTvSeasonAsync( + tvShowId, + seasonNumber, + language: TmdbUtils.NormalizeLanguage(language), + includeImageLanguage: imageLanguages, + extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (season != null) + { + _memoryCache.Set(key, season, TimeSpan.FromHours(CacheDurationInHours)); + } + + return season; + } + + /// <summary> + /// Gets a movie from the TMDb API based on the tv show's TMDb id. + /// </summary> + /// <param name="tvShowId">The tv show's TMDb id.</param> + /// <param name="seasonNumber">The season number.</param> + /// <param name="episodeNumber">The episode number.</param> + /// <param name="displayOrder">The display order.</param> + /// <param name="language">The episode's language.</param> + /// <param name="imageLanguages">A comma-separated list of image languages.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb tv episode information or null if not found.</returns> + public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; + if (_memoryCache.TryGetValue(key, out TvEpisode episode)) + { + return episode; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken).ConfigureAwait(false); + if (group != null) + { + var season = group.Groups.Find(s => s.Order == seasonNumber); + // Episode order starts at 0 + var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1); + if (ep != null) + { + seasonNumber = ep.SeasonNumber; + episodeNumber = ep.EpisodeNumber; + } + } + + episode = await _tmDbClient.GetTvEpisodeAsync( + tvShowId, + seasonNumber, + episodeNumber, + language: TmdbUtils.NormalizeLanguage(language), + includeImageLanguage: imageLanguages, + extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (episode != null) + { + _memoryCache.Set(key, episode, TimeSpan.FromHours(CacheDurationInHours)); + } + + return episode; + } + + /// <summary> + /// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id. + /// </summary> + /// <param name="personTmdbId">The person's TMDb id.</param> + /// <param name="language">The episode's language.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb person information or null if not found.</returns> + public async Task<Person> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken) + { + var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out Person person)) + { + return person; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + person = await _tmDbClient.GetPersonAsync( + personTmdbId, + TmdbUtils.NormalizeLanguage(language), + PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds, + cancellationToken).ConfigureAwait(false); + + if (person != null) + { + _memoryCache.Set(key, person, TimeSpan.FromHours(CacheDurationInHours)); + } + + return person; + } + + /// <summary> + /// Gets an item from the TMDb API based on its id from an external service eg. IMDb id, TvDb id. + /// </summary> + /// <param name="externalId">The item's external id.</param> + /// <param name="source">The source of the id eg. IMDb.</param> + /// <param name="language">The item's language.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb item or null if not found.</returns> + public async Task<FindContainer> FindByExternalIdAsync( + string externalId, + FindExternalSource source, + string language, + CancellationToken cancellationToken) + { + var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out FindContainer result)) + { + return result; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + result = await _tmDbClient.FindAsync( + source, + externalId, + TmdbUtils.NormalizeLanguage(language), + cancellationToken).ConfigureAwait(false); + + if (result != null) + { + _memoryCache.Set(key, result, TimeSpan.FromHours(CacheDurationInHours)); + } + + return result; + } + + /// <summary> + /// Searches for a tv show using the TMDb API based on its name. + /// </summary> + /// <param name="name">The name of the tv show.</param> + /// <param name="language">The tv show's language.</param> + /// <param name="year">The year the tv show first aired.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb tv show information.</returns> + public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default) + { + var key = $"searchseries-{name}-{language}"; + if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series)) + { + return series.Results; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var searchResults = await _tmDbClient + .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (searchResults.Results.Count > 0) + { + _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); + } + + return searchResults.Results; + } + + /// <summary> + /// Searches for a person based on their name using the TMDb API. + /// </summary> + /// <param name="name">The name of the person.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb person information.</returns> + public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken) + { + var key = $"searchperson-{name}"; + if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson> person)) + { + return person.Results; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var searchResults = await _tmDbClient + .SearchPersonAsync(name, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (searchResults.Results.Count > 0) + { + _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); + } + + return searchResults.Results; + } + + /// <summary> + /// Searches for a movie based on its name using the TMDb API. + /// </summary> + /// <param name="name">The name of the movie.</param> + /// <param name="language">The movie's language.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb movie information.</returns> + public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken) + { + return SearchMovieAsync(name, 0, language, cancellationToken); + } + + /// <summary> + /// Searches for a movie based on its name using the TMDb API. + /// </summary> + /// <param name="name">The name of the movie.</param> + /// <param name="year">The release year of the movie.</param> + /// <param name="language">The movie's language.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb movie information.</returns> + public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken) + { + var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie> movies)) + { + return movies.Results; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var searchResults = await _tmDbClient + .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (searchResults.Results.Count > 0) + { + _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); + } + + return searchResults.Results; + } + + /// <summary> + /// Searches for a collection based on its name using the TMDb API. + /// </summary> + /// <param name="name">The name of the collection.</param> + /// <param name="language">The collection's language.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb collection information.</returns> + public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken) + { + var key = $"collectionsearch-{name}-{language}"; + if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection> collections)) + { + return collections.Results; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var searchResults = await _tmDbClient + .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (searchResults.Results.Count > 0) + { + _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); + } + + return searchResults.Results; + } + + /// <summary> + /// Gets the absolute URL of the poster. + /// </summary> + /// <param name="posterPath">The relative URL of the poster.</param> + /// <returns>The absolute URL.</returns> + public string GetPosterUrl(string posterPath) + { + if (string.IsNullOrEmpty(posterPath)) + { + return null; + } + + return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString(); + } + + /// <summary> + /// Gets the absolute URL of the backdrop image. + /// </summary> + /// <param name="posterPath">The relative URL of the backdrop image.</param> + /// <returns>The absolute URL.</returns> + public string GetBackdropUrl(string posterPath) + { + if (string.IsNullOrEmpty(posterPath)) + { + return null; + } + + return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString(); + } + + /// <summary> + /// Gets the absolute URL of the profile image. + /// </summary> + /// <param name="actorProfilePath">The relative URL of the profile image.</param> + /// <returns>The absolute URL.</returns> + public string GetProfileUrl(string actorProfilePath) + { + if (string.IsNullOrEmpty(actorProfilePath)) + { + return null; + } + + return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString(); + } + + /// <summary> + /// Gets the absolute URL of the still image. + /// </summary> + /// <param name="filePath">The relative URL of the still image.</param> + /// <returns>The absolute URL.</returns> + public string GetStillUrl(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + { + return null; + } + + return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString(); + } + + private Task EnsureClientConfigAsync() + { + return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask; + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + +/// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _memoryCache?.Dispose(); + _tmDbClient?.Dispose(); + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 1415d69761..b713736a0f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -1,7 +1,10 @@ +#nullable enable + using System; -using System.Net.Mime; +using System.Collections.Generic; +using System.Text.RegularExpressions; using MediaBrowser.Model.Entities; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; +using TMDbLib.Objects.General; namespace MediaBrowser.Providers.Plugins.Tmdb { @@ -10,17 +13,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// </summary> public static class TmdbUtils { + private static readonly Regex _nonWords = new (@"[\W_]+", RegexOptions.Compiled); + /// <summary> /// URL of the TMDB instance to use. /// </summary> public const string BaseTmdbUrl = "https://www.themoviedb.org/"; /// <summary> - /// URL of the TMDB API instance to use. - /// </summary> - public const string BaseTmdbApiUrl = "https://api.themoviedb.org/"; - - /// <summary> /// Name of the provider. /// </summary> public const string ProviderName = "TheMovieDb"; @@ -31,9 +31,30 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public const string ApiKey = "4219e299c89411838049ab0dab19ebd5"; /// <summary> - /// Value of the Accept header for requests to the provider. + /// Maximum number of cast members to pull. + /// </summary> + public const int MaxCastMembers = 15; + + /// <summary> + /// The crew types to keep. /// </summary> - public static readonly string[] AcceptHeaders = { MediaTypeNames.Application.Json, "image/*" }; + public static readonly string[] WantedCrewTypes = + { + PersonType.Director, + PersonType.Writer, + PersonType.Producer + }; + + /// <summary> + /// Cleans the name according to TMDb requirements. + /// </summary> + /// <param name="name">The name of the entity.</param> + /// <returns>The cleaned name.</returns> + public static string CleanName(string name) + { + // TMDb expects a space separated list of words make sure that is the case + return _nonWords.Replace(name, " "); + } /// <summary> /// Maps the TMDB provided roles for crew members to Jellyfin roles. @@ -42,24 +63,136 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <returns>The Jellyfin person type.</returns> public static string MapCrewToPersonType(Crew crew) { - if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase) - && crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase)) + if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) + && crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase)) { return PersonType.Director; } - if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase) - && crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase)) + if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) + && crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase)) { return PersonType.Producer; } - if (crew.Department.Equals("writing", StringComparison.InvariantCultureIgnoreCase)) + if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)) { return PersonType.Writer; } - return null; + return string.Empty; + } + + /// <summary> + /// Determines whether a video is a trailer. + /// </summary> + /// <param name="video">The TMDb video.</param> + /// <returns>A boolean indicating whether the video is a trailer.</returns> + public static bool IsTrailerType(Video video) + { + return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase) + && (!video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase) + || !video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase)); + } + + /// <summary> + /// Normalizes a language string for use with TMDb's include image language parameter. + /// </summary> + /// <param name="preferredLanguage">The preferred language as either a 2 letter code with or without country code.</param> + /// <returns>The comma separated language string.</returns> + public static string GetImageLanguagesParam(string preferredLanguage) + { + var languages = new List<string>(); + + if (!string.IsNullOrEmpty(preferredLanguage)) + { + preferredLanguage = NormalizeLanguage(preferredLanguage); + + languages.Add(preferredLanguage); + + if (preferredLanguage.Length == 5) // like en-US + { + // Currently, TMDB supports 2-letter language codes only + // They are planning to change this in the future, thus we're + // supplying both codes if we're having a 5-letter code. + languages.Add(preferredLanguage.Substring(0, 2)); + } + } + + languages.Add("null"); + + if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase)) + { + languages.Add("en"); + } + + return string.Join(',', languages); + } + + /// <summary> + /// Normalizes a language string for use with TMDb's language parameter. + /// </summary> + /// <param name="language">The language code.</param> + /// <returns>The normalized language code.</returns> + public static string NormalizeLanguage(string language) + { + if (string.IsNullOrEmpty(language)) + { + return language; + } + + // They require this to be uppercase + // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api. + // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab + var parts = language.Split('-'); + + if (parts.Length == 2) + { + // TMDB doesn't support Switzerland (de-CH, it-CH or fr-CH) so use the language (de, it or fr) without country code + if (string.Equals(parts[1], "CH", StringComparison.OrdinalIgnoreCase)) + { + return parts[0]; + } + + language = parts[0] + "-" + parts[1].ToUpperInvariant(); + } + + return language; + } + + /// <summary> + /// Adjusts the image's language code preferring the 5 letter language code eg. en-US. + /// </summary> + /// <param name="imageLanguage">The image's actual language code.</param> + /// <param name="requestLanguage">The requested language code.</param> + /// <returns>The language code.</returns> + public static string AdjustImageLanguage(string imageLanguage, string requestLanguage) + { + if (!string.IsNullOrEmpty(imageLanguage) + && !string.IsNullOrEmpty(requestLanguage) + && requestLanguage.Length > 2 + && imageLanguage.Length == 2 + && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase)) + { + return requestLanguage; + } + + return imageLanguage; + } + + /// <summary> + /// Combines the metadata country code and the parental rating from the Api into the value we store in our database. + /// </summary> + /// <param name="countryCode">The Iso 3166-1 country code of the rating country.</param> + /// <param name="ratingValue">The rating value returned by the Tmdb Api.</param> + /// <returns>The combined parental rating of country code+rating value.</returns> + public static string BuildParentalRating(string countryCode, string ratingValue) + { + // exclude US because we store us values as TV-14 without the country code. + var ratingPrefix = string.Equals(countryCode, "US", StringComparison.OrdinalIgnoreCase) ? string.Empty : countryCode + "-"; + var newRating = ratingPrefix + ratingValue; + + return newRating.Replace("DE-", "FSK-", StringComparison.OrdinalIgnoreCase); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs deleted file mode 100644 index 10374bde9e..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers -{ - public class TmdbTrailerProvider : IHasOrder, IRemoteMetadataProvider<Trailer, TrailerInfo> - { - private readonly IHttpClientFactory _httpClientFactory; - - public TmdbTrailerProvider(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } - - public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken) - { - return TmdbMovieProvider.Current.GetMovieSearchResults(searchInfo, cancellationToken); - } - - public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken) - { - return TmdbMovieProvider.Current.GetItemMetadata<Trailer>(info, cancellationToken); - } - - public string Name => TmdbMovieProvider.Current.Name; - - public int Order => 0; - - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/Properties/AssemblyInfo.cs b/MediaBrowser.Providers/Properties/AssemblyInfo.cs index f1c46899ce..bd301b5f08 100644 --- a/MediaBrowser.Providers/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Providers/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -14,6 +15,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] +[assembly: InternalsVisibleTo("Jellyfin.Providers.Tests")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from diff --git a/MediaBrowser.Providers/Studios/StudioMetadataService.cs b/MediaBrowser.Providers/Studios/StudioMetadataService.cs index 78042b40de..091b33ce0e 100644 --- a/MediaBrowser.Providers/Studios/StudioMetadataService.cs +++ b/MediaBrowser.Providers/Studios/StudioMetadataService.cs @@ -17,7 +17,8 @@ namespace MediaBrowser.Providers.Studios IServerConfigurationManager serverConfigurationManager, ILogger<StudioMetadataService> logger, IProviderManager providerManager, - IFileSystem fileSystem, ILibraryManager libraryManager) + IFileSystem fileSystem, + ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { } diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index 321153c6be..63e78d15e8 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -8,6 +8,8 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -32,6 +34,8 @@ namespace MediaBrowser.Providers.Studios public string Name => "Emby Designs"; + public int Order => 0; + public bool Supports(BaseItem item) { return item is Studio; @@ -118,11 +122,9 @@ namespace MediaBrowser.Providers.Studios return EnsureList(url, file, _fileSystem, cancellationToken); } - public int Order => 0; - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return httpClient.GetAsync(url, cancellationToken); } @@ -140,10 +142,10 @@ namespace MediaBrowser.Providers.Studios if (!fileInfo.Exists || (DateTime.UtcNow - fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays > 1) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); Directory.CreateDirectory(Path.GetDirectoryName(file)); - await using var response = await httpClient.GetStreamAsync(url).ConfigureAwait(false); + await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false); await using var fileStream = new FileStream(file, FileMode.Create); await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } @@ -160,35 +162,29 @@ namespace MediaBrowser.Providers.Studios private string GetComparableName(string name) { - return name.Replace(" ", string.Empty) - .Replace(".", string.Empty) - .Replace("&", string.Empty) - .Replace("!", string.Empty) - .Replace(",", string.Empty) - .Replace("/", string.Empty); + return name.Replace(" ", string.Empty, StringComparison.Ordinal) + .Replace(".", string.Empty, StringComparison.Ordinal) + .Replace("&", string.Empty, StringComparison.Ordinal) + .Replace("!", string.Empty, StringComparison.Ordinal) + .Replace(",", string.Empty, StringComparison.Ordinal) + .Replace("/", string.Empty, StringComparison.Ordinal); } public IEnumerable<string> GetAvailableImages(string file) { - using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); + using var reader = new StreamReader(fileStream); + var lines = new List<string>(); + + foreach (var line in reader.ReadAllLines()) { - using (var reader = new StreamReader(fileStream)) + if (!string.IsNullOrWhiteSpace(line)) { - var lines = new List<string>(); - - while (!reader.EndOfStream) - { - var text = reader.ReadLine(); - - if (!string.IsNullOrWhiteSpace(text)) - { - lines.Add(text); - } - } - - return lines; + lines.Add(line); } } + + return lines; } } } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 0f7cb3f8fa..0c791a2fee 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -147,40 +147,14 @@ namespace MediaBrowser.Providers.Subtitles string subtitleId, CancellationToken cancellationToken) { - var parts = subtitleId.Split(new[] { '_' }, 2); + var parts = subtitleId.Split('_', 2); var provider = GetProvider(parts[0]); - var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia; - try { var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false); - using (var stream = response.Stream) - using (var memoryStream = new MemoryStream()) - { - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; - - var savePaths = new List<string>(); - var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); - - if (response.IsForced) - { - saveFileName += ".forced"; - } - - saveFileName += "." + response.Format.ToLowerInvariant(); - - if (saveInMediaFolder) - { - savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName)); - } - - savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); - - await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); - } + await TrySaveSubtitle(video, libraryOptions, response).ConfigureAwait(false); } catch (RateLimitExceededException) { @@ -199,9 +173,66 @@ namespace MediaBrowser.Providers.Subtitles } } + /// <inheritdoc /> + public Task UploadSubtitle(Video video, SubtitleResponse response) + { + var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video); + return TrySaveSubtitle(video, libraryOptions, response); + } + + private async Task TrySaveSubtitle( + Video video, + LibraryOptions libraryOptions, + SubtitleResponse response) + { + var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia; + + using var stream = response.Stream; + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + + var savePaths = new List<string>(); + var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); + + if (response.IsForced) + { + saveFileName += ".forced"; + } + + saveFileName += "." + response.Format.ToLowerInvariant(); + + if (saveInMediaFolder) + { + var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName)); + // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path."); + if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)) + { + savePaths.Add(mediaFolderPath); + } + } + + var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); + + // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path."); + if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) + { + savePaths.Add(internalPath); + } + + if (savePaths.Count > 0) + { + await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); + } + else + { + _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid."); + } + } + private async Task TrySaveToFiles(Stream stream, List<string> savePaths) { - Exception exceptionToThrow = null; + List<Exception> exs = null; foreach (var savePath in savePaths) { @@ -213,20 +244,23 @@ namespace MediaBrowser.Providers.Subtitles { Directory.CreateDirectory(Path.GetDirectoryName(savePath)); - using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, FileStreamBufferSize, true)) - { - await stream.CopyToAsync(fs).ConfigureAwait(false); - } + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true); + await stream.CopyToAsync(fs).ConfigureAwait(false); return; } catch (Exception ex) { - if (exceptionToThrow == null) - { - exceptionToThrow = ex; - } - } +// Bug in analyzer -- https://github.com/dotnet/roslyn-analyzers/issues/5160 +#pragma warning disable CA1508 + exs ??= new List<Exception>() + { + ex + }; +#pragma warning restore CA1508 + + } finally { _monitor.ReportFileSystemChangeComplete(savePath, false); @@ -235,9 +269,9 @@ namespace MediaBrowser.Providers.Subtitles stream.Position = 0; } - if (exceptionToThrow != null) + if (exs != null) { - throw exceptionToThrow; + throw new AggregateException(exs); } } @@ -303,7 +337,7 @@ namespace MediaBrowser.Providers.Subtitles private ISubtitleProvider GetProvider(string id) { - return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name))); + return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name), StringComparison.Ordinal)); } /// <inheritdoc /> @@ -314,7 +348,7 @@ namespace MediaBrowser.Providers.Subtitles Index = index, ItemId = item.Id, Type = MediaStreamType.Subtitle - }).First(); + })[0]; var path = stream.Path; _monitor.ReportFileSystemChangeBeginning(path); @@ -334,24 +368,24 @@ namespace MediaBrowser.Providers.Subtitles /// <inheritdoc /> public Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken) { - var parts = id.Split(new[] { '_' }, 2); + var parts = id.Split('_', 2); var provider = GetProvider(parts[0]); - id = parts.Last(); + id = parts[^1]; return provider.GetSubtitles(id, cancellationToken); } /// <inheritdoc /> - public SubtitleProviderInfo[] GetSupportedProviders(BaseItem video) + public SubtitleProviderInfo[] GetSupportedProviders(BaseItem item) { VideoContentType mediaType; - if (video is Episode) + if (item is Episode) { mediaType = VideoContentType.Episode; } - else if (video is Movie) + else if (item is Movie) { mediaType = VideoContentType.Movie; } diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs deleted file mode 100644 index 0c09cdef6c..0000000000 --- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs +++ /dev/null @@ -1,226 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.TV -{ - public class DummySeasonProvider - { - private readonly ILogger _logger; - private readonly ILocalizationManager _localization; - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _fileSystem; - - public DummySeasonProvider( - ILogger logger, - ILocalizationManager localization, - ILibraryManager libraryManager, - IFileSystem fileSystem) - { - _logger = logger; - _localization = localization; - _libraryManager = libraryManager; - _fileSystem = fileSystem; - } - - public async Task<bool> Run(Series series, CancellationToken cancellationToken) - { - var seasonsRemoved = RemoveObsoleteSeasons(series); - - var hasNewSeasons = await AddDummySeasonFolders(series, cancellationToken).ConfigureAwait(false); - - if (hasNewSeasons) - { - // var directoryService = new DirectoryService(_fileSystem); - - // await series.RefreshMetadata(new MetadataRefreshOptions(directoryService), cancellationToken).ConfigureAwait(false); - - // await series.ValidateChildren(new SimpleProgress<double>(), cancellationToken, new MetadataRefreshOptions(directoryService)) - // .ConfigureAwait(false); - } - - return seasonsRemoved || hasNewSeasons; - } - - private async Task<bool> AddDummySeasonFolders(Series series, CancellationToken cancellationToken) - { - var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode) - .Cast<Episode>() - .Where(i => !i.IsInSeasonFolder) - .ToList(); - - var hasChanges = false; - - List<Season> seasons = null; - - // Loop through the unique season numbers - foreach (var seasonNumber in episodesInSeriesFolder.Select(i => i.ParentIndexNumber ?? -1) - .Where(i => i >= 0) - .Distinct() - .ToList()) - { - if (seasons == null) - { - seasons = series.Children.OfType<Season>().ToList(); - } - - var existingSeason = seasons - .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); - - if (existingSeason == null) - { - await AddSeason(series, seasonNumber, false, cancellationToken).ConfigureAwait(false); - hasChanges = true; - seasons = null; - } - else if (existingSeason.IsVirtualItem) - { - existingSeason.IsVirtualItem = false; - await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - seasons = null; - } - } - - // Unknown season - create a dummy season to put these under - if (episodesInSeriesFolder.Any(i => !i.ParentIndexNumber.HasValue)) - { - if (seasons == null) - { - seasons = series.Children.OfType<Season>().ToList(); - } - - var existingSeason = seasons - .FirstOrDefault(i => !i.IndexNumber.HasValue); - - if (existingSeason == null) - { - await AddSeason(series, null, false, cancellationToken).ConfigureAwait(false); - - hasChanges = true; - seasons = null; - } - else if (existingSeason.IsVirtualItem) - { - existingSeason.IsVirtualItem = false; - await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - seasons = null; - } - } - - return hasChanges; - } - - /// <summary> - /// Adds the season. - /// </summary> - public async Task<Season> AddSeason(Series series, - int? seasonNumber, - bool isVirtualItem, - CancellationToken cancellationToken) - { - string seasonName; - if (seasonNumber == null) - { - seasonName = _localization.GetLocalizedString("NameSeasonUnknown"); - } - else if (seasonNumber == 0) - { - seasonName = _libraryManager.GetLibraryOptions(series).SeasonZeroDisplayName; - } - else - { - seasonName = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("NameSeasonNumber"), - seasonNumber.Value); - } - - _logger.LogInformation("Creating Season {0} entry for {1}", seasonName, series.Name); - - var season = new Season - { - Name = seasonName, - IndexNumber = seasonNumber, - Id = _libraryManager.GetNewItemId( - series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName, - typeof(Season)), - IsVirtualItem = isVirtualItem, - SeriesId = series.Id, - SeriesName = series.Name - }; - - season.SetParent(series); - - series.AddChild(season, cancellationToken); - - await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); - - return season; - } - - private bool RemoveObsoleteSeasons(Series series) - { - var existingSeasons = series.Children.OfType<Season>().ToList(); - - var physicalSeasons = existingSeasons - .Where(i => i.LocationType != LocationType.Virtual) - .ToList(); - - var virtualSeasons = existingSeasons - .Where(i => i.LocationType == LocationType.Virtual) - .ToList(); - - var seasonsToRemove = virtualSeasons - .Where(i => - { - if (i.IndexNumber.HasValue) - { - var seasonNumber = i.IndexNumber.Value; - - // If there's a physical season with the same number, delete it - if (physicalSeasons.Any(p => p.IndexNumber.HasValue && (p.IndexNumber.Value == seasonNumber))) - { - return true; - } - } - - // If there are no episodes with this season number, delete it - if (!i.GetEpisodes().Any()) - { - return true; - } - - return false; - }) - .ToList(); - - var hasChanges = false; - - foreach (var seasonToRemove in seasonsToRemove) - { - _logger.LogInformation("Removing virtual season {0} {1}", series.Name, seasonToRemove.IndexNumber); - - _libraryManager.DeleteItem(seasonToRemove, new DeleteOptions - { - DeleteFileLocation = true - - }, false); - - hasChanges = true; - } - - return hasChanges; - } - } -} diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs index 170f1bdd8c..08cb6ced90 100644 --- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs +++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs @@ -25,46 +25,46 @@ namespace MediaBrowser.Providers.TV } /// <inheritdoc /> - protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType currentUpdateType) + protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType updateType) { - var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType); + var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType); var seriesName = item.FindSeriesName(); if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal)) { item.SeriesName = seriesName; - updateType |= ItemUpdateType.MetadataImport; + updatedType |= ItemUpdateType.MetadataImport; } var seasonName = item.FindSeasonName(); if (!string.Equals(item.SeasonName, seasonName, StringComparison.Ordinal)) { item.SeasonName = seasonName; - updateType |= ItemUpdateType.MetadataImport; + updatedType |= ItemUpdateType.MetadataImport; } var seriesId = item.FindSeriesId(); if (!item.SeriesId.Equals(seriesId)) { item.SeriesId = seriesId; - updateType |= ItemUpdateType.MetadataImport; + updatedType |= ItemUpdateType.MetadataImport; } var seasonId = item.FindSeasonId(); if (!item.SeasonId.Equals(seasonId)) { item.SeasonId = seasonId; - updateType |= ItemUpdateType.MetadataImport; + updatedType |= ItemUpdateType.MetadataImport; } var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey(); if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal)) { item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; - updateType |= ItemUpdateType.MetadataImport; + updatedType |= ItemUpdateType.MetadataImport; } - return updateType; + return updatedType; } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs deleted file mode 100644 index 09850beb0d..0000000000 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ /dev/null @@ -1,388 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Providers.Plugins.TheTvdb; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.TV -{ - public class MissingEpisodeProvider - { - private const double UnairedEpisodeThresholdDays = 2; - - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; - private readonly IFileSystem _fileSystem; - private readonly TvdbClientManager _tvdbClientManager; - - public MissingEpisodeProvider( - ILogger logger, - IServerConfigurationManager config, - ILibraryManager libraryManager, - ILocalizationManager localization, - IFileSystem fileSystem, - TvdbClientManager tvdbClientManager) - { - _logger = logger; - _config = config; - _libraryManager = libraryManager; - _localization = localization; - _fileSystem = fileSystem; - _tvdbClientManager = tvdbClientManager; - } - - public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken) - { - var tvdbId = series.GetProviderId(MetadataProvider.Tvdb); - if (string.IsNullOrEmpty(tvdbId)) - { - return false; - } - - var episodes = await _tvdbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken); - - var episodeLookup = episodes - .Select(i => - { - DateTime.TryParse(i.FirstAired, out var firstAired); - var seasonNumber = i.AiredSeason.GetValueOrDefault(-1); - var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1); - return (seasonNumber, episodeNumber, firstAired); - }) - .Where(i => i.seasonNumber != -1 && i.episodeNumber != -1) - .OrderBy(i => i.seasonNumber) - .ThenBy(i => i.episodeNumber) - .ToList(); - - var allRecursiveChildren = series.GetRecursiveChildren(); - - var hasBadData = HasInvalidContent(allRecursiveChildren); - - // Be conservative here to avoid creating missing episodes for ones they already have - var addMissingEpisodes = !hasBadData && _libraryManager.GetLibraryOptions(series).ImportMissingEpisodes; - - var anySeasonsRemoved = RemoveObsoleteOrMissingSeasons(allRecursiveChildren, episodeLookup); - - if (anySeasonsRemoved) - { - // refresh this - allRecursiveChildren = series.GetRecursiveChildren(); - } - - var anyEpisodesRemoved = RemoveObsoleteOrMissingEpisodes(allRecursiveChildren, episodeLookup, addMissingEpisodes); - - if (anyEpisodesRemoved) - { - // refresh this - allRecursiveChildren = series.GetRecursiveChildren(); - } - - var hasNewEpisodes = false; - - if (addNewItems && series.IsMetadataFetcherEnabled(_libraryManager.GetLibraryOptions(series), TvdbSeriesProvider.Current.Name)) - { - hasNewEpisodes = await AddMissingEpisodes(series, allRecursiveChildren, addMissingEpisodes, episodeLookup, cancellationToken) - .ConfigureAwait(false); - } - - if (hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved) - { - return true; - } - - return false; - } - - /// <summary> - /// Returns true if a series has any seasons or episodes without season or episode numbers - /// If this data is missing no virtual items will be added in order to prevent possible duplicates. - /// </summary> - private bool HasInvalidContent(IList<BaseItem> allItems) - { - return allItems.OfType<Season>().Any(i => !i.IndexNumber.HasValue) || - allItems.OfType<Episode>().Any(i => - { - if (!i.ParentIndexNumber.HasValue) - { - return true; - } - - // You could have episodes under season 0 with no number - return false; - }); - } - - private async Task<bool> AddMissingEpisodes( - Series series, - IEnumerable<BaseItem> allItems, - bool addMissingEpisodes, - IReadOnlyCollection<(int seasonNumber, int episodenumber, DateTime firstAired)> episodeLookup, - CancellationToken cancellationToken) - { - var existingEpisodes = allItems.OfType<Episode>().ToList(); - - var seasonCounts = episodeLookup.GroupBy(e => e.seasonNumber).ToDictionary(g => g.Key, g => g.Count()); - - var hasChanges = false; - - foreach (var tuple in episodeLookup) - { - if (tuple.seasonNumber <= 0 || tuple.episodenumber <= 0) - { - // Ignore episode/season zeros - continue; - } - - var existingEpisode = GetExistingEpisode(existingEpisodes, seasonCounts, tuple); - - if (existingEpisode != null) - { - continue; - } - - var airDate = tuple.firstAired; - - var now = DateTime.UtcNow.AddDays(-UnairedEpisodeThresholdDays); - - if (airDate < now && addMissingEpisodes || airDate > now) - { - // tvdb has a lot of nearly blank episodes - _logger.LogInformation("Creating virtual missing/unaired episode {0} {1}x{2}", series.Name, tuple.seasonNumber, tuple.episodenumber); - await AddEpisode(series, tuple.seasonNumber, tuple.episodenumber, cancellationToken).ConfigureAwait(false); - - hasChanges = true; - } - } - - return hasChanges; - } - - /// <summary> - /// Removes the virtual entry after a corresponding physical version has been added. - /// </summary> - private bool RemoveObsoleteOrMissingEpisodes( - IEnumerable<BaseItem> allRecursiveChildren, - IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup, - bool allowMissingEpisodes) - { - var existingEpisodes = allRecursiveChildren.OfType<Episode>(); - - var physicalEpisodes = new List<Episode>(); - var virtualEpisodes = new List<Episode>(); - foreach (var episode in existingEpisodes) - { - if (episode.LocationType == LocationType.Virtual) - { - virtualEpisodes.Add(episode); - } - else - { - physicalEpisodes.Add(episode); - } - } - - var episodesToRemove = virtualEpisodes - .Where(i => - { - if (!i.IndexNumber.HasValue || !i.ParentIndexNumber.HasValue) - { - return true; - } - - var seasonNumber = i.ParentIndexNumber.Value; - var episodeNumber = i.IndexNumber.Value; - - // If there's a physical episode with the same season and episode number, delete it - if (physicalEpisodes.Any(p => - p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber && - p.ContainsEpisodeNumber(episodeNumber))) - { - return true; - } - - // If the episode no longer exists in the remote lookup, delete it - if (!episodeLookup.Any(e => e.seasonNumber == seasonNumber && e.episodeNumber == episodeNumber)) - { - return true; - } - - // If it's missing, but not unaired, remove it - return !allowMissingEpisodes && i.IsMissingEpisode && - (!i.PremiereDate.HasValue || - i.PremiereDate.Value.ToLocalTime().Date.AddDays(UnairedEpisodeThresholdDays) < - DateTime.Now.Date); - }); - - var hasChanges = false; - - foreach (var episodeToRemove in episodesToRemove) - { - _libraryManager.DeleteItem(episodeToRemove, new DeleteOptions - { - DeleteFileLocation = true - }, false); - - hasChanges = true; - } - - return hasChanges; - } - - /// <summary> - /// Removes the obsolete or missing seasons. - /// </summary> - /// <param name="allRecursiveChildren"></param> - /// <param name="episodeLookup">The episode lookup.</param> - /// <returns><see cref="bool" />.</returns> - private bool RemoveObsoleteOrMissingSeasons( - IList<BaseItem> allRecursiveChildren, - IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup) - { - var existingSeasons = allRecursiveChildren.OfType<Season>().ToList(); - - var physicalSeasons = new List<Season>(); - var virtualSeasons = new List<Season>(); - foreach (var season in existingSeasons) - { - if (season.LocationType == LocationType.Virtual) - { - virtualSeasons.Add(season); - } - else - { - physicalSeasons.Add(season); - } - } - - var allEpisodes = allRecursiveChildren.OfType<Episode>().ToList(); - - var seasonsToRemove = virtualSeasons - .Where(i => - { - if (i.IndexNumber.HasValue) - { - var seasonNumber = i.IndexNumber.Value; - - // If there's a physical season with the same number, delete it - if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber && string.Equals(p.Series.PresentationUniqueKey, i.Series.PresentationUniqueKey, StringComparison.Ordinal))) - { - return true; - } - - // If the season no longer exists in the remote lookup, delete it, but only if an existing episode doesn't require it - return episodeLookup.All(e => e.seasonNumber != seasonNumber) && allEpisodes.All(s => s.ParentIndexNumber != seasonNumber || s.IsInSeasonFolder); - } - - // Season does not have a number - // Remove if there are no episodes directly in series without a season number - return allEpisodes.All(s => s.ParentIndexNumber.HasValue || s.IsInSeasonFolder); - }); - - var hasChanges = false; - - foreach (var seasonToRemove in seasonsToRemove) - { - _libraryManager.DeleteItem(seasonToRemove, new DeleteOptions - { - DeleteFileLocation = true - }, false); - - hasChanges = true; - } - - return hasChanges; - } - - /// <summary> - /// Adds the episode. - /// </summary> - /// <param name="series">The series.</param> - /// <param name="seasonNumber">The season number.</param> - /// <param name="episodeNumber">The episode number.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken) - { - var season = series.Children.OfType<Season>() - .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); - - if (season == null) - { - var provider = new DummySeasonProvider(_logger, _localization, _libraryManager, _fileSystem); - season = await provider.AddSeason(series, seasonNumber, true, cancellationToken).ConfigureAwait(false); - } - - var name = "Episode " + episodeNumber.ToString(CultureInfo.InvariantCulture); - - var episode = new Episode - { - Name = name, - IndexNumber = episodeNumber, - ParentIndexNumber = seasonNumber, - Id = _libraryManager.GetNewItemId( - series.Id + seasonNumber.ToString(CultureInfo.InvariantCulture) + name, - typeof(Episode)), - IsVirtualItem = true, - SeasonId = season?.Id ?? Guid.Empty, - SeriesId = series.Id - }; - - season.AddChild(episode, cancellationToken); - - await episode.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); - } - - /// <summary> - /// Gets the existing episode. - /// </summary> - /// <param name="existingEpisodes">The existing episodes.</param> - /// <param name="seasonCounts"></param> - /// <param name="episodeTuple"></param> - /// <returns>Episode.</returns> - private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, IReadOnlyDictionary<int, int> seasonCounts, (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple) - { - var seasonNumber = episodeTuple.seasonNumber; - var episodeNumber = episodeTuple.episodeNumber; - - while (true) - { - var episode = GetExistingEpisode(existingEpisodes, seasonNumber, episodeNumber); - if (episode != null) - { - return episode; - } - - seasonNumber--; - - if (seasonCounts.ContainsKey(seasonNumber)) - { - episodeNumber += seasonCounts[seasonNumber]; - } - else - { - break; - } - } - - return null; - } - - private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, int season, int episode) - => existingEpisodes.FirstOrDefault(i => i.ParentIndexNumber == season && i.ContainsEpisodeNumber(episode)); - } -} diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs index 5431de623e..0f22f8a9b6 100644 --- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs @@ -28,9 +28,12 @@ namespace MediaBrowser.Providers.TV } /// <inheritdoc /> - protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType currentUpdateType) + protected override bool EnableUpdatingPremiereDateFromChildren => true; + + /// <inheritdoc /> + protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType updateType) { - var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType); + var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType); if (item.IndexNumber.HasValue && item.IndexNumber.Value == 0) { @@ -39,7 +42,7 @@ namespace MediaBrowser.Providers.TV if (!string.Equals(item.Name, seasonZeroDisplayName, StringComparison.OrdinalIgnoreCase)) { item.Name = seasonZeroDisplayName; - updateType = updateType | ItemUpdateType.MetadataEdit; + updatedType = updatedType | ItemUpdateType.MetadataEdit; } } @@ -47,30 +50,27 @@ namespace MediaBrowser.Providers.TV if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal)) { item.SeriesName = seriesName; - updateType |= ItemUpdateType.MetadataImport; + updatedType |= ItemUpdateType.MetadataImport; } var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey(); if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal)) { item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; - updateType |= ItemUpdateType.MetadataImport; + updatedType |= ItemUpdateType.MetadataImport; } var seriesId = item.FindSeriesId(); if (!item.SeriesId.Equals(seriesId)) { item.SeriesId = seriesId; - updateType |= ItemUpdateType.MetadataImport; + updatedType |= ItemUpdateType.MetadataImport; } - return updateType; + return updatedType; } /// <inheritdoc /> - protected override bool EnableUpdatingPremiereDateFromChildren => true; - - /// <inheritdoc /> protected override IList<BaseItem> GetChildrenForMetadataUpdates(Season item) => item.GetEpisodes(); diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index a2c0e62c19..dcb6934086 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -1,6 +1,8 @@ #pragma warning disable CS1591 -using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; @@ -11,15 +13,13 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; -using MediaBrowser.Providers.Plugins.TheTvdb; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV { public class SeriesMetadataService : MetadataService<Series, SeriesInfo> { - private readonly ILocalizationManager _localization; - private readonly TvdbClientManager _tvdbClientManager; + private readonly ILocalizationManager _localizationManager; public SeriesMetadataService( IServerConfigurationManager serverConfigurationManager, @@ -27,12 +27,10 @@ namespace MediaBrowser.Providers.TV IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager, - ILocalizationManager localization, - TvdbClientManager tvdbClientManager) + ILocalizationManager localizationManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - _localization = localization; - _tvdbClientManager = tvdbClientManager; + _localizationManager = localizationManager; } /// <inheritdoc /> @@ -40,26 +38,8 @@ namespace MediaBrowser.Providers.TV { await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); - var seasonProvider = new DummySeasonProvider(Logger, _localization, LibraryManager, FileSystem); - await seasonProvider.Run(item, cancellationToken).ConfigureAwait(false); - - // TODO why does it not register this itself omg - var provider = new MissingEpisodeProvider( - Logger, - ServerConfigurationManager, - LibraryManager, - _localization, - FileSystem, - _tvdbClientManager); - - try - { - await provider.Run(item, true, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path); - } + RemoveObsoleteSeasons(item); + await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false); } /// <inheritdoc /> @@ -101,5 +81,117 @@ namespace MediaBrowser.Providers.TV targetItem.AirDays = sourceItem.AirDays; } } + + private void RemoveObsoleteSeasons(Series series) + { + // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in FillInMissingSeasonsAsync. + var physicalSeasonNumbers = new HashSet<int>(); + var virtualSeasons = new List<Season>(); + foreach (var existingSeason in series.Children.OfType<Season>()) + { + if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue) + { + physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value); + } + else if (existingSeason.LocationType == LocationType.Virtual) + { + virtualSeasons.Add(existingSeason); + } + } + + foreach (var virtualSeason in virtualSeasons) + { + var seasonNumber = virtualSeason.IndexNumber; + // If there's a physical season with the same number or no episodes in the season, delete it + if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value)) + || !virtualSeason.GetEpisodes().Any()) + { + Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name); + + LibraryManager.DeleteItem( + virtualSeason, + new DeleteOptions + { + DeleteFileLocation = true + }, + false); + } + } + } + + /// <summary> + /// Creates seasons for all episodes that aren't in a season folder. + /// If no season number can be determined, a dummy season will be created. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The async task.</returns> + private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken) + { + var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode) + .Cast<Episode>() + .Where(i => !i.IsInSeasonFolder); + + List<Season> seasons = series.Children.OfType<Season>().ToList(); + + // Loop through the unique season numbers + foreach (var episode in episodesInSeriesFolder) + { + // Null season numbers will have a 'dummy' season created because seasons are always required. + var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null; + var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); + + if (existingSeason == null) + { + var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false); + seasons.Add(season); + } + else if (existingSeason.IsVirtualItem) + { + existingSeason.IsVirtualItem = false; + await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); + } + } + } + + /// <summary> + /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="seasonNumber">The season number.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The newly created season.</returns> + private async Task<Season> CreateSeasonAsync( + Series series, + int? seasonNumber, + CancellationToken cancellationToken) + { + string seasonName = seasonNumber switch + { + null => _localizationManager.GetLocalizedString("NameSeasonUnknown"), + 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName, + _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value) + }; + + Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); + + var season = new Season + { + Name = seasonName, + IndexNumber = seasonNumber, + Id = LibraryManager.GetNewItemId( + series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName, + typeof(Season)), + IsVirtualItem = false, + SeriesId = series.Id, + SeriesName = series.Name + }; + + series.AddChild(season); + + await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false); + + return season; + } } } diff --git a/MediaBrowser.Providers/TV/TvExternalIds.cs b/MediaBrowser.Providers/TV/TvExternalIds.cs deleted file mode 100644 index a6040edd15..0000000000 --- a/MediaBrowser.Providers/TV/TvExternalIds.cs +++ /dev/null @@ -1,82 +0,0 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.TheTvdb; - -namespace MediaBrowser.Providers.TV -{ - public class Zap2ItExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "Zap2It"; - - /// <inheritdoc /> - public string Key => MetadataProvider.Zap2It.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => null; - - /// <inheritdoc /> - public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Series; - } - - public class TvdbExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "TheTVDB"; - - /// <inheritdoc /> - public string Key => MetadataProvider.Tvdb.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => null; - - /// <inheritdoc /> - public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Series; - } - - public class TvdbSeasonExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "TheTVDB"; - - /// <inheritdoc /> - public string Key => MetadataProvider.Tvdb.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Season; - - /// <inheritdoc /> - public string UrlFormatString => null; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Season; - } - - public class TvdbEpisodeExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "TheTVDB"; - - /// <inheritdoc /> - public string Key => MetadataProvider.Tvdb.ToString(); - - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Episode; - - /// <inheritdoc /> - public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=episode&id={0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Episode; - } -} diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs new file mode 100644 index 0000000000..3cb18e4248 --- /dev/null +++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.TV +{ + public class Zap2ItExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "Zap2It"; + + /// <inheritdoc /> + public string Key => MetadataProvider.Zap2It.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => null; + + /// <inheritdoc /> + public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Series; + } +} |
