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,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 / <name>.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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user