diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/NumberingScheme.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/NumberingScheme.cs
index c8a99d2..c4ef00b 100644
--- a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/NumberingScheme.cs
+++ b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/NumberingScheme.cs
@@ -14,4 +14,10 @@ public enum NumberingScheme
/// YYYYMMDD (e.g. 20250804 for August 4th, 2025).
///
YYYYMMDD,
+
+ ///
+ /// The video position within its TubeArchivist playlist.
+ /// Falls back to when the video does not belong to a playlist.
+ ///
+ PlaylistIndex,
}
diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PlaylistSeasonMapEntry.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PlaylistSeasonMapEntry.cs
new file mode 100644
index 0000000..170843e
--- /dev/null
+++ b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PlaylistSeasonMapEntry.cs
@@ -0,0 +1,47 @@
+namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
+{
+ ///
+ /// A persisted association between a TubeArchivist playlist and the Jellyfin season number
+ /// used to represent it.
+ ///
+ ///
+ /// Jellyfin groups episodes into seasons using ParentIndexNumber, 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.
+ ///
+ public class PlaylistSeasonMapEntry
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A parameterless constructor is required by the XML serializer used to persist the
+ /// plugin configuration.
+ ///
+ public PlaylistSeasonMapEntry()
+ {
+ PlaylistId = string.Empty;
+ PlaylistName = string.Empty;
+ }
+
+ ///
+ /// Gets or sets the TubeArchivist playlist id.
+ ///
+ public string PlaylistId { get; set; }
+
+ ///
+ /// Gets or sets the Jellyfin season number representing the playlist.
+ ///
+ public int SeasonNumber { get; set; }
+
+ ///
+ /// Gets or sets the last known TubeArchivist playlist name.
+ ///
+ ///
+ /// Stored so seasons keep a usable label when TubeArchivist is unreachable.
+ ///
+ public string PlaylistName { get; set; }
+ }
+}
diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs
index ba1ac2c..9d303b9 100644
--- a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs
+++ b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs
@@ -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();
}
///
@@ -191,6 +196,49 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
///
public NumberingScheme EpisodeNumberingScheme { get; set; } = NumberingScheme.Default;
+ ///
+ /// Gets or sets a value indicating whether to group episodes into seasons by TubeArchivist
+ /// playlist instead of by upload year.
+ ///
+ ///
+ /// Changing this setting requires a "Refresh metadata" with "Replace all metadata" enabled:
+ /// Jellyfin only overwrites an existing season name when replacing metadata.
+ ///
+ public bool SortSeasonsByPlaylist { get; set; }
+
+ ///
+ /// Gets the persisted TubeArchivist playlist to Jellyfin season number associations.
+ ///
+ ///
+ ///
+ /// Populated automatically; not user editable. Entries are append-only so season numbers
+ /// remain stable once assigned.
+ ///
+ ///
+ /// 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
+ /// BasePlugin.UpdateConfiguration 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.
+ ///
+ ///
+ [JsonInclude]
+ public Collection PlaylistSeasonMap { get; private set; }
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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.
+ ///
+ public bool SeasonFetcherAutoEnabled { get; set; }
+
///
/// Gets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist.
///
diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Constants.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Constants.cs
index 4ddeece..1d6b850 100644
--- a/Jellyfin.Plugin.TubeArchivistMetadata/Constants.cs
+++ b/Jellyfin.Plugin.TubeArchivistMetadata/Constants.cs
@@ -19,5 +19,19 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
/// Providers name.
///
public const string ProviderName = "TubeArchivist";
+
+ ///
+ /// Season number used to group videos which do not belong to any TubeArchivist playlist.
+ ///
+ ///
+ /// Season 0 cannot be used: Jellyfin's SeasonMetadataService.BeforeSaveInternal
+ /// force-renames season 0 to the library's "Specials" display name.
+ ///
+ public const int UnsortedSeasonNumber = 9000;
+
+ ///
+ /// Display name of the season grouping videos which do not belong to any TubeArchivist playlist.
+ ///
+ public const string UnsortedSeasonName = "Unsorted";
}
}