aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers/Manager
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Providers/Manager')
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataLanguageUtils.cs44
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs204
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs18
4 files changed, 252 insertions, 16 deletions
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index d9a8c044b9..aa4ca5afd8 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -90,7 +90,7 @@ namespace MediaBrowser.Providers.Manager
{
ArgumentException.ThrowIfNullOrEmpty(mimeType);
- var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && item is not Audio;
+ var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && (item is AudioBook || item is not Audio);
if (type != ImageType.Primary && item is Episode)
{
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 118ccf8679..26dc8f9930 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -209,7 +209,10 @@ namespace MediaBrowser.Providers.Manager
}
}
- if (hasRefreshedMetadata && hasRefreshedImages)
+ var attemptedFetch = refreshOptions.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly
+ || refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly;
+
+ if (hasRefreshedMetadata && hasRefreshedImages && attemptedFetch)
{
item.DateLastRefreshed = DateTime.UtcNow;
updateType |= item.OnMetadataChanged();
@@ -260,21 +263,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);
@@ -680,11 +702,18 @@ namespace MediaBrowser.Providers.Manager
return providers;
}
- protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options)
+ protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, MetadataRefreshOptions options)
{
// Get providers to refresh
var providers = allImageProviders.Where(i => i is not ILocalImageProvider);
+ // When identifying, run the provider the user picked first so the correct image is used.
+ if (!string.IsNullOrEmpty(options.SearchResult?.SearchProviderName))
+ {
+ providers = providers
+ .OrderBy(i => string.Equals(i.Name, options.SearchResult.SearchProviderName, StringComparison.OrdinalIgnoreCase) ? 0 : 1);
+ }
+
var dateLastImageRefresh = item.DateLastRefreshed;
// Run all if either of these flags are true
@@ -828,6 +857,7 @@ namespace MediaBrowser.Providers.Manager
}
}
+ var hasRemoteMetadata = false;
var isLocalLocked = temp.Item.IsLocked;
if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly))
{
@@ -842,6 +872,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;
@@ -851,7 +882,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);
@@ -906,6 +942,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)
{
@@ -925,6 +965,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);
@@ -950,6 +1012,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
@@ -957,8 +1071,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;
+ }
}
}
@@ -1031,6 +1155,11 @@ namespace MediaBrowser.Providers.Manager
target.OriginalTitle = source.OriginalTitle;
}
+ if (replaceData || string.IsNullOrEmpty(target.HomePageUrl))
+ {
+ target.HomePageUrl = source.HomePageUrl;
+ }
+
if (replaceData || string.IsNullOrEmpty(target.OriginalLanguage))
{
target.OriginalLanguage = source.OriginalLanguage;
@@ -1092,6 +1221,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;
@@ -1107,7 +1239,7 @@ namespace MediaBrowser.Providers.Manager
target.PremiereDate = source.PremiereDate;
}
- if (replaceData || !target.ProductionYear.HasValue)
+ if (replaceData || target.ProductionYear is null)
{
target.ProductionYear = source.ProductionYear;
}
@@ -1116,7 +1248,7 @@ namespace MediaBrowser.Providers.Manager
{
if (replaceData || !target.RunTimeTicks.HasValue)
{
- if (target is not Audio && target is not Video)
+ if (target is not Audio && target is not Video && target is not Book)
{
target.RunTimeTicks = source.RunTimeTicks;
}
@@ -1163,17 +1295,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;
@@ -1239,6 +1387,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 73df6d03d2..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/>
@@ -436,6 +438,14 @@ namespace MediaBrowser.Providers.Manager
return false;
}
+ // Extras have no identity of their own in an online database, so remote artwork for them
+ // is always some other item's. Local and dynamic providers still apply, so an extra can
+ // keep an embedded thumbnail or an extracted frame.
+ if (item.ExtraType.HasValue && provider is IRemoteImageProvider)
+ {
+ return false;
+ }
+
return _baseItemManager.IsImageFetcherEnabled(item, libraryTypeOptions, provider.Name);
}
@@ -584,6 +594,14 @@ namespace MediaBrowser.Providers.Manager
return true;
}
+ // An extra is a local file belonging to another item and has no identity of its own in an
+ // online database. Looking it up matches whatever the surrounding folder happens to be
+ // called and overwrites the extra's name with a different item's title.
+ if (item.ExtraType.HasValue)
+ {
+ return false;
+ }
+
// Artists without a folder structure that are derived from metadata have no real path in the library,
// so GetLibraryOptions returns null. Allow all providers through rather than blocking them.
if (item is MusicArtist && libraryTypeOptions is null)