diff --git a/CHANGELOG.md b/CHANGELOG.md index a65b98139..8c11e16d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add UI language setting to **Settings** > **UI** - A small number of translations have been added for `Português (Brasil)` and `Polski` - Translation contributions are always welcome! -- Classic Schedules: add `Troubleshoot` button to playout details table to show info that may be helpful in determining the source of a playout item - - Info includes schedule, schedule item, scheduler, playback order, random seed, collection index +- Add `Troubleshoot` button to playout details table to show info that may be helpful in determining the source of a playout item + - Classic schedule info includes schedule, schedule item, scheduler, playback order, random seed, collection index + - Block schedule info includes block, block item, playback order, random seed, collection index - E.g. items with the same random seed are part of the same shuffle ### Changed diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/ProcessSchedulingContextHandler.cs b/ErsatzTV.Application/ProgramSchedules/Queries/ProcessSchedulingContextHandler.cs index c5863251e..0bf18ba87 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/ProcessSchedulingContextHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/ProcessSchedulingContextHandler.cs @@ -27,55 +27,114 @@ public class ProcessSchedulingContextHandler(IDbContextFactory dbCont } var classicContext = JsonConvert.DeserializeObject(request.SerializedContext); - if (classicContext is not null) + if (classicContext is not null && classicContext.ScheduleId > 0) { - await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); - - string scheduleName = await dbContext.ProgramSchedules - .Where(s => s.Id == classicContext.ScheduleId) - .Select(s => s.Name) - .SingleOrDefaultAsync(cancellationToken); - - var scheduleItem = await dbContext.ProgramScheduleItems - .AsNoTracking() - .AsSplitQuery() - .Include(si => si.SmartCollection) - .Include(si => si.Collection) - .Include(si => si.MultiCollection) - .Include(si => si.RerunCollection) - .Include(si => si.Playlist) - .SingleOrDefaultAsync(s => s.Id == classicContext.ItemId, cancellationToken); - - ClassicContextScheduleItem item; - if (scheduleItem is not null) - { - string collectionName = scheduleItem.CollectionType switch - { - CollectionType.SmartCollection => scheduleItem.SmartCollection.Name, - CollectionType.Collection => scheduleItem.Collection.Name, - CollectionType.MultiCollection => scheduleItem.MultiCollection.Name, - CollectionType.RerunRerun or CollectionType.RerunFirstRun => scheduleItem.RerunCollection.Name, - CollectionType.Playlist => scheduleItem.Playlist.Name, - _ => null - }; - - item = new ClassicContextScheduleItem(scheduleItem.Id, scheduleItem.CollectionType, collectionName); - } - else + return await GetClassicDetails(classicContext, cancellationToken); + } + + var blockContext = JsonConvert.DeserializeObject(request.SerializedContext); + if (blockContext is not null && blockContext.BlockId > 0) + { + return await GetBlockDetails(blockContext, cancellationToken); + } + + return request.SerializedContext; + } + + private async Task> GetClassicDetails( + ClassicSchedulingContext classicContext, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + string scheduleName = await dbContext.ProgramSchedules + .Where(s => s.Id == classicContext.ScheduleId) + .Select(s => s.Name) + .SingleOrDefaultAsync(cancellationToken); + + var scheduleItem = await dbContext.ProgramScheduleItems + .AsNoTracking() + .AsSplitQuery() + .Include(si => si.Collection) + .Include(si => si.MultiCollection) + .Include(si => si.Playlist) + .Include(si => si.RerunCollection) + .Include(si => si.SmartCollection) + .SingleOrDefaultAsync(s => s.Id == classicContext.ItemId, cancellationToken); + + ClassicContextScheduleItem item; + if (scheduleItem is not null) + { + string collectionName = scheduleItem.CollectionType switch { - item = new ClassicContextScheduleItem(classicContext.ItemId, null, null); - } + CollectionType.Collection => scheduleItem.Collection.Name, + CollectionType.MultiCollection => scheduleItem.MultiCollection.Name, + CollectionType.Playlist => scheduleItem.Playlist.Name, + CollectionType.RerunRerun or CollectionType.RerunFirstRun => scheduleItem.RerunCollection.Name, + CollectionType.SmartCollection => scheduleItem.SmartCollection.Name, + _ => null + }; + + item = new ClassicContextScheduleItem(scheduleItem.Id, scheduleItem.CollectionType, collectionName); + } + else + { + item = new ClassicContextScheduleItem(classicContext.ItemId, null, null); + } - var context = new ClassicContext( - new ContextScheduler("Classic", classicContext.Scheduler), - new ClassicContextSchedule(classicContext.ScheduleId, scheduleName ?? string.Empty), - item, - new ContextEnumerator(classicContext.Enumerator, classicContext.Seed, classicContext.Index)); + var context = new ClassicContext( + new ContextScheduler("Classic", classicContext.Scheduler), + new ClassicContextSchedule(classicContext.ScheduleId, scheduleName ?? string.Empty), + item, + new ContextEnumerator(classicContext.Enumerator, classicContext.Seed, classicContext.Index)); - return JsonSerializer.Serialize(context, Options); + return JsonSerializer.Serialize(context, Options); + } + + private async Task> GetBlockDetails(BlockSchedulingContext blockContext, CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + var block = await dbContext.Blocks + .Include(b => b.BlockGroup) + .SingleOrDefaultAsync(s => s.Id == blockContext.BlockId, cancellationToken); + + var blockItem = await dbContext.BlockItems + .AsNoTracking() + .AsSplitQuery() + .Include(si => si.Collection) + .Include(si => si.MultiCollection) + .Include(si => si.SmartCollection) + .SingleOrDefaultAsync(s => s.Id == blockContext.BlockItemId, cancellationToken); + + BlockContextBlockItem item; + if (blockItem is not null) + { + string collectionName = blockItem.CollectionType switch + { + CollectionType.Collection => blockItem.Collection.Name, + CollectionType.MultiCollection => blockItem.MultiCollection.Name, + CollectionType.SmartCollection => blockItem.SmartCollection.Name, + _ => null + }; + + item = new BlockContextBlockItem(blockItem.Id, blockItem.CollectionType, collectionName); + } + else + { + item = new BlockContextBlockItem(blockContext.BlockItemId, null, null); } - return request.SerializedContext; + var context = new BlockContext( + new ContextScheduler("Block", null), + new BlockContextBlock( + blockContext.BlockId, + block?.BlockGroup?.Name ?? string.Empty, + block?.Name ?? string.Empty), + item, + new ContextEnumerator(blockContext.Enumerator, blockContext.Seed, blockContext.Index)); + + return JsonSerializer.Serialize(context, Options); } private sealed record ClassicContext( @@ -91,4 +150,14 @@ public class ProcessSchedulingContextHandler(IDbContextFactory dbCont private sealed record ClassicContextScheduleItem(int Id, CollectionType? CollectionType, string CollectionName); private sealed record ContextEnumerator(string Name, int Seed, int Index); + + private sealed record BlockContext( + ContextScheduler Scheduler, + BlockContextBlock Block, + BlockContextBlockItem BlockItem, + ContextEnumerator Enumerator); + + private sealed record BlockContextBlock(int Id, string Group, string Name); + + private sealed record BlockContextBlockItem(int Id, CollectionType? CollectionType, string CollectionName); } diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs index 8e3a57146..aa79c8555 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using System.Text.Json.Serialization; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Scheduling; @@ -25,6 +27,13 @@ public class BlockPlayoutBuilder( NullValueHandling = NullValueHandling.Ignore }; + private static readonly JsonSerializerOptions Options = new() + { + Converters = { new JsonStringEnumConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + protected virtual ILogger Logger => logger; public virtual async Task> Build( @@ -262,7 +271,8 @@ public class BlockPlayoutBuilder( CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings), CollectionEtag = collectionEtags[collectionKey], PlayoutItemWatermarks = [], - PlayoutItemGraphicsElements = [] + PlayoutItemGraphicsElements = [], + SchedulingContext = GetSchedulingContext(blockItem, enumerator) }; foreach (BlockItemWatermark blockItemWatermark in blockItem.BlockItemWatermarks ?? []) @@ -448,4 +458,16 @@ public class BlockPlayoutBuilder( return result; } + + private static string GetSchedulingContext(BlockItem blockItem, IMediaCollectionEnumerator enumerator) + { + var context = new BlockSchedulingContext( + blockItem.BlockId, + blockItem.Id, + enumerator.SchedulingContextName, + enumerator.State.Seed, + enumerator.State.Index); + + return System.Text.Json.JsonSerializer.Serialize(context, Options); + } } diff --git a/ErsatzTV.Core/Scheduling/BlockSchedulingContext.cs b/ErsatzTV.Core/Scheduling/BlockSchedulingContext.cs new file mode 100644 index 000000000..a8707bc76 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/BlockSchedulingContext.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Core.Scheduling; + +public record BlockSchedulingContext( + int BlockId, + int BlockItemId, + string Enumerator, + int Seed, + int Index);