From 4c524f033f66d7a3a3a8b9b8ec706ea10330ea31 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 24 Aug 2026 21:49:53 +0200 Subject: Fix OMDB People handling --- .../Plugins/Omdb/OmdbProvider.cs | 53 +++++++++++----------- .../Omdb/OmdbProviderTests.cs | 51 +++++++++++++++++++++ 2 files changed, 77 insertions(+), 27 deletions(-) create mode 100644 tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index f562d64ddd..7a8e2b3ce7 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -420,41 +420,40 @@ namespace MediaBrowser.Providers.Plugins.Omdb return; } - if (!string.IsNullOrWhiteSpace(result.Director)) - { - var person = new PersonInfo - { - Name = result.Director.Trim(), - Type = PersonKind.Director - }; - - itemResult.AddPerson(person); - } + AddPeople(itemResult, result.Director, PersonKind.Director); + AddPeople(itemResult, result.Writer, PersonKind.Writer); + AddPeople(itemResult, result.Actors, PersonKind.Actor); + } - if (!string.IsNullOrWhiteSpace(result.Writer)) + internal static void AddPeople(MetadataResult itemResult, string credits, PersonKind type) + where T : BaseItem + { + if (string.IsNullOrWhiteSpace(credits)) { - var person = new PersonInfo - { - Name = result.Writer.Trim(), - Type = PersonKind.Writer - }; - - itemResult.AddPerson(person); + return; } - if (!string.IsNullOrWhiteSpace(result.Actors)) + foreach (var credit in credits.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { - var actorList = result.Actors.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - foreach (var actor in actorList) + // OMDb annotates the credited role in parentheses, e.g. "Mari Okada (screenplay)". The same + // person can be credited more than once this way, so strip it and let AddPerson deduplicate. + var name = credit; + var annotation = name.IndexOf('(', StringComparison.Ordinal); + if (annotation >= 0) { - var person = new PersonInfo - { - Name = actor, - Type = PersonKind.Actor - }; + name = name[..annotation].TrimEnd(); + } - itemResult.AddPerson(person); + if (string.IsNullOrEmpty(name)) + { + continue; } + + itemResult.AddPerson(new PersonInfo + { + Name = name, + Type = type + }); } } diff --git a/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs b/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs new file mode 100644 index 0000000000..d18c8c21a2 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Providers.Plugins.Omdb; +using Xunit; + +namespace Jellyfin.Providers.Tests.Omdb +{ + public class OmdbProviderTests + { + [Fact] + public void AddPeople_CommaSeparatedList_SplitsIntoIndividualPeople() + { + var result = new MetadataResult(); + + OmdbProvider.AddPeople(result, "Philip G. Epstein, Julius J. Epstein, Howard Koch", PersonKind.Writer); + + Assert.Equal( + new[] { "Philip G. Epstein", "Julius J. Epstein", "Howard Koch" }, + result.People!.Select(p => p.Name)); + Assert.All(result.People!, p => Assert.Equal(PersonKind.Writer, p.Type)); + } + + [Fact] + public void AddPeople_RoleAnnotations_AreStrippedAndDeduplicated() + { + var result = new MetadataResult(); + + OmdbProvider.AddPeople(result, "Mari Okada (screenplay), Mari Okada (story), Jun'ichi Satô (screenplay), Jun'ichi Satô (story)", PersonKind.Writer); + + Assert.Equal( + new[] { "Mari Okada", "Jun'ichi Satô" }, + result.People!.Select(p => p.Name)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("(uncredited)")] + public void AddPeople_NoUsableName_AddsNothing(string? credits) + { + var result = new MetadataResult(); + + OmdbProvider.AddPeople(result, credits!, PersonKind.Actor); + + Assert.Null(result.People); + } + } +} -- cgit v1.2.3 From 4147a83b9356a9b6d101dde8217aec21fefb51f4 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 24 Aug 2026 22:20:33 +0200 Subject: Handle generational suffixes --- .../Plugins/Omdb/OmdbProvider.cs | 32 +++++++++++++++++++++- .../Omdb/OmdbProviderTests.cs | 16 +++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 7a8e2b3ce7..7262cddd33 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -27,6 +27,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb /// Provider for OMDB service. public class OmdbProvider { + /// Generational suffixes that OMDb separates from the name with a comma. + private static readonly string[] NameSuffixes = ["Jr", "Jnr", "Sr", "Snr", "II", "III", "IV", "V"]; + private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IHttpClientFactory _httpClientFactory; @@ -425,6 +428,11 @@ namespace MediaBrowser.Providers.Plugins.Omdb AddPeople(itemResult, result.Actors, PersonKind.Actor); } + /// Adds the people from a comma separated OMDb credit list. + /// The item type. + /// The metadata result to add the people to. + /// The comma separated OMDb credit list. + /// The kind of person each credit describes. internal static void AddPeople(MetadataResult itemResult, string credits, PersonKind type) where T : BaseItem { @@ -433,6 +441,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb return; } + var names = new List(); + foreach (var credit in credits.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { // OMDb annotates the credited role in parentheses, e.g. "Mari Okada (screenplay)". The same @@ -444,11 +454,24 @@ namespace MediaBrowser.Providers.Plugins.Omdb name = name[..annotation].TrimEnd(); } - if (string.IsNullOrEmpty(name)) + if (name.Length == 0) { continue; } + // A generational suffix is separated from the name it belongs to by the same comma the list + // uses, e.g. "Jack Salvatore, Jr.", so it has to be joined back instead of becoming a credit. + if (names.Count > 0 && IsNameSuffix(name)) + { + names[^1] = names[^1] + ", " + name; + continue; + } + + names.Add(name); + } + + foreach (var name in names) + { itemResult.AddPerson(new PersonInfo { Name = name, @@ -457,6 +480,13 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } + private static bool IsNameSuffix(string value) + { + var suffix = value.EndsWith('.') ? value[..^1] : value; + + return NameSuffixes.Contains(suffix, StringComparer.OrdinalIgnoreCase); + } + private static bool IsConfiguredForEnglish(BaseItem item, string language) { if (string.IsNullOrEmpty(language)) diff --git a/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs b/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs index d18c8c21a2..5e053943af 100644 --- a/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs @@ -34,6 +34,22 @@ namespace Jellyfin.Providers.Tests.Omdb result.People!.Select(p => p.Name)); } + [Theory] + [InlineData("Jack Salvatore, Jr.", "Jack Salvatore, Jr.")] + [InlineData("Efrem Zimbalist, Jr., Tom Hanks", "Efrem Zimbalist, Jr.|Tom Hanks")] + [InlineData("Tom Hanks, Sammy Davis, Jr", "Tom Hanks|Sammy Davis, Jr")] + [InlineData("Harold Ramis, Ken Griffey, III (voice)", "Harold Ramis|Ken Griffey, III")] + [InlineData("Robert Downey Jr., Gwyneth Paltrow", "Robert Downey Jr.|Gwyneth Paltrow")] + [InlineData("Jr., Tom Hanks", "Jr.|Tom Hanks")] + public void AddPeople_GenerationalSuffix_StaysWithItsName(string credits, string expected) + { + var result = new MetadataResult(); + + OmdbProvider.AddPeople(result, credits, PersonKind.Actor); + + Assert.Equal(expected.Split('|'), result.People!.Select(p => p.Name)); + } + [Theory] [InlineData(null)] [InlineData("")] -- cgit v1.2.3 From 1d24c1df170d743d5b0d2c1c8c503e44486c6875 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 25 Aug 2026 20:12:52 +0200 Subject: Keep an OMDb credit whole when its annotation holds a comma --- .../Plugins/Omdb/OmdbProvider.cs | 32 ++++++++++++++++++++-- .../Omdb/OmdbProviderTests.cs | 14 ++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 7262cddd33..d51d913caa 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -443,7 +443,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var names = new List(); - foreach (var credit in credits.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + foreach (var credit in SplitCredits(credits)) { // OMDb annotates the credited role in parentheses, e.g. "Mari Okada (screenplay)". The same // person can be credited more than once this way, so strip it and let AddPerson deduplicate. @@ -451,9 +451,10 @@ namespace MediaBrowser.Providers.Plugins.Omdb var annotation = name.IndexOf('(', StringComparison.Ordinal); if (annotation >= 0) { - name = name[..annotation].TrimEnd(); + name = name[..annotation]; } + name = name.Trim(); if (name.Length == 0) { continue; @@ -480,6 +481,33 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } + // Only the commas between credits, never one inside an annotation: "Jerry Siegel (created by: + // Superman, Superboy)" is one credit, and splitting it blindly invents a person called "Superboy)". + private static IEnumerable SplitCredits(string credits) + { + var depth = 0; + var start = 0; + + for (var i = 0; i < credits.Length; i++) + { + switch (credits[i]) + { + case '(': + depth++; + break; + case ')': + depth = Math.Max(0, depth - 1); + break; + case ',' when depth == 0: + yield return credits[start..i]; + start = i + 1; + break; + } + } + + yield return credits[start..]; + } + private static bool IsNameSuffix(string value) { var suffix = value.EndsWith('.') ? value[..^1] : value; diff --git a/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs b/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs index 5e053943af..bd50a903f1 100644 --- a/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Omdb/OmdbProviderTests.cs @@ -34,6 +34,20 @@ namespace Jellyfin.Providers.Tests.Omdb result.People!.Select(p => p.Name)); } + [Theory] + [InlineData( + "Jerry Siegel (created by: Superman, Superboy), Bob Kane (created by: Batman)", + "Jerry Siegel|Bob Kane")] + [InlineData("Alan Moore (created by: John Constantine)", "Alan Moore")] + public void AddPeople_CommaInsideAnAnnotation_StaysOneCredit(string credits, string expected) + { + var result = new MetadataResult(); + + OmdbProvider.AddPeople(result, credits, PersonKind.Writer); + + Assert.Equal(expected.Split('|'), result.People!.Select(p => p.Name)); + } + [Theory] [InlineData("Jack Salvatore, Jr.", "Jack Salvatore, Jr.")] [InlineData("Efrem Zimbalist, Jr., Tom Hanks", "Efrem Zimbalist, Jr.|Tom Hanks")] -- cgit v1.2.3