Build a TMDb series cast from the aggregated credits
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -129,6 +132,100 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
return PersonKind.Unknown;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Providers.Plugins.Tmdb;
|
||||
using TMDbLib.Objects.TvShows;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Providers.Tests.Tmdb
|
||||
{
|
||||
public class TmdbUtilsCastTests
|
||||
{
|
||||
private static readonly PluginConfiguration _config = new() { MaxCastMembers = 10 };
|
||||
|
||||
[Fact]
|
||||
public void MapAggregateCast_MemberWithSeveralRoles_YieldsOneCreditPerRole()
|
||||
{
|
||||
var cast = new List<CastAggregate>
|
||||
{
|
||||
CreateAggregate("Megumi Toyoguchi", 1, 0, ("Tabby (voice)", 3), ("Mimiru (voice)", 12))
|
||||
};
|
||||
|
||||
var people = TmdbUtils.MapAggregateCast(cast, _config, _ => null).ToArray();
|
||||
|
||||
// The character they played the longest comes first, which is their own billing.
|
||||
Assert.Equal(["Mimiru (voice)", "Tabby (voice)"], people.Select(p => p.Role));
|
||||
Assert.All(people, p => Assert.Equal("Megumi Toyoguchi", p.Name));
|
||||
Assert.All(people, p => Assert.Equal(PersonKind.Actor, p.Type));
|
||||
Assert.All(people, p => Assert.Equal("1", p.GetProviderId(MetadataProvider.Tmdb)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapAggregateCast_MemberWithoutARole_IsStillCredited()
|
||||
{
|
||||
var cast = new List<CastAggregate> { CreateAggregate("Uncredited Actor", 2, 0) };
|
||||
|
||||
var person = Assert.Single(TmdbUtils.MapAggregateCast(cast, _config, _ => null));
|
||||
|
||||
Assert.Equal(string.Empty, person.Role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapAggregateCast_MoreThanConfigured_KeepsTheTopBilled()
|
||||
{
|
||||
var cast = Enumerable.Range(0, 5)
|
||||
.Select(i => CreateAggregate($"Actor {4 - i}", i + 1, 4 - i, ($"Role {4 - i}", 1)))
|
||||
.ToList();
|
||||
|
||||
var people = TmdbUtils.MapAggregateCast(cast, new PluginConfiguration { MaxCastMembers = 2 }, _ => null);
|
||||
|
||||
Assert.Equal(["Actor 0", "Actor 1"], people.Select(p => p.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapAggregateCast_HideMissingCastMembers_DropsTheOnesWithoutAProfile()
|
||||
{
|
||||
var withProfile = CreateAggregate("Has Profile", 1, 0, ("Hero", 1));
|
||||
withProfile.ProfilePath = "/profile.jpg";
|
||||
var cast = new List<CastAggregate> { withProfile, CreateAggregate("No Profile", 2, 1, ("Villain", 1)) };
|
||||
|
||||
var people = TmdbUtils.MapAggregateCast(
|
||||
cast,
|
||||
new PluginConfiguration { MaxCastMembers = 10, HideMissingCastMembers = true },
|
||||
_ => null);
|
||||
|
||||
Assert.Equal(["Has Profile"], people.Select(p => p.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapCast_FlatCredits_YieldOneCreditEach()
|
||||
{
|
||||
var cast = new List<Cast>
|
||||
{
|
||||
new() { Name = "Kevin Conroy", Id = 1, Order = 0, Character = " Batman (voice) " },
|
||||
new() { Name = " ", Id = 2, Order = 1, Character = "Nobody" }
|
||||
};
|
||||
|
||||
var person = Assert.Single(TmdbUtils.MapCast(cast, _config, _ => null));
|
||||
|
||||
Assert.Equal("Kevin Conroy", person.Name);
|
||||
Assert.Equal("Batman (voice)", person.Role);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void MapCast_NoCast_YieldsNothing(bool aggregate)
|
||||
{
|
||||
Assert.Empty(aggregate
|
||||
? TmdbUtils.MapAggregateCast(null, _config, _ => null)
|
||||
: TmdbUtils.MapCast(null, _config, _ => null));
|
||||
}
|
||||
|
||||
private static CastAggregate CreateAggregate(string name, int id, int order, params (string Character, int Episodes)[] roles)
|
||||
{
|
||||
return new CastAggregate
|
||||
{
|
||||
Name = name,
|
||||
Id = id,
|
||||
Order = order,
|
||||
Roles = roles.Select(role => new CastRole { Character = role.Character, EpisodeCount = role.Episodes }).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user