using System.Globalization; using System.Text.Json; using System.Text.RegularExpressions; namespace Jellyfin.Plugin.Vanguard.Seasons; /// Finds the best "when was this recorded" for a media file. public static partial class ItemTimestamp { public enum Source { Sidecar, Filename, FileTime } public sealed record Result(DateTimeOffset Utc, Source Source); /// /// 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. ) /// -> file last-write time. /// 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(@"(?\d{4})-(?\d{2})-(?\d{2})[_ T](?\d{2})-(?\d{2})-(?\d{2})")] private static partial Regex DashedName(); // ShadowPlay / NVIDIA App Game 2026.10.29 - 21.03.11.05.DVR [GeneratedRegex(@"(?\d{4})\.(?\d{2})\.(?\d{2}) - (?\d{2})\.(?\d{2})\.(?\d{2})")] private static partial Regex ShadowPlayName(); // Steam Game Recording clip_730_20261029_210311 (also plain 20261029_210311 / 20261029-210311) [GeneratedRegex(@"(?20\d{2})(?\d{2})(?\d{2})[_-](?\d{2})(?\d{2})(?\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; } }