Merge pull request #16052 from AliveDevil/feature/11274-tmdb-episode-group-id

Include TMDB EpisodeGroup support for shows
This commit is contained in:
Jöran Malek authored and GitHub committed 2026-10-07 18:44:27 +02:00
1 parent 32efb47601
commit dbd4295853
10 files changed
+123 -46

No files matched your search

+1
View File
@@ -231,6 +231,7 @@
- [bjorntp](https://github.com/bjorntp)
- [martenumberto](https://github.com/martenumberto)
- [ZeusCraft10](https://github.com/ZeusCraft10)
- [AliveDevil](https://github.com/AliveDevil)
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
- [LiHRaM](https://github.com/LiHRaM)
- [MSalman5230](https://github.com/MSalman5230)
@@ -546,6 +546,8 @@ namespace MediaBrowser.Controller.Entities.TV
{
var info = GetItemLookupInfo<SeriesInfo>();
info.DisplayOrder = DisplayOrder;
return info;
}
@@ -5,5 +5,9 @@ namespace MediaBrowser.Controller.Providers
/// </summary>
public class SeriesInfo : ItemLookupInfo
{
/// <summary>
/// Gets or sets the canned display order group.
/// </summary>
public string? DisplayOrder { get; set; }
}
}
@@ -0,0 +1,30 @@
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
/// <summary>
/// TMDb series or season group id.
/// </summary>
public class TmdbEpisodeGroupId : IExternalId
{
internal const string ProviderKey = "TmdbEpisodeGroup";
/// <inheritdoc />
public string ProviderName => TmdbUtils.ProviderName;
/// <inheritdoc />
public string Key => ProviderKey;
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
{
return item is Series or Season;
}
}
}
@@ -69,11 +69,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Enumerable.Empty<RemoteImageInfo>();
}
var episodeGroup = series.GetProviderId(TmdbEpisodeGroupId.ProviderKey);
var language = item.GetPreferredMetadataLanguage();
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var episodeResult = await _tmdbClientManager
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, null, cancellationToken)
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, episodeGroup, null, null, null, cancellationToken)
.ConfigureAwait(false);
var stills = episodeResult?.Images?.Stills;
@@ -104,6 +104,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return metadataResult;
}
var episodeGroup = info.SeriesProviderIds.GetValueOrDefault(TmdbEpisodeGroupId.ProviderKey);
TvEpisode? episodeResult = null;
if (info.IndexNumberEnd.HasValue)
{
@@ -112,7 +113,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
List<TvEpisode>? result = null;
for (int? episode = startindex; episode <= endindex; episode++)
{
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, episodeGroup, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (episodeInfo is not null)
{
(result ??= new List<TvEpisode>()).Add(episodeInfo);
@@ -157,7 +158,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
else
{
episodeResult = await _tmdbClientManager
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, episodeGroup, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
.ConfigureAwait(false);
}
@@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using TMDbLib.Objects.TvShows;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
@@ -64,6 +65,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return result;
}
TvGroup? groupResult = null;
if (info.SeriesProviderIds.TryGetValue(TmdbEpisodeGroupId.ProviderKey, out var episodeGroup) && !string.IsNullOrWhiteSpace(episodeGroup))
{
groupResult = await _tmdbClientManager.GetSeriesGroupAsync(episodeGroup, seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
result.HasMetadata = true;
result.Item = new Season
{
@@ -81,6 +88,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.Item.TrySetProviderId(MetadataProvider.Tmdb, seasonResult.Id?.ToString(CultureInfo.InvariantCulture));
result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds?.TvdbId);
if (groupResult is not null && !string.IsNullOrWhiteSpace(groupResult.Id))
{
result.Item.TrySetProviderId(TmdbEpisodeGroupId.ProviderKey, groupResult.Id);
}
var credits = seasonResult.Credits;
if (credits?.Cast is not null)
{
@@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (series is not null)
{
var remoteResult = MapTvShowToRemoteSearchResult(series);
var remoteResult = MapTvShowToRemoteSearchResult(series, searchInfo);
return new[] { remoteResult };
}
@@ -126,7 +126,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return remoteResults;
}
private RemoteSearchResult MapTvShowToRemoteSearchResult(TvShow series)
private RemoteSearchResult MapTvShowToRemoteSearchResult(TvShow series, SeriesInfo search)
{
var remoteResult = new RemoteSearchResult
{
@@ -144,6 +144,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
remoteResult.TrySetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId);
}
WithDisplayOrder(series, remoteResult, search);
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
remoteResult.ProductionYear = series.FirstAirDate?.Year;
@@ -231,7 +233,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result = new MetadataResult<Series>
{
Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode, info),
ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
};
@@ -245,7 +247,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return result;
}
private static Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
private static Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode, SeriesInfo search)
{
var series = new Series
{
@@ -359,6 +361,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.OriginalLanguage = seriesResult.OriginalLanguage;
}
WithDisplayOrder(seriesResult, series, search);
return series;
}
@@ -451,5 +455,39 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
private static TvGroupType? MapDisplayOrderToTvGroupType(string? displayOrder)
{
return displayOrder switch
{
"originalAirDate" => TvGroupType.OriginalAirDate,
"absolute" => TvGroupType.Absolute,
"dvd" => TvGroupType.DVD,
"digital" => TvGroupType.Digital,
"storyArc" => TvGroupType.StoryArc,
"production" => TvGroupType.Production,
"tv" => TvGroupType.TV,
_ => null,
};
}
private static void WithDisplayOrder(TvShow searchResult, IHasProviderIds providerIds, SeriesInfo search)
{
if (!search.TryGetProviderId(TmdbEpisodeGroupId.ProviderKey, out var episodeGroupId)
&& searchResult.EpisodeGroups?.Results is not null)
{
var tvGroupType = MapDisplayOrderToTvGroupType(search.DisplayOrder);
if (tvGroupType is not null)
{
var episodeGroup = searchResult.EpisodeGroups.Results.Find(g => g.Type == tvGroupType);
if (episodeGroup is not null)
{
episodeGroupId = episodeGroup.Id;
}
}
}
providerIds.TrySetProviderId(TmdbEpisodeGroupId.ProviderKey, episodeGroupId);
}
}
}
@@ -170,53 +170,31 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
/// <summary>
/// Gets a tv show episode group from the TMDb API based on the show id and the display order.
/// Gets a tv show episode group from the TMDb API based on the episode group.
/// </summary>
/// <param name="tvShowId">The tv show's TMDb id.</param>
/// <param name="displayOrder">The display order.</param>
/// <param name="episodeGroupId">The Tmdb episode group.</param>
/// <param name="seasonNumber">The season number of the episode group.</param>
/// <param name="language">The tv show's language.</param>
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show episode group information or null if not found.</returns>
private async Task<TvGroupCollection?> GetSeriesGroupAsync(int tvShowId, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
public async Task<TvGroup?> GetSeriesGroupAsync(string episodeGroupId, int seasonNumber, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
{
TvGroupType? groupType =
string.Equals(displayOrder, "originalAirDate", StringComparison.Ordinal) ? TvGroupType.OriginalAirDate :
string.Equals(displayOrder, "absolute", StringComparison.Ordinal) ? TvGroupType.Absolute :
string.Equals(displayOrder, "dvd", StringComparison.Ordinal) ? TvGroupType.DVD :
string.Equals(displayOrder, "digital", StringComparison.Ordinal) ? TvGroupType.Digital :
string.Equals(displayOrder, "storyArc", StringComparison.Ordinal) ? TvGroupType.StoryArc :
string.Equals(displayOrder, "production", StringComparison.Ordinal) ? TvGroupType.Production :
string.Equals(displayOrder, "tv", StringComparison.Ordinal) ? TvGroupType.TV :
null;
if (groupType is null)
{
return null;
}
var key = $"group-{tvShowId.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
if (_memoryCache.TryGetValue(key, out TvGroupCollection? group))
var key = $"group-{episodeGroupId}-{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out TvGroup? group))
{
return group;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
var series = await GetSeriesAsync(tvShowId, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
var episodeGroupId = series?.EpisodeGroups?.Results?.Find(g => g.Type == groupType)?.Id;
if (episodeGroupId is null)
{
return null;
}
group = await _tmDbClient.GetTvEpisodeGroupsAsync(
var groups = await _tmDbClient.GetTvEpisodeGroupsAsync(
episodeGroupId,
language: TmdbUtils.NormalizeLanguage(language, countryCode),
cancellationToken: cancellationToken).ConfigureAwait(false);
group = groups?.Groups?.Find(s => s.Order == seasonNumber);
if (group is not null)
{
Cache(key, group);
@@ -262,20 +240,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
/// <summary>
/// Gets a movie from the TMDb API based on the tv show's TMDb id.
/// Gets an episode from the TMDb API based on the tv show's TMDb id.
/// </summary>
/// <param name="tvShowId">The tv show's TMDb id.</param>
/// <param name="seasonNumber">The season number.</param>
/// <param name="episodeNumber">The episode number.</param>
/// <param name="displayOrder">The display order.</param>
/// <param name="episodeGroup">The containing episode group.</param>
/// <param name="language">The episode's language.</param>
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv episode information or null if not found.</returns>
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, long episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, long episodeNumber, string? episodeGroup, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
{
var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{episodeGroup}-{language}";
if (_memoryCache.TryGetValue(key, out TvEpisode? episode))
{
return episode;
@@ -283,12 +261,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
if (group is not null)
if (!string.IsNullOrWhiteSpace(episodeGroup))
{
var season = group.Groups?.Find(s => s.Order == seasonNumber);
// Episode order starts at 0
var ep = season?.Episodes?.Find(e => e.Order == episodeNumber - 1);
var group = await GetSeriesGroupAsync(episodeGroup, seasonNumber, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
var ep = group?.Episodes?.Find(e => e.Order == episodeNumber - 1);
if (ep is not null)
{
seasonNumber = ep.SeasonNumber;
@@ -5,6 +5,7 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Providers.Plugins.Tmdb.TV;
using TMDbLib.Objects.TvShows;
namespace MediaBrowser.Providers.Plugins.Tmdb;
@@ -26,6 +27,11 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider
if (item.TryGetProviderId(MetadataProvider.Tmdb, out var externalId))
{
yield return TmdbUtils.BaseTmdbUrl + $"tv/{externalId}";
if (item.TryGetProviderId(TmdbEpisodeGroupId.ProviderKey, out var episodeGroup))
{
yield return TmdbUtils.BaseTmdbUrl + $"tv/{externalId}/episode_group/{episodeGroup}";
}
}
break;
@@ -47,6 +53,12 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider
yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{seasonNumber}";
}
}
if (season.Series?.TryGetProviderId(TmdbEpisodeGroupId.ProviderKey, out var episodeGroup) == true
&& season.TryGetProviderId(TmdbEpisodeGroupId.ProviderKey, out var group))
{
yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/episode_group/{episodeGroup}/group/{group}";
}
}
break;