diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs index 6b8392c4c..32e0c2c04 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs @@ -25,7 +25,27 @@ public class GetChannelStatesForApiHandler( .ThenBy(c => c.Number) .ToListAsync(cancellationToken); - int[] channelIds = channels.Map(c => c.Id).ToArray(); + if (channels.Count == 0) + { + return []; + } + + Dictionary channelLookup = channels + .ToDictionary( + c => c.Id, + c => + { + TimeSpan offset = c.PlayoutOffset ?? TimeSpan.Zero; + return (c.MirrorSourceChannelId ?? c.Id, offset, now - offset); + }); + + int[] sourceChannelIds = channelLookup.Values + .Map(v => v.SourceChannelId) + .Distinct() + .ToArray(); + + DateTime minLookupTime = channelLookup.Values.Min(v => v.LookupTime); + DateTime maxLookupTime = channelLookup.Values.Max(v => v.LookupTime); List currentItems = await dbContext.PlayoutItems .AsNoTracking() @@ -51,26 +71,32 @@ public class GetChannelStatesForApiHandler( .ThenInclude(mi => (mi as Image).ImageMetadata) .Include(pi => pi.MediaItem) .ThenInclude(mi => (mi as RemoteStream).RemoteStreamMetadata) - .Where(pi => channelIds.Contains(pi.Playout.ChannelId)) - .Where(pi => pi.Start <= now && pi.Finish > now) + .Where(pi => sourceChannelIds.Contains(pi.Playout.ChannelId)) + .Where(pi => pi.Start <= maxLookupTime && pi.Finish > minLookupTime) .OrderBy(pi => pi.Start) .ToListAsync(cancellationToken); - Dictionary currentItemsByChannelId = currentItems + Dictionary> currentItemsBySourceChannelId = currentItems .GroupBy(pi => pi.Playout.ChannelId) - .ToDictionary(g => g.Key, g => g.First()); + .ToDictionary(g => g.Key, g => g.OrderBy(pi => pi.Start).ToList()); return channels .Map(channel => { - currentItemsByChannelId.TryGetValue(channel.Id, out PlayoutItem playoutItem); + (int sourceChannelId, TimeSpan offset, DateTime lookupTime) = channelLookup[channel.Id]; + PlayoutItem playoutItem = null; + + if (currentItemsBySourceChannelId.TryGetValue(sourceChannelId, out List candidates)) + { + playoutItem = candidates.FirstOrDefault(pi => pi.Start <= lookupTime && pi.Finish > lookupTime); + } ChannelNowPlayingResponseModel nowPlaying = playoutItem is null ? null : new ChannelNowPlayingResponseModel( PlayoutMapper.GetDisplayTitle(playoutItem.MediaItem, Optional(playoutItem.ChapterTitle)), - new DateTimeOffset(playoutItem.Start, TimeSpan.Zero), - new DateTimeOffset(playoutItem.Finish, TimeSpan.Zero)); + new DateTimeOffset(playoutItem.Start + offset, TimeSpan.Zero), + new DateTimeOffset(playoutItem.Finish + offset, TimeSpan.Zero)); return new ChannelStateResponseModel( channel.Id, diff --git a/ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs b/ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs index df777b0c2..411cc0e66 100644 --- a/ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs +++ b/ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs @@ -105,6 +105,56 @@ public class GetChannelStatesForApiHandlerTests state.NowPlaying.Title.ShouldBe("Live Source"); } + [Test] + public async Task Handle_Should_Resolve_Mirror_Channel_NowPlaying_From_Source_With_Offset() + { + TimeSpan offset = TimeSpan.FromHours(1); + DateTime sourceStart = DateTime.UtcNow.AddMinutes(-70); + DateTime sourceFinish = DateTime.UtcNow.AddMinutes(-40); + + await using TvContext context = _db.CreateContext(); + Channel source = MakeChannel(10, "10"); + Channel mirror = MakeChannel(11, "11"); + mirror.PlayoutSource = ChannelPlayoutSource.Mirror; + mirror.MirrorSourceChannelId = source.Id; + mirror.PlayoutOffset = offset; + + var movie = new Movie + { + Id = 110, + MovieMetadata = [new MovieMetadata { Title = "Offset Feature" }] + }; + var playout = new Playout { Id = 111, Channel = source, ChannelId = source.Id, Items = [] }; + var item = new PlayoutItem + { + Id = 112, + MediaItem = movie, + MediaItemId = movie.Id, + Playout = playout, + PlayoutId = playout.Id, + Start = sourceStart, + Finish = sourceFinish, + ChapterTitle = string.Empty + }; + + context.Channels.AddRange(source, mirror); + context.Movies.Add(movie); + context.Playouts.Add(playout); + context.PlayoutItems.Add(item); + await context.SaveChangesAsync(); + + var handler = new GetChannelStatesForApiHandler(_db.Factory, _segmenter); + + List result = + await handler.Handle(new GetChannelStatesForApi(), CancellationToken.None); + + ChannelStateResponseModel mirrorState = result.Single(s => s.ChannelId == mirror.Id); + mirrorState.NowPlaying.ShouldNotBeNull(); + mirrorState.NowPlaying.Title.ShouldBe("Offset Feature"); + mirrorState.NowPlaying.StartUtc.ShouldBe(new DateTimeOffset(sourceStart + offset, TimeSpan.Zero)); + mirrorState.NowPlaying.FinishUtc.ShouldBe(new DateTimeOffset(sourceFinish + offset, TimeSpan.Zero)); + } + private async Task SeedChannelWithMovie(DateTime start, DateTime finish) { await using TvContext context = _db.CreateContext();