diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-08-25 21:00:05 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-08-25 21:06:51 +0200 |
| commit | 79c37bfcd5c823711bb7675d5ec46f158c1154df (patch) | |
| tree | 06c8477cee51a80c047e45c4c6ccd651c6d986ab /MediaBrowser.Providers | |
| parent | 422b2bb3d9193bfe0813d3d654919151eb355d82 (diff) | |
Build a TMDb series cast from the aggregated credits
Diffstat (limited to 'MediaBrowser.Providers')
3 files changed, 107 insertions, 33 deletions
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 9e201f2d7c..6163e20194 100755 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -363,39 +363,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { var config = Plugin.Instance.Configuration; - if (seriesResult.Credits?.Cast is not null) + // The aggregated credits are what hold an actor's several characters apart; the flat ones + // put them in a single string. Only the aggregated list carries the whole run, so prefer it + // and fall back for the rare show TMDb has no aggregation for. + var cast = seriesResult.AggregateCredits?.Cast is { Count: > 0 } aggregated + ? TmdbUtils.MapAggregateCast(aggregated, config, _tmdbClientManager.GetProfileUrl) + : TmdbUtils.MapCast(seriesResult.Credits?.Cast, config, _tmdbClientManager.GetProfileUrl); + + foreach (var actor in cast) { - IEnumerable<Cast> castQuery = seriesResult.Credits.Cast.OrderBy(a => a.Order); - - if (config.HideMissingCastMembers) - { - castQuery = castQuery.Where(a => !string.IsNullOrEmpty(a.ProfilePath)); - } - - foreach (var actor in castQuery.Take(config.MaxCastMembers)) - { - if (string.IsNullOrWhiteSpace(actor.Name)) - { - continue; - } - - var personInfo = new PersonInfo - { - Name = actor.Name.Trim(), - Role = actor.Character?.Trim() ?? string.Empty, - Type = PersonKind.Actor, - SortOrder = actor.Order, - // NOTE: Null values are filtered out above - ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath!) - }; - - if (actor.Id > 0) - { - personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); - } - - yield return personInfo; - } + yield return actor; } if (seriesResult.Credits?.Crew is not null) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index c8e3a7aa52..5379796465 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -137,7 +137,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); - var extraMethods = TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups; + var extraMethods = TvShowMethods.Credits | TvShowMethods.CreditsAggregate | TvShowMethods.Images | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups; if (!(Plugin.Instance?.Configuration.ExcludeTagsSeries).GetValueOrDefault()) { extraMethods |= TvShowMethods.Keywords; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index c83174f97f..44a2f7291e 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -3,10 +3,13 @@ using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; using System.Text.RegularExpressions; using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; using TMDbLib.Objects.General; +using TMDbLib.Objects.TvShows; +using PersonInfo = MediaBrowser.Controller.Entities.PersonInfo; namespace MediaBrowser.Providers.Plugins.Tmdb { @@ -130,6 +133,100 @@ namespace MediaBrowser.Providers.Plugins.Tmdb } /// <summary> + /// Maps an aggregated TMDb cast list, whose entries hold every role their member played. + /// </summary> + /// <param name="cast">The aggregated cast list, or <c>null</c>.</param> + /// <param name="config">The configuration deciding how much of the cast to keep.</param> + /// <param name="getProfileUrl">Resolves a profile path into an absolute image url.</param> + /// <returns>One credit per role played.</returns> + internal static IEnumerable<PersonInfo> MapAggregateCast( + IReadOnlyList<CastAggregate>? cast, + PluginConfiguration config, + Func<string?, string?> getProfileUrl) + { + if (cast is null) + { + yield break; + } + + var billed = cast + .Where(member => !string.IsNullOrWhiteSpace(member.Name)) + .Where(member => !config.HideMissingCastMembers || !string.IsNullOrEmpty(member.ProfilePath)) + .OrderBy(member => member.Order) + .Take(config.MaxCastMembers); + + foreach (var member in billed) + { + // An actor playing several characters over the run gets one aggregated entry holding + // every role, so each of them becomes a credit of its own here. Their own billing puts + // the character they played the longest first. + var characters = member.Roles? + .Where(role => !string.IsNullOrWhiteSpace(role.Character)) + .OrderByDescending(role => role.EpisodeCount) + .Select(role => role.Character!.Trim()) + .ToArray(); + + if (characters is null || characters.Length == 0) + { + characters = [string.Empty]; + } + + foreach (var character in characters) + { + yield return CreateCredit(member.Name!, member.Id, member.ProfilePath, member.Order, character, getProfileUrl); + } + } + } + + /// <summary> + /// Maps a TMDb cast list whose entries hold the one character their member is credited for. + /// </summary> + /// <param name="cast">The cast list, or <c>null</c>.</param> + /// <param name="config">The configuration deciding how much of the cast to keep.</param> + /// <param name="getProfileUrl">Resolves a profile path into an absolute image url.</param> + /// <returns>One credit per cast entry.</returns> + internal static IEnumerable<PersonInfo> MapCast( + IReadOnlyList<Cast>? cast, + PluginConfiguration config, + Func<string?, string?> getProfileUrl) + { + if (cast is null) + { + yield break; + } + + var billed = cast + .Where(member => !string.IsNullOrWhiteSpace(member.Name)) + .Where(member => !config.HideMissingCastMembers || !string.IsNullOrEmpty(member.ProfilePath)) + .OrderBy(member => member.Order) + .Take(config.MaxCastMembers); + + foreach (var member in billed) + { + yield return CreateCredit(member.Name!, member.Id, member.ProfilePath, member.Order, member.Character?.Trim() ?? string.Empty, getProfileUrl); + } + } + + private static PersonInfo CreateCredit(string name, int id, string? profilePath, int? order, string role, Func<string?, string?> getProfileUrl) + { + var personInfo = new PersonInfo + { + Name = name.Trim(), + Role = role, + Type = PersonKind.Actor, + SortOrder = order, + ImageUrl = getProfileUrl(profilePath) + }; + + if (id > 0) + { + personInfo.SetProviderId(MetadataProvider.Tmdb, id.ToString(CultureInfo.InvariantCulture)); + } + + return personInfo; + } + + /// <summary> /// Determines whether a video is a trailer. /// </summary> /// <param name="video">The TMDb video.</param> |
