aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers/Manager
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Providers/Manager')
-rw-r--r--MediaBrowser.Providers/Manager/MetadataLanguageUtils.cs44
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs181
2 files changed, 214 insertions, 11 deletions
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);