From ebb6949ea75bd2f9953c9e1c7708442fa93197fb Mon Sep 17 00:00:00 2001 From: redinsch Date: Sun, 8 Mar 2026 11:29:54 +0100 Subject: [PATCH 1/2] Fix remote image language priority to prefer English over no-language Previously, images with no language were ranked higher (score 3) than English images (score 2), causing poorly rated languageless images to be selected over well-rated English alternatives for posters and logos. Swap the priority so English is preferred over no-language images. Backdrop images are unaffected as they have their own dedicated sorting. Add unit tests for OrderByLanguageDescending. Fixes #13310 --- .../Extensions/EnumerableExtensions.cs | 8 +- .../Extensions/EnumerableExtensionsTests.cs | 117 ++++++++++++++++++ 2 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs index 94f4252295..7c9ee18ca4 100644 --- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs +++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Extensions public static class EnumerableExtensions { /// - /// Orders by requested language in descending order, prioritizing "en" over other non-matches. + /// Orders by requested language in descending order, then "en", then no language, over other non-matches. /// /// The remote image infos. /// The requested language for the images. @@ -28,9 +28,9 @@ namespace MediaBrowser.Model.Extensions { // Image priority ordering: // - Images that match the requested language - // - Images with no language // - TODO: Images that match the original language // - Images in English + // - Images with no language // - Images that don't match the requested language if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) @@ -38,12 +38,12 @@ namespace MediaBrowser.Model.Extensions return 4; } - if (string.IsNullOrEmpty(i.Language)) + if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase)) { return 3; } - if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(i.Language)) { return 2; } diff --git a/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs new file mode 100644 index 0000000000..3b65a2636b --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs @@ -0,0 +1,117 @@ +using System.Linq; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Providers; +using Xunit; + +namespace Jellyfin.Model.Tests.Extensions +{ + public class EnumerableExtensionsTests + { + [Fact] + public void OrderByLanguageDescending_PreferredLanguageFirst() + { + var images = new[] + { + new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 100 }, + new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 200 }, + new RemoteImageInfo { Language = null, CommunityRating = 7.0, VoteCount = 50 }, + new RemoteImageInfo { Language = "fr", CommunityRating = 8.0, VoteCount = 150 }, + }; + + var result = images.OrderByLanguageDescending("de").ToList(); + + Assert.Equal("de", result[0].Language); + Assert.Equal("en", result[1].Language); + Assert.Null(result[2].Language); + Assert.Equal("fr", result[3].Language); + } + + [Fact] + public void OrderByLanguageDescending_EnglishBeforeNoLanguage() + { + var images = new[] + { + new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 }, + }; + + var result = images.OrderByLanguageDescending("de").ToList(); + + // English should come before no-language, even with lower rating + Assert.Equal("en", result[0].Language); + Assert.Null(result[1].Language); + } + + [Fact] + public void OrderByLanguageDescending_SameLanguageSortedByRatingThenVoteCount() + { + var images = new[] + { + new RemoteImageInfo { Language = "de", CommunityRating = 5.0, VoteCount = 100 }, + new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 50 }, + new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 200 }, + }; + + var result = images.OrderByLanguageDescending("de").ToList(); + + Assert.Equal(200, result[0].VoteCount); + Assert.Equal(50, result[1].VoteCount); + Assert.Equal(100, result[2].VoteCount); + } + + [Fact] + public void OrderByLanguageDescending_NullRequestedLanguage_DefaultsToEnglish() + { + var images = new[] + { + new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 10 }, + }; + + var result = images.OrderByLanguageDescending(null!).ToList(); + + // With null requested language, English becomes the preferred language (score 4) + Assert.Equal("en", result[0].Language); + Assert.Equal("fr", result[1].Language); + } + + [Fact] + public void OrderByLanguageDescending_EnglishRequested_NoDoubleBoost() + { + // When requested language IS English, "en" gets score 4 (requested match), + // no-language gets score 2, others get score 0 + var images = new[] + { + new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 }, + new RemoteImageInfo { Language = "fr", CommunityRating = 8.0, VoteCount = 300 }, + }; + + var result = images.OrderByLanguageDescending("en").ToList(); + + Assert.Equal("en", result[0].Language); + Assert.Null(result[1].Language); + Assert.Equal("fr", result[2].Language); + } + + [Fact] + public void OrderByLanguageDescending_FullPriorityOrder() + { + var images = new[] + { + new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = null, CommunityRating = 8.0, VoteCount = 400 }, + new RemoteImageInfo { Language = "en", CommunityRating = 7.0, VoteCount = 300 }, + new RemoteImageInfo { Language = "de", CommunityRating = 6.0, VoteCount = 200 }, + }; + + var result = images.OrderByLanguageDescending("de").ToList(); + + // Expected order: de (requested) > en > no-language > fr (other) + Assert.Equal("de", result[0].Language); + Assert.Equal("en", result[1].Language); + Assert.Null(result[2].Language); + Assert.Equal("fr", result[3].Language); + } + } +} From 3b293751790c32ee1d773a6332a63ba2f3bcab74 Mon Sep 17 00:00:00 2001 From: redinsch Date: Sun, 8 Mar 2026 12:02:08 +0100 Subject: [PATCH 2/2] Use file-scoped namespace in EnumerableExtensionsTests --- .../Extensions/EnumerableExtensionsTests.cs | 169 +++++++++--------- 1 file changed, 84 insertions(+), 85 deletions(-) diff --git a/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs index 3b65a2636b..135a139cdf 100644 --- a/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs @@ -3,115 +3,114 @@ using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; using Xunit; -namespace Jellyfin.Model.Tests.Extensions +namespace Jellyfin.Model.Tests.Extensions; + +public class EnumerableExtensionsTests { - public class EnumerableExtensionsTests + [Fact] + public void OrderByLanguageDescending_PreferredLanguageFirst() { - [Fact] - public void OrderByLanguageDescending_PreferredLanguageFirst() + var images = new[] { - var images = new[] - { - new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 100 }, - new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 200 }, - new RemoteImageInfo { Language = null, CommunityRating = 7.0, VoteCount = 50 }, - new RemoteImageInfo { Language = "fr", CommunityRating = 8.0, VoteCount = 150 }, - }; + new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 100 }, + new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 200 }, + new RemoteImageInfo { Language = null, CommunityRating = 7.0, VoteCount = 50 }, + new RemoteImageInfo { Language = "fr", CommunityRating = 8.0, VoteCount = 150 }, + }; - var result = images.OrderByLanguageDescending("de").ToList(); + var result = images.OrderByLanguageDescending("de").ToList(); - Assert.Equal("de", result[0].Language); - Assert.Equal("en", result[1].Language); - Assert.Null(result[2].Language); - Assert.Equal("fr", result[3].Language); - } + Assert.Equal("de", result[0].Language); + Assert.Equal("en", result[1].Language); + Assert.Null(result[2].Language); + Assert.Equal("fr", result[3].Language); + } - [Fact] - public void OrderByLanguageDescending_EnglishBeforeNoLanguage() + [Fact] + public void OrderByLanguageDescending_EnglishBeforeNoLanguage() + { + var images = new[] { - var images = new[] - { - new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 }, - new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 }, - }; + new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 }, + }; - var result = images.OrderByLanguageDescending("de").ToList(); + var result = images.OrderByLanguageDescending("de").ToList(); - // English should come before no-language, even with lower rating - Assert.Equal("en", result[0].Language); - Assert.Null(result[1].Language); - } + // English should come before no-language, even with lower rating + Assert.Equal("en", result[0].Language); + Assert.Null(result[1].Language); + } - [Fact] - public void OrderByLanguageDescending_SameLanguageSortedByRatingThenVoteCount() + [Fact] + public void OrderByLanguageDescending_SameLanguageSortedByRatingThenVoteCount() + { + var images = new[] { - var images = new[] - { - new RemoteImageInfo { Language = "de", CommunityRating = 5.0, VoteCount = 100 }, - new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 50 }, - new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 200 }, - }; + new RemoteImageInfo { Language = "de", CommunityRating = 5.0, VoteCount = 100 }, + new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 50 }, + new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 200 }, + }; - var result = images.OrderByLanguageDescending("de").ToList(); + var result = images.OrderByLanguageDescending("de").ToList(); - Assert.Equal(200, result[0].VoteCount); - Assert.Equal(50, result[1].VoteCount); - Assert.Equal(100, result[2].VoteCount); - } + Assert.Equal(200, result[0].VoteCount); + Assert.Equal(50, result[1].VoteCount); + Assert.Equal(100, result[2].VoteCount); + } - [Fact] - public void OrderByLanguageDescending_NullRequestedLanguage_DefaultsToEnglish() + [Fact] + public void OrderByLanguageDescending_NullRequestedLanguage_DefaultsToEnglish() + { + var images = new[] { - var images = new[] - { - new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 }, - new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 10 }, - }; + new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 10 }, + }; - var result = images.OrderByLanguageDescending(null!).ToList(); + var result = images.OrderByLanguageDescending(null!).ToList(); - // With null requested language, English becomes the preferred language (score 4) - Assert.Equal("en", result[0].Language); - Assert.Equal("fr", result[1].Language); - } + // With null requested language, English becomes the preferred language (score 4) + Assert.Equal("en", result[0].Language); + Assert.Equal("fr", result[1].Language); + } - [Fact] - public void OrderByLanguageDescending_EnglishRequested_NoDoubleBoost() + [Fact] + public void OrderByLanguageDescending_EnglishRequested_NoDoubleBoost() + { + // When requested language IS English, "en" gets score 4 (requested match), + // no-language gets score 2, others get score 0 + var images = new[] { - // When requested language IS English, "en" gets score 4 (requested match), - // no-language gets score 2, others get score 0 - var images = new[] - { - new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 }, - new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 }, - new RemoteImageInfo { Language = "fr", CommunityRating = 8.0, VoteCount = 300 }, - }; + new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 }, + new RemoteImageInfo { Language = "fr", CommunityRating = 8.0, VoteCount = 300 }, + }; - var result = images.OrderByLanguageDescending("en").ToList(); + var result = images.OrderByLanguageDescending("en").ToList(); - Assert.Equal("en", result[0].Language); - Assert.Null(result[1].Language); - Assert.Equal("fr", result[2].Language); - } + Assert.Equal("en", result[0].Language); + Assert.Null(result[1].Language); + Assert.Equal("fr", result[2].Language); + } - [Fact] - public void OrderByLanguageDescending_FullPriorityOrder() + [Fact] + public void OrderByLanguageDescending_FullPriorityOrder() + { + var images = new[] { - var images = new[] - { - new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 }, - new RemoteImageInfo { Language = null, CommunityRating = 8.0, VoteCount = 400 }, - new RemoteImageInfo { Language = "en", CommunityRating = 7.0, VoteCount = 300 }, - new RemoteImageInfo { Language = "de", CommunityRating = 6.0, VoteCount = 200 }, - }; + new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = null, CommunityRating = 8.0, VoteCount = 400 }, + new RemoteImageInfo { Language = "en", CommunityRating = 7.0, VoteCount = 300 }, + new RemoteImageInfo { Language = "de", CommunityRating = 6.0, VoteCount = 200 }, + }; - var result = images.OrderByLanguageDescending("de").ToList(); + var result = images.OrderByLanguageDescending("de").ToList(); - // Expected order: de (requested) > en > no-language > fr (other) - Assert.Equal("de", result[0].Language); - Assert.Equal("en", result[1].Language); - Assert.Null(result[2].Language); - Assert.Equal("fr", result[3].Language); - } + // Expected order: de (requested) > en > no-language > fr (other) + Assert.Equal("de", result[0].Language); + Assert.Equal("en", result[1].Language); + Assert.Null(result[2].Language); + Assert.Equal("fr", result[3].Language); } }