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,75 @@
|
||||
using System.Globalization;
|
||||
using Jellyfin.Plugin.Vanguard.Configuration;
|
||||
using Jellyfin.Plugin.Vanguard.Seasons;
|
||||
using Jellyfin.Plugin.Vanguard.Services;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Vanguard.Providers;
|
||||
|
||||
/// <summary>Season/episode numbers from the timestamp; title from the Discord event or the dock title.</summary>
|
||||
public sealed class VanguardEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
|
||||
{
|
||||
private readonly LibraryPaths _paths;
|
||||
private readonly EventStore _store;
|
||||
private readonly IdentityResolver _identity;
|
||||
private readonly IHttpClientFactory _http;
|
||||
private readonly ILogger<VanguardEpisodeProvider> _log;
|
||||
|
||||
public VanguardEpisodeProvider(LibraryPaths paths, EventStore store, IdentityResolver identity, IHttpClientFactory http, ILogger<VanguardEpisodeProvider> log)
|
||||
{ _paths = paths; _store = store; _identity = identity; _http = http; _log = log; }
|
||||
|
||||
public string Name => "Vanguard Metadata";
|
||||
public int Order => 0;
|
||||
|
||||
private static PluginConfiguration Cfg => Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||
|
||||
public static TimeZoneInfo Tz(PluginConfiguration cfg)
|
||||
{
|
||||
try { return TimeZoneInfo.FindSystemTimeZoneById(cfg.TimeZoneId); }
|
||||
catch (TimeZoneNotFoundException) { return TimeZoneInfo.Utc; }
|
||||
}
|
||||
|
||||
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken ct)
|
||||
{
|
||||
var result = new MetadataResult<Episode>();
|
||||
if (string.IsNullOrEmpty(info.Path)) return result;
|
||||
var cfg = Cfg;
|
||||
var tz = Tz(cfg);
|
||||
var when = ItemTimestamp.Resolve(info.Path, tz);
|
||||
var slot = SeasonPolicy.Resolve(when.Utc, DateTimeOffset.UtcNow, tz, cfg.BroadcastDayRolloverHour, SeasonPolicy.ParseOverrides(cfg.SeasonNameOverrides));
|
||||
var parts = _paths.Split(info.Path);
|
||||
|
||||
var ev = _store.FindEventAt(when.Utc, TimeSpan.FromHours(cfg.EventMatchSlackHours), TimeSpan.FromHours(4));
|
||||
var local = TimeZoneInfo.ConvertTime(when.Utc, tz);
|
||||
var dockTitle = parts?.Title ?? parts?.Stem ?? Path.GetFileNameWithoutExtension(info.Path);
|
||||
var title = cfg.EpisodeTitleFromEvent && ev is not null ? ev.Name : dockTitle;
|
||||
|
||||
var who = parts is null ? null : await _identity.ResolveFolderAsync(parts.SeriesFolder, ct).ConfigureAwait(false);
|
||||
var lines = new List<string>();
|
||||
if (ev is not null && !string.IsNullOrWhiteSpace(ev.Description)) lines.Add(ev.Description!.Trim());
|
||||
lines.Add($"Recorded {local:dddd, MMMM d yyyy 'at' h:mm tt} by {who?.DisplayName ?? parts?.SeriesFolder ?? "unknown"}.");
|
||||
if (ev is not null && title != dockTitle && !string.IsNullOrWhiteSpace(dockTitle)) lines.Add($"Stream title: {dockTitle}");
|
||||
|
||||
result.Item = new Episode
|
||||
{
|
||||
Name = $"{title} — {local:MMM d, h:mm tt}",
|
||||
IndexNumber = slot.EpisodeNumber,
|
||||
ParentIndexNumber = slot.SeasonNumber,
|
||||
PremiereDate = when.Utc.UtcDateTime,
|
||||
ProductionYear = local.Year,
|
||||
Overview = string.Join("\n\n", lines),
|
||||
};
|
||||
result.HasMetadata = true;
|
||||
_log.LogDebug("Episode {Path}: season {Season} ({Name}) episode {Ep} from {Src}", info.Path, slot.SeasonNumber, slot.SeasonName, slot.EpisodeNumber, when.Source);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken ct) =>
|
||||
Task.FromResult<IEnumerable<RemoteSearchResult>>([]);
|
||||
|
||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken ct) =>
|
||||
_http.CreateClient(MediaBrowser.Common.Net.NamedClient.Default).GetAsync(url, ct);
|
||||
}
|
||||
Reference in New Issue
Block a user