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,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
<PackageReference Include="xunit" Version="2.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Jellyfin.Plugin.Vanguard.Core/Jellyfin.Plugin.Vanguard.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,115 @@
using Jellyfin.Plugin.Vanguard.Seasons;
using Xunit;
namespace Jellyfin.Plugin.Vanguard.Tests;
public class SeasonPolicyTests
{
private static readonly TimeZoneInfo NY = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
private static DateTimeOffset Ny(int y, int mo, int d, int h, int mi, int s = 0)
{
var local = new DateTime(y, mo, d, h, mi, s, DateTimeKind.Unspecified);
return new DateTimeOffset(local, NY.GetUtcOffset(local));
}
// "now" = Thursday 2026-10-30 12:00 Eastern (ISO week 44)
private static readonly DateTimeOffset Now = Ny(2026, 10, 30, 12, 0);
[Fact]
public void SameWeek_IsDaySeason()
{
var slot = SeasonPolicy.Resolve(Ny(2026, 10, 29, 21, 3, 11), Now, NY, 4);
Assert.Equal(SeasonKind.Day, slot.Kind);
Assert.Equal(20261029, slot.SeasonNumber);
Assert.Equal("Thursday, Oct 29", slot.SeasonName);
Assert.Equal(1029210311, slot.EpisodeNumber);
}
[Fact]
public void AfterMidnight_BelongsToPreviousBroadcastDay()
{
var slot = SeasonPolicy.Resolve(Ny(2026, 10, 30, 1, 15), Now, NY, 4);
Assert.Equal(new DateOnly(2026, 10, 29), slot.BroadcastDay);
Assert.Equal(20261029, slot.SeasonNumber);
Assert.Equal(1030011500, slot.EpisodeNumber); // episode number keeps the real clock time
}
[Fact]
public void EarlierInMonth_IsWeekSeason()
{
var slot = SeasonPolicy.Resolve(Ny(2026, 10, 14, 20, 0), Now, NY, 4); // ISO week 42
Assert.Equal(SeasonKind.Week, slot.Kind);
Assert.Equal(1202642, slot.SeasonNumber);
Assert.Equal("Week 42", slot.SeasonName);
}
[Fact]
public void EarlierInYear_IsMonthSeason()
{
var slot = SeasonPolicy.Resolve(Ny(2026, 7, 4, 20, 0), Now, NY, 4);
Assert.Equal(SeasonKind.Month, slot.Kind);
Assert.Equal(202607, slot.SeasonNumber);
Assert.Equal("July 2026", slot.SeasonName);
}
[Fact]
public void PreviousYear_IsYearSeason()
{
var slot = SeasonPolicy.Resolve(Ny(2025, 12, 31, 23, 0), Now, NY, 4);
Assert.Equal(SeasonKind.Year, slot.Kind);
Assert.Equal(2025, slot.SeasonNumber);
Assert.Equal("2025", slot.SeasonName);
}
[Fact]
public void EpisodeNumber_IsStableAcrossRolls()
{
var ts = Ny(2026, 10, 29, 21, 3, 11);
var e1 = SeasonPolicy.Resolve(ts, Now, NY, 4).EpisodeNumber;
var e2 = SeasonPolicy.Resolve(ts, Ny(2026, 11, 20, 12, 0), NY, 4).EpisodeNumber; // weeks later: week bucket? no -> month bucket
var e3 = SeasonPolicy.Resolve(ts, Ny(2027, 3, 1, 12, 0), NY, 4).EpisodeNumber; // next year -> year bucket
Assert.Equal(e1, e2);
Assert.Equal(e1, e3);
Assert.Equal(202610, SeasonPolicy.Resolve(ts, Ny(2026, 11, 20, 12, 0), NY, 4).SeasonNumber);
Assert.Equal(2026, SeasonPolicy.Resolve(ts, Ny(2027, 3, 1, 12, 0), NY, 4).SeasonNumber);
}
[Fact]
public void Overrides_ReplaceGeneratedName()
{
var over = SeasonPolicy.ParseOverrides("# comment\n202610 = Fall Training Block\nbad line\n");
var slot = SeasonPolicy.Resolve(Ny(2026, 10, 14, 20, 0), Ny(2026, 12, 1, 12, 0), NY, 4, over);
Assert.Equal("Fall Training Block", slot.SeasonName);
}
[Fact]
public void DstTransition_UsesCorrectOffset()
{
// 2026-11-01 02:30 local does not exist twice in a way that breaks us; 01:30 is ambiguous: must not throw
var slot = SeasonPolicy.Resolve(Ny(2026, 11, 1, 1, 30), Ny(2026, 11, 2, 12, 0), NY, 4);
Assert.Equal(new DateOnly(2026, 10, 31), slot.BroadcastDay);
}
[Fact]
public void Describe_RecoversKindAndName()
{
Assert.Equal((SeasonKind.Day, "Thursday, Oct 29"), SeasonPolicy.Describe(20261029));
Assert.Equal((SeasonKind.Week, "Week 42"), SeasonPolicy.Describe(1202642));
Assert.Equal((SeasonKind.Month, "October 2026"), SeasonPolicy.Describe(202610));
Assert.Equal((SeasonKind.Year, "2025"), SeasonPolicy.Describe(2025));
Assert.Null(SeasonPolicy.Describe(7));
Assert.Equal("Fall Block", SeasonPolicy.Describe(202610, new Dictionary<int, string> { [202610] = "Fall Block" })!.Value.Name);
}
[Fact]
public void Filename_ParsesMediaMtxPattern()
{
var ts = ItemTimestamp.TryParseFilename("2026-10-29_21-03-11", NY);
Assert.NotNull(ts);
Assert.Equal(Ny(2026, 10, 29, 21, 3, 11), ts!.Value);
Assert.Null(ItemTimestamp.TryParseFilename("clip-final-v2", NY));
Assert.Equal(Ny(2026, 10, 29, 21, 3, 11), ItemTimestamp.TryParseFilename("Overwatch 2 2026.10.29 - 21.03.11.05.DVR", NY));
Assert.Equal(Ny(2026, 10, 29, 21, 3, 11), ItemTimestamp.TryParseFilename("Halo Infinite 2026-10-29 21-03-11", NY));
Assert.Equal(Ny(2026, 10, 29, 21, 3, 11), ItemTimestamp.TryParseFilename("clip_730_20261029_210311", NY));
}
}