Composite "create channel from lineup" endpoint #63
Closed
opened 2026-07-01 23:01:47 +02:00 by timothy
·
4 comments
No Branch/Tag Specified
main
renovate/lucene.net
renovate/dotnet-monorepo
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
renovate/meziantou.analyzer-3.x
renovate/cliwrap-3.x
913-scripted-playout-401
release/v26.15.0-notes
issue-806-guard-populations
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.16.0
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Blocks
#89 ChicoryTV SPA: Channel Builder (primary deliverable)
timothy/ersatztv
Reference: timothy/ersatztv#63
Reference in New Issue
Block a user
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.
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
Notes
Starting implementation. Current approach after reading the existing create flows and /api/library/browse contract:
POST /api/channels/from-lineupoperation on the channel API. Request carries normalized channel basics,templateId, optional advanced overrides for template-stamped knobs, and an orderedlineuparray. Response will be a small composite payload with the createdchannelId,collectionId,programScheduleId, andplayoutIdso #89 can navigate/follow up without extra lookups./api/library/browse:Movie,TelevisionShow,TelevisionSeason,Artist,Collection,SmartCollection,MultiCollection,RerunCollection(mapped to first-run), andPlaylist. I am not addingSearchQueryor the non-browse leaf media types as a side door in this endpoint.collectionType+ the matching typed id field, not by browseidalone because browse ids are table-local and overlap. Mismatched/extra ids and unsupported kinds will be clean 422s; missing/deletedtemplateIdand 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.TvContextand 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 callSaveChangeseagerly.shuffleScheduleItems,randomStartPoint,fixedStartTimeBehavior) and item filler defaults (preRoll,midRoll,postRoll, fallback). #71/#77 can add more advanced knobs later without changing the basic shape.BuildPlayout(..., Reset)andRefreshChannelListonly 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.
Implementation progress:
POST /api/channels/from-lineup(CreateChannelFromLineup) with a composite response containingchannelId,collectionId,programScheduleId, andplayoutId.TvContexttransaction and commits Channel + generated lineup Collection + ProgramSchedule + schedule items + classic Playout together.BuildPlayout(..., Reset),RefreshChannelList, and search target refresh happen only after commit.NotFoundError; malformed/mismatched lineup entries return 422 validation errors.Verification completed so far:
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 typecheckpassedweb: npm run buildpassedExisting NU1903 advisory warnings remain for Scriban.Signed and Microsoft.OpenApi; no new test failures.
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 underErsatzTV/Controllers/Api/Requests/; channel controller route; Core response model; controller/handler/OpenAPI metadata tests; regeneratedErsatzTV/wwwroot/openapi/v1.jsonandweb/src/api/generated/v1.d.ts.Verification:
TZ=UTC dotnet build ErsatzTV.slnpassed;TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-restorepassed (431);TZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-restorepassed (493 + 1 skipped);npm run generate:apipassed;npm run typecheckpassed;npm run buildpassed. Existing NU1903 advisory warnings remain.Deferred: PR #134 is intentionally left open for Fable read-only review and merge. Gitea CI for head
6857a0d1is still queued/pending (Build & test, EF migration integrity; image job blocked by required checks) after repeated polling.Follow-up issues: none opened.
SearchQueryand 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.
Closing summary (merged via PR #134, main @
a126970d)What shipped —
POST /api/channels/from-lineup(operationIdCreateChannelFromLineup): 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):IsSystemPlaylist 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).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.