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>
- 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.
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>
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>
- GET /api/playouts — paged list over GetPagedPlayouts (query/pageNum/pageSize), list DTO with buildStatus (#100, #107)
- GET /api/playouts/{id}/items — paged future items + UNSCHEDULED gaps over GetFuturePlayoutItemsById; 404 ProblemDetails for unknown playout (matches /api/schedules/{id}/items precedent) (#101)
- buildStatus {lastBuild, success, message} on GET /api/playouts/{id} (BuildStatus now included by GetPlayoutByIdHandler) (#107)
- GET /api/playouts/warnings/count — failed-build count for the warnings badge (#107)
- POST /api/playouts/reset-all — 202 Accepted, wraps ResetAllPlayouts (#110)
- POST /api/channels/{channelNumber}/playout/reset — optional ?mode= override; default branches by ScheduleKind (Classic→Refresh, others→Reset) to match Blazor semantics (#110)
- Option<FillerKind>→FillerKind? projection uses MatchUnsafe (Match throws on null-returning branch; idiom per Health/Mapper.cs)
- Tests: controller unit tests (routes, projections, 404s, reset-mode defaults), OpenAPI ProblemDetails contract entry for /api/playouts/{id}/items get 404
- Regenerated wwwroot/openapi/v1.json
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix the WIP health-check API slice to match established patterns:
- HealthController: add Name="GetHealthChecks" route name and move
[EndpointGroupName("general")] to method level, matching
FillerPresetController/FFmpegProfileController exactly (the
precedent for parameterless 200-only GET actions).
- HealthCheckResponseModel: enable #nullable for the file and mark
Link as string? since the mapper can emit null when
HealthCheckResult.Link is None.
- Mapper: fix a real bug - LanguageExt's Option.Match throws
ResultIsNullException.ResultIsNull if either branch returns null
(by design, to catch accidental nulls). The WIP's
`Link.Match(l => l.Link, () => null)` crashed on every health
check without a Link. Switch to MatchUnsafe, the LanguageExt-
sanctioned way to intentionally produce a nullable result from
Option<T>.
- HealthControllerTests: add the idiomatic-route-assertion test
(route template + Name) and empty-list case, matching
FillerPresetControllerTests.
Verified: GetAllHealthCheckResultsForApiHandler already matches the
existing GetAllHealthCheckResultsHandler's cancellation handling
(both swallow TaskCanceledException/OperationCanceledException), so
no change was needed there. Confirmed no OpenApi contract test
enumerates all endpoints for error-response metadata (it's an
explicit TestCase allowlist), so the new GET needed no new entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The WIP GET /api/filler-presets, /api/watermarks, and
/api/graphics-elements endpoints omitted the Name= route attribute
used by the other API GET actions (e.g. FFmpegProfileController). Add
Name = "GetFillerPresets" / "GetWatermarks" / "GetGraphicsElements" for
consistency.
Also add handler tests (FillerPresetHandlerTests, WatermarkHandlerTests,
GraphicsElementHandlerTests) covering the GetAll*ForApi handlers using
the InMemoryTvContext harness: multi-row projection to (Id, Name) DTOs,
the empty-DB case, and the graphics-element ordering rule (named
elements sort before elements whose projected Name equals FileName).
Adversarial review fixes for the #97 endpoint:
- nowPlaying now resolves like the XMLTV guide: when the wall-clock item
is pre/mid-roll filler, surface the guide group's program item (title +
full program bounds) instead of the filler; a guide entry with no
program item (e.g. fallback loop) reports null. Previously an ad break
would retitle the channel and reset progress every 30 seconds.
- covering-item lookup now runs one cheap projected query per distinct
playout offset instead of hydrating the full metadata include tree for
a window widened by the largest offset across ALL channels (up to 24h);
metadata is fetched in a second query scoped to the covering guide
groups, time-bounded to stay safe against GuideGroup recycling (mod
10000). Regression caught while testing: covering items are keyed by
(source channel, lookup time) so a source channel no longer inherits
its offset mirror's lookup result.
- GetChannelStatesForApi now carries Now explicitly (controller passes
DateTime.UtcNow), making boundary semantics testable: new tests pin
now == Start (playing) and now == Finish (not playing), plus mid-roll,
filler-only, and mirror-source-row cases.
- ChannelNowPlayingResponseModel title is non-nullable in the schema
(#nullable enable) — GetDisplayTitle always returns a string.
- GetState uses the typed-return controller convention; Playouts.Mapper
reverted to internal (handler lives in the same assembly).
- Spec doc notes the OnDemand drift caveat and filler semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First slice of the REST API (ersatztv#2). Idiomatic REST over existing MediatR handlers; design in docs/rest-api.md.
Foundation: NotFoundError : BaseError (Core) to split 404 from 422; ApiResults mapping helpers (Either/Option -> 201+Location / 200 / 204 / 404 / 422), additive (existing *ToActionResult untouched); ApiKeyAuthorizationFilter (optional X-Api-Key on mutating actions, gated by Api:WriteKey, no-op when unset) — independent of the IPTV JWT toggle so Jellyfin/Dispatcharr reads are unaffected, applied at controller-class level so every mutating action incl. ResetPlayout is covered fail-safe.
Channels: POST /api/channels (201+Location), PUT/{id} (200/404), DELETE/{id} (204/404), GET/{id} (200/404). Request DTOs -> existing commands; responses reuse ChannelViewModel. Ported page-only validations into handlers (ShowInEpg-when-disabled, external-logo-URL, Group NotEmpty) + FFmpegProfile/Watermark/Filler existence on update for create-parity. Fixed a latent 500: the .Filter(c>0) existence pattern threw TaskCanceledException on not-found; rewritten to AnyAsync.
Tests (NUnit, TZ=UTC): handler success/404/422, ApiResults mapping, API-key filter, controller status/Location/mapping, a controller-security regression net, and a create->read->delete EF integration test on a new in-memory SQLite harness. ErsatzTV.Tests 46/46, Architecture 5/5.
Refs #34
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>