diff options
Diffstat (limited to 'MediaBrowser.Providers')
27 files changed, 583 insertions, 172 deletions
diff --git a/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs b/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs index 2bd2676ceb..a06de95fce 100644 --- a/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs +++ b/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs @@ -204,7 +204,7 @@ public class ComicBookInfoProvider : IComicProvider { try { - return CultureInfo.GetCultureInfo(language).DisplayName; + return CultureInfo.GetCultureInfo(language).TwoLetterISOLanguageName; } catch (CultureNotFoundException) { diff --git a/MediaBrowser.Providers/Books/ComicInfo/ExternalComicInfoProvider.cs b/MediaBrowser.Providers/Books/ComicInfo/ExternalComicInfoProvider.cs index cfd22a850e..e3d1f544cf 100644 --- a/MediaBrowser.Providers/Books/ComicInfo/ExternalComicInfoProvider.cs +++ b/MediaBrowser.Providers/Books/ComicInfo/ExternalComicInfoProvider.cs @@ -52,7 +52,7 @@ public class ExternalComicInfoProvider : IComicProvider var metadataResult = new MetadataResult<Book> { Item = book, HasMetadata = true }; ComicInfoReader.ReadPeopleMetadata(comicInfoXml, metadataResult); - ComicInfoReader.ReadCultureInfoInto(comicInfoXml, "ComicInfo/LanguageISO", cultureInfo => metadataResult.ResultLanguage = cultureInfo.ThreeLetterISOLanguageName); + ComicInfoReader.ReadCultureInfoInto(comicInfoXml, "ComicInfo/LanguageISO", cultureInfo => metadataResult.ResultLanguage = cultureInfo.TwoLetterISOLanguageName); return metadataResult; } diff --git a/MediaBrowser.Providers/Books/ComicInfo/InternalComicInfoProvider.cs b/MediaBrowser.Providers/Books/ComicInfo/InternalComicInfoProvider.cs index 19062452b9..4b14837441 100644 --- a/MediaBrowser.Providers/Books/ComicInfo/InternalComicInfoProvider.cs +++ b/MediaBrowser.Providers/Books/ComicInfo/InternalComicInfoProvider.cs @@ -50,7 +50,7 @@ public class InternalComicInfoProvider : IComicProvider var metadataResult = new MetadataResult<Book> { Item = book, HasMetadata = true }; ComicInfoReader.ReadPeopleMetadata(comicInfoXml, metadataResult); - ComicInfoReader.ReadCultureInfoInto(comicInfoXml, "ComicInfo/LanguageISO", cultureInfo => metadataResult.ResultLanguage = cultureInfo.ThreeLetterISOLanguageName); + ComicInfoReader.ReadCultureInfoInto(comicInfoXml, "ComicInfo/LanguageISO", cultureInfo => metadataResult.ResultLanguage = cultureInfo.TwoLetterISOLanguageName); return metadataResult; } diff --git a/MediaBrowser.Providers/Manager/MetadataLanguageUtils.cs b/MediaBrowser.Providers/Manager/MetadataLanguageUtils.cs new file mode 100644 index 0000000000..92a16feaee --- /dev/null +++ b/MediaBrowser.Providers/Manager/MetadataLanguageUtils.cs @@ -0,0 +1,44 @@ +using System; + +namespace MediaBrowser.Providers.Manager; + +/// <summary> +/// Helpers for comparing the language of fetched metadata with the language that was requested. +/// </summary> +internal static class MetadataLanguageUtils +{ + /// <summary> + /// Gets the language subtag of a language tag, e.g. "es" for "es-ES". + /// </summary> + /// <param name="language">The language tag.</param> + /// <returns>The language subtag, lowercased, or <c>null</c> if none was given.</returns> + public static string? GetLanguageSubtag(string? language) + { + if (string.IsNullOrEmpty(language)) + { + return null; + } + + var separator = language.IndexOf('-', StringComparison.Ordinal); + + return (separator == -1 ? language : language[..separator]).ToLowerInvariant(); + } + + /// <summary> + /// Determines whether a provider result can be considered to be in the requested language. + /// </summary> + /// <param name="resultLanguage">The language the provider reported for its result, if any.</param> + /// <param name="preferredLanguage">The language that was requested, if any.</param> + /// <returns><c>true</c> if the result is in the requested language or either language is unknown.</returns> + public static bool MatchesPreferredLanguage(string? resultLanguage, string? preferredLanguage) + { + // A provider that doesn't report a language cannot be judged, assume it honored the request + if (string.IsNullOrEmpty(resultLanguage) || string.IsNullOrEmpty(preferredLanguage)) + { + return true; + } + + // Compare on the language subtag only so that e.g. "es" matches "es-ES" + return string.Equals(GetLanguageSubtag(resultLanguage), GetLanguageSubtag(preferredLanguage), StringComparison.Ordinal); + } +} diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 40f2775bd3..c810fa523e 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -260,21 +260,40 @@ namespace MediaBrowser.Providers.Manager switch (lookupInfo) { case EpisodeInfo episodeInfo: - episodeInfo.SeriesProviderIds = result.ProviderIds; + episodeInfo.SeriesProviderIds = GetValidProviderIds(result.ProviderIds); episodeInfo.ProviderIds.Clear(); break; case SeasonInfo seasonInfo: - seasonInfo.SeriesProviderIds = result.ProviderIds; + seasonInfo.SeriesProviderIds = GetValidProviderIds(result.ProviderIds); seasonInfo.ProviderIds.Clear(); break; default: - lookupInfo.ProviderIds = result.ProviderIds; + lookupInfo.SetProviderIds(result.ProviderIds); lookupInfo.Name = result.Name; lookupInfo.Year = result.ProductionYear; break; } } + private static Dictionary<string, string> GetValidProviderIds(IReadOnlyDictionary<string, string> providerIds) + { + var validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + if (providerIds is null) + { + return validProviderIds; + } + + foreach (var (name, value) in providerIds) + { + if (ProviderIdsExtensions.IsValidProviderId(name, value)) + { + validProviderIds[name] = value; + } + } + + return validProviderIds; + } + protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken) { await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); @@ -835,6 +854,7 @@ namespace MediaBrowser.Providers.Manager } } + var hasRemoteMetadata = false; var isLocalLocked = temp.Item.IsLocked; if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly)) { @@ -849,6 +869,7 @@ namespace MediaBrowser.Providers.Manager var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, remoteProviders, cancellationToken).ConfigureAwait(false); + hasRemoteMetadata = remoteResult.UpdateType.HasFlag(ItemUpdateType.MetadataDownload); refreshResult.UpdateType |= remoteResult.UpdateType; refreshResult.ErrorMessage = remoteResult.ErrorMessage; refreshResult.Failures += remoteResult.Failures; @@ -858,7 +879,12 @@ namespace MediaBrowser.Providers.Manager { if (refreshResult.UpdateType > ItemUpdateType.None) { - if (!options.RemoveOldMetadata) + // Erasing the old values is only safe when a remote provider returned something to + // replace them with. If every one of them failed there is no replacement, and wiping the + // item would turn a provider being temporarily unreachable into permanent data loss. + // A single failure is not enough: Identify asks for the erasure precisely because the + // previous match was wrong, and an unrelated provider throwing must not undo that. + if (!options.RemoveOldMetadata || (refreshResult.Failures > 0 && !hasRemoteMetadata)) { // Add existing metadata to provider result if it does not exist there MergeData(metadata, temp, [], false, false); @@ -913,6 +939,10 @@ namespace MediaBrowser.Providers.Manager private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool replaceData, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken) { var refreshResult = new RefreshResult(); + var preferredLanguage = id?.MetadataLanguage; + + var overviewIsFallback = false; + var taglineIsFallback = false; if (id is not null) { @@ -932,6 +962,28 @@ namespace MediaBrowser.Providers.Manager { result.Provider = provider.Name; + if (MetadataLanguageUtils.MatchesPreferredLanguage(result.ResultLanguage, preferredLanguage)) + { + if (overviewIsFallback && !string.IsNullOrEmpty(result.Item.Overview)) + { + temp.Item.Overview = null; + overviewIsFallback = false; + } + + if (taglineIsFallback && !string.IsNullOrEmpty(result.Item.Tagline)) + { + temp.Item.Tagline = null; + taglineIsFallback = false; + } + } + else + { + overviewIsFallback |= string.IsNullOrEmpty(temp.Item.Overview) && !string.IsNullOrEmpty(result.Item.Overview); + taglineIsFallback |= string.IsNullOrEmpty(temp.Item.Tagline) && !string.IsNullOrEmpty(result.Item.Tagline); + } + + LogInvalidProviderIds(result, providerName, logName); + MergeData(result, temp, [], replaceData, false); MergeNewData(temp.Item, id); @@ -957,6 +1009,58 @@ namespace MediaBrowser.Providers.Manager return refreshResult; } + /// <summary> + /// Reports the ids a provider returned that cannot belong to the provider they are filed under. + /// </summary> + /// <remarks> + /// The ids are dropped when merging, this names the provider that produced them so the source of a + /// recurring bad id can be found. + /// </remarks> + private void LogInvalidProviderIds(MetadataResult<TItemType> result, string providerName, string logName) + { + if (!Logger.IsEnabled(LogLevel.Debug)) + { + return; + } + + LogInvalidProviderIds(result.Item?.ProviderIds, providerName, logName, null); + + if (result.People is null) + { + return; + } + + foreach (var person in result.People) + { + LogInvalidProviderIds(person.ProviderIds, providerName, logName, person.Name); + } + } + + private void LogInvalidProviderIds(IReadOnlyDictionary<string, string> providerIds, string providerName, string logName, string personName) + { + if (providerIds is null) + { + return; + } + + foreach (var (key, value) in providerIds) + { + if (ProviderIdsExtensions.IsValidProviderId(key, value)) + { + continue; + } + + if (personName is null) + { + Logger.LogDebug("Discarding {Key} id '{Value}' returned by {Provider} for {Item}", key, value, providerName, logName); + } + else + { + Logger.LogDebug("Discarding {Key} id '{Value}' returned by {Provider} for {Person} of {Item}", key, value, providerName, personName, logName); + } + } + } + private void MergeNewData(TItemType source, TIdType lookupInfo) { // Copy new provider id's that may have been obtained @@ -964,8 +1068,18 @@ namespace MediaBrowser.Providers.Manager { var key = providerId.Key; - // Don't replace existing Id's. - lookupInfo.ProviderIds.TryAdd(key, providerId.Value); + if (!ProviderIdsExtensions.IsValidProviderId(key, providerId.Value)) + { + continue; + } + + // Don't replace existing Id's, unless the one already there is unusable - handing that + // one to the providers that have yet to run is what makes them fail. + if (!lookupInfo.ProviderIds.TryGetValue(key, out var existingId) + || !ProviderIdsExtensions.IsValidProviderId(key, existingId)) + { + lookupInfo.ProviderIds[key] = providerId.Value; + } } } @@ -1104,6 +1218,9 @@ namespace MediaBrowser.Providers.Manager if (!lockedFields.Contains(MetadataField.Cast)) { + RemoveInvalidProviderIds(sourceResult.People); + RemoveInvalidProviderIds(targetResult.People); + if (replaceData || targetResult.People is null || targetResult.People.Count == 0) { targetResult.People = sourceResult.People; @@ -1175,17 +1292,33 @@ namespace MediaBrowser.Providers.Manager { var key = id.Key; - // Don't replace existing Id's. - if (replaceData) + // An id that cannot belong to the provider it is filed under only breaks that provider on + // the next refresh, so never let one in - not even when replacing all metadata. + if (!ProviderIdsExtensions.IsValidProviderId(key, id.Value)) { - target.ProviderIds[key] = id.Value; + continue; } - else + + // Don't replace existing Id's, unless the stored one is unusable - that one is the bad + // match the refresh is meant to repair. + if (replaceData + || !target.ProviderIds.TryGetValue(key, out var existingId) + || !ProviderIdsExtensions.IsValidProviderId(key, existingId)) { - target.ProviderIds.TryAdd(key, id.Value); + target.ProviderIds[key] = id.Value; } } + // A bad id no provider offered a replacement for still has to go, otherwise the item keeps + // failing the same way on every refresh. + foreach (var key in target.ProviderIds + .Where(id => !ProviderIdsExtensions.IsValidProviderId(id.Key, id.Value)) + .Select(id => id.Key) + .ToArray()) + { + target.ProviderIds.Remove(key); + } + if (replaceData || !target.CriticRating.HasValue) { target.CriticRating = source.CriticRating; @@ -1251,6 +1384,32 @@ namespace MediaBrowser.Providers.Manager } } + private static void RemoveInvalidProviderIds(IReadOnlyList<PersonInfo> people) + { + if (people is null) + { + return; + } + + foreach (var person in people) + { + if (person.ProviderIds is null || person.ProviderIds.Count == 0) + { + continue; + } + + var invalidKeys = person.ProviderIds + .Where(id => !ProviderIdsExtensions.IsValidProviderId(id.Key, id.Value)) + .Select(id => id.Key) + .ToArray(); + + foreach (var key in invalidKeys) + { + person.ProviderIds.Remove(key); + } + } + } + private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target) { var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase); diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 45fbe4d348..fbd9e5435e 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -163,6 +163,8 @@ namespace MediaBrowser.Providers.Manager _externalUrlProviders = externalUrlProviders.OrderBy(i => i.Name).ToArray(); _savers = metadataSavers.ToArray(); + + ClearMetadataProviderCache(); } /// <inheritdoc/> diff --git a/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs index d3fce37c71..2923dd3290 100644 --- a/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs +++ b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs @@ -23,11 +23,11 @@ namespace MediaBrowser.Providers.Music public static string? GetReleaseGroupId(this AlbumInfo info) { - var id = info.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); + var id = MusicBrainzId(MetadataProvider.MusicBrainzReleaseGroup, info.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup)); if (string.IsNullOrEmpty(id)) { - return info.SongInfos.Select(i => i.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup)) + return info.SongInfos.Select(i => MusicBrainzId(MetadataProvider.MusicBrainzReleaseGroup, i.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup))) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } @@ -36,11 +36,11 @@ namespace MediaBrowser.Providers.Music public static string? GetReleaseId(this AlbumInfo info) { - var id = info.GetProviderId(MetadataProvider.MusicBrainzAlbum); + var id = MusicBrainzId(MetadataProvider.MusicBrainzAlbum, info.GetProviderId(MetadataProvider.MusicBrainzAlbum)); if (string.IsNullOrEmpty(id)) { - return info.SongInfos.Select(i => i.GetProviderId(MetadataProvider.MusicBrainzAlbum)) + return info.SongInfos.Select(i => MusicBrainzId(MetadataProvider.MusicBrainzAlbum, i.GetProviderId(MetadataProvider.MusicBrainzAlbum))) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } @@ -50,15 +50,17 @@ namespace MediaBrowser.Providers.Music public static string? GetMusicBrainzArtistId(this AlbumInfo info) { info.ProviderIds.TryGetValue(MetadataProvider.MusicBrainzAlbumArtist.ToString(), out string? id); + id = MusicBrainzId(MetadataProvider.MusicBrainzAlbumArtist, id); if (string.IsNullOrEmpty(id)) { info.ArtistProviderIds.TryGetValue(MetadataProvider.MusicBrainzArtist.ToString(), out id); + id = MusicBrainzId(MetadataProvider.MusicBrainzArtist, id); } if (string.IsNullOrEmpty(id)) { - return info.SongInfos.Select(i => i.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)) + return info.SongInfos.Select(i => MusicBrainzId(MetadataProvider.MusicBrainzAlbumArtist, i.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist))) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } @@ -68,14 +70,21 @@ namespace MediaBrowser.Providers.Music public static string? GetMusicBrainzArtistId(this ArtistInfo info) { info.ProviderIds.TryGetValue(MetadataProvider.MusicBrainzArtist.ToString(), out var id); + id = MusicBrainzId(MetadataProvider.MusicBrainzArtist, id); if (string.IsNullOrEmpty(id)) { - return info.SongInfos.Select(i => i.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)) + return info.SongInfos.Select(i => MusicBrainzId(MetadataProvider.MusicBrainzAlbumArtist, i.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist))) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } return id; } + + /// <summary> + /// Returns the id if it can be an id of the given provider, otherwise <c>null</c>. + /// </summary> + private static string? MusicBrainzId(MetadataProvider provider, string? id) + => ProviderIdsExtensions.IsValidProviderId(provider.ToString(), id) ? id : null; } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index 0acd44afbe..1903adfbdd 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -21,6 +21,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Music; namespace MediaBrowser.Providers.Plugins.AudioDb @@ -77,7 +78,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { result.Item = new MusicAlbum(); result.HasMetadata = true; - ProcessResult(result.Item, obj.album[0], info.MetadataLanguage); + ProcessResult(result, obj.album[0], info.MetadataLanguage); } } } @@ -85,8 +86,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb return result; } - private void ProcessResult(MusicAlbum item, Album result, string preferredLanguage) + private void ProcessResult(MetadataResult<MusicAlbum> metadataResult, Album result, string preferredLanguage) { + var item = metadataResult.Item; + if (Plugin.Instance.Configuration.ReplaceAlbumName && !string.IsNullOrWhiteSpace(result.strAlbum)) { item.Album = result.strAlbum; @@ -113,43 +116,48 @@ namespace MediaBrowser.Providers.Plugins.AudioDb item.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, result.strMusicBrainzArtistID); item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, result.strMusicBrainzID); - string overview = null; - - if (string.Equals(preferredLanguage, "de", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strDescriptionDE; - } - else if (string.Equals(preferredLanguage, "fr", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strDescriptionFR; - } - else if (string.Equals(preferredLanguage, "nl", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strDescriptionNL; - } - else if (string.Equals(preferredLanguage, "ru", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strDescriptionRU; - } - else if (string.Equals(preferredLanguage, "it", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strDescriptionIT; - } - else if ((preferredLanguage ?? string.Empty).StartsWith("pt", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strDescriptionPT; - } + var language = MetadataLanguageUtils.GetLanguageSubtag(preferredLanguage); + var overview = GetDescription(result, language); if (string.IsNullOrWhiteSpace(overview)) { overview = string.IsNullOrWhiteSpace(result.strDescriptionEN) ? result.strDescription : result.strDescriptionEN; + + // The description is not in the requested language, mark it as English so it does not + // block a provider further down the list that can serve the requested language + metadataResult.ResultLanguage = "en"; + } + else + { + metadataResult.ResultLanguage = language; } item.Overview = (overview ?? string.Empty).StripHtml(); } + private static string GetDescription(Album result, string language) + => language switch + { + "de" => result.strDescriptionDE, + "en" => result.strDescriptionEN, + "es" => result.strDescriptionES, + "fr" => result.strDescriptionFR, + "he" => result.strDescriptionIL, + "hu" => result.strDescriptionHU, + "it" => result.strDescriptionIT, + "ja" => result.strDescriptionJP, + "nl" => result.strDescriptionNL, + "no" or "nb" or "nn" => result.strDescriptionNO, + "pl" => result.strDescriptionPL, + "pt" => result.strDescriptionPT, + "ru" => result.strDescriptionRU, + "sv" => result.strDescriptionSE, + "zh" => result.strDescriptionCN, + _ => null + }; + internal async Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken) { var xmlPath = GetAlbumInfoPath(_config.ApplicationPaths, musicBrainzReleaseGroupId); diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index c4f4833857..2d9fe4448f 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Music; namespace MediaBrowser.Providers.Plugins.AudioDb @@ -148,7 +149,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { result.Item = new MusicArtist(); result.HasMetadata = true; - ProcessResult(result.Item, artist, info.MetadataLanguage); + ProcessResult(result, artist, info.MetadataLanguage); } return result; @@ -193,8 +194,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb return null; } - private void ProcessResult(MusicArtist item, Artist result, string preferredLanguage) + private void ProcessResult(MetadataResult<MusicArtist> metadataResult, Artist result, string preferredLanguage) { + var item = metadataResult.Item; + if (!string.IsNullOrWhiteSpace(result.strWebsite)) { item.HomePageUrl = result.strWebsite; @@ -229,43 +232,48 @@ namespace MediaBrowser.Providers.Plugins.AudioDb item.SetProviderId(MetadataProvider.AudioDbArtist, result.idArtist); item.SetProviderId(MetadataProvider.MusicBrainzArtist, result.strMusicBrainzID); - string overview = null; - - if (string.Equals(preferredLanguage, "de", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strBiographyDE; - } - else if (string.Equals(preferredLanguage, "fr", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strBiographyFR; - } - else if (string.Equals(preferredLanguage, "nl", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strBiographyNL; - } - else if (string.Equals(preferredLanguage, "ru", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strBiographyRU; - } - else if (string.Equals(preferredLanguage, "it", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strBiographyIT; - } - else if ((preferredLanguage ?? string.Empty).StartsWith("pt", StringComparison.OrdinalIgnoreCase)) - { - overview = result.strBiographyPT; - } + var language = MetadataLanguageUtils.GetLanguageSubtag(preferredLanguage); + var overview = GetBiography(result, language); if (string.IsNullOrWhiteSpace(overview)) { overview = string.IsNullOrWhiteSpace(result.strBiographyEN) ? result.strBiography : result.strBiographyEN; + + // The biography is not in the requested language, mark it as English so it does not + // block a provider further down the list that can serve the requested language + metadataResult.ResultLanguage = "en"; + } + else + { + metadataResult.ResultLanguage = language; } item.Overview = (overview ?? string.Empty).StripHtml(); } + private static string GetBiography(Artist result, string language) + => language switch + { + "de" => result.strBiographyDE, + "en" => result.strBiographyEN, + "es" => result.strBiographyES, + "fr" => result.strBiographyFR, + "he" => result.strBiographyIL, + "hu" => result.strBiographyHU, + "it" => result.strBiographyIT, + "ja" => result.strBiographyJP, + "nl" => result.strBiographyNL, + "no" or "nb" or "nn" => result.strBiographyNO, + "pl" => result.strBiographyPL, + "pt" => result.strBiographyPT, + "ru" => result.strBiographyRU, + "sv" => result.strBiographySE, + "zh" => result.strBiographyCN, + _ => null + }; + internal async Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken) { var xmlPath = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 397c916a4f..7dee5fd31d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -14,6 +14,7 @@ using MediaBrowser.Providers.Music; using MetaBrainz.MusicBrainz; using MetaBrainz.MusicBrainz.Interfaces.Entities; using MetaBrainz.MusicBrainz.Interfaces.Searches; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.MusicBrainz; @@ -22,6 +23,17 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// </summary> public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder { + private readonly ILogger<MusicBrainzAlbumProvider> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + public MusicBrainzAlbumProvider(ILogger<MusicBrainzAlbumProvider> logger) + { + _logger = logger; + } + /// <inheritdoc /> public string Name => "MusicBrainz"; @@ -32,21 +44,26 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) { var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery; - var releaseId = searchInfo.GetReleaseId(); - var releaseGroupId = searchInfo.GetReleaseGroupId(); + var releaseId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchInfo.GetReleaseId(), "release", _logger); + var releaseGroupId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchInfo.GetReleaseGroupId(), "release group", _logger); - if (!string.IsNullOrEmpty(releaseId)) + if (releaseId is not null) { - var releaseResult = await query.LookupReleaseAsync(new Guid(releaseId), Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false); - return GetReleaseResult(releaseResult).SingleItemAsEnumerable(); + var releaseResult = await query.LookupReleaseOrNullAsync(releaseId.Value, Include.Artists | Include.ReleaseGroups, _logger, cancellationToken).ConfigureAwait(false); + if (releaseResult is not null) + { + return GetReleaseResult(releaseResult).SingleItemAsEnumerable(); + } } - if (!string.IsNullOrEmpty(releaseGroupId)) + if (releaseGroupId is not null) { - var releaseGroupResult = await query.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, null, cancellationToken).ConfigureAwait(false); - - // No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to ToBlockingEnumerable - return GetReleaseGroupResultAsync(releaseGroupResult.Releases, CancellationToken.None).ToBlockingEnumerable(cancellationToken); + var releaseGroupResult = await query.LookupReleaseGroupOrNullAsync(releaseGroupId.Value, Include.Releases, _logger, cancellationToken).ConfigureAwait(false); + if (releaseGroupResult is not null) + { + // No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to ToBlockingEnumerable + return GetReleaseGroupResultAsync(releaseGroupResult.Releases, CancellationToken.None).ToBlockingEnumerable(cancellationToken); + } } var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId(); @@ -102,8 +119,11 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu foreach (var result in releaseSearchResults) { // Fetch full release info, otherwise artists are missing - var fullResult = await query.LookupReleaseAsync(result.Id, Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false); - yield return GetReleaseResult(fullResult); + var fullResult = await query.LookupReleaseOrNullAsync(result.Id, Include.Artists | Include.ReleaseGroups, _logger, cancellationToken).ConfigureAwait(false); + if (fullResult is not null) + { + yield return GetReleaseResult(fullResult); + } } } @@ -156,8 +176,8 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken) { var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery; - var releaseId = info.GetReleaseId(); - var releaseGroupId = info.GetReleaseGroupId(); + var releaseId = MusicBrainzQueryExtensions.ParseMusicBrainzId(info.GetReleaseId(), "release", _logger); + var releaseGroupId = MusicBrainzQueryExtensions.ParseMusicBrainzId(info.GetReleaseGroupId(), "release group", _logger); var result = new MetadataResult<MusicAlbum> { @@ -165,15 +185,15 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu }; // If there is a release group, but no release ID, try to match the release - if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId)) + if (releaseId is null && releaseGroupId is not null) { // TODO: Actually try to match the release. Simply taking the first result is stupid. - var releaseGroupLookup = await query.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false); - releaseId = releaseGroupLookup.Releases?.Count > 0 ? releaseGroupLookup.Releases[0].Id.ToString() : null; + var releaseGroupLookup = await query.LookupReleaseGroupOrNullAsync(releaseGroupId.Value, Include.None, _logger, cancellationToken).ConfigureAwait(false); + releaseId = releaseGroupLookup?.Releases?.Count > 0 ? releaseGroupLookup.Releases[0].Id : null; } // If there is no release ID, lookup a release with the info we have - if (string.IsNullOrWhiteSpace(releaseId)) + if (releaseId is null) { var artistMusicBrainzId = info.GetMusicBrainzArtistId(); IRelease? releaseResult = null; @@ -193,55 +213,61 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu if (releaseResult is not null) { - releaseId = releaseResult.Id.ToString(); + releaseId = releaseResult.Id; if (releaseResult.ReleaseGroup?.Id is not null) { - releaseGroupId = releaseResult.ReleaseGroup.Id.ToString(); + releaseGroupId = releaseResult.ReleaseGroup.Id; } } } - if (string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId)) + if (releaseId is null && releaseGroupId is null) { return result; } // Fetch the full release (and its release group) so we can populate everything MusicBrainz returns. IRelease? release = null; - if (!string.IsNullOrWhiteSpace(releaseId)) + if (releaseId is not null) { - release = await query.LookupReleaseAsync( - new Guid(releaseId), + release = await query.LookupReleaseOrNullAsync( + releaseId.Value, Include.Artists | Include.ReleaseGroups | Include.Labels | Include.Genres | Include.Tags, + _logger, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(releaseGroupId) && release?.ReleaseGroup?.Id is not null) + if (releaseGroupId is null && release?.ReleaseGroup?.Id is not null) { - releaseGroupId = release.ReleaseGroup.Id.ToString(); + releaseGroupId = release.ReleaseGroup.Id; } } IReleaseGroup? releaseGroup = null; - if (!string.IsNullOrWhiteSpace(releaseGroupId)) + if (releaseGroupId is not null) { - releaseGroup = await query.LookupReleaseGroupAsync( - new Guid(releaseGroupId), + releaseGroup = await query.LookupReleaseGroupOrNullAsync( + releaseGroupId.Value, Include.Artists | Include.Genres | Include.Tags, - null, + _logger, cancellationToken).ConfigureAwait(false); } + if (release is null && releaseGroup is null) + { + return result; + } + result.HasMetadata = true; - if (!string.IsNullOrEmpty(releaseId)) + if (releaseId is not null) { - result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId); + result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId.Value.ToString()); } - if (!string.IsNullOrEmpty(releaseGroupId)) + if (releaseGroupId is not null) { - result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId); + result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId.Value.ToString()); } Populate(result.Item, release, releaseGroup); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index a9e950fb64..c3d13ed42c 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -13,6 +13,7 @@ using MediaBrowser.Providers.Music; using MetaBrainz.MusicBrainz; using MetaBrainz.MusicBrainz.Interfaces.Entities; using MetaBrainz.MusicBrainz.Interfaces.Searches; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.MusicBrainz; @@ -21,6 +22,17 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// </summary> public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IHasOrder { + private readonly ILogger<MusicBrainzArtistProvider> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="MusicBrainzArtistProvider"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + public MusicBrainzArtistProvider(ILogger<MusicBrainzArtistProvider> logger) + { + _logger = logger; + } + /// <inheritdoc /> public string Name => "MusicBrainz"; @@ -32,12 +44,15 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) { var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery; - var artistId = searchInfo.GetMusicBrainzArtistId(); + var artistId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchInfo.GetMusicBrainzArtistId(), "artist", _logger); - if (!string.IsNullOrWhiteSpace(artistId)) + if (artistId is not null) { - var artistResult = await query.LookupArtistAsync(new Guid(artistId), Include.Aliases, null, null, cancellationToken).ConfigureAwait(false); - return GetResultFromResponse(artistResult).SingleItemAsEnumerable(); + var artistResult = await query.LookupArtistOrNullAsync(artistId.Value, Include.Aliases, _logger, cancellationToken).ConfigureAwait(false); + if (artistResult is not null) + { + return GetResultFromResponse(artistResult).SingleItemAsEnumerable(); + } } if (string.IsNullOrWhiteSpace(searchInfo.Name)) @@ -99,22 +114,22 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar { var result = new MetadataResult<MusicArtist> { Item = new MusicArtist() }; - var musicBrainzId = info.GetMusicBrainzArtistId(); + var musicBrainzId = MusicBrainzQueryExtensions.ParseMusicBrainzId(info.GetMusicBrainzArtistId(), "artist", _logger); // If we don't have an id yet, resolve one by name so we can look the artist up. - if (string.IsNullOrWhiteSpace(musicBrainzId)) + if (musicBrainzId is null) { var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); - musicBrainzId = searchResults.FirstOrDefault()?.GetProviderId(MetadataProvider.MusicBrainzArtist); + musicBrainzId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchResults.FirstOrDefault()?.GetProviderId(MetadataProvider.MusicBrainzArtist), "artist", _logger); } - if (string.IsNullOrWhiteSpace(musicBrainzId)) + if (musicBrainzId is null) { return result; } var query = Plugin.Instance!.MusicBrainzQuery; - var artist = await query.LookupArtistAsync(new Guid(musicBrainzId), Include.Genres | Include.Tags, null, null, cancellationToken).ConfigureAwait(false); + var artist = await query.LookupArtistOrNullAsync(musicBrainzId.Value, Include.Genres | Include.Tags, _logger, cancellationToken).ConfigureAwait(false); if (artist is null) { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzQueryExtensions.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzQueryExtensions.cs new file mode 100644 index 0000000000..f3df41e942 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzQueryExtensions.cs @@ -0,0 +1,112 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MetaBrainz.Common; +using MetaBrainz.MusicBrainz; +using MetaBrainz.MusicBrainz.Interfaces.Entities; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// <summary> +/// Helpers for talking to MusicBrainz with identifiers that are not guaranteed to be valid. +/// </summary> +internal static class MusicBrainzQueryExtensions +{ + /// <summary> + /// Parses a MusicBrainz identifier, which may come from user-supplied tags or NFO files and is therefore not + /// guaranteed to be a valid GUID. + /// </summary> + /// <param name="id">The identifier to parse.</param> + /// <param name="entityType">The type of entity the identifier refers to, used for logging.</param> + /// <param name="logger">The logger.</param> + /// <returns>The parsed identifier, or <see langword="null"/> if it is missing or malformed.</returns> + public static Guid? ParseMusicBrainzId(string? id, string entityType, ILogger logger) + { + if (string.IsNullOrWhiteSpace(id)) + { + return null; + } + + if (!Guid.TryParse(id, out var parsedId)) + { + logger.LogDebug("Ignoring malformed MusicBrainz {EntityType} id {Id}", entityType, id); + return null; + } + + return parsedId; + } + + /// <summary> + /// Looks up a release, treating an unknown identifier as missing data rather than an error. + /// </summary> + /// <param name="query">The MusicBrainz query client.</param> + /// <param name="releaseId">The release identifier.</param> + /// <param name="include">The additional data to include in the lookup.</param> + /// <param name="logger">The logger.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The release, or <see langword="null"/> if MusicBrainz does not have it.</returns> + public static Task<IRelease?> LookupReleaseOrNullAsync(this Query query, Guid releaseId, Include include, ILogger logger, CancellationToken cancellationToken) + => NotFoundAsNullAsync( + () => query.LookupReleaseAsync(releaseId, include, cancellationToken), + "release", + releaseId, + logger); + + /// <summary> + /// Looks up a release group, treating an unknown identifier as missing data rather than an error. + /// </summary> + /// <param name="query">The MusicBrainz query client.</param> + /// <param name="releaseGroupId">The release group identifier.</param> + /// <param name="include">The additional data to include in the lookup.</param> + /// <param name="logger">The logger.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The release group, or <see langword="null"/> if MusicBrainz does not have it.</returns> + public static Task<IReleaseGroup?> LookupReleaseGroupOrNullAsync(this Query query, Guid releaseGroupId, Include include, ILogger logger, CancellationToken cancellationToken) + => NotFoundAsNullAsync( + () => query.LookupReleaseGroupAsync(releaseGroupId, include, null, cancellationToken), + "release group", + releaseGroupId, + logger); + + /// <summary> + /// Looks up an artist, treating an unknown identifier as missing data rather than an error. + /// </summary> + /// <param name="query">The MusicBrainz query client.</param> + /// <param name="artistId">The artist identifier.</param> + /// <param name="include">The additional data to include in the lookup.</param> + /// <param name="logger">The logger.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The artist, or <see langword="null"/> if MusicBrainz does not have it.</returns> + public static Task<IArtist?> LookupArtistOrNullAsync(this Query query, Guid artistId, Include include, ILogger logger, CancellationToken cancellationToken) + => NotFoundAsNullAsync( + () => query.LookupArtistAsync(artistId, include, null, null, cancellationToken), + "artist", + artistId, + logger); + + /// <summary> + /// Runs a lookup, mapping a "not found" response to <see langword="null"/>. Identifiers stored on a library item + /// can refer to entities that no longer exist in MusicBrainz, which is not an error worth failing a refresh over. + /// </summary> + /// <typeparam name="T">The type of entity being looked up.</typeparam> + /// <param name="lookup">The lookup to run.</param> + /// <param name="entityType">The type of entity being looked up, used for logging.</param> + /// <param name="id">The identifier being looked up, used for logging.</param> + /// <param name="logger">The logger.</param> + /// <returns>The entity, or <see langword="null"/> if MusicBrainz does not have it.</returns> + private static async Task<T?> NotFoundAsNullAsync<T>(Func<Task<T>> lookup, string entityType, Guid id, ILogger logger) + where T : class + { + try + { + return await lookup().ConfigureAwait(false); + } + catch (HttpError ex) when (ex.Status == HttpStatusCode.NotFound) + { + logger.LogDebug("MusicBrainz has no {EntityType} with id {Id}", entityType, id); + return null; + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs index ccff31ebaa..437a997c11 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs @@ -44,7 +44,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb var result = new MetadataResult<Episode> { Item = new Episode(), - QueriedById = true + QueriedById = true, + // OMDb is not localized, everything it returns is English + ResultLanguage = "en" }; // Allowing this will dramatically increase scan times diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index e84f1359b7..7b245ea5a7 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -218,7 +218,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb var result = new MetadataResult<T> { Item = new T(), - QueriedById = true + QueriedById = true, + // OMDb is not localized, everything it returns is English + ResultLanguage = "en" }; var imdbId = info.GetProviderId(MetadataProvider.Imdb); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index 78be5804e3..23f8d89c67 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -56,7 +54,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - var tmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + item.TryGetTmdbId(out var tmdbId); if (tmdbId <= 0) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index a7bba2d539..0a75b71264 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -42,7 +41,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken) { - var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + searchInfo.TryGetTmdbId(out var tmdbId); var language = searchInfo.MetadataLanguage; if (tmdbId > 0) @@ -97,7 +96,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets /// <inheritdoc /> public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo info, CancellationToken cancellationToken) { - var tmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + info.TryGetTmdbId(out var tmdbId); var language = info.MetadataLanguage; // We don't already have an Id, need to fetch it @@ -115,7 +114,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets } } - var result = new MetadataResult<BoxSet>(); + var result = new MetadataResult<BoxSet> + { + ResultLanguage = language + }; if (tmdbId > 0) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs index b188f5deb4..e686577311 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -61,7 +59,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var language = item.GetPreferredMetadataLanguage(); var countryCode = item.GetPreferredMetadataCountryCode(); - var movieTmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + item.TryGetTmdbId(out var movieTmdbId); if (movieTmdbId <= 0) { var movieImdbId = item.GetProviderId(MetadataProvider.Imdb); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 8811a1787a..ef952082da 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -54,11 +54,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken) { - if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var id)) + if (searchInfo.TryGetTmdbId(out var tmdbId)) { var movie = await _tmdbClientManager .GetMovieAsync( - int.Parse(id, CultureInfo.InvariantCulture), + tmdbId, searchInfo.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode), searchInfo.MetadataCountryCode, @@ -90,7 +90,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies } IReadOnlyList<SearchMovie>? movieResults = null; - if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out id)) + if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out var id)) { var result = await _tmdbClientManager.FindByExternalIdAsync( id, @@ -151,11 +151,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// <inheritdoc /> public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken) { - var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); + // A stored id that is not a TMDb id is treated as no id, so the search below can repair it + // rather than the lookup failing for as long as the bad id stays on the item. + info.TryGetTmdbId(out var tmdbId); var imdbId = info.GetProviderId(MetadataProvider.Imdb); var config = Plugin.Instance.Configuration; - if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId)) + if (tmdbId <= 0 && string.IsNullOrEmpty(imdbId)) { // ParseName is required here. // Caller provides the filename with extension stripped and NOT the parsed filename @@ -166,26 +168,26 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies if (searchResults?.Count > 0) { - tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); + tmdbId = searchResults[0].Id; } } - if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId)) + if (tmdbId <= 0 && !string.IsNullOrEmpty(imdbId)) { var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); if (movieResultFromImdbId?.MovieResults?.Count > 0) { - tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture); + tmdbId = movieResultFromImdbId.MovieResults[0].Id; } } - if (string.IsNullOrEmpty(tmdbId)) + if (tmdbId <= 0) { return new MetadataResult<Movie>(); } var movieResult = await _tmdbClientManager - .GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken) + .GetMovieAsync(tmdbId, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken) .ConfigureAwait(false); if (movieResult is null) @@ -208,7 +210,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies Item = movie }; - movie.SetProviderId(MetadataProvider.Tmdb, tmdbId); + movie.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString(CultureInfo.InvariantCulture)); movie.TrySetProviderId(MetadataProvider.Imdb, movieResult.ImdbId); if (movieResult.BelongsToCollection is not null) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index 33888ddf4f..d38614811c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -54,14 +53,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People { var person = (Person)item; - if (!person.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId)) + if (!person.TryGetTmdbId(out var personTmdbId)) { return Enumerable.Empty<RemoteImageInfo>(); } var language = item.GetPreferredMetadataLanguage(); var countryCode = item.GetPreferredMetadataCountryCode(); - var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), language, countryCode, cancellationToken).ConfigureAwait(false); + var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, language, countryCode, cancellationToken).ConfigureAwait(false); if (personResult?.Images?.Profiles is null) { return Enumerable.Empty<RemoteImageInfo>(); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 64ab98b262..695f347a9a 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Net.Http; @@ -37,9 +36,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) { - if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId)) + if (searchInfo.TryGetTmdbId(out var personTmdbId)) { - var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false); + var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false); if (personResult is not null) { @@ -89,7 +88,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People /// <inheritdoc /> public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo info, CancellationToken cancellationToken) { - var personTmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + // A person can carry another provider's id under the TMDb key, which is no more usable here + // than no id at all, so both take the search path and get the stored id repaired. + info.TryGetTmdbId(out var personTmdbId); // We don't already have an Id, need to fetch it if (personTmdbId <= 0) @@ -101,7 +102,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People } } - var result = new MetadataResult<Person>(); + var result = new MetadataResult<Person> + { + ResultLanguage = info.MetadataLanguage + }; if (personTmdbId > 0) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index 7ae54cdcd3..1f8c87397d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -56,9 +54,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var episode = (Controller.Entities.TV.Episode)item; var series = episode.Series; - var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + var seriesTmdbId = 0; - if (series is null || seriesTmdbId <= 0) + if (series?.TryGetTmdbId(out seriesTmdbId) != true) { return Enumerable.Empty<RemoteImageInfo>(); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 21b822c97c..8172ab14df 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -91,8 +91,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string? tmdbId); - var seriesTmdbId = Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture); - if (seriesTmdbId <= 0) + if (!TmdbUtils.TryParseTmdbId(tmdbId, out var seriesTmdbId)) { return metadataResult; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index 5b2f0d26e4..bc44d0266d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -57,9 +55,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var season = (Season)item; var series = season?.Series; - var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + var seriesTmdbId = 0; - if (seriesTmdbId <= 0 || season?.IndexNumber is null) + if (season?.IndexNumber is null || series?.TryGetTmdbId(out seriesTmdbId) != true) { return Enumerable.Empty<RemoteImageInfo>(); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 9c41d64253..9b8803f171 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -41,20 +40,23 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV /// <inheritdoc /> public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken) { - var result = new MetadataResult<Season>(); + var result = new MetadataResult<Season> + { + ResultLanguage = info.MetadataLanguage + }; var config = Plugin.Instance.Configuration; info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string? seriesTmdbId); var seasonNumber = info.IndexNumber; - if (string.IsNullOrWhiteSpace(seriesTmdbId) || !seasonNumber.HasValue) + if (!seasonNumber.HasValue || !TmdbUtils.TryParseTmdbId(seriesTmdbId, out var seriesId)) { return result; } var seasonResult = await _tmdbClientManager - .GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken) + .GetSeasonAsync(seriesId, seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken) .ConfigureAwait(false); if (seasonResult is null) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index f2e7d0c6e4..dc4f860604 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -57,9 +55,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - var tmdbId = item.GetProviderId(MetadataProvider.Tmdb); - - if (string.IsNullOrEmpty(tmdbId)) + if (!item.TryGetTmdbId(out var tmdbId)) { return Enumerable.Empty<RemoteImageInfo>(); } @@ -68,7 +64,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV // 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, null, cancellationToken) + .GetSeriesAsync(tmdbId, null, null, null, cancellationToken) .ConfigureAwait(false); if (series?.Images is null) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 9bb15ca479..9e201f2d7c 100755 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -54,10 +54,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) { - if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)) + if (searchInfo.TryGetTmdbId(out var tmdbId)) { var series = await _tmdbClientManager - .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken) + .GetSeriesAsync(tmdbId, searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken) .ConfigureAwait(false); if (series is not null) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 7e6b9beee9..c83174f97f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text.RegularExpressions; using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; @@ -63,6 +64,33 @@ namespace MediaBrowser.Providers.Plugins.Tmdb private static partial Regex NonWordRegex(); /// <summary> + /// Gets the TMDb id of an item, if it has one TMDb can be queried with. + /// </summary> + /// <param name="instance">The item.</param> + /// <param name="tmdbId">The TMDb id.</param> + /// <returns><c>true</c> if the item has a usable TMDb id; otherwise, <c>false</c>.</returns> + public static bool TryGetTmdbId(this IHasProviderIds instance, out int tmdbId) + { + instance.TryGetProviderId(MetadataProvider.Tmdb, out var value); + + return TryParseTmdbId(value, out tmdbId); + } + + /// <summary> + /// Parses a TMDb id. + /// </summary> + /// <param name="value">The stored id.</param> + /// <param name="tmdbId">The TMDb id.</param> + /// <returns><c>true</c> if the value is a usable TMDb id; otherwise, <c>false</c>.</returns> + public static bool TryParseTmdbId(string? value, out int tmdbId) + { + // Another provider can have filed one of its own ids under the TMDb key, e.g. an IMDb person + // id. Reporting that as "no id" lets the caller fall back to a search and repair the id, + // instead of throwing on every refresh of the item. + return int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out tmdbId) && tmdbId > 0; + } + + /// <summary> /// Cleans the name according to TMDb requirements. /// </summary> /// <param name="name">The name of the entity.</param> |
