Regenerated ErsatzTV/wwwroot/openapi/v1.json and web/src/api/generated/v1.d.ts.
Accepted lineup kinds
V1 accepts the browse-backed schedulable kinds: Movie, TelevisionShow, TelevisionSeason, Artist, Collection, SmartCollection, MultiCollection, RerunCollection (first-run), and Playlist. It validates exactly one matching typed id per item and checks target existence.
Transaction strategy
The handler owns one TvContext and one EF transaction. It creates Channel + generated lineup Collection + ProgramSchedule + schedule items + classic Playout together, commits once, and only then queues BuildPlayout(..., Reset), RefreshChannelList, and search-target refresh. This avoids chaining existing create commands because those handlers each create their own context and save eagerly.
Template stamping
The selected ChannelTemplate is stamped at create time, not live-linked. Advanced overrides win over template values. Channel technical defaults, schedule defaults, and schedule-item filler defaults are copied into the created entities.
Verification
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 generate:api passed
web: npm run typecheck passed
web: npm run build passed
Known existing warnings: NU1903 advisories for Scriban.Signed and Microsoft.OpenApi.
Closes #63.
## What changed
- Added `POST /api/channels/from-lineup` (`CreateChannelFromLineup`) for the Channel Builder flow.
- Request shape: channel basics + `templateId` + optional `advanced` overrides + ordered `lineup` items using `/api/library/browse` typed scheduling ids.
- Response shape: `{ channelId, collectionId, programScheduleId, playoutId }` for #89-friendly follow-up/navigation.
- Regenerated `ErsatzTV/wwwroot/openapi/v1.json` and `web/src/api/generated/v1.d.ts`.
## Accepted lineup kinds
V1 accepts the browse-backed schedulable kinds: Movie, TelevisionShow, TelevisionSeason, Artist, Collection, SmartCollection, MultiCollection, RerunCollection (first-run), and Playlist. It validates exactly one matching typed id per item and checks target existence.
## Transaction strategy
The handler owns one `TvContext` and one EF transaction. It creates Channel + generated lineup Collection + ProgramSchedule + schedule items + classic Playout together, commits once, and only then queues `BuildPlayout(..., Reset)`, `RefreshChannelList`, and search-target refresh. This avoids chaining existing create commands because those handlers each create their own context and save eagerly.
## Template stamping
The selected `ChannelTemplate` is stamped at create time, not live-linked. Advanced overrides win over template values. Channel technical defaults, schedule defaults, and schedule-item filler defaults are copied into the created entities.
## Verification
- `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 generate:api` passed
- `web: npm run typecheck` passed
- `web: npm run build` passed
Known existing warnings: NU1903 advisories for Scriban.Signed and Microsoft.OpenApi.
Adversarial review found the previous multi-flood design non-viable:
PlayoutModeSchedulerFlood never yields to a following Dynamic-start
schedule item, so only the first item ever played, and grouping
media items into a Collection silently dropped the requested order.
Redesign:
- Single-item lineup: one ProgramScheduleItemFlood referencing the
target directly (media item / collection / smart / multi / rerun /
playlist); no generated collection or playlist. Response PlaylistId
is null.
- Multi-item lineup (>= 2): one generated IsSystem Playlist in a
get-or-created IsSystem PlaylistGroup ("Channel Lineups"), one
PlaylistItem per entry in lineup order with PlayAll=true, referenced
by a single Flood schedule item. Rerun collections and playlists are
rejected (422) in multi lineups (PlaylistItem/CollectionKey lack the
fields to enumerate them).
Review fixes:
- Normalize + strict-validate MediaType<->CollectionType pairs once up
front (422 on mismatch / wrong id / not exactly one id).
- Reject MultiCollection with non-Shuffle order (mirrors
PlayoutModeMustBeValid), Mirror playout source, all via 422.
- OnDemand parity: queue TimeShiftOnDemandPlayout post-commit.
- De-collide generated ProgramSchedule and Playlist names against their
unique indexes instead of leaking a UNIQUE-constraint DbUpdateException.
- Generic 422 on save failure + ILogger; AnyAsync existence checks;
Either/Validation unwrap via Match; XML doc on request DTO + endpoint.
- Response model: ChannelId, PlaylistId (nullable), ProgramScheduleId,
PlayoutId (CollectionId removed). Regenerated OpenAPI v1.json + v1.d.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fable review + in-session fix (Codex out of usage)
Two parallel review lenses (fable correctness + sonnet contract/tests) on head 6857a0d1 returned needs-work: the core multi-item lineup design was non-viable — every schedule item was a Dynamic-start ProgramScheduleItemFlood, and PlayoutModeSchedulerFlood never yields to a following Dynamic item (PlayoutModeSchedulerFlood.cs:50-53,128-129,163-177), so everything after lineup item 1 never played. Plus 7 more substantial findings: RerunCollection normalized only on a local copy in validation (build trusted the raw request → FK DbUpdateException reachable), advanced.playbackOrder inert for most kinds, MultiCollection bypassing PlayoutModeMustBeValid, missing OnDemand TimeShiftOnDemandPlayout, PlayoutSource.Mirror unhandled, recreate-after-delete leaking a raw UNIQUE-constraint message, and an orphan empty collection for all-collection-like lineups. Atomicity, 404/422 mapping, OpenAPI sync, nullable conventions, and channel-validation parity all checked out.
Single-item lineup: one Flood schedule item referencing the target directly (media via MediaItemId, RerunCollection → RerunFirstRun, Collection/Smart/Multi/Playlist via ids). No generated entities; response playlistId null.
Multi-item lineup: ONE generated IsSystem Playlist in a get-or-created system PlaylistGroup "Channel Lineups" (Trakt-lists precedent), one PlaylistItem per entry in lineup order with PlayAll=true + effective playbackOrder (genuinely honored by PlaylistEnumerator), referenced by a single Flood item. RerunCollection/Playlist entries rejected with 422 in multi-item lineups (PlaylistItem cannot reference them).
Lineup normalized ONCE (strict MediaType↔CollectionType pairs, 422 naming the index); MultiCollection + non-Shuffle/ShuffleInOrder → 422; OnDemand parity post-commit; Mirror → 422; name de-collision (" 2", " 3"…) for ProgramSchedule + Playlist; generated Collection removed entirely; response model collectionId → nullable playlistId.
Nits: Either unwrap idiom, stable 422 message + ILogger (no provider internals leaked), AnyAsync existence checks, XML docs, endpoint description states the real semantics.
Tests 5 → 21 on the handler (override-beats-template with differing values, multi-item ordering/PlayAll, RerunCollection paths, recreate-after-delete, OnDemand queue order, Mirror/MultiCollection 422s, rollback kept). ErsatzTV.Tests 447 green, Core.Tests 493+1skip, OpenAPI regen zero-diff, web typegen/typecheck/build green.
Backlog nit (LOW, not blocking): a pre-existing user-created (non-system) PlaylistGroup named "Channel Lineups" would break multi-item creates with a generic 422 — a specific pre-check message would be kinder.
## Fable review + in-session fix (Codex out of usage)
Two parallel review lenses (fable correctness + sonnet contract/tests) on head 6857a0d1 returned **needs-work**: the core multi-item lineup design was non-viable — every schedule item was a Dynamic-start `ProgramScheduleItemFlood`, and `PlayoutModeSchedulerFlood` never yields to a following Dynamic item (`PlayoutModeSchedulerFlood.cs:50-53,128-129,163-177`), so everything after lineup item 1 never played. Plus 7 more substantial findings: RerunCollection normalized only on a local copy in validation (build trusted the raw request → FK `DbUpdateException` reachable), `advanced.playbackOrder` inert for most kinds, MultiCollection bypassing `PlayoutModeMustBeValid`, missing OnDemand `TimeShiftOnDemandPlayout`, `PlayoutSource.Mirror` unhandled, recreate-after-delete leaking a raw UNIQUE-constraint message, and an orphan empty collection for all-collection-like lineups. Atomicity, 404/422 mapping, OpenAPI sync, nullable conventions, and channel-validation parity all checked out.
**Fix pushed as cf36c309** (opus implementer subagent, fable fork verification pass — approved):
- **Single-item lineup**: one Flood schedule item referencing the target directly (media via `MediaItemId`, RerunCollection → `RerunFirstRun`, Collection/Smart/Multi/Playlist via ids). No generated entities; response `playlistId` null.
- **Multi-item lineup**: ONE generated `IsSystem` Playlist in a get-or-created system PlaylistGroup "Channel Lineups" (Trakt-lists precedent), one `PlaylistItem` per entry in lineup order with `PlayAll=true` + effective `playbackOrder` (genuinely honored by `PlaylistEnumerator`), referenced by a single Flood item. RerunCollection/Playlist entries rejected with 422 in multi-item lineups (`PlaylistItem` cannot reference them).
- Lineup normalized ONCE (strict MediaType↔CollectionType pairs, 422 naming the index); MultiCollection + non-Shuffle/ShuffleInOrder → 422; OnDemand parity post-commit; Mirror → 422; name de-collision (" 2", " 3"…) for ProgramSchedule + Playlist; generated Collection removed entirely; response model `collectionId` → nullable `playlistId`.
- Nits: Either unwrap idiom, stable 422 message + `ILogger` (no provider internals leaked), `AnyAsync` existence checks, XML docs, endpoint description states the real semantics.
- Tests 5 → 21 on the handler (override-beats-template with differing values, multi-item ordering/PlayAll, RerunCollection paths, recreate-after-delete, OnDemand queue order, Mirror/MultiCollection 422s, rollback kept). ErsatzTV.Tests 447 green, Core.Tests 493+1skip, OpenAPI regen zero-diff, web typegen/typecheck/build green.
Backlog nit (LOW, not blocking): a pre-existing user-created (non-system) PlaylistGroup named "Channel Lineups" would break multi-item creates with a generic 422 — a specific pre-check message would be kinder.
timothy
merged commit a126970dcd into main2026-07-06 21:28:02 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #63.
What changed
POST /api/channels/from-lineup(CreateChannelFromLineup) for the Channel Builder flow.templateId+ optionaladvancedoverrides + orderedlineupitems using/api/library/browsetyped scheduling ids.{ channelId, collectionId, programScheduleId, playoutId }for #89-friendly follow-up/navigation.ErsatzTV/wwwroot/openapi/v1.jsonandweb/src/api/generated/v1.d.ts.Accepted lineup kinds
V1 accepts the browse-backed schedulable kinds: Movie, TelevisionShow, TelevisionSeason, Artist, Collection, SmartCollection, MultiCollection, RerunCollection (first-run), and Playlist. It validates exactly one matching typed id per item and checks target existence.
Transaction strategy
The handler owns one
TvContextand one EF transaction. It creates Channel + generated lineup Collection + ProgramSchedule + schedule items + classic Playout together, commits once, and only then queuesBuildPlayout(..., Reset),RefreshChannelList, and search-target refresh. This avoids chaining existing create commands because those handlers each create their own context and save eagerly.Template stamping
The selected
ChannelTemplateis stamped at create time, not live-linked. Advanced overrides win over template values. Channel technical defaults, schedule defaults, and schedule-item filler defaults are copied into the created entities.Verification
TZ=UTC dotnet build ErsatzTV.slnpassedTZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-restorepassed: 431 testsTZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-restorepassed: 493 passed, 1 skippedweb: npm run generate:apipassedweb: npm run typecheckpassedweb: npm run buildpassedKnown existing warnings: NU1903 advisories for Scriban.Signed and Microsoft.OpenApi.
Adversarial review found the previous multi-flood design non-viable: PlayoutModeSchedulerFlood never yields to a following Dynamic-start schedule item, so only the first item ever played, and grouping media items into a Collection silently dropped the requested order. Redesign: - Single-item lineup: one ProgramScheduleItemFlood referencing the target directly (media item / collection / smart / multi / rerun / playlist); no generated collection or playlist. Response PlaylistId is null. - Multi-item lineup (>= 2): one generated IsSystem Playlist in a get-or-created IsSystem PlaylistGroup ("Channel Lineups"), one PlaylistItem per entry in lineup order with PlayAll=true, referenced by a single Flood schedule item. Rerun collections and playlists are rejected (422) in multi lineups (PlaylistItem/CollectionKey lack the fields to enumerate them). Review fixes: - Normalize + strict-validate MediaType<->CollectionType pairs once up front (422 on mismatch / wrong id / not exactly one id). - Reject MultiCollection with non-Shuffle order (mirrors PlayoutModeMustBeValid), Mirror playout source, all via 422. - OnDemand parity: queue TimeShiftOnDemandPlayout post-commit. - De-collide generated ProgramSchedule and Playlist names against their unique indexes instead of leaking a UNIQUE-constraint DbUpdateException. - Generic 422 on save failure + ILogger; AnyAsync existence checks; Either/Validation unwrap via Match; XML doc on request DTO + endpoint. - Response model: ChannelId, PlaylistId (nullable), ProgramScheduleId, PlayoutId (CollectionId removed). Regenerated OpenAPI v1.json + v1.d.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Fable review + in-session fix (Codex out of usage)
Two parallel review lenses (fable correctness + sonnet contract/tests) on head
6857a0d1returned needs-work: the core multi-item lineup design was non-viable — every schedule item was a Dynamic-startProgramScheduleItemFlood, andPlayoutModeSchedulerFloodnever yields to a following Dynamic item (PlayoutModeSchedulerFlood.cs:50-53,128-129,163-177), so everything after lineup item 1 never played. Plus 7 more substantial findings: RerunCollection normalized only on a local copy in validation (build trusted the raw request → FKDbUpdateExceptionreachable),advanced.playbackOrderinert for most kinds, MultiCollection bypassingPlayoutModeMustBeValid, missing OnDemandTimeShiftOnDemandPlayout,PlayoutSource.Mirrorunhandled, recreate-after-delete leaking a raw UNIQUE-constraint message, and an orphan empty collection for all-collection-like lineups. Atomicity, 404/422 mapping, OpenAPI sync, nullable conventions, and channel-validation parity all checked out.Fix pushed as
cf36c309(opus implementer subagent, fable fork verification pass — approved):MediaItemId, RerunCollection →RerunFirstRun, Collection/Smart/Multi/Playlist via ids). No generated entities; responseplaylistIdnull.IsSystemPlaylist in a get-or-created system PlaylistGroup "Channel Lineups" (Trakt-lists precedent), onePlaylistItemper entry in lineup order withPlayAll=true+ effectiveplaybackOrder(genuinely honored byPlaylistEnumerator), referenced by a single Flood item. RerunCollection/Playlist entries rejected with 422 in multi-item lineups (PlaylistItemcannot reference them).collectionId→ nullableplaylistId.ILogger(no provider internals leaked),AnyAsyncexistence checks, XML docs, endpoint description states the real semantics.Backlog nit (LOW, not blocking): a pre-existing user-created (non-system) PlaylistGroup named "Channel Lineups" would break multi-item creates with a generic 422 — a specific pre-check message would be kinder.