Vanguard media stack: ingest, copyparty, Syncthing, normalize sweep, Authentik, Jellyfin + Vanguard Metadata plugin, MariaDB, Gitea, bot runtime
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.Vanguard.Configuration;
|
||||
using Jellyfin.Plugin.Vanguard.Providers;
|
||||
using Jellyfin.Plugin.Vanguard.Seasons;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Vanguard.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Nightly after the broadcast-day rollover: re-bucket every episode (day -> week -> month -> year),
|
||||
/// rename seasons, and refresh series so virtual seasons appear/disappear. Paths never change, so
|
||||
/// item ids and watched state are kept.
|
||||
/// </summary>
|
||||
public sealed class SeasonRollTask : IScheduledTask
|
||||
{
|
||||
private readonly ILibraryManager _library;
|
||||
private readonly IFileSystem _fs;
|
||||
private readonly ILogger<SeasonRollTask> _log;
|
||||
public SeasonRollTask(ILibraryManager library, IFileSystem fs, ILogger<SeasonRollTask> log) { _library = library; _fs = fs; _log = log; }
|
||||
|
||||
public string Name => "Vanguard: roll seasons";
|
||||
public string Key => "VanguardSeasonRoll";
|
||||
public string Description => "Moves recordings between day/week/month/year seasons as time passes and fixes season names.";
|
||||
public string Category => "Vanguard";
|
||||
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken ct)
|
||||
{
|
||||
var cfg = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||
var tz = VanguardEpisodeProvider.Tz(cfg);
|
||||
var overrides = SeasonPolicy.ParseOverrides(cfg.SeasonNameOverrides);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var episodes = _library.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.Episode], Recursive = true, IsVirtualItem = false });
|
||||
var touchedSeries = new HashSet<Guid>();
|
||||
int moved = 0, i = 0;
|
||||
foreach (var item in episodes.OfType<Episode>())
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
progress.Report(80.0 * ++i / Math.Max(1, episodes.Count));
|
||||
if (string.IsNullOrEmpty(item.Path)) continue;
|
||||
var when = ItemTimestamp.Resolve(item.Path, tz);
|
||||
var slot = SeasonPolicy.Resolve(when.Utc, now, tz, cfg.BroadcastDayRolloverHour, overrides);
|
||||
if (item.ParentIndexNumber == slot.SeasonNumber && item.IndexNumber == slot.EpisodeNumber) continue;
|
||||
item.ParentIndexNumber = slot.SeasonNumber;
|
||||
item.IndexNumber = slot.EpisodeNumber;
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, ct).ConfigureAwait(false);
|
||||
if (item.SeriesId != Guid.Empty) touchedSeries.Add(item.SeriesId);
|
||||
moved++;
|
||||
}
|
||||
|
||||
// let each touched series re-evaluate its (virtual) seasons, then name every season from its number
|
||||
var options = new MetadataRefreshOptions(new DirectoryService(_fs)) { MetadataRefreshMode = MetadataRefreshMode.Default, ImageRefreshMode = MetadataRefreshMode.None };
|
||||
foreach (var sid in touchedSeries)
|
||||
{
|
||||
if (_library.GetItemById(sid) is Series s) await s.RefreshMetadata(options, ct).ConfigureAwait(false);
|
||||
}
|
||||
var seasons = _library.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.Season], Recursive = true });
|
||||
int renamed = 0;
|
||||
foreach (var s in seasons.OfType<Season>())
|
||||
{
|
||||
if (!s.IndexNumber.HasValue) continue;
|
||||
var d = SeasonPolicy.Describe(s.IndexNumber.Value, overrides);
|
||||
if (d is null || string.Equals(s.Name, d.Value.Name, StringComparison.Ordinal)) continue;
|
||||
s.Name = d.Value.Name;
|
||||
await s.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, ct).ConfigureAwait(false);
|
||||
renamed++;
|
||||
}
|
||||
_log.LogInformation("Season roll: {Moved} episodes re-bucketed, {Renamed} seasons renamed, {Series} series refreshed", moved, renamed, touchedSeries.Count);
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
var cfg = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||
var hour = Math.Clamp(cfg.BroadcastDayRolloverHour, 0, 23);
|
||||
return
|
||||
[
|
||||
new TaskTriggerInfo { Type = TaskTriggerInfoType.DailyTrigger, TimeOfDayTicks = TimeSpan.FromHours(hour).Add(TimeSpan.FromMinutes(10)).Ticks },
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user