80 lines
3.8 KiB
C#
80 lines
3.8 KiB
C#
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;
|
|
}
|
|
}
|