Composite "create channel from lineup" endpoint #63

Closed
opened 2026-07-01 23:01:47 +02:00 by timothy · 4 comments
Owner

Part of #62 (Create Channel backend prerequisites).

Goal

A single API operation that atomically creates the full set of entities a working channel needs — today these are separate operations across the Channel, Schedule, Schedule Items, and Playout editors.

Scope

  • Accept: channel basics (name, number, image, shuffle/order, always-on, template ref) + an ordered lineup of library items.
  • Create, in one transaction: Channel + Collection (from the lineup) + Schedule + Schedule Items + Playout.
  • Roll back cleanly on any failure.
  • Apply the selected Channel Template's technical defaults (see #62 sub-issue for templates).

Notes

  • Depends conceptually on Channel Templates (technical defaults) and the library-item references from the browse API.
  • Keep it in the REST API layer (#2 conventions).
Part of #62 (Create Channel backend prerequisites). ## Goal A single API operation that atomically creates the full set of entities a working channel needs — today these are separate operations across the Channel, Schedule, Schedule Items, and Playout editors. ## Scope - Accept: channel basics (name, number, image, shuffle/order, always-on, template ref) + an ordered lineup of library items. - Create, in one transaction: **Channel + Collection (from the lineup) + Schedule + Schedule Items + Playout**. - Roll back cleanly on any failure. - Apply the selected Channel Template's technical defaults (see #62 sub-issue for templates). ## Notes - Depends conceptually on Channel Templates (technical defaults) and the library-item references from the browse API. - Keep it in the REST API layer (#2 conventions).
timothy added a new dependency 2026-07-02 01:08:42 +02:00
Author
Owner

Starting implementation. Current approach after reading the existing create flows and /api/library/browse contract:

  • Endpoint/DTO: add a named POST /api/channels/from-lineup operation on the channel API. Request carries normalized channel basics, templateId, optional advanced overrides for template-stamped knobs, and an ordered lineup array. Response will be a small composite payload with the created channelId, collectionId, programScheduleId, and playoutId so #89 can navigate/follow up without extra lookups.
  • Accepted lineup kinds for v1: exactly the browse-backed schedulable kinds emitted by /api/library/browse: Movie, TelevisionShow, TelevisionSeason, Artist, Collection, SmartCollection, MultiCollection, RerunCollection (mapped to first-run), and Playlist. I am not adding SearchQuery or the non-browse leaf media types as a side door in this endpoint.
  • Validation: validate by collectionType + the matching typed id field, not by browse id alone because browse ids are table-local and overlap. Mismatched/extra ids and unsupported kinds will be clean 422s; missing/deleted templateId and missing lineup targets will be clean 404s with the failing item called out. Channel name/number uniqueness will validate against the same normalized values persisted.
  • Transaction strategy: implement as one Application handler using one TvContext and an explicit EF transaction. It will create Channel + generated manual Collection + ordered CollectionItems + ProgramSchedule + schedule items + classic Playout atomically, then commit once. This intentionally does not chain the existing create commands because they each own their own context and call SaveChanges eagerly.
  • Template stamping: template values are copied at create time, not live-linked. Overrides win over template values. Template fields stamp Channel technical defaults (FFmpeg/watermark/fallback filler/stream/audio/subtitle/music-video/transcode/idle/playout mode/source) plus schedule defaults (shuffleScheduleItems, randomStartPoint, fixedStartTimeBehavior) and item filler defaults (preRoll, midRoll, postRoll, fallback). #71/#77 can add more advanced knobs later without changing the basic shape.
  • Playout build timing: enqueue BuildPlayout(..., Reset) and RefreshChannelList only after the transaction commits, matching existing create-playout behavior while avoiding background work against uncommitted/rolled-back rows.

Next I am adding focused tests first: successful atomic create/stamping/order, missing template/lineup validation, duplicate channel validation, and rollback on a mid-transaction failure.

Starting implementation. Current approach after reading the existing create flows and /api/library/browse contract: - Endpoint/DTO: add a named `POST /api/channels/from-lineup` operation on the channel API. Request carries normalized channel basics, `templateId`, optional advanced overrides for template-stamped knobs, and an ordered `lineup` array. Response will be a small composite payload with the created `channelId`, `collectionId`, `programScheduleId`, and `playoutId` so #89 can navigate/follow up without extra lookups. - Accepted lineup kinds for v1: exactly the browse-backed schedulable kinds emitted by `/api/library/browse`: `Movie`, `TelevisionShow`, `TelevisionSeason`, `Artist`, `Collection`, `SmartCollection`, `MultiCollection`, `RerunCollection` (mapped to first-run), and `Playlist`. I am not adding `SearchQuery` or the non-browse leaf media types as a side door in this endpoint. - Validation: validate by `collectionType` + the matching typed id field, not by browse `id` alone because browse ids are table-local and overlap. Mismatched/extra ids and unsupported kinds will be clean 422s; missing/deleted `templateId` and missing lineup targets will be clean 404s with the failing item called out. Channel name/number uniqueness will validate against the same normalized values persisted. - Transaction strategy: implement as one Application handler using one `TvContext` and an explicit EF transaction. It will create Channel + generated manual Collection + ordered CollectionItems + ProgramSchedule + schedule items + classic Playout atomically, then commit once. This intentionally does not chain the existing create commands because they each own their own context and call `SaveChanges` eagerly. - Template stamping: template values are copied at create time, not live-linked. Overrides win over template values. Template fields stamp Channel technical defaults (FFmpeg/watermark/fallback filler/stream/audio/subtitle/music-video/transcode/idle/playout mode/source) plus schedule defaults (`shuffleScheduleItems`, `randomStartPoint`, `fixedStartTimeBehavior`) and item filler defaults (`preRoll`, `midRoll`, `postRoll`, fallback). #71/#77 can add more advanced knobs later without changing the basic shape. - Playout build timing: enqueue `BuildPlayout(..., Reset)` and `RefreshChannelList` only after the transaction commits, matching existing create-playout behavior while avoiding background work against uncommitted/rolled-back rows. Next I am adding focused tests first: successful atomic create/stamping/order, missing template/lineup validation, duplicate channel validation, and rollback on a mid-transaction failure.
Author
Owner

Implementation progress:

  • Added POST /api/channels/from-lineup (CreateChannelFromLineup) with a composite response containing channelId, collectionId, programScheduleId, and playoutId.
  • Handler uses one TvContext transaction and commits Channel + generated lineup Collection + ProgramSchedule + schedule items + classic Playout together. BuildPlayout(..., Reset), RefreshChannelList, and search target refresh happen only after commit.
  • Template defaults are stamped onto Channel/Schedule/schedule-item fields at create time; advanced overrides win. Pre/mid/post-roll template fillers are stamped onto created schedule items.
  • Lineup validation accepts the browse-backed kinds and validates exactly one typed id with existence checks. Missing template/lineup targets return NotFoundError; malformed/mismatched lineup entries return 422 validation errors.
  • Regenerated OpenAPI and SPA API types.

Verification completed so far:

  • TZ=UTC dotnet build ErsatzTV.sln passed
  • TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-restore passed: 431 tests
  • TZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-restore passed: 493 passed, 1 skipped
  • web: npm run typecheck passed
  • web: npm run build passed

Existing NU1903 advisory warnings remain for Scriban.Signed and Microsoft.OpenApi; no new test failures.

Implementation progress: - Added `POST /api/channels/from-lineup` (`CreateChannelFromLineup`) with a composite response containing `channelId`, `collectionId`, `programScheduleId`, and `playoutId`. - Handler uses one `TvContext` transaction and commits Channel + generated lineup Collection + ProgramSchedule + schedule items + classic Playout together. `BuildPlayout(..., Reset)`, `RefreshChannelList`, and search target refresh happen only after commit. - Template defaults are stamped onto Channel/Schedule/schedule-item fields at create time; advanced overrides win. Pre/mid/post-roll template fillers are stamped onto created schedule items. - Lineup validation accepts the browse-backed kinds and validates exactly one typed id with existence checks. Missing template/lineup targets return `NotFoundError`; malformed/mismatched lineup entries return 422 validation errors. - Regenerated OpenAPI and SPA API types. Verification completed so far: - `TZ=UTC dotnet build ErsatzTV.sln` passed - `TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-restore` passed: 431 tests - `TZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-restore` passed: 493 passed, 1 skipped - `web: npm run typecheck` passed - `web: npm run build` passed Existing NU1903 advisory warnings remain for Scriban.Signed and Microsoft.OpenApi; no new test failures.
Author
Owner

Done

What was done: Implemented PR #134 for POST /api/channels/from-lineup, adding a composite create-channel API that creates the channel, generated lineup collection, program schedule, schedule items, and classic playout in one transaction. The endpoint stamps selected channel-template defaults at create time, applies advanced overrides, validates browse-backed typed lineup ids, queues playout build/refresh after commit, and returns created ids for #89.

Root cause: n/a — feature implementation.

Files changed: Application command/handler under ErsatzTV.Application/Channels/Commands/; API request DTO under ErsatzTV/Controllers/Api/Requests/; channel controller route; Core response model; controller/handler/OpenAPI metadata tests; regenerated ErsatzTV/wwwroot/openapi/v1.json and web/src/api/generated/v1.d.ts.

Verification: TZ=UTC dotnet build ErsatzTV.sln passed; TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-restore passed (431); TZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-restore passed (493 + 1 skipped); npm run generate:api passed; npm run typecheck passed; npm run build passed. Existing NU1903 advisory warnings remain.

Deferred: PR #134 is intentionally left open for Fable read-only review and merge. Gitea CI for head 6857a0d1 is still queued/pending (Build & test, EF migration integrity; image job blocked by required checks) after repeated polling.

Follow-up issues: none opened. SearchQuery and non-browse leaf media kinds are intentionally not accepted in v1 until the picker emits them.

Docs updated: OpenAPI and generated SPA API types updated; no operational docs impact.

## Done **What was done**: Implemented PR #134 for `POST /api/channels/from-lineup`, adding a composite create-channel API that creates the channel, generated lineup collection, program schedule, schedule items, and classic playout in one transaction. The endpoint stamps selected channel-template defaults at create time, applies advanced overrides, validates browse-backed typed lineup ids, queues playout build/refresh after commit, and returns created ids for #89. **Root cause**: n/a — feature implementation. **Files changed**: Application command/handler under `ErsatzTV.Application/Channels/Commands/`; API request DTO under `ErsatzTV/Controllers/Api/Requests/`; channel controller route; Core response model; controller/handler/OpenAPI metadata tests; regenerated `ErsatzTV/wwwroot/openapi/v1.json` and `web/src/api/generated/v1.d.ts`. **Verification**: `TZ=UTC dotnet build ErsatzTV.sln` passed; `TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-restore` passed (431); `TZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-restore` passed (493 + 1 skipped); `npm run generate:api` passed; `npm run typecheck` passed; `npm run build` passed. Existing NU1903 advisory warnings remain. **Deferred**: PR #134 is intentionally left open for Fable read-only review and merge. Gitea CI for head `6857a0d1` is still queued/pending (`Build & test`, EF migration integrity; image job blocked by required checks) after repeated polling. **Follow-up issues**: none opened. `SearchQuery` and non-browse leaf media kinds are intentionally not accepted in v1 until the picker emits them. **Docs updated**: OpenAPI and generated SPA API types updated; no operational docs impact.
Author
Owner

Closing summary (merged via PR #134, main @ a126970d)

What shipped — POST /api/channels/from-lineup (operationId CreateChannelFromLineup): atomically creates Channel + ProgramSchedule + schedule item(s) + Classic Playout in one TvContext transaction, stamping the selected ChannelTemplate's defaults with Advanced overrides winning. The lineup design changed after review (the as-implemented Codex version used per-item Dynamic floods, which never advance — items after the first would never play):

  • Single-item lineup: one Flood schedule item referencing the target directly (Movie/Show/Season/Artist via MediaItemId; Collection/SmartCollection/MultiCollection via ids; RerunCollection → RerunFirstRun; Playlist via PlaylistId). No generated entities.
  • Multi-item lineup: ONE generated IsSystem Playlist in system PlaylistGroup "Channel Lineups", one PlaylistItem per entry in lineup order (PlayAll=true, effective playbackOrder honored per entry), referenced by a single Flood item. RerunCollection/Playlist entries → 422 in multi-item lineups (PlaylistItem cannot reference them).
  • Response: channelId, playlistId (null for single-item), programScheduleId, playoutId. 404 for missing template/references, 422 for validation/mismatched typed ids; generated names de-collide (" 2"…) instead of leaking UNIQUE-constraint errors; OnDemand queues TimeShiftOnDemandPlayout post-commit; PlayoutSource.Mirror rejected.

Files — CreateChannelFromLineup{,Handler}.cs (Application/Channels/Commands), CreateChannelFromLineupResponseModel.cs (Core/Api), ChannelController.cs + CreateChannelFromLineupRequest.cs, handler/controller/contract tests, regenerated v1.json + web v1.d.ts.

Verification — ErsatzTV.Tests 447 green (handler tests 5→21), Core.Tests 493+1skip, OpenAPI regen zero-diff, web typegen/typecheck/build green; PR CI green; review: fable correctness + sonnet contract lenses, opus fix implementer, fable fork verification (all in PR #134 comments).

Deferred / backlog nits — pre-existing non-system PlaylistGroup named "Channel Lineups" breaks multi-item creates with a generic 422 (pre-check message would be kinder); non-DbUpdateException failures surface as bare 500 (rollback still clean); SearchQuery + non-browse leaf kinds intentionally rejected until /api/library/browse emits them.

Docs — handoff docs/handoffs/chicorytv-issue-queue.md updated (pop #63 → next #89). Epic #62's backend contract is now complete.

## Closing summary (merged via PR #134, main @ a126970d) **What shipped** — `POST /api/channels/from-lineup` (operationId `CreateChannelFromLineup`): atomically creates Channel + ProgramSchedule + schedule item(s) + Classic Playout in one TvContext transaction, stamping the selected ChannelTemplate's defaults with Advanced overrides winning. **The lineup design changed after review** (the as-implemented Codex version used per-item Dynamic floods, which never advance — items after the first would never play): - **Single-item lineup**: one Flood schedule item referencing the target directly (Movie/Show/Season/Artist via MediaItemId; Collection/SmartCollection/MultiCollection via ids; RerunCollection → RerunFirstRun; Playlist via PlaylistId). No generated entities. - **Multi-item lineup**: ONE generated `IsSystem` Playlist in system PlaylistGroup "Channel Lineups", one PlaylistItem per entry in lineup order (`PlayAll=true`, effective playbackOrder honored per entry), referenced by a single Flood item. RerunCollection/Playlist entries → 422 in multi-item lineups (PlaylistItem cannot reference them). - Response: `channelId`, `playlistId` (null for single-item), `programScheduleId`, `playoutId`. 404 for missing template/references, 422 for validation/mismatched typed ids; generated names de-collide (" 2"…) instead of leaking UNIQUE-constraint errors; OnDemand queues TimeShiftOnDemandPlayout post-commit; PlayoutSource.Mirror rejected. **Files** — CreateChannelFromLineup{,Handler}.cs (Application/Channels/Commands), CreateChannelFromLineupResponseModel.cs (Core/Api), ChannelController.cs + CreateChannelFromLineupRequest.cs, handler/controller/contract tests, regenerated v1.json + web v1.d.ts. **Verification** — ErsatzTV.Tests 447 green (handler tests 5→21), Core.Tests 493+1skip, OpenAPI regen zero-diff, web typegen/typecheck/build green; PR CI green; review: fable correctness + sonnet contract lenses, opus fix implementer, fable fork verification (all in PR #134 comments). **Deferred / backlog nits** — pre-existing non-system PlaylistGroup named "Channel Lineups" breaks multi-item creates with a generic 422 (pre-check message would be kinder); non-DbUpdateException failures surface as bare 500 (rollback still clean); SearchQuery + non-browse leaf kinds intentionally rejected until /api/library/browse emits them. **Docs** — handoff docs/handoffs/chicorytv-issue-queue.md updated (pop #63 → next #89). Epic #62's backend contract is now complete.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: timothy/ersatztv#63