Fix English metadata blocking localized providers ranked below it
This commit is contained in:
@@ -16,11 +16,6 @@ namespace MediaBrowser.Controller.Providers
|
||||
private List<(string Url, ImageType Type)> _remoteImages;
|
||||
private List<PersonInfo> _people;
|
||||
|
||||
public MetadataResult()
|
||||
{
|
||||
ResultLanguage = "en";
|
||||
}
|
||||
|
||||
public List<LocalImageInfo> Images
|
||||
{
|
||||
get => _images ??= [];
|
||||
@@ -43,6 +38,9 @@ namespace MediaBrowser.Controller.Providers
|
||||
|
||||
public T Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the language the fetched metadata is in.
|
||||
/// </summary>
|
||||
public string ResultLanguage { get; set; }
|
||||
|
||||
public string Provider { get; set; }
|
||||
|
||||
@@ -204,7 +204,7 @@ public class ComicBookInfoProvider : IComicProvider
|
||||
{
|
||||
try
|
||||
{
|
||||
return CultureInfo.GetCultureInfo(language).DisplayName;
|
||||
return CultureInfo.GetCultureInfo(language).TwoLetterISOLanguageName;
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -913,6 +913,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 +936,26 @@ 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);
|
||||
}
|
||||
|
||||
MergeData(result, temp, [], replaceData, false);
|
||||
MergeNewData(temp.Item, id);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -115,7 +115,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
}
|
||||
}
|
||||
|
||||
var result = new MetadataResult<BoxSet>();
|
||||
var result = new MetadataResult<BoxSet>
|
||||
{
|
||||
ResultLanguage = language
|
||||
};
|
||||
|
||||
if (tmdbId > 0)
|
||||
{
|
||||
|
||||
@@ -101,7 +101,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
}
|
||||
}
|
||||
|
||||
var result = new MetadataResult<Person>();
|
||||
var result = new MetadataResult<Person>
|
||||
{
|
||||
ResultLanguage = info.MetadataLanguage
|
||||
};
|
||||
|
||||
if (personTmdbId > 0)
|
||||
{
|
||||
|
||||
@@ -41,7 +41,10 @@ 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);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using MediaBrowser.Providers.Manager;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Providers.Tests.Manager
|
||||
{
|
||||
public class MetadataLanguageUtilsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("es", "es")]
|
||||
[InlineData("es-ES", "es")]
|
||||
[InlineData("pt-BR", "pt")]
|
||||
[InlineData("ES", "es")]
|
||||
[InlineData(null, null)]
|
||||
[InlineData("", null)]
|
||||
public void GetLanguageSubtag_ReturnsLowercasedSubtag(string? language, string? expected)
|
||||
{
|
||||
Assert.Equal(expected, MetadataLanguageUtils.GetLanguageSubtag(language));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("es", "es", true)]
|
||||
[InlineData("es", "es-ES", true)]
|
||||
[InlineData("es-MX", "es-ES", true)]
|
||||
[InlineData("ES", "es", true)]
|
||||
[InlineData("en", "en", true)]
|
||||
[InlineData("en", "es-ES", false)]
|
||||
[InlineData("en", "es", false)]
|
||||
// An unknown language on either side cannot be judged and is assumed to match
|
||||
[InlineData(null, "es", true)]
|
||||
[InlineData("", "es", true)]
|
||||
[InlineData("en", null, true)]
|
||||
[InlineData("en", "", true)]
|
||||
public void MatchesPreferredLanguage_ComparesLanguageSubtag(string? resultLanguage, string? preferredLanguage, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, MetadataLanguageUtils.MatchesPreferredLanguage(resultLanguage, preferredLanguage));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user