merge(521): migrate decisions.md range C tail [decisions-edit]
This commit is contained in:
@@ -2502,6 +2502,11 @@ for its whole slot. This closes that gap.
|
||||
**Signals:** GetOrAddFolder, LibraryFolders navigation, ArgumentNullException · paths: `ILibraryRepository.GetOrAddFolder`, `JellyfinMusicVideoLibraryScanner` · issues: #488
|
||||
**Mechanics:** `LibraryRepositoryTests` (`LibraryPath.LibraryFolders == null` case)
|
||||
|
||||
`key: scan.getoraddfolder-db-lookup` · `status: active` · `since: 2026-07-20` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** `GetOrAddFolder` looks the existing folder up with a DB query on `(LibraryPathId, Path)`, not the caller's `LibraryPath.LibraryFolders` navigation collection (which is null on the remote/Jellyfin path).
|
||||
**Signals:** folder lookup contract, remote scanner crash, ArgumentNullException, eager-load assumption · paths: `ILibraryRepository.GetOrAddFolder`, `LibraryRepositoryTests`, `JellyfinMusicVideoLibraryScanner` · issues: #488
|
||||
**Mechanics:** `ILibraryRepository.GetOrAddFolder`; test coverage in `LibraryRepositoryTests`
|
||||
|
||||
`GetOrAddFolder` looked the existing folder up by reading `libraryPath.LibraryFolders` in memory. That
|
||||
navigation collection is only eager-loaded on the **local** scan path — `LibraryRepository.GetLibrary`
|
||||
`.Include(l => l.Paths).ThenInclude(p => p.LibraryFolders)` — which every `*FolderScanner` goes through. The
|
||||
@@ -2538,6 +2543,11 @@ remote scanner tripped it, and the feature had never run in prod, CI, or locally
|
||||
**Signals:** music-video trash sweep, path-based identity, cross-kind safety · paths: `JellyfinMusicVideoLibraryScanner.TrashMissingMusicVideos`, `FindMusicVideoPaths`/`DeleteByPath` · issues: #494, #477, #488, #496
|
||||
**Mechanics:** `ScanLibrary_Should_Not_CrossDelete_Movie_Or_Show_Sharing_The_LibraryPath`; `ScanLibrary_Should_Not_Sweep_When_Jellyfin_Returns_Zero_Items`
|
||||
|
||||
`key: scan.musicvideo-reconciliation` · `status: active` · `since: 2026-07-20` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** `JellyfinMusicVideoLibraryScanner` reconciles by a library-scoped local-path diff and hard-deletes music videos gone upstream, since it has no server itemId/etag identity to diff against.
|
||||
**Signals:** music-video trash sweep, path-keyed identity, cross-kind safety, empty-fetch guard reuse · paths: `JellyfinMusicVideoLibraryScanner`, `TrashMissingMusicVideos`, `IMusicVideoRepository`, `MediaServerReconciliationGuard` · issues: #494, #477, #488, #496
|
||||
**Mechanics:** `JellyfinMusicVideoLibraryScanner.ScanLibrary`/`TrashMissingMusicVideos`; integration tests extending the #488 harness
|
||||
|
||||
The Jellyfin music-video scanner did add/update only — a music video removed on the Jellyfin side lingered in
|
||||
ErsatzTV forever and could still be scheduled. It now runs a trash sweep at the end of `ScanLibrary`
|
||||
(`TrashMissingMusicVideos`), mirroring the `MediaServer{Movie,Television,OtherVideo}LibraryScanner` "gone
|
||||
@@ -2579,6 +2589,11 @@ media-server identity those base scanners rely on.
|
||||
**Signals:** mixed-content library, `LibraryMediaKind.Mixed`, per-kind sequential scan · paths: `JellyfinApiClient.Project()`, `ScanMixedLibrary`, `SynchronizeJellyfinLibraryByIdHandler` · issues: #489, #474, #488
|
||||
**Mechanics:** local mixed libraries deliberately unsupported (`LocalFolderScanner.VideoFileExtensions` hazard)
|
||||
|
||||
`key: scan.jellyfin-mixed-content-library` · `status: active` · `since: 2026-07-20` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** a Jellyfin library whose collection type is `mixed` or absent maps to `LibraryMediaKind.Mixed` and runs the movie, TV and music-video scanners against it in sequence, instead of being dropped.
|
||||
**Signals:** mixed-content library, LibraryMediaKind, per-kind scan dispatch, silent-success bug · paths: `JellyfinApiClient.Project`, `ScanMixedLibrary`, `ScanLocalLibraryHandler`, `SynchronizeJellyfinLibraryByIdHandler` · issues: #489, #474, #488
|
||||
**Mechanics:** `LibraryMediaKind.Mixed` dispatch in the Jellyfin sync handlers; per-kind scanners queried by `parentId` + `includeItemTypes`
|
||||
|
||||
A Jellyfin library whose collection type is `mixed` — or absent — now maps to `LibraryMediaKind.Mixed`
|
||||
instead of being dropped by `JellyfinApiClient.Project()`'s `_ => None`. Scanning it runs the movie,
|
||||
television and music-video scanners in sequence against that one library.
|
||||
@@ -2644,6 +2659,11 @@ fixed here:
|
||||
**Signals:** channel logo, watermark bug preset, ChannelWatermark seeding · paths: `ChannelWatermarkImageSource.ChannelLogo`, `DbInitializer.Initialize`, `watermark.channel_bug_seeded` · issues: #67, #502
|
||||
**Mechanics:** `DbInitializerChannelBugWatermarkTests`; `WatermarkResponseModel.imageSource`
|
||||
|
||||
`key: iptv.logo-drives-bug-preset` · `status: active` · `since: 2026-07-20` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** one uploaded logo drives both the listing logo and the on-screen bug via a shared, seeded `ChannelLogo`-sourced watermark preset, not per-channel bug schema.
|
||||
**Signals:** channel bug, ChannelWatermark, ChannelLogo imageSource, seed/adopt semantics, quick-add · paths: `ChannelWatermarkImageSource`, `DbInitializer.Initialize`, `WatermarkResponseModel`, `DbInitializerChannelBugWatermarkTests` · issues: #67, #286, #502
|
||||
**Mechanics:** `DbInitializer` seeded `Channel Bug` watermark + `watermark.channel_bug_seeded` ConfigElement marker; `WatermarkResponseModel.imageSource`
|
||||
|
||||
#67 asked that one uploaded image drive both the listing logo and the on-screen bug, separably
|
||||
overridable, with preview. Most of it already existed: `ChannelWatermarkImageSource.ChannelLogo`
|
||||
resolves the channel's own logo artwork at render time at all three watermark precedence levels, and
|
||||
@@ -2687,6 +2707,11 @@ creating a second preset on the Watermarks screen.
|
||||
**Signals:** QSV, native VA-API decode, Dolby Vision passthrough · paths: `FFmpegProfile.QsvPreferNativeDecoder`, `QsvPipelineBuilder.SetTonemap` · issues: #498, #505
|
||||
**Mechanics:** `docs/superpowers/specs/2026-07-20-qsv-native-decode-design.md`; migration `HasDefaultValue(true)`
|
||||
|
||||
`key: ffmpeg.qsv-decode-encode-split` · `status: active` · `since: 2026-07-20` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** `FFmpegProfile.QsvPreferNativeDecoder` (default on, Linux-only) decodes QSV profiles with the native VA-API decoder while still encoding with QSV, mirroring Jellyfin's hybrid decode/encode toggle instead of a general decode-family enum.
|
||||
**Signals:** QSV, VA-API, Dolby Vision passthrough, hybrid decode/encode, HDR tonemap fallback · paths: `FFmpegProfile.QsvPreferNativeDecoder`, `QsvPipelineBuilder`, `docs/superpowers/specs/2026-07-20-qsv-native-decode-design.md` · issues: #498, #505
|
||||
**Mechanics:** `QsvPipelineBuilder` decoder-mode branch; migration `HasDefaultValue(true)` on the nullable `bool?` column
|
||||
|
||||
`FFmpegProfile.HardwareAcceleration` picked one pipeline builder for **both** decode and encode, so an
|
||||
Intel QSV profile decoded with the QSV decoder — which is materially less tolerant of imperfect H.264 than
|
||||
FFmpeg's native VA-API decoder and cannot carry Dolby Vision metadata. Jellyfin, on identical hardware,
|
||||
@@ -2730,6 +2755,11 @@ ErsatzTV. Full design: `docs/superpowers/specs/2026-07-20-qsv-native-decode-desi
|
||||
**Signals:** CI lane definition, per-job memory cap, small lane widening · paths: `.gitea/workflows/docker-build.yml`, `.gitea/workflows/ci-image.yml` · issues: server-management#639, #406, #604, #574
|
||||
**Mechanics:** sum-of-caps rule (#406/#604); second jazz runner at `--cpu-shares=128`
|
||||
|
||||
`key: ci.small-lane-git-only` · `status: active` · `since: 2026-07-20` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** the `small` CI lane is defined by what a job does (git-only), not its typical runtime; both `docker build` jobs move to `ubuntu-latest` so `small`'s per-slot memory cap can shrink and its concurrency widen.
|
||||
**Signals:** runner lane definition, memory cap vs capacity, act setup-phase hang, docker build placement · paths: `docker-build.yml`, `ci-image.yml`, runner config · issues: server-management#639, #406, #604, #574
|
||||
**Mechanics:** `.gitea/workflows/docker-build.yml` and `ci-image.yml` `runs-on`; `docs/ci-cd.md`
|
||||
|
||||
- **The `small` lane is defined by what a job *does*, not by how long it usually takes.** Both jobs
|
||||
removed from it here were justified as small on a runtime argument that only held in the common case:
|
||||
`docker-build.yml`'s `build` is a 1-second skip on PR runs (but a real image build on main/tags), and
|
||||
@@ -2815,6 +2845,11 @@ again — the duplication is what let the defect exist in triplicate. Covered by
|
||||
|
||||
## 2026-07-20 — HLS cold start is fixed with `-readrate_initial_burst`, not by raising the work-ahead limit (#350)
|
||||
|
||||
`key: ffmpeg.hls-cold-start-burst` · `status: active` · `since: 2026-07-20` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** HLS cold-start latency is fixed with a bounded `-readrate_initial_burst` (gated on FFmpeg ≥6.1 capability detection), not by raising `work_ahead_limit`, which would remove the concurrency guarantee it exists for.
|
||||
**Signals:** HLS cold start, readrate, work_ahead_limit, HlsSessionWorker, FFmpegKnownOption capability gate · paths: `HlsSessionWorker`, `SetRealtimeInput`, `FFmpegPlaybackSettingsCalculator`, `FFmpegKnownOption`/`HasOption` · issues: #350
|
||||
**Mechanics:** `SetRealtimeInput` readrate-burst option; `FFmpegKnownOption.HasOption` version-capability gate
|
||||
|
||||
- **`-readrate` throttles from the first read, so it sets a floor on time-to-first-segment.** The
|
||||
realtime playback path pins input reading to 1.05× wall clock so a channel behaves like live TV.
|
||||
Since `OutputFormatHls.SegmentSeconds` is 4 and the segmenter serves the playlist only once the
|
||||
@@ -2859,6 +2894,11 @@ again — the duplication is what let the defect exist in triplicate. Covered by
|
||||
|
||||
## 2026-07-20 — MCP server (`ErsatzTV.Mcp`) built fresh over frozen `/api/v1`: read + cautious writes (#58)
|
||||
|
||||
`key: mcp.server-foundation` · `status: active` · `since: 2026-07-20` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** `ErsatzTV.Mcp` is a fresh stdio JSON-RPC server wrapping frozen `/api/v1` with explicit narrow per-endpoint tools, read-only-by-default enforced at runtime (`ERSATZTV_ALLOW_WRITES`), machine-key auth, and opt-in `If-Match`.
|
||||
**Signals:** MCP server, tool catalog, read-only default, ERSATZTV_ALLOW_WRITES, X-Api-Key, If-Match round-trip · paths: `ErsatzTV.Mcp`, `docs/mcp.md` · issues: #58, #286, #197, #76, #289, #63, #64, #65, #66, #67, #68
|
||||
**Mechanics:** `docs/mcp.md`; `ErsatzTV.Mcp` tool executor and catalog
|
||||
|
||||
The MCP server (issue #58, gated on #286 route-freeze + #197 security review, both closed) is built
|
||||
**fresh** as `ErsatzTV.Mcp` — a stdio JSON-RPC server wrapping `/api/v1` — superseding the closed
|
||||
read-only PR #76 rather than rebasing it. Full doc: `docs/mcp.md`. Decisions frozen:
|
||||
@@ -2886,6 +2926,11 @@ read-only PR #76 rather than rebasing it. Full doc: `docs/mcp.md`. Decisions fro
|
||||
their backend endpoints exist. No `/api` endpoint was added, so no OpenAPI regen.
|
||||
## 2026-07-20 — Remote graphics-engine images are fetched through a bounded, pooled `IRemoteImageFetcher`; re-fetched per element init, not cached (#511)
|
||||
|
||||
`key: ffmpeg.remote-image-fetcher-bounded` · `status: active` · `since: 2026-07-20` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** remote graphics-engine images are fetched through `IRemoteImageFetcher` with a pooled `HttpClientFactory` client, a body-covering deadline, a wire-transfer size cap, and a decoder-enforced `DecoderOptions.MaxFrames` bound re-verified post-decode — never cached, re-fetched per element init.
|
||||
**Signals:** remote image fetch, decompression bomb, MaxFrames, decode budget vs retention budget, SSRF accepted risk · paths: `IRemoteImageFetcher`, `HttpRemoteImageFetcher`, `ImageElementBase.LoadImage`, `DecoderOptions.MaxFrames`, `WatermarkElementRemoteImageTests` · issues: #511, #502, #289
|
||||
**Mechanics:** `HttpRemoteImageFetcher` (Infrastructure) over `IHttpClientFactory`; `EnsureDecodeAffordable`/`EnsureScaledFramesAffordable` pure-function budget checks
|
||||
|
||||
`ImageElementBase.LoadImage` fetched `http(s)` images with a throwaway `new HttpClient()` and
|
||||
`GetStreamAsync`. That is unbounded in three directions at once — no timeout override (the 100s
|
||||
`HttpClient` default), no response size cap, and a new connection pool per element — and it runs
|
||||
|
||||
Reference in New Issue
Block a user