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>
This commit is contained in:
2026-07-04 00:54:58 +02:00
co-authored by Claude Fable 5
parent 43f81d3a49
commit 9ecefd75f3
8 changed files with 565 additions and 315 deletions
@@ -22,6 +22,20 @@ public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerCh
[EndpointGroupName("general")]
public async Task<List<ChannelResponseModel>> GetAll() => await mediator.Send(new GetAllChannelsForApi());
[HttpGet("/api/guide")]
[Tags("Channels")]
[EndpointSummary("Get the JSON channel guide (EPG)")]
[EndpointDescription(
"Returns per-channel programme arrays for the EPG grid. When omitted, start defaults to now " +
"and end defaults to now plus the configured XmltvDaysToBuild window.")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(ChannelGuideResponseModel), StatusCodes.Status200OK)]
public async Task<ChannelGuideResponseModel> GetGuide(
[FromQuery] DateTimeOffset? start,
[FromQuery] DateTimeOffset? end,
CancellationToken cancellationToken) =>
await mediator.Send(new GetChannelGuideData(start, end), cancellationToken);
[HttpGet("/api/channels/{id:int}", Name = "GetChannelById")]
[Tags("Channels")]
[EndpointSummary("Get a channel by id")]