diff options
| author | Cody Robibero <cody@robibe.ro> | 2026-09-07 17:42:38 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-07 17:42:38 -0400 |
| commit | 2ffa74e7eacd8873847537b0257d93225dcf354b (patch) | |
| tree | c9db555b4e2aae011c3ad1c7bfc9923b4494ccf9 /tests | |
| parent | 837c34aee275890f4fd14e8205312afc4f571860 (diff) | |
| parent | 04a7568902d385f85c1855ed3197c2feea601dc9 (diff) | |
Merge pull request #17819 from Shadowghost/fix-tmdb-search
Fix TMDb search result ranking
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs index 4c4dd5e92f..8926a7b13c 100644 --- a/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs +++ b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs @@ -1,6 +1,9 @@ +using System; +using System.Collections.Generic; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Model.Entities; using MediaBrowser.Providers.Plugins.Tmdb; +using TMDbLib.Objects.Search; using Xunit; namespace Jellyfin.Providers.Tests.Tmdb @@ -71,5 +74,169 @@ namespace Jellyfin.Providers.Tests.Tmdb Assert.False(new Movie().TryGetTmdbId(out var tmdbId)); Assert.Equal(0, tmdbId); } + + [Theory] + [InlineData("The Amityville Horror", "The Amityville Horror")] + [InlineData("WALL-E", "WALL E")] + // The interpunct is kept, it matches the TMDb title better than a space does. + [InlineData("WALL·E", "WALL·E")] + [InlineData("50-50", "50 50")] + [InlineData("A Christmas No. 1", "A Christmas No 1")] + // Vulgar fractions are numbers, dropping them turned "8½" into a search for "8". + [InlineData("8½", "8½")] + [InlineData("9½ Weeks", "9½ Weeks")] + [InlineData(" Léon: The Professional ", "Léon The Professional")] + public static void CleanName_Valid_Success(string name, string expected) + { + Assert.Equal(expected, TmdbUtils.CleanName(name)); + } + + [Theory] + [InlineData("WALL-E", "wall e")] + [InlineData("WALL·E", "wall e")] + [InlineData("WALL E", "wall e")] + [InlineData("8½", "8½")] + [InlineData("Ocean's Eleven", "ocean s eleven")] + [InlineData(null, "")] + [InlineData(" ", "")] + public static void NormalizeTitle_Valid_Success(string? title, string expected) + { + Assert.Equal(expected, TmdbUtils.NormalizeTitle(title)); + } + + [Theory] + [MemberData(nameof(FindBestMatch_Movies_TestData))] + public static void FindBestMatch_Movies_PicksExpected(string description, string name, int year, IReadOnlyList<SearchMovie> results, int expectedId) + { + var match = TmdbUtils.FindBestMatch(results, name, year); + + Assert.NotNull(match); + Assert.True(expectedId == match.Id, $"{description}: expected {expectedId} but matched {match.Id}"); + } + + [Fact] + public static void FindBestMatch_Series_PicksMatchingFirstAirYear() + { + IReadOnlyList<SearchTv> results = + [ + Series(10042, "Doc", "Doc", 2001), + Series(101048, "Doc", "Doc", 2020), + Series(255055, "Doc", "Doc", 2025), + Series(2430, "Doc Martin", "Doc Martin", 2004) + ]; + + var match = TmdbUtils.FindBestMatch(results, "Doc", 2025); + + Assert.NotNull(match); + Assert.Equal(255055, match.Id); + } + + [Fact] + public static void FindBestMatch_NoResults_ReturnsNull() + { + Assert.Null(TmdbUtils.FindBestMatch(Array.Empty<SearchMovie>(), "Mulan", 2020)); + Assert.Null(TmdbUtils.FindBestMatch(Array.Empty<SearchTv>(), "Doc", 2025)); + Assert.Null(TmdbUtils.FindBestMatch((IReadOnlyList<SearchMovie>?)null, "Mulan", 2020)); + Assert.Null(TmdbUtils.FindBestMatch((IReadOnlyList<SearchTv>?)null, "Doc", 2025)); + } + + public static TheoryData<string, string, int, IReadOnlyList<SearchMovie>, int> FindBestMatch_Movies_TestData() + => new() + { + // TMDb's year parameter does not filter, so the remake and the original both come back and + // the wrong one is first. Results are in the order the live API returned them. + { + "Mulan (2020)", "Mulan", 2020, + [Movie(10674, "Mulan", "Mulan", 1998), Movie(337401, "Mulan", "Mulan", 2020), Movie(752662, "Hua Mulan", "花木兰", 2020)], + 337401 + }, + { + "Mulan (1998)", "Mulan", 1998, + [Movie(10674, "Mulan", "Mulan", 1998), Movie(337401, "Mulan", "Mulan", 2020), Movie(752662, "Hua Mulan", "花木兰", 2020)], + 10674 + }, + { + "Aladdin (2019)", "Aladdin", 2019, + [Movie(812, "Aladdin", "Aladdin", 1992), Movie(420817, "Aladdin", "Aladdin", 2019), Movie(602411, "Adventures of Aladdin", "Adventures of Aladdin", 2019)], + 420817 + }, + { + "The Lion King (2019)", "The Lion King", 2019, + [Movie(8587, "The Lion King", "The Lion King", 1994), Movie(420818, "The Lion King", "The Lion King", 2019)], + 420818 + }, + { + "The Amityville Horror (1979)", "The Amityville Horror", 1979, + [Movie(10065, "The Amityville Horror", "The Amityville Horror", 2005), Movie(11449, "The Amityville Horror", "The Amityville Horror", 1979)], + 11449 + }, + // A featurette outranks the film it belongs to. The interpunct must not stop "WALL-E" from + // matching "WALL·E", or the prefix match on the featurette wins. + { + "WALL-E (2008)", "WALL-E", 2008, + [Movie(877268, "WALL·E's Treasures & Trinkets", "WALL·E's Treasures & Trinkets", 2008), Movie(10681, "WALL·E", "WALL·E", 2008), Movie(10673, "Wall Street", "Wall Street", 1987)], + 10681 + }, + // The name only survives as "8" if the fraction is stripped, and then every 1963 result ties. + { + "8½ (1963)", "8½", 1963, + [Movie(422801, "Interpol Code 8", "国際秘密警察 指令第8号", 1963), Movie(520251, "Um 8 Uhr kommt Sadowski", "Um 8 Uhr kommt Sadowski", 1963), Movie(422, "8½", "8½", 1963)], + 422 + }, + // Matched on the original title, the localized one is unrecognizable. + { + "Ściany mają uszy (1966)", "Ściany mają uszy", 1966, + [Movie(1, "Something Else", "Something Else", 1966), Movie(2, "Walls Have Ears", "Ściany mają uszy", 1966)], + 2 + }, + // Regional release dates straddle the new year, so a year that is off by one still matches. + { + "Off by one year", "Some Movie", 2011, + [Movie(1, "Some Movie", "Some Movie", 2015), Movie(2, "Some Movie", "Some Movie", 2010)], + 2 + }, + // Nothing matches the name, so TMDb's own ordering is kept. + { + "A Christmas No. 1 (2021)", "A Christmas No. 1", 2021, + [Movie(878111, "A Christmas Number One", "A Christmas Number One", 2021), Movie(2, "Ten Hours for Christmas", "10 Horas para o Natal", 2021)], + 878111 + }, + // A title that matches always beats one that only shares the year. + { + "Title outranks year", "Some Movie", 2020, + [Movie(1, "A Different Movie", "A Different Movie", 2020), Movie(2, "Some Movie", "Some Movie", 1994)], + 2 + }, + // Without a year the title alone decides, and equally good titles keep TMDb's order. + { + "No year known", "Mulan", 0, + [Movie(10674, "Mulan", "Mulan", 1998), Movie(337401, "Mulan", "Mulan", 2020)], + 10674 + }, + // An unparsable name must not throw or reorder anything. + { + "Empty name", " ", 2020, + [Movie(1, "Some Movie", "Some Movie", 1994), Movie(2, "Some Movie", "Some Movie", 2020)], + 1 + } + }; + + private static SearchMovie Movie(int id, string title, string originalTitle, int year) + => new() + { + Id = id, + Title = title, + OriginalTitle = originalTitle, + ReleaseDate = new DateTime(year, 6, 1, 0, 0, 0, DateTimeKind.Utc) + }; + + private static SearchTv Series(int id, string name, string originalName, int year) + => new() + { + Id = id, + Name = name, + OriginalName = originalName, + FirstAirDate = new DateTime(year, 6, 1, 0, 0, 0, DateTimeKind.Utc) + }; } } |
