Browse Source

add scheduling context to playout details table (#2817)

* add scheduling context to playout details table

* fix missing context copies
pull/2820/head
Jason Dove 6 months ago committed by GitHub
parent
commit
4fa11b6943
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 1
      ErsatzTV.Application/Playouts/Mapper.cs
  3. 8
      ErsatzTV.Application/Playouts/PlayoutItemViewModel.cs
  4. 1
      ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs
  5. 2
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs
  6. 9
      ErsatzTV.Core/Domain/PlayoutItem.cs
  7. 1
      ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs
  8. 2
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutShuffledMediaCollectionEnumerator.cs
  9. 2
      ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs
  10. 2
      ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs
  11. 2
      ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs
  12. 25
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  13. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  14. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  15. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  16. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  17. 2
      ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs
  18. 2
      ErsatzTV.Core/Scheduling/RandomizedRotatingMediaCollectionEnumerator.cs
  19. 9
      ErsatzTV.Core/Scheduling/SchedulingContext.cs
  20. 2
      ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs
  21. 2
      ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs
  22. 2
      ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs
  23. 1
      ErsatzTV.Core/Scheduling/SingleMediaItemEnumerator.cs
  24. 7042
      ErsatzTV.Infrastructure.MySql/Migrations/20260205190138_Add_PlayoutItemSchedulingContext.Designer.cs
  25. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20260205190138_Add_PlayoutItemSchedulingContext.cs
  26. 3
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  27. 6869
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260205190056_Add_PlayoutItemSchedulingContext.Designer.cs
  28. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260205190056_Add_PlayoutItemSchedulingContext.cs
  29. 3
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  30. 2
      ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs
  31. 2
      ErsatzTV.Infrastructure/Scheduling/RerunMediaCollectionEnumerator.cs
  32. 21
      ErsatzTV/Pages/Playouts.razor
  33. 50
      ErsatzTV/Shared/SchedulingContextDialog.razor

3
CHANGELOG.md

@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add UI language setting to **Settings** > **UI** - Add UI language setting to **Settings** > **UI**
- A small number of translations have been added for `Português (Brasil)` and `Polski` - A small number of translations have been added for `Português (Brasil)` and `Polski`
- Translation contributions are always welcome! - 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
- E.g. items with the same random seed are part of the same shuffle
### Changed ### Changed
- Move dark/light mode toggle to **Settings** > **UI** - Move dark/light mode toggle to **Settings** > **UI**

1
ErsatzTV.Application/Playouts/Mapper.cs

@ -23,6 +23,7 @@ internal static class Mapper
playoutItem.StartOffset, playoutItem.StartOffset,
playoutItem.FinishOffset, playoutItem.FinishOffset,
playoutItem.GetDisplayDuration(), playoutItem.GetDisplayDuration(),
playoutItem.SchedulingContext,
Some(playoutItem.FillerKind)); Some(playoutItem.FillerKind));
internal static PlayoutAlternateScheduleViewModel ProjectToViewModel( internal static PlayoutAlternateScheduleViewModel ProjectToViewModel(

8
ErsatzTV.Application/Playouts/PlayoutItemViewModel.cs

@ -2,4 +2,10 @@ using ErsatzTV.Core.Domain.Filler;
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
public record PlayoutItemViewModel(string Title, DateTimeOffset Start, DateTimeOffset Finish, string Duration, Option<FillerKind> FillerKind); public record PlayoutItemViewModel(
string Title,
DateTimeOffset Start,
DateTimeOffset Finish,
string Duration,
string SchedulingContext,
Option<FillerKind> FillerKind);

1
ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs

@ -113,6 +113,7 @@ public class GetFuturePlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbCon
TimeSpan.FromSeconds(Math.Round(gapDuration.TotalSeconds)).ToString( TimeSpan.FromSeconds(Math.Round(gapDuration.TotalSeconds)).ToString(
gapDuration.TotalHours >= 1 ? @"h\:mm\:ss" : @"mm\:ss", gapDuration.TotalHours >= 1 ? @"h\:mm\:ss" : @"mm\:ss",
CultureInfo.CurrentCulture.DateTimeFormat), CultureInfo.CurrentCulture.DateTimeFormat),
string.Empty,
None None
); );
}).ToList(); }).ToList();

2
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs

@ -799,5 +799,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
Random random, Random random,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
throw new NotSupportedException(); throw new NotSupportedException();
protected override string SchedulingContextName => "Test";
} }
} }

9
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -37,6 +37,9 @@ public class PlayoutItem
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime(); public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime(); public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();
// for troubleshooting
public string SchedulingContext { get; set; }
public DateTimeOffset? GuideFinishOffset => GuideFinish.HasValue public DateTimeOffset? GuideFinishOffset => GuideFinish.HasValue
? new DateTimeOffset(GuideFinish.Value, TimeSpan.Zero).ToLocalTime() ? new DateTimeOffset(GuideFinish.Value, TimeSpan.Zero).ToLocalTime()
: null; : null;
@ -81,7 +84,8 @@ public class PlayoutItem
CollectionKey = CollectionKey, CollectionKey = CollectionKey,
CollectionEtag = CollectionEtag, CollectionEtag = CollectionEtag,
PlayoutItemWatermarks = watermarksCopy, PlayoutItemWatermarks = watermarksCopy,
PlayoutItemGraphicsElements = graphicsElementsCopy PlayoutItemGraphicsElements = graphicsElementsCopy,
SchedulingContext = SchedulingContext
}; };
} }
@ -127,7 +131,8 @@ public class PlayoutItem
CollectionKey = CollectionKey, CollectionKey = CollectionKey,
CollectionEtag = CollectionEtag, CollectionEtag = CollectionEtag,
PlayoutItemWatermarks = watermarksCopy, PlayoutItemWatermarks = watermarksCopy,
PlayoutItemGraphicsElements = graphicsElementsCopy PlayoutItemGraphicsElements = graphicsElementsCopy,
SchedulingContext = SchedulingContext
}; };
} }

1
ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs

@ -4,6 +4,7 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IMediaCollectionEnumerator public interface IMediaCollectionEnumerator
{ {
string SchedulingContextName { get; }
CollectionEnumeratorState State { get; } CollectionEnumeratorState State { get; }
Option<MediaItem> Current { get; } Option<MediaItem> Current { get; }
Option<bool> CurrentIncludeInProgramGuide { get; } Option<bool> CurrentIncludeInProgramGuide { get; }

2
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutShuffledMediaCollectionEnumerator.cs

@ -40,6 +40,8 @@ public class BlockPlayoutShuffledMediaCollectionEnumerator : IMediaCollectionEnu
} }
} }
public string SchedulingContextName => "Block Shuffle";
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None; public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;

2
ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs

@ -40,6 +40,8 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
// seed doesn't matter in chronological // seed doesn't matter in chronological
State.Index = state.Index; State.Index = state.Index;
public string SchedulingContextName => "Chronological";
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None; public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None;

2
ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs

@ -36,6 +36,8 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
// seed doesn't matter here // seed doesn't matter here
State.Index = state.Index; State.Index = state.Index;
public string SchedulingContextName => "Custom Order";
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None; public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None;

2
ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs

@ -35,6 +35,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
// seed doesn't matter here // seed doesn't matter here
State.Index = state.Index; State.Index = state.Index;
public string SchedulingContextName => "Playlist";
public CollectionEnumeratorState State { get; private set; } public CollectionEnumeratorState State { get; private set; }
public Option<MediaItem> Current => _sortedEnumerators.Count > 0 public Option<MediaItem> Current => _sortedEnumerators.Count > 0

25
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
@ -13,6 +15,14 @@ namespace ErsatzTV.Core.Scheduling;
public abstract class PlayoutModeSchedulerBase<T>(ILogger logger) : IPlayoutModeScheduler<T> public abstract class PlayoutModeSchedulerBase<T>(ILogger logger) : IPlayoutModeScheduler<T>
where T : ProgramScheduleItem where T : ProgramScheduleItem
{ {
// ReSharper disable once StaticMemberInGenericType
private static readonly JsonSerializerOptions Options = new()
{
Converters = { new JsonStringEnumConverter() },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
private readonly Random _random = new(); private readonly Random _random = new();
protected ILogger Logger { get; } = logger; protected ILogger Logger { get; } = logger;
@ -899,4 +909,19 @@ public abstract class PlayoutModeSchedulerBase<T>(ILogger logger) : IPlayoutMode
return result; return result;
} }
protected abstract string SchedulingContextName { get; }
protected string GetSchedulingContext(ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator)
{
var context = new SchedulingContext(
SchedulingContextName,
scheduleItem.ProgramScheduleId,
scheduleItem.Id,
enumerator.SchedulingContextName,
enumerator.State.Seed,
enumerator.State.Index);
return JsonSerializer.Serialize(context, Options);
}
} }

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -164,7 +164,8 @@ public class PlayoutModeSchedulerDuration(ILogger logger)
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode, SubtitleMode = scheduleItem.SubtitleMode,
PlayoutItemWatermarks = [], PlayoutItemWatermarks = [],
PlayoutItemGraphicsElements = [] PlayoutItemGraphicsElements = [],
SchedulingContext = GetSchedulingContext(scheduleItem, contentEnumerator)
}; };
foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem
@ -351,4 +352,6 @@ public class PlayoutModeSchedulerDuration(ILogger logger)
return new PlayoutSchedulerResult(nextState, playoutItems, warnings); return new PlayoutSchedulerResult(nextState, playoutItems, warnings);
} }
protected override string SchedulingContextName => "Duration";
} }

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -80,7 +80,8 @@ public class PlayoutModeSchedulerFlood(ILogger logger) : PlayoutModeSchedulerBas
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode, SubtitleMode = scheduleItem.SubtitleMode,
PlayoutItemWatermarks = [], PlayoutItemWatermarks = [],
PlayoutItemGraphicsElements = [] PlayoutItemGraphicsElements = [],
SchedulingContext = GetSchedulingContext(scheduleItem, contentEnumerator)
}; };
foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem
@ -205,4 +206,6 @@ public class PlayoutModeSchedulerFlood(ILogger logger) : PlayoutModeSchedulerBas
return new PlayoutSchedulerResult(nextState, playoutItems, warnings); return new PlayoutSchedulerResult(nextState, playoutItems, warnings);
} }
protected override string SchedulingContextName => "Flood";
} }

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -107,7 +107,8 @@ public class PlayoutModeSchedulerMultiple(Map<CollectionKey, int> collectionItem
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode, SubtitleMode = scheduleItem.SubtitleMode,
PlayoutItemWatermarks = [], PlayoutItemWatermarks = [],
PlayoutItemGraphicsElements = [] PlayoutItemGraphicsElements = [],
SchedulingContext = GetSchedulingContext(scheduleItem, contentEnumerator)
}; };
foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem
@ -206,4 +207,6 @@ public class PlayoutModeSchedulerMultiple(Map<CollectionKey, int> collectionItem
return new PlayoutSchedulerResult(nextState, playoutItems, warnings); return new PlayoutSchedulerResult(nextState, playoutItems, warnings);
} }
protected override string SchedulingContextName => "Multiple";
} }

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -56,7 +56,8 @@ public class PlayoutModeSchedulerOne(ILogger logger) : PlayoutModeSchedulerBase<
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode, SubtitleMode = scheduleItem.SubtitleMode,
PlayoutItemWatermarks = [], PlayoutItemWatermarks = [],
PlayoutItemGraphicsElements = [] PlayoutItemGraphicsElements = [],
SchedulingContext = GetSchedulingContext(scheduleItem, contentEnumerator)
}; };
foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem
@ -137,4 +138,6 @@ public class PlayoutModeSchedulerOne(ILogger logger) : PlayoutModeSchedulerBase<
return new PlayoutSchedulerResult(playoutBuilderState, [], warnings); return new PlayoutSchedulerResult(playoutBuilderState, [], warnings);
} }
protected override string SchedulingContextName => "One";
} }

2
ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs

@ -41,6 +41,8 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
// seed never changes here, no need to reset // seed never changes here, no need to reset
State.Index = state.Index; State.Index = state.Index;
public string SchedulingContextName => "Random";
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None; public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None;

2
ErsatzTV.Core/Scheduling/RandomizedRotatingMediaCollectionEnumerator.cs

@ -65,6 +65,8 @@ public class RandomizedRotatingMediaCollectionEnumerator : IMediaCollectionEnume
// seed never changes here, no need to reset // seed never changes here, no need to reset
State.Index = state.Index; State.Index = state.Index;
public string SchedulingContextName => "Random Rotating";
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None; public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None;

9
ErsatzTV.Core/Scheduling/SchedulingContext.cs

@ -0,0 +1,9 @@
namespace ErsatzTV.Core.Scheduling;
public record SchedulingContext(
string Scheduler,
int ScheduleId,
int ItemId,
string Enumerator,
int Seed,
int Index);

2
ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs

@ -39,6 +39,8 @@ public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnu
// seed doesn't matter here // seed doesn't matter here
State.Index = state.Index; State.Index = state.Index;
public string SchedulingContextName => "Season, Episode";
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None; public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None;

2
ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs

@ -59,6 +59,8 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
State.Index = state.Index; State.Index = state.Index;
} }
public string SchedulingContextName => "Shuffle in Order";
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _shuffled.Length != 0 ? _shuffled[State.Index % _mediaItemCount] : None; public Option<MediaItem> Current => _shuffled.Length != 0 ? _shuffled[State.Index % _mediaItemCount] : None;

2
ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs

@ -56,6 +56,8 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
State.Index = state.Index; State.Index = state.Index;
} }
public string SchedulingContextName => "Shuffle";
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None; public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;

1
ErsatzTV.Core/Scheduling/SingleMediaItemEnumerator.cs

@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Scheduling;
public class SingleMediaItemEnumerator(MediaItem mediaItem) : IMediaCollectionEnumerator public class SingleMediaItemEnumerator(MediaItem mediaItem) : IMediaCollectionEnumerator
{ {
public string SchedulingContextName => "Single";
public CollectionEnumeratorState State { get; } = new(); public CollectionEnumeratorState State { get; } = new();
public Option<MediaItem> Current => mediaItem; public Option<MediaItem> Current => mediaItem;
public Option<bool> CurrentIncludeInProgramGuide => Option<bool>.None; public Option<bool> CurrentIncludeInProgramGuide => Option<bool>.None;

7042
ErsatzTV.Infrastructure.MySql/Migrations/20260205190138_Add_PlayoutItemSchedulingContext.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20260205190138_Add_PlayoutItemSchedulingContext.cs

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutItemSchedulingContext : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SchedulingContext",
table: "PlayoutItem",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SchedulingContext",
table: "PlayoutItem");
}
}
}

3
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -2092,6 +2092,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<string>("PreferredSubtitleLanguageCode") b.Property<string>("PreferredSubtitleLanguageCode")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("SchedulingContext")
.HasColumnType("longtext");
b.Property<DateTime>("Start") b.Property<DateTime>("Start")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");

6869
ErsatzTV.Infrastructure.Sqlite/Migrations/20260205190056_Add_PlayoutItemSchedulingContext.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20260205190056_Add_PlayoutItemSchedulingContext.cs

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutItemSchedulingContext : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SchedulingContext",
table: "PlayoutItem",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SchedulingContext",
table: "PlayoutItem");
}
}
}

3
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -1999,6 +1999,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<string>("PreferredSubtitleLanguageCode") b.Property<string>("PreferredSubtitleLanguageCode")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("SchedulingContext")
.HasColumnType("TEXT");
b.Property<DateTime>("Start") b.Property<DateTime>("Start")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

2
ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs

@ -102,6 +102,8 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
State.Index = state.Index; State.Index = state.Index;
} }
public string SchedulingContextName => "Multi-Episode Shuffle";
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None; public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;

2
ErsatzTV.Infrastructure/Scheduling/RerunMediaCollectionEnumerator.cs

@ -16,6 +16,8 @@ public class RerunMediaCollectionEnumerator : IMediaCollectionEnumerator
_rerunHelper = rerunHelper; _rerunHelper = rerunHelper;
} }
public string SchedulingContextName => "Rerun";
public CollectionEnumeratorState State => _enumerator.State; public CollectionEnumeratorState State => _enumerator.State;
public Option<MediaItem> Current public Option<MediaItem> Current
{ {

21
ErsatzTV/Pages/Playouts.razor

@ -243,12 +243,25 @@
<MudTh>Finish</MudTh> <MudTh>Finish</MudTh>
<MudTh>Media Item</MudTh> <MudTh>Media Item</MudTh>
<MudTh>Duration</MudTh> <MudTh>Duration</MudTh>
<MudTh />
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd DataLabel="Start">@context.Start.ToString("G", _dtf)</MudTd> <MudTd DataLabel="Start">@context.Start.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Finish">@context.Finish.ToString("G", _dtf)</MudTd> <MudTd DataLabel="Finish">@context.Finish.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Media Item">@context.Title</MudTd> <MudTd DataLabel="Media Item">@context.Title</MudTd>
<MudTd DataLabel="Duration">@context.Duration</MudTd> <MudTd DataLabel="Duration">@context.Duration</MudTd>
<MudTd>
@if (!string.IsNullOrWhiteSpace(context.SchedulingContext))
{
<div style="align-items: center; display: flex;">
<MudTooltip Text="Troubleshoot">
<MudIconButton Icon="@Icons.Material.Filled.Info"
OnClick="@(_ => ShowSchedulingContext(context))">
</MudIconButton>
</MudTooltip>
</div>
}
</MudTd>
</RowTemplate> </RowTemplate>
<PagerContent> <PagerContent>
<MudTablePager/> <MudTablePager/>
@ -506,4 +519,12 @@
return string.Empty; return string.Empty;
} }
private async Task ShowSchedulingContext(PlayoutItemViewModel item)
{
var parameters = new DialogParameters { { "PlayoutItem", item } };
var options = new DialogOptions { CloseButton = true, CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
IDialogReference dialog = await Dialog.ShowAsync<SchedulingContextDialog>(item.Title, parameters, options);
DialogResult _ = await dialog.Result;
}
} }

50
ErsatzTV/Shared/SchedulingContextDialog.razor

@ -0,0 +1,50 @@
@using ErsatzTV.Application.Playouts
@inject IJSRuntime JsRuntime
<div>
<MudDialog>
<DialogContent>
<div class="overflow-y-scroll" style="max-height: 500px">
<pre>
<code @ref="_infoView">@_info</code>
</pre>
</div>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => CopyToClipboard(_infoView))">
Copy
</MudButton>
<MudButton Color="Color.Primary" OnClick="@Close">Close</MudButton>
</DialogActions>
</MudDialog>
</div>
@code {
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; }
[Parameter]
public PlayoutItemViewModel PlayoutItem { get; set; }
private string _info;
private ElementReference _infoView;
protected override Task OnParametersSetAsync()
{
try
{
_info = PlayoutItem.SchedulingContext;
}
catch (Exception ex)
{
_info = ex.ToString();
}
return Task.CompletedTask;
}
private async Task CopyToClipboard(ElementReference view) => await JsRuntime.InvokeVoidAsync("clipboardCopy.copyText", view);
private void Close() => MudDialog.Close(DialogResult.Ok(true));
}
Loading…
Cancel
Save