feat(config): add playlist season settings and persistent season map

Adds the configuration surface for grouping seasons by TubeArchivist
playlist. No behaviour yet; the settings are inert until the providers
land.

- SortSeasonsByPlaylist toggles the feature.
- PlaylistSeasonMap persists playlist to season number associations so
  numbers stay stable across restarts and TubeArchivist outages.
- SeasonFetcherAutoEnabled records the one time library migration.
- NumberingScheme gains PlaylistIndex. Both serializers persist enums by
  name, so appending a member is safe.
- Constants gains the reserved Unsorted season (9000). Season 0 is never
  assigned because Jellyfin force-renames it to "Specials".

PlaylistSeasonMap carries [JsonInclude] deliberately. Jellyfin stores
plugin configuration as XML, which round-trips a private setter via the
ICollection add pattern, but the configuration API endpoint deserializes
with System.Text.Json, which ignores non-public setters. Without the
attribute the collection is dropped on every configuration page save and
BasePlugin.UpdateConfiguration writes the emptied collection back to
disk. A public setter is not an alternative: CA2227 is enabled and
TreatWarningsAsErrors would fail the build.
This commit is contained in:
7hr08ik 2026-08-09 01:27:12 +01:00
parent 72ed3359b2
commit fb15b8d93d
4 changed files with 115 additions and 0 deletions

View File

@ -14,4 +14,10 @@ public enum NumberingScheme
/// YYYYMMDD (e.g. 20250804 for August 4th, 2025).
/// </summary>
YYYYMMDD,
/// <summary>
/// The video position within its TubeArchivist playlist.
/// Falls back to <see cref="Default"/> when the video does not belong to a playlist.
/// </summary>
PlaylistIndex,
}

View File

@ -0,0 +1,47 @@
namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
{
/// <summary>
/// A persisted association between a TubeArchivist playlist and the Jellyfin season number
/// used to represent it.
/// </summary>
/// <remarks>
/// Jellyfin groups episodes into seasons using <c>ParentIndexNumber</c>, which is an integer,
/// while TubeArchivist identifies playlists by string id. This entry persists the mapping so
/// season numbers stay stable across restarts and metadata refreshes.
/// A plain dictionary is not used because Jellyfin's XML configuration serializer does not
/// reliably round-trip dictionary types.
/// </remarks>
public class PlaylistSeasonMapEntry
{
/// <summary>
/// Initializes a new instance of the <see cref="PlaylistSeasonMapEntry"/> class.
/// </summary>
/// <remarks>
/// A parameterless constructor is required by the XML serializer used to persist the
/// plugin configuration.
/// </remarks>
public PlaylistSeasonMapEntry()
{
PlaylistId = string.Empty;
PlaylistName = string.Empty;
}
/// <summary>
/// Gets or sets the TubeArchivist playlist id.
/// </summary>
public string PlaylistId { get; set; }
/// <summary>
/// Gets or sets the Jellyfin season number representing the playlist.
/// </summary>
public int SeasonNumber { get; set; }
/// <summary>
/// Gets or sets the last known TubeArchivist playlist name.
/// </summary>
/// <remarks>
/// Stored so seasons keep a usable label when TubeArchivist is unreachable.
/// </remarks>
public string PlaylistName { get; set; }
}
}

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
using MediaBrowser.Model.Plugins;
using Microsoft.Extensions.Logging;
@ -47,6 +49,9 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
TAJFProgressTaskInterval = 60;
JFTAPlaylistsSyncTaskInterval = 60;
TAJFPlaylistsSyncTaskInterval = 60;
SortSeasonsByPlaylist = false;
SeasonFetcherAutoEnabled = false;
PlaylistSeasonMap = new Collection<PlaylistSeasonMapEntry>();
}
/// <summary>
@ -191,6 +196,49 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
/// </summary>
public NumberingScheme EpisodeNumberingScheme { get; set; } = NumberingScheme.Default;
/// <summary>
/// Gets or sets a value indicating whether to group episodes into seasons by TubeArchivist
/// playlist instead of by upload year.
/// </summary>
/// <remarks>
/// Changing this setting requires a "Refresh metadata" with "Replace all metadata" enabled:
/// Jellyfin only overwrites an existing season name when replacing metadata.
/// </remarks>
public bool SortSeasonsByPlaylist { get; set; }
/// <summary>
/// Gets the persisted TubeArchivist playlist to Jellyfin season number associations.
/// </summary>
/// <remarks>
/// <para>
/// Populated automatically; not user editable. Entries are append-only so season numbers
/// remain stable once assigned.
/// </para>
/// <para>
/// <see cref="JsonIncludeAttribute"/> is required and must not be removed. Jellyfin persists
/// plugin configuration as XML, but the plugin configuration API endpoint deserializes the
/// posted body with System.Text.Json, which ignores non-public setters. Without this
/// attribute the collection is dropped whenever the configuration page is saved, and
/// <c>BasePlugin.UpdateConfiguration</c> then writes the emptied collection back to disk.
/// A public setter is not an option because it violates CA2227.
/// This is unrelated to the Newtonsoft usage of the TubeArchivist API models.
/// </para>
/// </remarks>
[JsonInclude]
public Collection<PlaylistSeasonMapEntry> PlaylistSeasonMap { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the one time migration which enables this
/// plugin's Season metadata fetcher on existing libraries has already run.
/// </summary>
/// <remarks>
/// Libraries created before the Season provider existed store an empty Season fetcher list,
/// which Jellyfin treats as "all disabled" rather than "use the defaults". The migration
/// repairs that once; keeping this flag means a fetcher removed by hand afterwards stays
/// removed instead of being re-added on every restart.
/// </remarks>
public bool SeasonFetcherAutoEnabled { get; set; }
/// <summary>
/// Gets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist.
/// </summary>

View File

@ -19,5 +19,19 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
/// Providers name.
/// </summary>
public const string ProviderName = "TubeArchivist";
/// <summary>
/// Season number used to group videos which do not belong to any TubeArchivist playlist.
/// </summary>
/// <remarks>
/// Season 0 cannot be used: Jellyfin's <c>SeasonMetadataService.BeforeSaveInternal</c>
/// force-renames season 0 to the library's "Specials" display name.
/// </remarks>
public const int UnsortedSeasonNumber = 9000;
/// <summary>
/// Display name of the season grouping videos which do not belong to any TubeArchivist playlist.
/// </summary>
public const string UnsortedSeasonName = "Unsorted";
}
}