feat: group episodes into playlist seasons
Wires PlaylistCache into episode metadata. With SortSeasonsByPlaylist enabled, an episode's ParentIndexNumber comes from its playlist's season number instead of its upload year. ParentIndexNumber is the only real grouping key available: the media is a flat folder, so every season is virtual and Jellyfin overwrites any provider-set SeasonName. ToEpisode gains an overload taking a nullable PlaylistAssignment; the existing signature delegates to it so upstream callers are untouched. The decision to bucket a video into Unsorted lives in the provider, not in ToEpisode, because only the provider can distinguish "this video is in no playlist" from "playlist data is unavailable". Conflating them meant a partial TubeArchivist outage, where /api/video responds but /api/playlist does not, wrote season 9000 to every episode. That value is sticky: MergeData only overwrites empty targets, so a later refresh cannot repair it. The provider now consults HasPlaylistDataAsync and falls back to the upload year when playlist data genuinely is missing. NumberingScheme.PlaylistIndex maps an episode's IndexNumber to its position within the playlist, and is left unset when no assignment exists.
This commit is contained in:
parent
838176a83d
commit
1e7bdfb99d
|
|
@ -65,8 +65,31 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
|
|||
ImageUrl = video.Channel.ThumbUrl,
|
||||
Type = Data.Enums.PersonKind.Actor,
|
||||
});
|
||||
PlaylistAssignment? playlistAssignment = null;
|
||||
if (Plugin.Instance?.Configuration.SortSeasonsByPlaylist == true)
|
||||
{
|
||||
var playlistCache = PlaylistCache.GetInstance();
|
||||
playlistAssignment = await playlistCache.GetAssignmentAsync(videoTAId, cancellationToken).ConfigureAwait(true);
|
||||
|
||||
if (playlistAssignment == null)
|
||||
{
|
||||
// Only bucket into "Unsorted" when the playlists were actually retrieved.
|
||||
// If TubeArchivist is unreachable the video falls back to its upload year,
|
||||
// which Jellyfin can still correct on a later refresh.
|
||||
if (await playlistCache.HasPlaylistDataAsync(cancellationToken).ConfigureAwait(true))
|
||||
{
|
||||
_logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "No TubeArchivist playlist found for video {0}. Grouping it into the {1} season.", videoTAId, Constants.UnsortedSeasonName));
|
||||
playlistAssignment = new PlaylistAssignment(string.Empty, Constants.UnsortedSeasonName, Constants.UnsortedSeasonNumber, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("{Message}", string.Format(CultureInfo.CurrentCulture, "TubeArchivist playlists unavailable. Grouping video {0} by upload year.", videoTAId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.HasMetadata = true;
|
||||
result.Item = video.ToEpisode();
|
||||
result.Item = video.ToEpisode(playlistAssignment);
|
||||
result.Item.Path = info.Path;
|
||||
result.Provider = Name;
|
||||
result.People = peopleInfo;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using MediaBrowser.Controller.Entities;
|
|||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
|
|
@ -121,15 +122,38 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
|||
/// <returns>The video equivalent Jellyfin <see cref="Episode"/> object.</returns>
|
||||
public Episode ToEpisode()
|
||||
{
|
||||
return ToEpisode(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the TubeArchivist API video to a Jellyfin <see cref="Episode"/> object,
|
||||
/// optionally grouping it into the season representing its TubeArchivist playlist.
|
||||
/// </summary>
|
||||
/// <param name="playlistAssignment">
|
||||
/// The playlist the video belongs to, or <c>null</c> to group the video by upload year.
|
||||
/// </param>
|
||||
/// <returns>The video equivalent Jellyfin <see cref="Episode"/> object.</returns>
|
||||
public Episode ToEpisode(PlaylistAssignment? playlistAssignment)
|
||||
{
|
||||
// A null assignment always means "group by upload year". Deciding when an unassigned
|
||||
// video belongs in the Unsorted season needs to know whether playlist data was actually
|
||||
// retrieved, so the caller makes that call and passes an Unsorted assignment instead.
|
||||
var seasonNumber = playlistAssignment?.SeasonNumber ?? Published.Year;
|
||||
var seasonName = playlistAssignment?.SeasonName ?? Published.Year.ToString(CultureInfo.CurrentCulture);
|
||||
|
||||
return new Episode
|
||||
{
|
||||
Name = Title,
|
||||
Overview = Utils.FormatDescription(Description),
|
||||
SeasonName = Published.Year.ToString(CultureInfo.CurrentCulture),
|
||||
ParentIndexNumber = Published.Year,
|
||||
|
||||
// Jellyfin overwrites SeasonName from the parent Season entity; it is set here only
|
||||
// for consistency. Season naming is handled by SeasonMetadataProvider.
|
||||
SeasonName = seasonName,
|
||||
ParentIndexNumber = seasonNumber,
|
||||
IndexNumber = Plugin.Instance?.Configuration?.EpisodeNumberingScheme switch
|
||||
{
|
||||
NumberingScheme.YYYYMMDD => (Published.Year * 10000) + (Published.Month * 100) + Published.Day,
|
||||
NumberingScheme.PlaylistIndex => playlistAssignment?.Index,
|
||||
_ => null
|
||||
},
|
||||
SeriesName = Channel.Name,
|
||||
|
|
|
|||
Loading…
Reference in New Issue