Auto-tune channels can now carry per-content-source rotation weights (weighted
round-robin, e.g. 3x Show A / 1x Show B) and query corrections (exclude /
add-untagged), supplied at bulk-create time via an optional
`sources: [{sourceId, weight, excluded}]` on each AutoTunedChannelRequest.
Design (Option A, reuse #70): when a source is customized the channel is backed
by a system-owned MultiCollection of per-source SmartCollections carrying the
weights, with PlaybackOrder.WeightedShuffle -- the exact path
WeightedShuffleCollectionEnumerator already consumes. All-default weights keep
the #69 single-SmartCollection fair-share shape.
- Discriminators: TV -> live show_title:"X" (episodes carry no parent-show id in
the index); movies -> stable id:{mediaItemId}.
- Materialization is axis-dependent: TV materializes every base show individually
(un-weighted shows keep per-show fair-share) + a live remainder at weight 1;
MovieGenre materializes only touched movies + one count-weighted remainder.
- Remainder = (base) AND NOT (materialized union excluded) -- a partition.
- New nullable OwnedByChannelId on SmartCollection + MultiCollection
(dual-provider migration); owned rows are hidden from the collection lists and
cascade-cleaned on channel delete.
Tests: AutoTuneAxisMap query/partition units; DB-backed weighted-path handler
tests (TV materialize-all, movie count-remainder, exclusion, no-customization
fallback); delete-cleanup. Docs: decisions.md, domain-model.md, api-conventions.md;
OpenAPI trio regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend mutation-hardening cluster from the 2026-07-11 mutation-safety audit
sweep (adversarial-reviewer #22/#23), the parallel-safe backend-isolated slice.
audit#22 F4 — standardize post-commit enqueues on CancellationToken.None:
20 command handlers under MediaCollections/, ProgramSchedules/, Playouts/,
Channels/ threaded the request cancellationToken into work that runs AFTER
SaveChangesAsync commits (WriteAsync rebuild/refresh enqueues, mediator.Publish,
reindex, cache Refresh, and post-commit lookups that gate an enqueue). A late
client-disconnect then turns an already-durable commit into a thrown request AND
drops the side effect. Generalizes the #251 deco-handler fix. Excludes
BuildPlayoutHandler (worker/background token, not a client-disconnect token),
the config/FFmpeg multi-upsert handlers (partial-commit case, separate
follow-up), and response-projection reloads (correctly keep the request token).
audit#22 F2 — DeleteChannelHandler/DeletePlayoutHandler now delete the channel
guide {number}.xml through IFileSystem.File.Delete (observable under
MockFileSystem) and BEFORE the commit (a post-commit delete orphans the xml on a
crash; the xml is regenerable on demand, so pre-commit delete is the safe order).
audit#23 F4 — ReplacePlayoutAlternateScheduleItemsHandler rejects an empty item
list in the handler (not only the controller pre-guard) so a direct caller can't
trip the Max()-on-empty crash.
Docs: api-conventions.md §7a (post-commit token convention + boundaries),
decisions.md entry (rationale, sweep scope, #253 PR2-4 coordination note).
Tests: guide-cache-delete-through-FS for both delete handlers, empty-list guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First slice of the REST API (ersatztv#2). Idiomatic REST over existing MediatR handlers; design in docs/rest-api.md.
Foundation: NotFoundError : BaseError (Core) to split 404 from 422; ApiResults mapping helpers (Either/Option -> 201+Location / 200 / 204 / 404 / 422), additive (existing *ToActionResult untouched); ApiKeyAuthorizationFilter (optional X-Api-Key on mutating actions, gated by Api:WriteKey, no-op when unset) — independent of the IPTV JWT toggle so Jellyfin/Dispatcharr reads are unaffected, applied at controller-class level so every mutating action incl. ResetPlayout is covered fail-safe.
Channels: POST /api/channels (201+Location), PUT/{id} (200/404), DELETE/{id} (204/404), GET/{id} (200/404). Request DTOs -> existing commands; responses reuse ChannelViewModel. Ported page-only validations into handlers (ShowInEpg-when-disabled, external-logo-URL, Group NotEmpty) + FFmpegProfile/Watermark/Filler existence on update for create-parity. Fixed a latent 500: the .Filter(c>0) existence pattern threw TaskCanceledException on not-found; rewritten to AnyAsync.
Tests (NUnit, TZ=UTC): handler success/404/422, ApiResults mapping, API-key filter, controller status/Location/mapping, a controller-security regression net, and a create->read->delete EF integration test on a new in-memory SQLite harness. ErsatzTV.Tests 46/46, Architecture 5/5.
Refs #34
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>