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:
2026-10-30 15:43:30 -05:00
commit c602a6ff52
66 changed files with 4229 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace Jellyfin.Plugin.Vanguard.Seasons;
/// <summary>Finds the best "when was this recorded" for a media file.</summary>
public static partial class ItemTimestamp
{
public enum Source { Sidecar, Filename, FileTime }
public sealed record Result(DateTimeOffset Utc, Source Source);
/// <summary>
/// Precedence: clip.json / &lt;name&gt;.json sidecar "timestamp" (UTC ISO-8601, written by the clip bot)
/// -> MediaMTX filename yyyy-MM-dd_HH-mm-ss (local time of the server, i.e. <paramref name="tz"/>)
/// -> file last-write time.
/// </summary>
public static Result Resolve(string path, TimeZoneInfo tz)
{
var sidecar = TryReadSidecar(path);
if (sidecar is not null) return new Result(sidecar.Value, Source.Sidecar);
var fromName = TryParseFilename(Path.GetFileNameWithoutExtension(path), tz);
if (fromName is not null) return new Result(fromName.Value, Source.Filename);
var mtime = File.Exists(path) ? File.GetLastWriteTimeUtc(path) : DateTime.UtcNow;
return new Result(new DateTimeOffset(mtime, TimeSpan.Zero), Source.FileTime);
}
// MediaMTX 2026-10-29_21-03-11 Xbox Game Bar Game 2026-10-29 21-03-11
[GeneratedRegex(@"(?<y>\d{4})-(?<mo>\d{2})-(?<d>\d{2})[_ T](?<h>\d{2})-(?<mi>\d{2})-(?<s>\d{2})")]
private static partial Regex DashedName();
// ShadowPlay / NVIDIA App Game 2026.10.29 - 21.03.11.05.DVR
[GeneratedRegex(@"(?<y>\d{4})\.(?<mo>\d{2})\.(?<d>\d{2}) - (?<h>\d{2})\.(?<mi>\d{2})\.(?<s>\d{2})")]
private static partial Regex ShadowPlayName();
// Steam Game Recording clip_730_20261029_210311 (also plain 20261029_210311 / 20261029-210311)
[GeneratedRegex(@"(?<![\d])(?<y>20\d{2})(?<mo>\d{2})(?<d>\d{2})[_-](?<h>\d{2})(?<mi>\d{2})(?<s>\d{2})(?![\d])")]
private static partial Regex CompactName();
public static DateTimeOffset? TryParseFilename(string stem, TimeZoneInfo tz)
{
var m = DashedName().Match(stem);
if (!m.Success) m = ShadowPlayName().Match(stem);
if (!m.Success) m = CompactName().Match(stem);
if (!m.Success) return null;
try
{
var local = new DateTime(
int.Parse(m.Groups["y"].Value, CultureInfo.InvariantCulture), int.Parse(m.Groups["mo"].Value, CultureInfo.InvariantCulture), int.Parse(m.Groups["d"].Value, CultureInfo.InvariantCulture),
int.Parse(m.Groups["h"].Value, CultureInfo.InvariantCulture), int.Parse(m.Groups["mi"].Value, CultureInfo.InvariantCulture), int.Parse(m.Groups["s"].Value, CultureInfo.InvariantCulture),
DateTimeKind.Unspecified);
var offset = tz.GetUtcOffset(local);
return new DateTimeOffset(local, offset);
}
catch (ArgumentOutOfRangeException) { return null; }
}
private static DateTimeOffset? TryReadSidecar(string path)
{
var dir = Path.GetDirectoryName(path) ?? string.Empty;
var stem = Path.GetFileNameWithoutExtension(path);
foreach (var candidate in new[] { Path.Combine(dir, stem + ".json"), Path.Combine(dir, stem + ".clip.json") })
{
if (!File.Exists(candidate)) continue;
try
{
using var doc = JsonDocument.Parse(File.ReadAllText(candidate));
if (doc.RootElement.TryGetProperty("timestamp", out var ts) && ts.ValueKind == JsonValueKind.String &&
DateTimeOffset.TryParse(ts.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed))
{
return parsed;
}
}
catch (JsonException) { /* ignore broken sidecar */ }
}
return null;
}
}