Build REST API for channel/collection/schedule CRUD #2

Closed
opened 2026-02-26 23:45:57 +01:00 by timothy · 3 comments
Owner

Problem

ErsatzTV has no REST API for creating, updating, or deleting channels, collections, schedules, or playouts. The only way to manage these is through the Blazor Server UI or direct SQLite database manipulation.

Direct DB manipulation is fragile because:

  • EF Core uses TPT inheritance (e.g. ProgramScheduleItem → ProgramScheduleOneItem) — must insert into subtypes
  • Many NOT NULL constraints and enum values to get right (ScheduleKind, PlaybackOrder, CollectionType, etc.)
  • Container must be stopped for writes (WAL mode only allows concurrent reads)
  • No validation — easy to create broken state (e.g. ScheduleKind=0 causes "Cannot build playout type None")

Proposed API

REST endpoints under /api/ following the existing pattern:

Channels

POST   /api/channels                    # Create channel
PUT    /api/channels/{id}               # Update channel
DELETE /api/channels/{id}               # Delete channel
GET    /api/channels                    # (exists) List channels
GET    /api/channels/{number}           # (exists) Get channel by number

Collections

POST   /api/collections                 # Create collection
PUT    /api/collections/{id}            # Update collection
DELETE /api/collections/{id}            # Delete collection
POST   /api/collections/{id}/items      # Add items to collection
DELETE /api/collections/{id}/items/{itemId}  # Remove item

Schedules

POST   /api/schedules                   # Create schedule
PUT    /api/schedules/{id}              # Update schedule
DELETE /api/schedules/{id}              # Delete schedule
POST   /api/schedules/{id}/items        # Add schedule item
DELETE /api/schedules/{id}/items/{itemId}    # Remove schedule item

Playouts

POST   /api/playouts                    # Create playout (link channel to schedule)
DELETE /api/playouts/{id}               # Delete playout
POST   /api/channels/{number}/playout/reset  # (exists) Reset playout

Implementation Approach

  • Follow existing MediatR CQRS pattern — create Command + Handler pairs in ErsatzTV.Application/
  • Add controllers in ErsatzTV/Controllers/
  • Reuse existing domain validation from Blazor page code-behind
  • Return proper HTTP status codes (201 Created, 404 Not Found, 422 Validation Error)
  • All mutations go through EF Core (no direct SQL) so cascade deletes and constraints are handled
## Problem ErsatzTV has no REST API for creating, updating, or deleting channels, collections, schedules, or playouts. The only way to manage these is through the Blazor Server UI or direct SQLite database manipulation. Direct DB manipulation is fragile because: - EF Core uses TPT inheritance (e.g. `ProgramScheduleItem` → `ProgramScheduleOneItem`) — must insert into subtypes - Many NOT NULL constraints and enum values to get right (ScheduleKind, PlaybackOrder, CollectionType, etc.) - Container must be stopped for writes (WAL mode only allows concurrent reads) - No validation — easy to create broken state (e.g. ScheduleKind=0 causes "Cannot build playout type None") ## Proposed API REST endpoints under `/api/` following the existing pattern: ### Channels ``` POST /api/channels # Create channel PUT /api/channels/{id} # Update channel DELETE /api/channels/{id} # Delete channel GET /api/channels # (exists) List channels GET /api/channels/{number} # (exists) Get channel by number ``` ### Collections ``` POST /api/collections # Create collection PUT /api/collections/{id} # Update collection DELETE /api/collections/{id} # Delete collection POST /api/collections/{id}/items # Add items to collection DELETE /api/collections/{id}/items/{itemId} # Remove item ``` ### Schedules ``` POST /api/schedules # Create schedule PUT /api/schedules/{id} # Update schedule DELETE /api/schedules/{id} # Delete schedule POST /api/schedules/{id}/items # Add schedule item DELETE /api/schedules/{id}/items/{itemId} # Remove schedule item ``` ### Playouts ``` POST /api/playouts # Create playout (link channel to schedule) DELETE /api/playouts/{id} # Delete playout POST /api/channels/{number}/playout/reset # (exists) Reset playout ``` ## Implementation Approach - Follow existing MediatR CQRS pattern — create Command + Handler pairs in `ErsatzTV.Application/` - Add controllers in `ErsatzTV/Controllers/` - Reuse existing domain validation from Blazor page code-behind - Return proper HTTP status codes (201 Created, 404 Not Found, 422 Validation Error) - All mutations go through EF Core (no direct SQL) so cascade deletes and constraints are handled
timothy added the apienhancementpriority: medium labels 2026-03-08 23:02:36 +01:00
Author
Owner

Phase 1 complete → Phase 2 underway

Phase 1 investigation (4 parallel code-grounded agents) + design is signed off. Design doc landed at docs/rest-api.md.

Key findings:

  • No migration needed — pure CRUD over existing tables; the 4 ProgramScheduleItem TPT subtype tables already exist.
  • Every command already exists as a MediatR handler returning Either<BaseError, T> → the API is mostly thin controllers over existing handlers.
  • Substantive validation already lives in the handlers (uniqueness, FK-exists, playout business rules); only a few format/UX checks are page-only and get ported.
  • OpenAPI/Scalar, camelCase JSON, enums-as-strings already wired.

Decisions:

  • Routes → idiomatic REST (modernize/standardize mandate).
  • Writes auth → dedicated API key (Api__WriteKey), decoupled from the IPTV JWT toggle → no change needed on Jellyfin/Dispatcharr (they only consume /iptv/* reads, untouched). Reusing JWT was rejected because its single global toggle also gates the IPTV feeds and its tokens expire in 1 day.
  • Status codes → 201+Location / 200 / 204 / 404 / 422 (+ NotFoundError type). Request DTOs in, *ViewModels out.
  • Standardize as we go — retrofit the 2 non-idiomatic controllers with characterization tests (test-before/after).

Increment plan (this issue = tracker):

  • #34 — #2a API foundation + Channels (pattern-setter) ← starting now
  • #35 — #2b Collections (+ retrofit SmartCollectionController)
  • #36 — #2c Schedules + items (TPT-heavy)
  • #37 — #2d Playouts
  • #38 — #2e Standardization cleanup

Each slice = one branch = one PR (test + migrations green to merge). Work is isolated in a git worktree to avoid colliding with the parallel #28 session on main.

## Phase 1 complete → Phase 2 underway Phase 1 investigation (4 parallel code-grounded agents) + design is signed off. Design doc landed at `docs/rest-api.md`. **Key findings:** - **No migration needed** — pure CRUD over existing tables; the 4 `ProgramScheduleItem` TPT subtype tables already exist. - **Every command already exists** as a MediatR handler returning `Either<BaseError, T>` → the API is mostly thin controllers over existing handlers. - **Substantive validation already lives in the handlers** (uniqueness, FK-exists, playout business rules); only a few format/UX checks are page-only and get ported. - OpenAPI/Scalar, camelCase JSON, enums-as-strings already wired. **Decisions:** - Routes → **idiomatic REST** (modernize/standardize mandate). - Writes auth → **dedicated API key (`Api__WriteKey`), decoupled from the IPTV JWT toggle** → no change needed on Jellyfin/Dispatcharr (they only consume `/iptv/*` reads, untouched). Reusing JWT was rejected because its single global toggle also gates the IPTV feeds and its tokens expire in 1 day. - Status codes → 201+`Location` / 200 / 204 / 404 / 422 (+ `NotFoundError` type). Request DTOs in, `*ViewModel`s out. - **Standardize as we go** — retrofit the 2 non-idiomatic controllers with characterization tests (test-before/after). **Increment plan (this issue = tracker):** - #34 — #2a API foundation + Channels (pattern-setter) ← **starting now** - #35 — #2b Collections (+ retrofit SmartCollectionController) - #36 — #2c Schedules + items (TPT-heavy) - #37 — #2d Playouts - #38 — #2e Standardization cleanup Each slice = one branch = one PR (`test` + `migrations` green to merge). Work is isolated in a git worktree to avoid colliding with the parallel #28 session on `main`.
Author
Owner

Progress update

#35 / REST API #2b is complete and merged via PR #45. Main CI run 173 passed build/test, migration integrity, and image build/push.

Follow-ups created from completion review:

  • #46 — REST API error response body contract. Runtime ApiResults currently returns plain error values for 404/422 while OpenAPI documents ProblemDetails. This is cross-cutting across #2a/#2b and should be standardized before adding the larger schedule surface.
  • server-management#497 — enable delete-branch-after-merge by default for the ErsatzTV repo. PR #45's branch was cleaned up manually.

Recommended next task: do #46 before #36. It is not part of the original #2a–#2e sequence, but it prevents repeating or later rewriting error response metadata across #36 schedules and #37 playouts.

## Progress update #35 / REST API #2b is complete and merged via PR #45. Main CI run 173 passed build/test, migration integrity, and image build/push. Follow-ups created from completion review: - #46 — REST API error response body contract. Runtime `ApiResults` currently returns plain error values for 404/422 while OpenAPI documents `ProblemDetails`. This is cross-cutting across #2a/#2b and should be standardized before adding the larger schedule surface. - server-management#497 — enable delete-branch-after-merge by default for the ErsatzTV repo. PR #45's branch was cleaned up manually. Recommended next task: do #46 before #36. It is not part of the original #2a–#2e sequence, but it prevents repeating or later rewriting error response metadata across #36 schedules and #37 playouts.
Author
Owner

Done

What was done: Completed the REST API tracker across the planned slices and follow-up error contract cleanup:

  • #34 / #2a: API foundation + Channels CRUD, including API-key write auth, request DTOs, REST status mapping, OpenAPI conventions, and channel handler validation parity.
  • #35 / #2b: Collections CRUD/items and SmartCollectionController REST retrofit.
  • #46: Standardized REST 404/422 runtime bodies and OpenAPI contract on ProblemDetails.
  • #36 / #2c: Schedules CRUD and schedule item endpoints, including TPT subtype coverage.
  • #37 / #2d: Playout create/delete endpoints.
  • #38 / #2e: FFmpegProfileController REST retrofit, OpenAPI/doc polish, CORS review, /api sweep, 401 ProblemDetails, and API-key protection for legacy mutating library/maintenance endpoints.

The REST API now routes management mutations through MediatR/EF handlers instead of direct SQLite writes for channels, collections, smart collections, schedules/items, and playouts, with generated OpenAPI coverage for client/MCP use.

Root cause: n/a; this was feature delivery. The original risk being addressed was that direct DB writes bypassed EF TPT mapping, validation, constraints, and WAL safety.

Files changed: Implemented across PRs #44, #45, #47, #50, #51, and #52. Main areas: ErsatzTV/Controllers/Api, ErsatzTV/Controllers/Api/Requests, relevant MediatR handlers under ErsatzTV.Application, shared API result/filter helpers, controller/handler/integration/OpenAPI tests, ErsatzTV/wwwroot/openapi/v1.json, and docs/rest-api.md.

Verification: Each slice merged only after local verification and green PR CI. Final slice #38 / PR #52 passed local LSP diagnostics, scripts/update-openapi.sh, dotnet build ErsatzTV.sln, TZ=UTC dotnet test ErsatzTV.sln -v normal (263 passed), plus PR CI: Build & test (.NET) and EF migration integrity (SQLite + MySql) both successful.

Deferred: CORS remains the existing global AllowAll; tightening is deferred as an operational exposure decision if write APIs are exposed beyond LAN. Legacy operational route reshaping for library/maintenance actions is deferred to a future operational API cleanup if needed; those mutating endpoints are now covered by API-key write protection.

Follow-up issues: none required for the REST API tracker itself. Existing unrelated backlog remains (#39 logo testability, #9 FFmpeg 8 app compatibility, #3 CI/CD audit, Renovate dashboard #22).

Docs updated: docs/rest-api.md documents the final API conventions, completed standardization, CORS review, and deferred items.

Closing #2 as complete.

## Done **What was done**: Completed the REST API tracker across the planned slices and follow-up error contract cleanup: - #34 / #2a: API foundation + Channels CRUD, including API-key write auth, request DTOs, REST status mapping, OpenAPI conventions, and channel handler validation parity. - #35 / #2b: Collections CRUD/items and SmartCollectionController REST retrofit. - #46: Standardized REST 404/422 runtime bodies and OpenAPI contract on `ProblemDetails`. - #36 / #2c: Schedules CRUD and schedule item endpoints, including TPT subtype coverage. - #37 / #2d: Playout create/delete endpoints. - #38 / #2e: FFmpegProfileController REST retrofit, OpenAPI/doc polish, CORS review, `/api` sweep, 401 `ProblemDetails`, and API-key protection for legacy mutating library/maintenance endpoints. The REST API now routes management mutations through MediatR/EF handlers instead of direct SQLite writes for channels, collections, smart collections, schedules/items, and playouts, with generated OpenAPI coverage for client/MCP use. **Root cause**: n/a; this was feature delivery. The original risk being addressed was that direct DB writes bypassed EF TPT mapping, validation, constraints, and WAL safety. **Files changed**: Implemented across PRs #44, #45, #47, #50, #51, and #52. Main areas: `ErsatzTV/Controllers/Api`, `ErsatzTV/Controllers/Api/Requests`, relevant MediatR handlers under `ErsatzTV.Application`, shared API result/filter helpers, controller/handler/integration/OpenAPI tests, `ErsatzTV/wwwroot/openapi/v1.json`, and `docs/rest-api.md`. **Verification**: Each slice merged only after local verification and green PR CI. Final slice #38 / PR #52 passed local LSP diagnostics, `scripts/update-openapi.sh`, `dotnet build ErsatzTV.sln`, `TZ=UTC dotnet test ErsatzTV.sln -v normal` (263 passed), plus PR CI: `Build & test (.NET)` and `EF migration integrity (SQLite + MySql)` both successful. **Deferred**: CORS remains the existing global `AllowAll`; tightening is deferred as an operational exposure decision if write APIs are exposed beyond LAN. Legacy operational route reshaping for library/maintenance actions is deferred to a future operational API cleanup if needed; those mutating endpoints are now covered by API-key write protection. **Follow-up issues**: none required for the REST API tracker itself. Existing unrelated backlog remains (#39 logo testability, #9 FFmpeg 8 app compatibility, #3 CI/CD audit, Renovate dashboard #22). **Docs updated**: `docs/rest-api.md` documents the final API conventions, completed standardization, CORS review, and deferred items. Closing #2 as complete.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: timothy/ersatztv#2