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,51 @@
using MediaBrowser.Model.Plugins;
namespace Jellyfin.Plugin.Vanguard.Configuration;
/// <summary>Plugin settings (edited on the plugin's config page).</summary>
public class PluginConfiguration : BasePluginConfiguration
{
// --- Discord -------------------------------------------------------------------------
public string DiscordBotToken { get; set; } = string.Empty;
public string DiscordGuildId { get; set; } = string.Empty;
// --- Discord feed (new stream / new clip notices) ---------------------------------------
/// <summary>Channel to post notices in (same guild). Empty = feed off.</summary>
public string NotifyChannelId { get; set; } = string.Empty;
public bool NotifyNewStreams { get; set; } = true;
public bool NotifyNewClips { get; set; } = true;
/// <summary>Public Jellyfin URL used in links, e.g. https://watch.example.com</summary>
public string PublicUrl { get; set; } = string.Empty;
/// <summary>Library names that count as streams / clips (case-insensitive substring match on the library name or path).</summary>
public string StreamsLibraryHint { get; set; } = "stream";
public string ClipsLibraryHint { get; set; } = "clip";
// --- Playback policy (no hardware encoder on this box) --------------------------------------
/// <summary>Set direct-play-only on all users automatically (new SSO users included). Turn off when the server can transcode.</summary>
public bool EnforceDirectPlayOnly { get; set; } = true;
public bool EnforceOnAdminsToo { get; set; } = false;
// --- Authentik (streamer username -> Discord user id via the OAuth source connection) --
public string AuthentikUrl { get; set; } = "https://auth.example.com";
public string AuthentikApiToken { get; set; } = string.Empty;
// --- Time / seasons ---------------------------------------------------------------------
public string TimeZoneId { get; set; } = "America/New_York";
/// <summary>Hour (0-23) at which the "broadcast day" rolls over. 4 = a 01:00 clip belongs to the previous evening.</summary>
public int BroadcastDayRolloverHour { get; set; } = 4;
/// <summary>Season name overrides: "202611=Winter Scrims Block", one per line.</summary>
public string SeasonNameOverrides { get; set; } = string.Empty;
/// <summary>Prefer the overlapping Discord scheduled event's name as the episode title; fallback is the dock title (folder name).</summary>
public bool EpisodeTitleFromEvent { get; set; } = true;
/// <summary>
/// Folder name -> identity, one per line: "techy=123456789012345678" (Discord user id) or
/// "techy=authentik:someuser" (Authentik username). Folders not listed are looked up in Authentik
/// by username = folder name.
/// </summary>
public string FolderIdentityMap { get; set; } = string.Empty;
/// <summary>Hours of slack when matching a recording to a Discord scheduled event (before start / after end).</summary>
public double EventMatchSlackHours { get; set; } = 1.0;
}

View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Vanguard Metadata</title></head>
<body>
<div id="VanguardConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-checkbox">
<div data-role="content"><div class="content-primary">
<form id="VanguardConfigForm">
<h2>Discord</h2>
<div class="inputContainer"><input is="emby-input" id="DiscordBotToken" type="password" label="Bot token" /></div>
<div class="inputContainer"><input is="emby-input" id="DiscordGuildId" type="text" label="Server (guild) ID" /></div>
<h2>Discord feed</h2>
<div class="inputContainer"><input is="emby-input" id="NotifyChannelId" type="text" label="Channel ID for new stream / clip notices (empty = off)" /></div>
<div class="inputContainer"><input is="emby-input" id="PublicUrl" type="text" label="Public Jellyfin URL for links (https://watch.example.com)" /></div>
<div class="checkboxContainer"><label><input is="emby-checkbox" id="NotifyNewStreams" type="checkbox" /><span>Announce new stream recordings</span></label></div>
<div class="checkboxContainer"><label><input is="emby-checkbox" id="NotifyNewClips" type="checkbox" /><span>Announce new clips</span></label></div>
<div class="inputContainer"><input is="emby-input" id="StreamsLibraryHint" type="text" label="Streams library name contains" /></div>
<div class="inputContainer"><input is="emby-input" id="ClipsLibraryHint" type="text" label="Clips library name contains" /></div>
<h2>Playback policy</h2>
<div class="checkboxContainer"><label><input is="emby-checkbox" id="EnforceDirectPlayOnly" type="checkbox" /><span>Enforce direct-play-only (no video transcoding) on all users, every 30 min — turn off once the server has a hardware encoder</span></label></div>
<div class="checkboxContainer"><label><input is="emby-checkbox" id="EnforceOnAdminsToo" type="checkbox" /><span>Apply to administrators too</span></label></div>
<h2>Authentik</h2>
<div class="inputContainer"><input is="emby-input" id="AuthentikUrl" type="text" label="Authentik URL" /></div>
<div class="inputContainer"><input is="emby-input" id="AuthentikApiToken" type="password" label="API token (read-only service account)" /></div>
<div class="inputContainer"><textarea is="emby-textarea" id="FolderIdentityMap" rows="4" label="Folder → identity (folder=discordId or folder=authentik:username, one per line; default: Authentik username = folder)"></textarea></div>
<h2>Seasons</h2>
<div class="inputContainer"><input is="emby-input" id="TimeZoneId" type="text" label="Time zone (IANA)" /></div>
<div class="inputContainer"><input is="emby-input" id="BroadcastDayRolloverHour" type="number" min="0" max="23" label="Broadcast day rolls over at hour" /></div>
<div class="inputContainer"><textarea is="emby-textarea" id="SeasonNameOverrides" rows="4" label="Season name overrides (seasonNumber=Name, one per line)"></textarea></div>
<div class="checkboxContainer"><label><input is="emby-checkbox" id="EpisodeTitleFromEvent" type="checkbox" /><span>Episode title from the overlapping Discord event (fallback: dock title)</span></label></div>
<button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
</form>
</div></div>
<script type="text/javascript">
(function () {
var pluginId = "d00ebd97-f987-4678-8089-c8f0f8a6ab3e";
var fields = ["DiscordBotToken","DiscordGuildId","AuthentikUrl","AuthentikApiToken","TimeZoneId","BroadcastDayRolloverHour","SeasonNameOverrides","FolderIdentityMap","NotifyChannelId","PublicUrl","StreamsLibraryHint","ClipsLibraryHint"];
var checks = ["EpisodeTitleFromEvent","NotifyNewStreams","NotifyNewClips","EnforceDirectPlayOnly","EnforceOnAdminsToo"];
document.querySelector('#VanguardConfigPage').addEventListener('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(pluginId).then(function (c) {
fields.forEach(function (f) { document.querySelector('#' + f).value = c[f] == null ? '' : c[f]; });
checks.forEach(function (f) { document.querySelector('#' + f).checked = !!c[f]; });
Dashboard.hideLoadingMsg();
});
});
document.querySelector('#VanguardConfigForm').addEventListener('submit', function (e) {
e.preventDefault(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(pluginId).then(function (c) {
fields.forEach(function (f) { c[f] = document.querySelector('#' + f).value; });
c.BroadcastDayRolloverHour = parseInt(c.BroadcastDayRolloverHour, 10) || 4;
checks.forEach(function (f) { c[f] = document.querySelector('#' + f).checked; });
ApiClient.updatePluginConfiguration(pluginId, c).then(Dashboard.processPluginConfigurationUpdateResult);
});
});
})();
</script>
</div>
</body>
</html>