Files
jellyfin/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
Marc Brooks 6dc61a430b Sort embedded collections in Nfo files
Because the Nfo files emit the collections as they are in-memory, the
files are not stable in format, genres, tags, albums, people, etc. are emitted in random orders. Add ordering of the collections when emitting the Nfo files so the file remains stable (unchanged) when underlying media information doesn't change.

In the process of this, it became clear that most of the providers and probes don't trim the strings like people's names, genre names, etc. so did a pass of Trim cleanup too.

Specific ordering: (alphabetical/numeric ascending after trimming blanks and defaulting to zero for missing numbers)

BaseItem: Directors, Writers, Trailers (by Url), Production Locations, Genres, Studios, Tags, Custom Provider Data (by key), Linked Children  (by Path>LibraryItemId), Backdrop Images (by path), Actors (by SortOrder>Name)

AlbumNfo: Artists, Album Artists, Tracks (by ParentIndexNumber>IndexNumber>Name)

ArtistNfo: Albums (by Production Year>SortName>Name)

MovieNfo: Artists

Fix Debug build lint


Fix CI debug build lint issue.


Fix review issues

Fixed debug-build lint issues.
Emits the `disc` number to NFO for tracks with a non-zero ParentIndexNumber and only emit `position` if non-zero.
Removed the exception filtering I put in for testing.

Don't emit actors for MusicAlbums or MusicArtists


Swap from String.Trimmed() to ?.Trim()
Addressing PR feedback

Can't use ReadOnlySpan in an async method

Removed now-unused namespace
2024-09-18 20:33:18 -05:00

270 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
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
{
/// <summary>
/// TV episode provider powered by TheMovieDb.
/// </summary>
public class TmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly TmdbClientManager _tmdbClientManager;
/// <summary>
/// Initializes a new instance of the <see cref="TmdbEpisodeProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
/// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param>
public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
_httpClientFactory = httpClientFactory;
_tmdbClientManager = tmdbClientManager;
}
/// <inheritdoc />
public int Order => 1;
/// <inheritdoc />
public string Name => TmdbUtils.ProviderName;
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
// The search query must either provide an episode number or date
if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
{
return Enumerable.Empty<RemoteSearchResult>();
}
var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
if (!metadataResult.HasMetadata)
{
return Enumerable.Empty<RemoteSearchResult>();
}
var item = metadataResult.Item;
return new[]
{
new RemoteSearchResult
{
IndexNumber = item.IndexNumber,
Name = item.Name,
ParentIndexNumber = item.ParentIndexNumber,
PremiereDate = item.PremiereDate,
ProductionYear = item.ProductionYear,
ProviderIds = item.ProviderIds,
SearchProviderName = Name,
IndexNumberEnd = item.IndexNumberEnd
}
};
}
/// <inheritdoc />
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{
var metadataResult = new MetadataResult<Episode>();
// Allowing this will dramatically increase scan times
if (info.IsMissingEpisode)
{
return metadataResult;
}
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string? tmdbId);
var seriesTmdbId = Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture);
if (seriesTmdbId <= 0)
{
return metadataResult;
}
var seasonNumber = info.ParentIndexNumber;
var episodeNumber = info.IndexNumber;
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
{
return metadataResult;
}
TvEpisode? episodeResult = null;
if (info.IndexNumberEnd.HasValue)
{
var startindex = episodeNumber;
var endindex = info.IndexNumberEnd;
List<TvEpisode>? result = null;
for (int? episode = startindex; episode <= endindex; episode++)
{
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
if (episodeInfo is not null)
{
(result ??= new List<TvEpisode>()).Add(episodeInfo);
}
}
if (result is not null)
{
// Forces a deep copy of the first TvEpisode, so we don't modify the original because it's cached
episodeResult = new TvEpisode()
{
Name = result[0].Name,
Overview = result[0].Overview,
AirDate = result[0].AirDate,
VoteAverage = result[0].VoteAverage,
ExternalIds = result[0].ExternalIds,
Videos = result[0].Videos,
Credits = result[0].Credits
};
if (result.Count > 1)
{
var name = new StringBuilder(episodeResult.Name);
var overview = new StringBuilder(episodeResult.Overview);
for (int i = 1; i < result.Count; i++)
{
name.Append(" / ").Append(result[i].Name);
overview.Append(" / ").Append(result[i].Overview);
}
episodeResult.Name = name.ToString();
episodeResult.Overview = overview.ToString();
}
}
else
{
return metadataResult;
}
}
else
{
episodeResult = await _tmdbClientManager
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
.ConfigureAwait(false);
}
if (episodeResult is null)
{
return metadataResult;
}
metadataResult.HasMetadata = true;
metadataResult.QueriedById = true;
if (!string.IsNullOrEmpty(episodeResult.Overview))
{
// if overview is non-empty, we can assume that localized data was returned
metadataResult.ResultLanguage = info.MetadataLanguage;
}
var item = new Episode
{
IndexNumber = info.IndexNumber,
ParentIndexNumber = info.ParentIndexNumber,
IndexNumberEnd = info.IndexNumberEnd,
Name = episodeResult.Name,
PremiereDate = episodeResult.AirDate,
ProductionYear = episodeResult.AirDate?.Year,
Overview = episodeResult.Overview,
CommunityRating = Convert.ToSingle(episodeResult.VoteAverage)
};
var externalIds = episodeResult.ExternalIds;
item.TrySetProviderId(MetadataProvider.Tvdb, externalIds?.TvdbId);
item.TrySetProviderId(MetadataProvider.Imdb, externalIds?.ImdbId);
item.TrySetProviderId(MetadataProvider.TvRage, externalIds?.TvrageId);
if (episodeResult.Videos?.Results is not null)
{
foreach (var video in episodeResult.Videos.Results)
{
if (TmdbUtils.IsTrailerType(video))
{
item.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key);
}
}
}
var credits = episodeResult.Credits;
if (credits?.Cast is not null)
{
foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
metadataResult.AddPerson(new PersonInfo
{
Name = actor.Name.Trim(),
Role = actor.Character.Trim(),
Type = PersonKind.Actor,
SortOrder = actor.Order
});
}
}
if (credits?.GuestStars is not null)
{
foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
metadataResult.AddPerson(new PersonInfo
{
Name = guest.Name.Trim(),
Role = guest.Character.Trim(),
Type = PersonKind.GuestStar,
SortOrder = guest.Order
});
}
}
// and the rest from crew
if (credits?.Crew is not null)
{
foreach (var person in credits.Crew)
{
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
if (!TmdbUtils.WantedCrewKinds.Contains(type)
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
metadataResult.AddPerson(new PersonInfo
{
Name = person.Name.Trim(),
Role = person.Job?.Trim(),
Type = type
});
}
}
metadataResult.Item = item;
return metadataResult;
}
/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}