Commit Graph
123 Commits
Author SHA1 Message Date
timothyandClaude Fable 5 28910ff557 fix(api): gate playout mutations on EntityLocker build lock (409) + mirror lock state in SPA (fixes #215)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Blazor disabled per-playout Reset/Erase/Delete/Edit while a BuildPlayout was
in flight (EntityLocker.IsPlayoutLocked); the REST API had no equivalent, so a
client could race an in-flight build with a destructive ExecuteDelete and leave
a half-built playout. After Blazor removal this safety invariant would vanish
entirely (adversarial-reviewer#18 removal gate).

Server:
- Add public ApiResults.ConflictProblem(title, detail) (409, mirrors NotFoundProblem).
- Inject IEntityLocker into PlayoutController; guard every id-keyed mutation
  (PUT {id}, PUT .../deco, PUT .../alternate-schedules, PUT .../templates,
  POST .../erase-items, POST .../erase-items-and-history, DELETE {id}) → 409
  when IsPlayoutLocked(id); add [ProducesResponseType(...409)] to each.
- Guard ChannelController.ResetPlayout the same way after resolving the id.
- reset-all stays 202 (ResetAllPlayoutsHandler already skips locked playouts).
- Stamp IsLocked onto PlayoutListItemResponseModel from IsPlayoutLocked.

SPA:
- Disable Reset/Erase/Erase-and-history/Delete for a locked row + show a
  "Building…" Badge; on a 409 surface the error and refresh the list.

Tests: controller-level 409 guard tests (delete/erase/PUT/deco/channel-reset)
+ IsLocked projection test; new OpenAPI contract + metadata 409 rows.
Docs: api-conventions §3a, blazor-route-parity playouts verdict, decisions.md.
Regenerated v1.json + web types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:34:23 +02:00
timothy 819e751cab feat(api): playlist add-items + search all-items endpoints (#208 #209)
- POST /api/playlists/{id:int}/items wraps the existing AddItemsToPlaylist
  command (mirrors CollectionController.AddItems); controller pre-checks
  playlist existence for a real 404, and the handler now rejects adds to
  system (generated) playlists, matching the guard already applied to
  rename/delete/replace-items so the Blazor path gets the same protection.
- GET /api/search/all-items wraps the existing QuerySearchIndexAllItems
  query, returning a new SearchResultAllItemsResponseModel (never expose
  the VM directly) so the SPA's shared "add all to collection/playlist"
  component can materialize ids before calling the add endpoints, same
  two-step flow Blazor's Search.razor already uses.
- Show-detail DTO check: ShowDetailResponseModel already exposes
  libraryId, title, and mediaSourceKind (serialized as a string enum via
  the global StringEnumConverter) - no changes needed.

Adds controller tests (route table + per-action) for both endpoints and
regenerates the OpenAPI document, endpoint index, and SPA client types.
2026-07-09 23:56:50 +02:00
timothyandClaude Fable 5 e66b926206 feat(api): expose PlayoutItem row id on the playout-items list DTO (#210)
The SPA needs the item's row id to call
GET /api/playouts/items/{id}/scheduling-context. Plumbed through
PlayoutItemViewModel -> PlayoutItemResponseModel as a nullable Id
(null for synthesized UNSCHEDULED gap rows, which are PlayoutGaps,
not PlayoutItems). Additive for existing consumers (Playouts.razor
reads the VM by property). Regenerated v1.json + v1.d.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:10:45 +02:00
timothyandClaude Fable 5 f1b3879d51 feat(api): playout erase/scheduling-context + collection custom-order (#210, #211)
Backend slice for the ChicoryTV playouts and collections screens.

PlayoutController:
- POST /api/playouts/{id}/erase-items (204; 404 pre-check; 422 unless
  Block/Sequential/Scripted) -> ErasePlayoutItems
- POST /api/playouts/{id}/erase-items-and-history (204; 404; 422 unless
  Classic/Block/Sequential/Scripted) -> ErasePlayoutHistory
- GET /api/playouts/items/{id}/scheduling-context (200/404) decodes a
  playout item's stored context by row id via a new
  GetPlayoutItemSchedulingContext query that reuses ProcessSchedulingContext
- PlayoutItemResponseModel gains HasSchedulingContext (no raw JSON in list)
- PlayoutListItemResponseModel gains PlayoutMode (ChannelNumber already present)

CollectionController:
- PUT /api/collections/{id}/custom-order (204; 404 pre-check; 422) with
  UpdateCollectionCustomOrderRequest deriving CustomIndex from array order
- GetCollectionItemsHandler orders by CustomIndex (nulls last) then title/id
  when the collection's UseCustomPlaybackOrder is set

Tests: controller route + behavior tests, OpenAPI ProblemDetails TestCases,
GetCollectionItems custom-order handler test. Regenerated v1.json, v1.d.ts,
endpoint-index.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:05:51 +02:00
timothyandClaude Fable 5 6e2e0e1858 feat(api): optional refresh param on GET /api/graphics-elements
Review SHOULD-FIX (#145): the SPA graphics picker could go stale because
Blazor ran RefreshGraphicsElements (disk->DB sync) before listing, while the
API endpoint never refreshed — a newly added .yml would not appear. GET
/api/graphics-elements?refresh=true now sends RefreshGraphicsElements before
the list query; default false leaves existing callers untouched. The playback
troubleshooting screen passes refresh=true. Controller tests cover
refresh-iff-true ordering; regenerated OpenAPI v1.json (endpoint index and
generated TS schemas unchanged - query params are not part of either).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 07:52:14 +02:00
timothyandClaude Fable 5 5815e4b437 feat(api): add troubleshooting stream-selectors, subtitles, and status endpoints
Add three GET endpoints to TroubleshootController for the SPA port of the
playback troubleshooting page:
- /api/troubleshoot/playback/stream-selectors (List<string>)
- /api/troubleshoot/playback/subtitles/{mediaItemId} (404 pre-check via
  GetMediaItemInfo; maps SubtitleViewModel -> TroubleshootingSubtitleResponseModel)
- /api/troubleshoot/playback/status (TroubleshootingPlaybackStatusResponseModel:
  idle/running/completed/failed + exitCode/speed + logs.txt tail)

Regenerate v1.json, endpoint-index.md, and the web API types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:46:35 +02:00
timothyandClaude Opus 4.8 59edec1e85 fix(api): guard system playlists/groups + validate preview draft (#153 review)
Hardening from adversarial review of the #153 playlist API:

- PUT /api/playlists/{id}: guard IsSystem in the controller after the
  existence pre-check -> 422, so a system (generated) playlist can no
  longer be renamed/wiped. ReplacePlaylistItems is never sent for it.
- PUT /api/playlists/groups/{id}: add controller existence pre-check
  (404 for missing, mirroring DeleteGroup) plus an IsSystem 422 guard;
  RenamePlaylistGroupHandler also gains a system guard (defense-in-depth
  for the Blazor path). Missing/system are now distinct outcomes despite
  LanguageExtensions.Apply collapsing NotFoundError to a plain BaseError.
- POST /api/playlists/preview: validate each draft item at the controller
  boundary (the id required for its collection type must be present) ->
  422 before the shared PreviewPlaylistPlayoutHandler runs, preventing a
  NRE/500 in the playout builder. Logic lives in ReplacePlaylistRequest so
  it stays parallel with ReplacePlaylistItemsHandler's PUT-path check.

Tests: controller cases for system-playlist PUT, system-group PUT,
missing-group 404, and invalid-preview 422 (each asserting the handler is
not invoked); handler tests for RenamePlaylistGroup system/missing/success.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 21:22:19 +02:00
timothyandClaude Opus 4.8 7d5850214e feat(api): playlist CRUD + item management + preview (#153 backend)
Add write endpoints to PlaylistController for the playlist editor: group
create/rename/delete, playlist create/read/update/delete, item list read,
and draft playout preview. Introduces a RenamePlaylistGroup command/handler
(the one missing Application-layer operation) plus request/response DTOs.

Endpoints (verb / route / route-Name):
- POST   /api/playlists/groups        CreatePlaylistGroup
- PUT    /api/playlists/groups/{id}    UpdatePlaylistGroup (rename)
- DELETE /api/playlists/groups/{id}    DeletePlaylistGroup
- GET    /api/playlists/{id}           GetPlaylistById
- GET    /api/playlists/{id}/items     GetPlaylistItems
- POST   /api/playlists                CreatePlaylistInGroup
- PUT    /api/playlists/{id}           UpdatePlaylist (rename + replace items)
- DELETE /api/playlists/{id}           DeletePlaylist
- POST   /api/playlists/preview        PreviewPlaylist

404-vs-422: unknown-id on GET items / PUT / DELETE returns 404 via a
controller-side existence pre-check (mirrors TemplateController.DeleteGroup),
leaving existing shared handlers untouched; validation failures return 422.
RenamePlaylistGroup returns NotFoundError -> 404 for a missing group.

Regenerated wwwroot/openapi/v1.json, docs/endpoint-index.md, and the SPA
client types (web/src/api/generated/v1.d.ts). No SPA screen in this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:52:07 +02:00
timothy 282f59ea60 Merge remote-tracking branch 'origin/main' into feat/multi-rerun-collections-api
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m15s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m51s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	docs/blazor-route-parity.md
#	docs/endpoint-index.md
2026-07-08 18:43:08 +02:00
timothyandClaude Opus 4.8 dc459c6cd5 fix(api): reject unsupported CollectionType on rerun-collection create/update (#152 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The rerun request mapping's catch-all routed Playlist/RerunFirstRun/RerunRerun/
SearchQuery/Fake* to MediaItemId, so POST/PUT with those persisted a wrong-kind id
and returned 201 instead of 422. Guard both verbs with IsSupportedSelectionType.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:30:06 +02:00
timothyandClaude Opus 4.8 f869dfe87a feat(api): GET /api/collections/{id}/items (paged) + confirm POST-items 422 guard (#155)
Adds a paged collection-items endpoint reusing LibraryBrowseItemResponseModel
so the SPA lists a manual collection's full contents (all media kinds), replacing
the lossy Lucene name-based preview. Confirms POST /items already returns 422 for
bogus ids (guarded by ValidateMediaItems, fb3f2856); adds endpoint-level coverage.

fixes #155

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:26:52 +02:00
timothyandClaude Opus 4.8 a496dad9c9 feat(api): REST CRUD for multi-collections and rerun-collections (#151, #152)
Adds MultiCollectionController + RerunCollectionController (list/get/create/update/
delete) over the existing MediatR handlers, with response/request DTOs, controller +
contract tests, regenerated OpenAPI + TS types, and thin SPA api-client modules. SPA
editor screens are a follow-up. Rerun request DTO resolves the chosen entity id into
the minimal VM the handler reads (documented inline).

Refs #151, #152

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:18:32 +02:00
timothyandClaude Fable 5 685894517d merge main into feat/141-media-detail (post-#182); regenerate OpenAPI artifacts
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m23s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 23:01:17 +02:00
timothyandClaude Fable 5 cd31c755bb feat(api): media detail + info + image-folder endpoints (#141/#161)
Add REST endpoints backing the SPA media detail pages and image browser:

- GET /api/movies/{id}, /api/shows/{id}, /api/seasons/{id}, /api/artists/{id}
  wrapping the existing detail queries; 404 on None.
- GET /api/media-items/{id}/info wrapping GetMediaItemInfo; UnableToLocateMediaItem
  -> 404, other errors -> 422.
- GET /api/images/folders?parentId= and PUT /api/images/folders/{id}/duration
  (validates null-or-positive -> 400; existence guard via new ImageFolderExists
  query -> 404).
- Extend GetLibraryBrowseItems parentId drill-in to Episode (episodes of a season,
  episode-number order) and MusicVideo (an artist's music videos, album/track order),
  alongside the existing TelevisionSeason branch.

Response DTOs live in ErsatzTV.Core/Api/* and never expose Application VMs. Artwork
values are rooted for the SPA via a shared ErsatzTV.Core/Api/ApiArtwork helper
(mirrors the #180/#181 browse-handler logic; handles jellyfin/emby proxy prefixes,
http passthrough, empty). Regenerated OpenAPI v1.json + web v1.d.ts. New controllers
registered in ApiControllerSecurityTests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:43:47 +02:00
timothyandClaude Fable 5 d2096c0675 merge main into feat/145-block-history (post-#181); regenerate OpenAPI artifacts
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m52s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:25:49 +02:00
timothyandClaude Fable 5 f0a423c2de feat(api): block-playout history + sequential-schedule validation endpoints
Adds the REST surface for the #145 troubleshooting leftovers (#158 items 4-5):

- GET /api/playouts/{id}/blocks - blocks a block playout schedules
- GET /api/playouts/{id}/blocks/{blockId}/history - paged block history
- GET /api/playouts/history/{id} - decode a history row by id
- POST /api/troubleshoot/validate-schedule - validate sequential YAML

New MediatR queries GetPlayoutHistoryDetails (Either, 404 unknown row /
422 malformed JSON) and ValidateSequentialSchedule (wraps
ISequentialScheduleValidator, never throws). DecodePlayoutHistoryHandler
and the new by-id handler now share PlayoutHistoryDecoder. Regenerated
openapi/v1.json.

Refs #145 #158

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:10:37 +02:00
timothyandClaude Fable 5 82d3b586f2 docs(api): describe library-browse parentId param in OpenAPI (#180 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m53s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:50:17 +02:00
timothyandClaude Fable 5 54b18e73cd fix(api): root library-browse artwork URLs and add season drill-in (#180)
Part of the #180 library picker fixes (API side).

Broken artwork: GetLibraryBrowseItemsHandler.Artwork returned Blazor-convention
values (a bare cache filename, or a RELATIVE jellyfin/emby proxy path). Blazor's
GetPosterUrl prefixes those with "artwork/posters/" against <base href="/">, but
the React SPA renders item.artwork raw as <img src> from under /app/, so every
image 404'd. The handler now returns rooted, directly-usable URLs
(/artwork/posters/... or /artwork/thumbnails/... per ArtworkKind; jellyfin/emby
mapped to their proxy routes with fillHeight/maxHeight; absolute http(s) URLs
passed through; empty stays empty).

Seasons with no poster of their own now fall back to the parent show's poster
(SeasonArtwork + the extra ShowMetadata.Artwork include).

Season drill-in: GET /api/library/browse gains an optional `parentId` query
param that, with mediaType=TelevisionSeason, returns that show's seasons
(season-number order, bypassing Lucene) so the SPA can expand a show into its
seasons. OpenAPI spec regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:42:10 +02:00
timothy d7617b7456 fix(api): validate date-range month/day bounds on playout PUT actions
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m59s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
AlternateScheduleSelector.GetScheduleForDate constructs
new DateTime(year, StartMonth, StartDay) and only recovers from an
out-of-range *day* (rolling to the 1st of the next month); an out-of-range
*month* (e.g. 0 or 13) throws again from inside that recovery path and is
never caught, crashing playout building.

Validate StartMonth/EndMonth (1..12) and StartDay/EndDay (1..31) on both
PUT /api/playouts/{id}/alternate-schedules and PUT /api/playouts/{id}/templates
whenever an item sets LimitToDateRange, returning 422 before dispatching to
the mediator. Both actions already documented 422 in their ProducesResponseType
metadata.
2026-07-07 20:56:45 +02:00
timothyandClaude Fable 5 50b13afac3 feat(api): REST endpoints for playout alternate schedules and templates (#144 S6 (#162))
Adds per-playout REST for classic-playout alternate schedules and
block-playout templates, plus the default-deco read-side deferred from S3:

- GET/PUT /api/playouts/{id}/alternate-schedules (Classic only; 422 otherwise)
- GET/PUT /api/playouts/{id}/templates (Block only; 422 otherwise)
- PlayoutResponseModel gains decoId/decoName (GetPlayoutById includes Deco)

PUT assigns Index from array order (top = highest priority, last = catch-all
default), mirroring the Blazor editors. Alternate-schedule PUT requires a
non-empty list and existing ProgramScheduleIds; template PUT requires existing
TemplateIds and any supplied DecoTemplateId. Regenerates the OpenAPI spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:29:29 +02:00
timothy cda5c12e34 feat(api): REST endpoints for deco templates and deco template groups (#144 S4 (#162))
Adds DecoTemplateController mirroring TemplateController: CRUD for deco
template groups, deco templates (flat list + by-id), item listing, and a
full-replace PUT for a deco template's item list.

Hardening (deviation from a literal port of the existing handlers, matching
the #144 S2 fix for ReplaceTemplateItemsHandler):
- CreateDecoTemplateHandler now validates DecoTemplateGroupId exists before
  insert (previously a bad id hit the FK constraint at SaveChanges and
  surfaced as a 500; now a 422).
- ReplaceDecoTemplateItemsHandler now rejects invalid items (unknown DecoId,
  StartTime >= EndTime unless EndTime is the end-of-day sentinel 00:00:00,
  or overlapping ranges) with a 422 instead of silently dropping/persisting
  them - the same silent-drop/silent-overlap bug class already fixed for
  templates.

Response DTOs serialize the raw item TimeSpans (via .TimeOfDay), so an
end-of-day item still round-trips as StartTime=22:00:00/EndTime=00:00:00
regardless of the ViewModel's day-wrapping DateTime representation.
2026-07-07 19:51:00 +02:00
timothyandClaude Fable 5 57bb320af8 feat(api): REST endpoints for decos, deco groups, playout default deco (#144 S3 (#162))
Adds DecoController (groups + decos CRUD + full-state PUT), playlist and
artist/multi-collection search picker wrappers, a PlaylistController for the
break-content playlist cascade, and PUT /api/playouts/{id}/deco. Fixes the
CreateDecoHandler missing deco-group existence check (FK 500 -> 422), matching
the CreateBlock/CreateTemplate precedent. ReplaceDecoRequest.ToCommand rejects
Merge on the non-mergeable sections (422).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:22:39 +02:00
timothyandClaude Fable 5 ae10eba4d7 feat(api): REST endpoints for templates, template groups, items + block copy
Adds TemplateController mirroring BlockController: groups CRUD, template
CRUD, item replace (PUT full-replace via ReplaceTemplateItems), and a
copy endpoint. Also wires the missing POST /api/blocks/{id}/copy since
CopyBlock's CQRS shape is directly analogous to CopyTemplate.

Fixes CreateTemplateHandler to validate the target template group exists
before insert (previously an unhandled FK violation -> 500), matching the
CreateBlockHandler fix (1e34e00e) for the same class of bug.

New DTOs in ErsatzTV.Core/Api/Scheduling; TimeSpan start times serialize
the same way as PlayoutResponseModel.DailyRebuildTime.

#144 S2 (#162)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:31:17 +02:00
timothy e3f2cd631a Merge remote-tracking branch 'origin/main' into feat/144-s1-blocks
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m31s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-07 18:15:29 +02:00
timothyandClaude Fable 5 6b5bc8e29a feat(api): REST endpoints for blocks, block groups, items, preview + scheduling search pickers
Adds BlockController (block groups + blocks CRUD, items GET, full replace,
non-persisting preview) mirroring ScheduleController, plus four scheduling
search picker endpoints on SearchController (collections, television shows,
television seasons, smart collections). Response DTOs in ErsatzTV.Core/Api/
Scheduling; request DTOs with ToCommand index auto-assignment.

#144 S1 (#162)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:42:40 +02:00
timothyandClaude Fable 5 f06bc064a0 fix(api): apply schedule-file update before daily rebuild time in PUT /api/playouts/{id} (#170 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
A rejected scripted schedule file no longer leaves DailyRebuildTime
half-applied; the file update is the only fallible step post-pre-check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:30:38 +02:00
timothy 5f202a2175 feat(api): REST endpoints to create playouts of all kinds + update details (#144 S5 (#162))
- CreatePlayoutRequest now carries a PlayoutScheduleKind discriminator, a
  nullable ProgramScheduleId, and a ScheduleFile so POST /api/playouts can
  create Classic, Block, Sequential, Scripted, or ExternalJson playouts (not
  just Classic). ToCommand() validates per-kind requirements and returns
  Either<BaseError, CreatePlayout>, surfacing 422 on mismatched fields via the
  existing ToErrorResult() mapping.
- Add PUT /api/playouts/{id} (UpdatePlayoutDetailsRequest): DailyRebuildTime is
  always applied (null clears the daily reset, matching the Blazor
  SchedulePlayoutReset "Do not automatically reset" semantics); ScheduleFile is
  only valid for Sequential/Scripted/ExternalJson playouts (422 otherwise) and
  dispatches the matching Update*Playout command.
- Playout existence is checked via GetPlayoutById (real 404) before dispatching
  UpdatePlayout, since the command's own "Playout does not exist." validation
  produces a plain BaseError (422), not NotFoundError -- an existing quirk in
  UpdatePlayoutHandler left as-is (out of scope for this slice).
- Extend ApiErrorResponseMetadataTests + PlayoutControllerTests for the new
  Update action and the widened Create action (block/sequential/file-kind
  validation paths).
- Regenerate ErsatzTV/wwwroot/openapi/v1.json via update-openapi.sh.
2026-07-07 17:08:43 +02:00
timothy 7083301ce6 Merge remote-tracking branch 'origin/main' into feat/141-media-browse
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m5s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	ErsatzTV.Tests/Controllers/OpenApiErrorResponseContractTests.cs
#	web/src/App.tsx
#	web/src/screens/SettingsScreen.tsx
2026-07-07 15:50:03 +02:00
timothyandClaude Fable 5 9c03184707 fix: address #167 review — release Trakt lock on failed enqueue; mark busy after generatePlaylist save
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:26:01 +02:00
timothyandClaude Fable 5 6c68c291a0 feat(api): browse all media kinds, grouped search, delete media-items (#141, #161)
- Extend /api/library/browse to episodes, music videos, songs, other videos,
  images and remote streams (new LibraryBrowseMediaType values + hydrators);
  add optional Subtitle to LibraryBrowseItemResponseModel for leaf-item context
- Add GET /api/search: grouped per-kind results reusing the browse query/shape;
  empty query -> 422
- Add DELETE /api/media-items: body { ids }, empty -> 422, success -> 204
- Tests: SearchController, MediaItemsController, security + OpenAPI contract entries
- Regenerate openapi v1.json + web v1.d.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:23:07 +02:00
timothy 1b9d85896c Merge remote-tracking branch 'origin/main' into feat/142-trakt-lists
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m28s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m40s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	ErsatzTV.Tests/Controllers/ApiControllerSecurityTests.cs
#	web/src/App.tsx
#	web/src/screens/SettingsScreen.tsx
2026-07-07 15:13:22 +02:00
timothy 4306b39ef8 feat(api): Trakt lists REST endpoints (#160)
Adds TraktController (GET/POST /api/trakt/lists, GET/PUT/DELETE by id,
POST .../match, GET /api/trakt/status) covering the API gap for SPA
parity issue #142. Add/match/delete dispatch to the same
ChannelWriter<IBackgroundServiceRequest> the classic Blazor "Trakt
Lists" page uses, gated by the existing IEntityLocker (LockTrakt/
IsTraktLocked) singleton; GET /api/trakt/status wraps IsTraktLocked as
the HTTP-observable substitute for the Blazor page's OnTraktChanged
event. URL validation mirrors AddTraktListHandler.ValidateUrl's regexes
(replicated, since that method is private to the handler and returns a
handler-private record) so an obviously-invalid URL gets a synchronous
422 before dispatch.

Adds TraktListResponseModel/PagedTraktListsResponseModel/
TraktStatusResponseModel DTOs, AddTraktListRequest/UpdateTraktListRequest,
controller tests, ApiControllerSecurityTests + OpenApiErrorResponseContractTests
coverage, and regenerates the checked-in OpenAPI document.
2026-07-07 15:09:12 +02:00
timothy 7454411dee Merge remote-tracking branch 'origin/main' into feat/143-transcoding-editors
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	web/src/App.tsx
#	web/src/screens/SettingsScreen.tsx
2026-07-07 14:55:55 +02:00
timothyandClaude Fable 5 46c8c2e2dc feat(api): filler preset + watermark editor CRUD; ffmpeg profile round-trip (#159)
Backend plumbing for the SPA transcoding editors (#143 blocker):

- FFmpeg profile response DTO round-trip: add NormalizeAudio, NormalizeVideo,
  PadMode, TargetLoudness, NormalizeColors, ResolutionId (keep Resolution name
  for compat); DeinterlaceVideo now bool (was bool?). #nullable enable on the DTO.
- New GET /api/ffmpeg/hardware-acceleration-kinds wrapping
  GetSupportedHardwareAccelerationKinds (returns enum names as strings).
- Filler preset CRUD: GET-by-id/POST/PUT/DELETE on FillerPresetController with
  full request/response DTOs; new FillerPresetFullResponseModel +
  GetFillerPresetByIdForApi. CreateFillerPreset now returns the new id
  (CreateFillerPresetResult) to match sibling Create commands and enable a
  201+Location; updated the one Blazor call site.
- Watermark CRUD: GET-by-id/POST/PUT/DELETE on WatermarkController with full
  DTOs (path+contentType, all 15 fields); new WatermarkFullResponseModel +
  GetWatermarkByIdForApi.
- Startup: register WatermarkLocation/WatermarkSize as OpenAPI string enums
  (they live in ErsatzTV.FFmpeg.State and were documented as ints, mismatching
  the Newtonsoft StringEnumConverter runtime serialization).
- Tests: full CRUD controller tests for filler + watermark; contract-test
  entries (404/401/422) for the new mutating endpoints; regenerated v1.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:06:26 +02:00
timothyandClaude Fable 5 24a26cbe34 fix: address #165 review findings (stale-response guard, security-test coverage, route/help-text nits)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m37s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:55:05 +02:00
timothy dfa561a97e feat(api): logs and troubleshooting-info endpoints for SPA parity (#145, #158)
Add GET /api/logs (thin wrapper over GetRecentLogEntries; paged, filtered,
clamped pageSize like LibraryBrowseController) and GET /api/troubleshoot/info
(wraps GetTroubleshootingInfo; General section serialized to the same JSON
shape the legacy Blazor Troubleshooting page renders, plus per-platform
capability dumps). Also adds [EndpointGroupName("general")] to the existing
troubleshoot playback endpoints so they surface in the OpenAPI spec (#158
item 3).
2026-07-07 13:34:47 +02:00
timothyandClaude Fable 5 540def7f17 fix: address #93 adversarial review findings
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m51s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m21s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Backend: 401 documented on all mutating settings/resolution routes;
resolution delete distinguishes 404 (unknown) from 422 (not custom);
XMLTV enum bridging via exhaustive switch instead of int casts;
field-level Arg.Is assertions incl. non-null watermark/filler flow.

Frontend: resolution add/delete failures surfaced inline (were
silent); partial saves merge succeeded groups via allSettled; empty
numeric fields invalid + tunerCount min 1; HLS Direct select shows
out-of-list wire values; media-source rows show derived last-scan.

ErsatzTV.Tests 495; web suite 145.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:22:51 +02:00
timothyandClaude Fable 5 57905bc0f4 feat(api): settings read/write endpoints (#93)
GET/PUT /api/settings/{ffmpeg,playout,xmltv,scanner,logging,ui,hdhr}
wrapping the existing MediatR settings handlers, plus custom resolution
list/create/delete under /api/settings/resolutions. String-enum OpenAPI
schemas extended to OutputFormatKind + LogEventLevel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:22:50 +02:00
timothyandClaude Fable 5 cf36c30997 fix(api): rework from-lineup to generated playlist design + review fixes (#63)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m54s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
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>
2026-07-06 21:09:00 +02:00
timothy 6857a0d191 feat(api): composite create-channel endpoint (#63)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-06 20:27:02 +02:00
timothy 7858ac002a feat(api): add channel templates
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m58s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-06 18:43:39 +02:00
timothy 630119f382 fix(api): address library browse review findings
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m3s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
refs #65

refs PR #130
2026-07-06 07:45:39 +02:00
timothy 127a130737 feat(api): add library browse endpoint
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
refs #65
2026-07-06 00:01:06 +02:00
timothyandClaude Fable 5 8d89ab1624 fix(api): add nullable context to #96/#98 DTOs; regen OpenAPI + SPA types
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 3m57s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m53s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
ChannelResponseModel and BulkChannelRequests DTOs lacked file-scoped
#nullable enable, so the OpenAPI generator emitted every string
property as a ["null","string"] union (project Nullable default is
disable), polluting SPA typegen with needless `| null` unions.
ChannelStateResponseModel already complied with this convention
(precedent: PlayoutResponseModel). All properties in both files are
always populated (never null in practice), so no property needed to
become explicitly optional/nullable - just adding #nullable enable
was sufficient to clean up the generated schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 08:02:28 +02:00
timothy 06355b7590 Merge main into feat/59-spa-foundation (post-#119 backend API sync)
# Conflicts:
#	ErsatzTV.Tests/Controllers/ChannelControllerTests.cs
#	ErsatzTV/Controllers/Api/ChannelController.cs
#	ErsatzTV/wwwroot/openapi/v1.json
2026-07-04 07:51:56 +02:00
timothy 0585f4a7f8 test(api): pin mirror-offset guide behavior; clarify guide end-default wording (#102)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m1s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m57s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
- Add GetChannelGuideDataHandlerTests coverage for the mirror-channel path:
  asserts programme Start/Stop shift by PlayoutOffset and that the source
  channel's seeded PlayoutItem entities are not mutated, pinning the
  WithPlayoutOffset copy fix.
- ChannelController.GetGuide Swagger description said end defaults to "now
  plus XmltvDaysToBuild"; it actually defaults to start plus that window
  (matters when start is supplied). Reworded and regenerated v1.json.
2026-07-04 01:19:58 +02:00
timothyandClaude Fable 5 9ecefd75f3 feat(api): add JSON channel guide endpoint (#102)
Add GET /api/guide returning a per-channel EPG grid (ChannelGuideResponseModel).
start defaults to now, end defaults to now + XmltvDaysToBuild.

Extract the guide-group/filler-merge projection (ChannelGuideProjector) and
programme-metadata resolution (ChannelGuideMetadata) plus the metadata eager-load
(IncludeGuideMetadata) out of RefreshChannelDataHandler so the XMLTV cache builder
and the JSON guide share one source of truth and cannot drift. XMLTV output is
unchanged (ChannelGuideGoldenTests pass without regeneration).

The JSON handler mirrors XMLTV semantics: ShowInEpg-only channels, decimal channel
ordering, ExternalJson playouts skipped, mirror playout offset applied to copies
(never mutating the loaded entities). SubTitle/Category are nullable.

refs #102

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:54:58 +02:00
timothyandClaude Fable 5 d0652d4adb wip: partial implementation salvaged from interrupted workflow run
Untrusted draft — no build/test had run yet. Review before building on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:41:57 +02:00
timothyandClaude Fable 5 3d086aabc1 feat(api): artwork upload endpoint (#104)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m4s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m1s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
POST /api/artwork/uploads (multipart/form-data) accepting logo and
watermark images. Validates content type (png/jpeg/gif/webp) and size
(SystemEnvironment.MaximumUploadMb, default 10MB) mirroring the Blazor
upload path; stores via IImageCache.SaveArtworkToCache; returns
{ path, contentType } consumable by channel create/update.

Includes handler + controller tests, OpenAPI 422 contract-test entry,
and regenerated v1.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:32:22 +02:00
timothy 1b7b0e549a feat(api): add media sources and scan status endpoints (#103 #106) 2026-07-03 23:02:58 +02:00