diff --git a/ErsatzTV.Tests/Application/Channels/GetChannelGuideDataHandlerTests.cs b/ErsatzTV.Tests/Application/Channels/GetChannelGuideDataHandlerTests.cs index cf7cca668..806c7a64d 100644 --- a/ErsatzTV.Tests/Application/Channels/GetChannelGuideDataHandlerTests.cs +++ b/ErsatzTV.Tests/Application/Channels/GetChannelGuideDataHandlerTests.cs @@ -205,6 +205,41 @@ public class GetChannelGuideDataHandlerTests (result.End - result.Start).ShouldBe(TimeSpan.FromDays(3)); } + [Test] + public async Task Handle_Should_Shift_Mirror_Channel_Programmes_By_PlayoutOffset_Without_Mutating_Source_Items() + { + PlayoutItem sourceItem = MakeItem(BaseTime, BaseTime.AddHours(1), guideGroup: 1, movieTitle: "Live Show"); + var playoutOffset = TimeSpan.FromHours(3); + + await using (TvContext context = _db.CreateContext()) + { + DomainChannel source = NewChannel("1", "Source", showInEpg: false); + source.Playouts = [MakePlayout(source, PlayoutScheduleKind.Classic, [sourceItem])]; + + DomainChannel mirror = NewChannel("2", "Mirror", showInEpg: true); + mirror.PlayoutSource = ChannelPlayoutSource.Mirror; + mirror.MirrorSourceChannel = source; + mirror.PlayoutOffset = playoutOffset; + + context.Channels.AddRange(source, mirror); + await context.SaveChangesAsync(); + } + + ChannelGuideResponseModel result = await MakeHandler().Handle( + new GetChannelGuideData(BaseTime, BaseTime.AddDays(1)), + CancellationToken.None); + + ChannelGuideProgrammeResponseModel programme = result.Channels.Single(c => c.Number == "2").Programmes.Single(); + programme.Title.ShouldBe("Live Show"); + programme.Start.ShouldBe(new DateTimeOffset(BaseTime.Add(playoutOffset), TimeSpan.Zero)); + programme.Stop.ShouldBe(new DateTimeOffset(BaseTime.AddHours(1).Add(playoutOffset), TimeSpan.Zero)); + + // the WithPlayoutOffset copy fix: applying the mirror offset must not mutate the source playout's + // own (shared, AsNoTracking) PlayoutItem entities in place. + sourceItem.Start.ShouldBe(BaseTime); + sourceItem.Finish.ShouldBe(BaseTime.AddHours(1)); + } + [Test] public async Task Handle_Should_Return_Empty_Programmes_For_Channel_Without_Playout() { diff --git a/ErsatzTV/Controllers/Api/ChannelController.cs b/ErsatzTV/Controllers/Api/ChannelController.cs index 272764e70..c5d71d8ad 100644 --- a/ErsatzTV/Controllers/Api/ChannelController.cs +++ b/ErsatzTV/Controllers/Api/ChannelController.cs @@ -26,8 +26,8 @@ public class ChannelController(ChannelWriter workerCh [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.")] + "Returns per-channel programme arrays for the EPG grid. When omitted, start defaults to now, " + + "and end defaults to start plus the configured XmltvDaysToBuild window.")] [EndpointGroupName("general")] [ProducesResponseType(typeof(ChannelGuideResponseModel), StatusCodes.Status200OK)] public async Task GetGuide( diff --git a/ErsatzTV/wwwroot/openapi/v1.json b/ErsatzTV/wwwroot/openapi/v1.json index ffd9ea5c5..72654b915 100644 --- a/ErsatzTV/wwwroot/openapi/v1.json +++ b/ErsatzTV/wwwroot/openapi/v1.json @@ -221,7 +221,7 @@ "Channels" ], "summary": "Get the JSON channel guide (EPG)", - "description": "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.", + "description": "Returns per-channel programme arrays for the EPG grid. When omitted, start defaults to now, and end defaults to start plus the configured XmltvDaysToBuild window.", "parameters": [ { "name": "start",