diff --git a/README.md b/README.md index 85a62dd..a58eae8 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,9 @@ Pulls everything older than 15 days off the server, including the hidden `.src/` ## Layout on the server /srv/media/streams///.live/<ts>.mp4 recording in progress (hidden from Jellyfin/copyparty) - /srv/media/streams/<user>/<Title>/2025-10-10_21-03-11.mp4 finished stream, flat MP4 published by the sweep + /srv/media/streams/<user>/<Title>__<ts>.mp4 finished stream, one flat MP4 per recording, directly in + the streamer's folder (several files inside one title + folder would be ONE Jellyfin episode with "versions") /srv/media/clips/<user>/clip.mp4 clip (synced as-is, or converted; raw removed) /srv/media/clips/<user>/.stlocal Syncthing ignore list maintained by the sweep /srv/media/clips/<user>/.src/clip.mkv raw source, only for clips that did NOT arrive via Syncthing @@ -272,7 +274,7 @@ upscaled; aspect kept). | input | action | |-------|--------| | already compliant | nothing | -| MediaMTX recording in `.live/` (fragmented MP4) | once idle 3 min: lossless remux to a flat MP4 in the title folder, `.live` source removed | +| MediaMTX recording in `<Title>/.live/` (fragmented MP4) | once idle 3 min: lossless remux to `<user>/<Title>__<ts>.mp4`, `.live` source and empty title folder removed | | in a Syncthing folder (`.stfolder` marker) | compliant / index-only: untouched; otherwise convert → `name.mp4`, add raw to `.stlocal`, delete raw (one copy) | | HEVC / 10-bit / VFR / MKV / >1440p / >60fps / non-AAC | full-res re-encode (x264 veryfast CRF 18) → `name.mp4`; source kept in `.src/` | | ffmpeg fails or duration mismatch | `name.normalize-failed.log` written, file skipped until that log is deleted | diff --git a/STATUS.md b/STATUS.md index 973f2d7..6b2324c 100644 --- a/STATUS.md +++ b/STATUS.md @@ -11,7 +11,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea | Continuous | OBS streams record into a hidden `streams/<user>/<Title>/.live/` folder, invisible to Jellyfin and copyparty until finished | MediaMTX | | Continuous | Gamers' clip folders mirror onto the server (video files only, their structure untouched, receive-only) | Syncthing | | Continuous | New recordings and clips are announced in a Discord channel with the streamer's name and a frame grab | Jellyfin plugin (needs bot token + channel in its config) | -| Every 5 min | Finished files are checked. Streams idle ≥3 min: lossless remux out of `.live/` into the title folder as a flat, seekable MP4 (this is when they appear in Jellyfin/copyparty and the Discord notice fires). Non-compliant clips: converted to the editing standard, raw removed, one copy per clip | normalize sweep (systemd timer) | +| Every 5 min | Finished files are checked. Streams idle ≥3 min: lossless remux out of `<Title>/.live/` to `<user>/<Title>__<ts>.mp4`, one file per recording (this is when they appear in Jellyfin/copyparty and the Discord notice fires). Non-compliant clips: converted to the editing standard, raw removed, one copy per clip | normalize sweep (systemd timer) | | Every 5 min | Discord scheduled events snapshotted into a history so recordings can be matched to their session | Jellyfin plugin | | Every 30 min + startup | Every non-admin Jellyfin user set to direct-play-only (no hardware encoder on this box) | Jellyfin plugin | | Daily 04:10 ET | Recordings roll between weekday → week → month → year seasons as the calendar moves; no files change | Jellyfin plugin | @@ -53,7 +53,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea | Caddy | TLS + routing for all hostnames | live | | Authentik (+Postgres, Redis) | Discord login, groups, guild-membership policy | live | | Jellyfin | watch page, SSO via Authentik, direct play only | live | -| Vanguard Metadata plugin | seasons by tag, Discord names/avatars/events, feed, playback policy | **0.1.0 live, 0.1.2 built** (policy task, "new recording" feed wording) — install pending | +| Vanguard Metadata plugin | seasons by tag, Discord names/avatars/events, feed, playback policy | **0.1.3 built** (flat `<Title>__<ts>` names, policy task, feed wording) — install pending; server has 0.1.0 or 0.1.2 | | MariaDB | databases for the bot and Gitea; public :3306, root local-only | live | | Gitea | git hosting on MariaDB, Actions enabled; holds this repo | live, local admin | | act_runner | CI jobs in Docker | **not registered** | @@ -83,6 +83,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea ## Verified behaviours worth remembering - **Never scp generated configs** (`mediamtx.yml`, `copyparty.conf`, `Caddyfile`, `dump-users.txt`, `.env`): a template copy wiped all streamers once. They are gitignored now; `*.example.*` are reference only. - Caddy runs in a container: anything it serves from disk must be a mount (`/srv/live` was missing → 404). +- Several videos inside one non-season folder = ONE Jellyfin episode with "versions". Streams are therefore published flat as `<Title>__<ts>.mp4` in the streamer's folder (plugin 0.1.3 parses the title from the name). Four or five helmet cams a night each get their own episode. - The sweep's VFR check used a gawk-only function; Debian's mawk made it pass everything silently. Portable now. - A deploy block that is only half-run leaves old binaries in `/usr/local/bin`; `grep -c` the installed script before blaming logic. - MediaMTX stamps recording filenames in its container clock: without `TZ` it is UTC and the plugin reads names as Eastern (4 h off). `TZ=America/New_York` is set on the container. diff --git a/bin/normalize-sweep.sh b/bin/normalize-sweep.sh index c40f550..4319f40 100755 --- a/bin/normalize-sweep.sh +++ b/bin/normalize-sweep.sh @@ -6,8 +6,9 @@ # - anything else (HEVC, 10-bit, VFR, MKV, >1440p, >60fps): full-res re-encode -> name.mp4, # raw source kept in a hidden .src/ subfolder next to it (Jellyfin/copyparty hide dot-dirs) # - streams/<user>/<Title>/.live/<ts>.mp4 (MediaMTX writes here, hidden from Jellyfin/copyparty while live): -# when finished -> flat MP4 written to the parent folder, the .live source deleted. One copy, and nothing -# indexes a recording in progress. +# when finished -> flat MP4 published as streams/<user>/<Title>__<ts>.mp4 (one file per recording directly +# in the streamer's folder: Jellyfin would merge several files inside one title folder into ONE episode +# with "versions"), the .live source deleted, the empty title folder removed. # - Syncthing folders (marker dir .stfolder in the folder root, receive-only): compliant files are left # alone (zero copies); a non-compliant file is converted to name.mp4 (name.h264.mp4 if the raw is # already .mp4), the raw's name is appended to .stlocal (an #include'd ignore list) so Syncthing @@ -74,7 +75,8 @@ while IFS= read -r -d '' src; do live=0 if [[ $(basename "$(dirname "$src")") == .live ]]; then live=1 - base="$(dirname "$(dirname "$src")")/$(basename "${src%.*}")" + titledir="$(dirname "$(dirname "$src")")" # streams/<user>/<Title> + base="$(dirname "$titledir")/$(basename "$titledir")__$(basename "${src%.*}")" # streams/<user>/<Title>__<ts> fi synced=0; stroot="" d="$(dirname "$src")" @@ -122,7 +124,7 @@ while IFS= read -r -d '' src; do if (( rc == 0 )) && [[ -n $d1 && -n $d2 ]] && (( d2 >= d1 - 2 && d2 <= d1 + 2 )); then if (( live )); then mv -f "$tmp" "$out"; chown 1000:1000 "$out" 2>/dev/null - rm -f "$src"; rmdir "$(dirname "$src")" 2>/dev/null || true + rm -f "$src"; rmdir "$(dirname "$src")" "$titledir" 2>/dev/null || true # drop .live and the empty title folder log "done $out"; continue elif (( synced )); then rel_src="${src#"$stroot"/}"; rel_out="${out#"$stroot"/}" diff --git a/jellyfin-plugin-vanguard/Jellyfin.Plugin.Vanguard/Jellyfin.Plugin.Vanguard.csproj b/jellyfin-plugin-vanguard/Jellyfin.Plugin.Vanguard/Jellyfin.Plugin.Vanguard.csproj index 7141001..066e051 100644 --- a/jellyfin-plugin-vanguard/Jellyfin.Plugin.Vanguard/Jellyfin.Plugin.Vanguard.csproj +++ b/jellyfin-plugin-vanguard/Jellyfin.Plugin.Vanguard/Jellyfin.Plugin.Vanguard.csproj @@ -2,8 +2,8 @@ <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <RootNamespace>Jellyfin.Plugin.Vanguard</RootNamespace> - <AssemblyVersion>0.1.2.0</AssemblyVersion> - <FileVersion>0.1.2.0</FileVersion> + <AssemblyVersion>0.1.3.0</AssemblyVersion> + <FileVersion>0.1.3.0</FileVersion> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <GenerateDocumentationFile>false</GenerateDocumentationFile> diff --git a/jellyfin-plugin-vanguard/Jellyfin.Plugin.Vanguard/Services/LibraryPaths.cs b/jellyfin-plugin-vanguard/Jellyfin.Plugin.Vanguard/Services/LibraryPaths.cs index 322aa36..d951f44 100644 --- a/jellyfin-plugin-vanguard/Jellyfin.Plugin.Vanguard/Services/LibraryPaths.cs +++ b/jellyfin-plugin-vanguard/Jellyfin.Plugin.Vanguard/Services/LibraryPaths.cs @@ -26,6 +26,8 @@ public sealed class LibraryPaths var rel = full[(root.Length + 1)..].Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); if (rel.Length < 2) return null; var stem = Path.GetFileNameWithoutExtension(rel[^1]); - return rel.Length >= 3 ? new Parts(rel[0], rel[^2], stem) : new Parts(rel[0], null, stem); + if (rel.Length >= 3) return new Parts(rel[0], rel[^2], stem); // legacy: <user>/<Title>/<ts>.mp4 + var sep = stem.IndexOf("__", StringComparison.Ordinal); // flat: <user>/<Title>__<ts>.mp4 + return sep > 0 ? new Parts(rel[0], stem[..sep], stem[(sep + 2)..]) : new Parts(rel[0], null, stem); } } diff --git a/jellyfin-plugin-vanguard/build.yaml b/jellyfin-plugin-vanguard/build.yaml index 638ee3e..f3dca19 100644 --- a/jellyfin-plugin-vanguard/build.yaml +++ b/jellyfin-plugin-vanguard/build.yaml @@ -1,6 +1,6 @@ name: "Vanguard Metadata" guid: "d00ebd97-f987-4678-8089-c8f0f8a6ab3e" -version: "0.1.2.0" +version: "0.1.3.0" targetAbi: "12.4.0.0" framework: "net10.0" overview: "Season/episode metadata for gameplay recordings from timestamps, Discord and Authentik." @@ -14,6 +14,7 @@ artifacts: - "Jellyfin.Plugin.Vanguard.dll" - "Jellyfin.Plugin.Vanguard.Core.dll" changelog: > + 0.1.3.0 flat stream files <Title>__<ts>.mp4 (title parsed from the name) 0.1.2.0 feed says "New recording" (streams are published only after they finish) 0.1.1.0 playback-policy enforcement task (direct-play-only for all users) 0.1.0.0 initial skeleton