Browse Source

allow selecting multiple watermarks on schedule items (#2286)

* add and populate new table

* add watermark multiselect

* remove old column

* update changelog

* fix tests
pull/2288/head
Jason Dove 1 week ago committed by GitHub
parent
commit
942cf9e225
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 6
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 1
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  4. 1
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  5. 23
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  6. 1
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  7. 16
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  8. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  9. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs
  10. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  11. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs
  12. 2
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  13. 12
      ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs
  14. 2
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  15. 2
      ErsatzTV.Core/Domain/ChannelWatermark.cs
  16. 3
      ErsatzTV.Core/Domain/PlayoutItem.cs
  17. 4
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  18. 9
      ErsatzTV.Core/Domain/ProgramScheduleItemWatermark.cs
  19. 13
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  20. 13
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  21. 13
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  22. 13
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  23. 6318
      ErsatzTV.Infrastructure.MySql/Migrations/20250809125941_Add_ProgramScheduleItemWatermarks.Designer.cs
  24. 51
      ErsatzTV.Infrastructure.MySql/Migrations/20250809125941_Add_ProgramScheduleItemWatermarks.cs
  25. 6318
      ErsatzTV.Infrastructure.MySql/Migrations/20250809130844_Populate_ProgramScheduleItemWatermarks.Designer.cs
  26. 23
      ErsatzTV.Infrastructure.MySql/Migrations/20250809130844_Populate_ProgramScheduleItemWatermarks.cs
  27. 6306
      ErsatzTV.Infrastructure.MySql/Migrations/20250809133650_Remove_ProgramScheduleItemWatermark.Designer.cs
  28. 49
      ErsatzTV.Infrastructure.MySql/Migrations/20250809133650_Remove_ProgramScheduleItemWatermark.cs
  29. 49
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  30. 6153
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809130350_Add_ProgramScheduleItemWatermarks.Designer.cs
  31. 50
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809130350_Add_ProgramScheduleItemWatermarks.cs
  32. 6153
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809130815_Populate_ProgramScheduleItemWatermarks.Designer.cs
  33. 23
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809130815_Populate_ProgramScheduleItemWatermarks.cs
  34. 6141
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809133712_Remove_ProgramScheduleItemWatermark.Designer.cs
  35. 49
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809133712_Remove_ProgramScheduleItemWatermark.cs
  36. 49
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  37. 17
      ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs
  38. 2
      ErsatzTV/Pages/Artist.razor
  39. 29
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  40. 2
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  41. 2
      ErsatzTV/Pages/TelevisionSeasonList.razor
  42. 1
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

1
CHANGELOG.md

@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- Allow multiple watermarks on a single playout item - Allow multiple watermarks on a single playout item
- Allow multiple watermarks in playback troubleshooting - Allow multiple watermarks in playback troubleshooting
- Classic schedules: allow selecting multiple watermarks on schedule items
- YAML playout: `watermark` instruction changes: - YAML playout: `watermark` instruction changes:
- When value is `true`, will add named watermark to list of active watermarks - When value is `true`, will add named watermark to list of active watermarks
- When value is `false` and `name` is specified, will remove named watermark from list of active watermarks - When value is `false` and `name` is specified, will remove named watermark from list of active watermarks

6
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -148,7 +148,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{ {
// copy playout item ids back to watermarks // copy playout item ids back to watermarks
var allWatermarks = result.AddedItems.SelectMany(item => var allWatermarks = result.AddedItems.SelectMany(item =>
item.PlayoutItemWatermarks.Select(watermark => (item.PlayoutItemWatermarks ?? []).Select(watermark =>
{ {
watermark.PlayoutItemId = item.Id; watermark.PlayoutItemId = item.Id;
watermark.PlayoutItem = null; watermark.PlayoutItem = null;
@ -163,7 +163,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{ {
// copy playout item ids back to graphics elements // copy playout item ids back to graphics elements
var allGraphicsElements = result.AddedItems.SelectMany(item => var allGraphicsElements = result.AddedItems.SelectMany(item =>
item.PlayoutItemGraphicsElements.Select(graphicsElement => (item.PlayoutItemGraphicsElements ?? []).Select(graphicsElement =>
{ {
graphicsElement.PlayoutItemId = item.Id; graphicsElement.PlayoutItemId = item.Id;
graphicsElement.PlayoutItem = null; graphicsElement.PlayoutItem = null;
@ -333,6 +333,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.AsNoTracking() .AsNoTracking()
.Where(ps => ps.Playouts.Any(p => p.Id == playoutId)) .Where(ps => ps.Playouts.Any(p => p.Id == playoutId))
.Include(ps => ps.Items) .Include(ps => ps.Items)
.ThenInclude(psi => psi.ProgramScheduleItemWatermarks)
.ThenInclude(psi => psi.Watermark) .ThenInclude(psi => psi.Watermark)
.Include(ps => ps.Items) .Include(ps => ps.Items)
.ThenInclude(psi => psi.Collection) .ThenInclude(psi => psi.Collection)
@ -355,6 +356,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.Where(pt => pt.PlayoutId == playoutId) .Where(pt => pt.PlayoutId == playoutId)
.Include(a => a.ProgramSchedule) .Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.ProgramScheduleItemWatermarks)
.ThenInclude(psi => psi.Watermark) .ThenInclude(psi => psi.Watermark)
.Include(a => a.ProgramSchedule) .Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)

1
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs

@ -31,6 +31,7 @@ public record AddProgramScheduleItem(
int? TailFillerId, int? TailFillerId,
int? FallbackFillerId, int? FallbackFillerId,
int? WatermarkId, int? WatermarkId,
List<int> WatermarkIds,
string PreferredAudioLanguageCode, string PreferredAudioLanguageCode,
string PreferredAudioTitle, string PreferredAudioTitle,
string PreferredSubtitleLanguageCode, string PreferredSubtitleLanguageCode,

1
ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs

@ -29,6 +29,7 @@ public interface IProgramScheduleItemRequest
int? TailFillerId { get; } int? TailFillerId { get; }
int? FallbackFillerId { get; } int? FallbackFillerId { get; }
int? WatermarkId { get; } int? WatermarkId { get; }
List<int> WatermarkIds { get; }
string PreferredAudioLanguageCode { get; } string PreferredAudioLanguageCode { get; }
string PreferredAudioTitle { get; } string PreferredAudioTitle { get; }
string PreferredSubtitleLanguageCode { get; } string PreferredSubtitleLanguageCode { get; }

23
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -184,8 +184,9 @@ public abstract class ProgramScheduleItemCommandBase
protected static ProgramScheduleItem BuildItem( protected static ProgramScheduleItem BuildItem(
ProgramSchedule programSchedule, ProgramSchedule programSchedule,
int index, int index,
IProgramScheduleItemRequest item) => IProgramScheduleItemRequest item)
item.PlayoutMode switch {
ProgramScheduleItem result = item.PlayoutMode switch
{ {
PlayoutMode.Flood => new ProgramScheduleItemFlood PlayoutMode.Flood => new ProgramScheduleItemFlood
{ {
@ -208,7 +209,6 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId, PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId, TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId, FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode, PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle, PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
@ -235,7 +235,6 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId, PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId, TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId, FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode, PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle, PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
@ -264,7 +263,6 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId, PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId, TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId, FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode, PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle, PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
@ -296,7 +294,6 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId, PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId, TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId, FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode, PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle, PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
@ -305,6 +302,20 @@ public abstract class ProgramScheduleItemCommandBase
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}") _ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
}; };
foreach (var watermarkId in item.WatermarkIds)
{
result.ProgramScheduleItemWatermarks ??= [];
result.ProgramScheduleItemWatermarks.Add(
new ProgramScheduleItemWatermark
{
ProgramScheduleItem = result,
WatermarkId = watermarkId
});
}
return result;
}
private static TimeSpan? FixStartTime(TimeSpan? startTime) => private static TimeSpan? FixStartTime(TimeSpan? startTime) =>
startTime.HasValue && startTime.Value >= TimeSpan.FromDays(1) startTime.HasValue && startTime.Value >= TimeSpan.FromDays(1)
? startTime.Value.Subtract(TimeSpan.FromDays(1)) ? startTime.Value.Subtract(TimeSpan.FromDays(1))

1
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs

@ -31,6 +31,7 @@ public record ReplaceProgramScheduleItem(
int? TailFillerId, int? TailFillerId,
int? FallbackFillerId, int? FallbackFillerId,
int? WatermarkId, int? WatermarkId,
List<int> WatermarkIds,
string PreferredAudioLanguageCode, string PreferredAudioLanguageCode,
string PreferredAudioTitle, string PreferredAudioTitle,
string PreferredSubtitleLanguageCode, string PreferredSubtitleLanguageCode,

16
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -66,9 +66,7 @@ internal static class Mapper
duration.FallbackFiller != null duration.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(duration.FallbackFiller) ? Filler.Mapper.ProjectToViewModel(duration.FallbackFiller)
: null, : null,
duration.Watermark != null duration.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(),
? Watermarks.Mapper.ProjectToViewModel(duration.Watermark)
: null,
duration.PreferredAudioLanguageCode, duration.PreferredAudioLanguageCode,
duration.PreferredAudioTitle, duration.PreferredAudioTitle,
duration.PreferredSubtitleLanguageCode, duration.PreferredSubtitleLanguageCode,
@ -119,9 +117,7 @@ internal static class Mapper
flood.FallbackFiller != null flood.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(flood.FallbackFiller) ? Filler.Mapper.ProjectToViewModel(flood.FallbackFiller)
: null, : null,
flood.Watermark != null flood.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(),
? Watermarks.Mapper.ProjectToViewModel(flood.Watermark)
: null,
flood.PreferredAudioLanguageCode, flood.PreferredAudioLanguageCode,
flood.PreferredAudioTitle, flood.PreferredAudioTitle,
flood.PreferredSubtitleLanguageCode, flood.PreferredSubtitleLanguageCode,
@ -174,9 +170,7 @@ internal static class Mapper
multiple.FallbackFiller != null multiple.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(multiple.FallbackFiller) ? Filler.Mapper.ProjectToViewModel(multiple.FallbackFiller)
: null, : null,
multiple.Watermark != null multiple.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(),
? Watermarks.Mapper.ProjectToViewModel(multiple.Watermark)
: null,
multiple.PreferredAudioLanguageCode, multiple.PreferredAudioLanguageCode,
multiple.PreferredAudioTitle, multiple.PreferredAudioTitle,
multiple.PreferredSubtitleLanguageCode, multiple.PreferredSubtitleLanguageCode,
@ -227,9 +221,7 @@ internal static class Mapper
one.FallbackFiller != null one.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(one.FallbackFiller) ? Filler.Mapper.ProjectToViewModel(one.FallbackFiller)
: null, : null,
one.Watermark != null one.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(),
? Watermarks.Mapper.ProjectToViewModel(one.Watermark)
: null,
one.PreferredAudioLanguageCode, one.PreferredAudioLanguageCode,
one.PreferredAudioTitle, one.PreferredAudioTitle,
one.PreferredSubtitleLanguageCode, one.PreferredSubtitleLanguageCode,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -33,7 +33,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
FillerPresetViewModel postRollFiller, FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller, FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller, FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark, List<WatermarkViewModel> watermarks,
string preferredAudioLanguageCode, string preferredAudioLanguageCode,
string preferredAudioTitle, string preferredAudioTitle,
string preferredSubtitleLanguageCode, string preferredSubtitleLanguageCode,
@ -59,7 +59,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
postRollFiller, postRollFiller,
tailFiller, tailFiller,
fallbackFiller, fallbackFiller,
watermark, watermarks,
preferredAudioLanguageCode, preferredAudioLanguageCode,
preferredAudioTitle, preferredAudioTitle,
preferredSubtitleLanguageCode, preferredSubtitleLanguageCode,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -30,7 +30,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
FillerPresetViewModel postRollFiller, FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller, FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller, FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark, List<WatermarkViewModel> watermarks,
string preferredAudioLanguageCode, string preferredAudioLanguageCode,
string preferredAudioTitle, string preferredAudioTitle,
string preferredSubtitleLanguageCode, string preferredSubtitleLanguageCode,
@ -56,7 +56,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
postRollFiller, postRollFiller,
tailFiller, tailFiller,
fallbackFiller, fallbackFiller,
watermark, watermarks,
preferredAudioLanguageCode, preferredAudioLanguageCode,
preferredAudioTitle, preferredAudioTitle,
preferredSubtitleLanguageCode, preferredSubtitleLanguageCode,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -32,7 +32,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
FillerPresetViewModel postRollFiller, FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller, FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller, FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark, List<WatermarkViewModel> watermarks,
string preferredAudioLanguageCode, string preferredAudioLanguageCode,
string preferredAudioTitle, string preferredAudioTitle,
string preferredSubtitleLanguageCode, string preferredSubtitleLanguageCode,
@ -58,7 +58,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
postRollFiller, postRollFiller,
tailFiller, tailFiller,
fallbackFiller, fallbackFiller,
watermark, watermarks,
preferredAudioLanguageCode, preferredAudioLanguageCode,
preferredAudioTitle, preferredAudioTitle,
preferredSubtitleLanguageCode, preferredSubtitleLanguageCode,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -30,7 +30,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
FillerPresetViewModel postRollFiller, FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller, FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller, FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark, List<WatermarkViewModel> watermarks,
string preferredAudioLanguageCode, string preferredAudioLanguageCode,
string preferredAudioTitle, string preferredAudioTitle,
string preferredSubtitleLanguageCode, string preferredSubtitleLanguageCode,
@ -56,7 +56,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
postRollFiller, postRollFiller,
tailFiller, tailFiller,
fallbackFiller, fallbackFiller,
watermark, watermarks,
preferredAudioLanguageCode, preferredAudioLanguageCode,
preferredAudioTitle, preferredAudioTitle,
preferredSubtitleLanguageCode, preferredSubtitleLanguageCode,

2
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -29,7 +29,7 @@ public abstract record ProgramScheduleItemViewModel(
FillerPresetViewModel PostRollFiller, FillerPresetViewModel PostRollFiller,
FillerPresetViewModel TailFiller, FillerPresetViewModel TailFiller,
FillerPresetViewModel FallbackFiller, FillerPresetViewModel FallbackFiller,
WatermarkViewModel Watermark, List<WatermarkViewModel> Watermarks,
string PreferredAudioLanguageCode, string PreferredAudioLanguageCode,
string PreferredAudioTitle, string PreferredAudioTitle,
string PreferredSubtitleLanguageCode, string PreferredSubtitleLanguageCode,

12
ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs

@ -6,19 +6,14 @@ using static ErsatzTV.Application.ProgramSchedules.Mapper;
namespace ErsatzTV.Application.ProgramSchedules; namespace ErsatzTV.Application.ProgramSchedules;
public class GetProgramScheduleItemsHandler : public class GetProgramScheduleItemsHandler(IDbContextFactory<TvContext> dbContextFactory) :
IRequestHandler<GetProgramScheduleItems, List<ProgramScheduleItemViewModel>> IRequestHandler<GetProgramScheduleItems, List<ProgramScheduleItemViewModel>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetProgramScheduleItemsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<ProgramScheduleItemViewModel>> Handle( public async Task<List<ProgramScheduleItemViewModel>> Handle(
GetProgramScheduleItems request, GetProgramScheduleItems request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<ProgramSchedule> maybeProgramSchedule = Option<ProgramSchedule> maybeProgramSchedule =
await dbContext.ProgramSchedules.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id); await dbContext.ProgramSchedules.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id);
@ -50,7 +45,8 @@ public class GetProgramScheduleItemsHandler :
.Include(i => i.PostRollFiller) .Include(i => i.PostRollFiller)
.Include(i => i.TailFiller) .Include(i => i.TailFiller)
.Include(i => i.FallbackFiller) .Include(i => i.FallbackFiller)
.Include(i => i.Watermark) .Include(i => i.ProgramScheduleItemWatermarks)
.ThenInclude(i => i.Watermark)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel) .Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel)
.Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList()); .Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList());

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

@ -414,6 +414,7 @@ public class ScheduleIntegrationTests
.AsNoTracking() .AsNoTracking()
.Where(ps => ps.Playouts.Any(p => p.Id == playoutId)) .Where(ps => ps.Playouts.Any(p => p.Id == playoutId))
.Include(ps => ps.Items) .Include(ps => ps.Items)
.ThenInclude(psi => psi.ProgramScheduleItemWatermarks)
.ThenInclude(psi => psi.Watermark) .ThenInclude(psi => psi.Watermark)
.Include(ps => ps.Items) .Include(ps => ps.Items)
.ThenInclude(psi => psi.Collection) .ThenInclude(psi => psi.Collection)
@ -436,6 +437,7 @@ public class ScheduleIntegrationTests
.Where(pt => pt.PlayoutId == playoutId) .Where(pt => pt.PlayoutId == playoutId)
.Include(a => a.ProgramSchedule) .Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.ProgramScheduleItemWatermarks)
.ThenInclude(psi => psi.Watermark) .ThenInclude(psi => psi.Watermark)
.Include(a => a.ProgramSchedule) .Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)

2
ErsatzTV.Core/Domain/ChannelWatermark.cs

@ -22,6 +22,8 @@ public class ChannelWatermark
public string OpacityExpression { get; set; } public string OpacityExpression { get; set; }
public List<PlayoutItem> PlayoutItems { get; set; } public List<PlayoutItem> PlayoutItems { get; set; }
public List<PlayoutItemWatermark> PlayoutItemWatermarks { get; set; } public List<PlayoutItemWatermark> PlayoutItemWatermarks { get; set; }
public List<ProgramScheduleItem> ProgramScheduleItems { get; set; }
public List<ProgramScheduleItemWatermark> ProgramScheduleItemWatermarks { get; set; }
public int ZIndex { get; set; } public int ZIndex { get; set; }
} }

3
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -65,7 +65,8 @@ public class PlayoutItem
SubtitleMode = SubtitleMode, SubtitleMode = SubtitleMode,
BlockKey = BlockKey, BlockKey = BlockKey,
CollectionKey = CollectionKey, CollectionKey = CollectionKey,
CollectionEtag = CollectionEtag CollectionEtag = CollectionEtag,
PlayoutItemWatermarks = PlayoutItemWatermarks?.ToList()
}; };
public string GetDisplayDuration() public string GetDisplayDuration()

4
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -41,8 +41,8 @@ public abstract class ProgramScheduleItem
public FillerPreset TailFiller { get; set; } public FillerPreset TailFiller { get; set; }
public int? FallbackFillerId { get; set; } public int? FallbackFillerId { get; set; }
public FillerPreset FallbackFiller { get; set; } public FillerPreset FallbackFiller { get; set; }
public ChannelWatermark Watermark { get; set; } public List<ChannelWatermark> Watermarks { get; set; }
public int? WatermarkId { get; set; } public List<ProgramScheduleItemWatermark> ProgramScheduleItemWatermarks { get; set; }
public string PreferredAudioLanguageCode { get; set; } public string PreferredAudioLanguageCode { get; set; }
public string PreferredAudioTitle { get; set; } public string PreferredAudioTitle { get; set; }
public string PreferredSubtitleLanguageCode { get; set; } public string PreferredSubtitleLanguageCode { get; set; }

9
ErsatzTV.Core/Domain/ProgramScheduleItemWatermark.cs

@ -0,0 +1,9 @@
namespace ErsatzTV.Core.Domain;
public class ProgramScheduleItemWatermark
{
public int ProgramScheduleItemId { get; set; }
public ProgramScheduleItem ProgramScheduleItem { get; set; }
public int? WatermarkId { get; set; }
public ChannelWatermark Watermark { get; set; }
}

13
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -161,13 +161,18 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredAudioTitle = scheduleItem.PreferredAudioTitle, PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode SubtitleMode = scheduleItem.SubtitleMode,
PlayoutItemWatermarks = []
}; };
if (scheduleItem.WatermarkId is not null) foreach (var programScheduleItemWatermark in scheduleItem.ProgramScheduleItemWatermarks ?? [])
{ {
playoutItem.Watermarks ??= []; playoutItem.PlayoutItemWatermarks.Add(
playoutItem.Watermarks.Add(scheduleItem.Watermark); new PlayoutItemWatermark
{
PlayoutItem = playoutItem,
WatermarkId = programScheduleItemWatermark.WatermarkId
});
} }
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime); durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);

13
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -80,13 +80,18 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredAudioTitle = scheduleItem.PreferredAudioTitle, PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode SubtitleMode = scheduleItem.SubtitleMode,
PlayoutItemWatermarks = []
}; };
if (scheduleItem.WatermarkId is not null) foreach (var programScheduleItemWatermark in scheduleItem.ProgramScheduleItemWatermarks ?? [])
{ {
playoutItem.Watermarks ??= []; playoutItem.PlayoutItemWatermarks.Add(
playoutItem.Watermarks.Add(scheduleItem.Watermark); new PlayoutItemWatermark
{
PlayoutItem = playoutItem,
WatermarkId = programScheduleItemWatermark.WatermarkId
});
} }
var enumeratorStates = new Dictionary<CollectionKey, CollectionEnumeratorState>(); var enumeratorStates = new Dictionary<CollectionKey, CollectionEnumeratorState>();

13
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -104,13 +104,18 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredAudioTitle = scheduleItem.PreferredAudioTitle, PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode SubtitleMode = scheduleItem.SubtitleMode,
PlayoutItemWatermarks = []
}; };
if (scheduleItem.WatermarkId is not null) foreach (var programScheduleItemWatermark in scheduleItem.ProgramScheduleItemWatermarks ?? [])
{ {
playoutItem.Watermarks ??= []; playoutItem.PlayoutItemWatermarks.Add(
playoutItem.Watermarks.Add(scheduleItem.Watermark); new PlayoutItemWatermark
{
PlayoutItem = playoutItem,
WatermarkId = programScheduleItemWatermark.WatermarkId
});
} }
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime); // LogScheduledItem(scheduleItem, mediaItem, itemStartTime);

13
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -51,13 +51,18 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredAudioTitle = scheduleItem.PreferredAudioTitle, PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode SubtitleMode = scheduleItem.SubtitleMode,
PlayoutItemWatermarks = []
}; };
if (scheduleItem.WatermarkId is not null) foreach (var programScheduleItemWatermark in scheduleItem.ProgramScheduleItemWatermarks ?? [])
{ {
playoutItem.Watermarks ??= []; playoutItem.PlayoutItemWatermarks.Add(
playoutItem.Watermarks.Add(scheduleItem.Watermark); new PlayoutItemWatermark
{
PlayoutItem = playoutItem,
WatermarkId = programScheduleItemWatermark.WatermarkId
});
} }
List<PlayoutItem> playoutItems = AddFiller( List<PlayoutItem> playoutItems = AddFiller(

6318
ErsatzTV.Infrastructure.MySql/Migrations/20250809125941_Add_ProgramScheduleItemWatermarks.Designer.cs generated

File diff suppressed because it is too large Load Diff

51
ErsatzTV.Infrastructure.MySql/Migrations/20250809125941_Add_ProgramScheduleItemWatermarks.cs

@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_ProgramScheduleItemWatermarks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ProgramScheduleItemWatermark",
columns: table => new
{
ProgramScheduleItemId = table.Column<int>(type: "int", nullable: false),
WatermarkId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProgramScheduleItemWatermark", x => new { x.ProgramScheduleItemId, x.WatermarkId });
table.ForeignKey(
name: "FK_ProgramScheduleItemWatermark_ChannelWatermark_WatermarkId",
column: x => x.WatermarkId,
principalTable: "ChannelWatermark",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ProgramScheduleItemWatermark_ProgramScheduleItem_ProgramSche~",
column: x => x.ProgramScheduleItemId,
principalTable: "ProgramScheduleItem",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleItemWatermark_WatermarkId",
table: "ProgramScheduleItemWatermark",
column: "WatermarkId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ProgramScheduleItemWatermark");
}
}
}

6318
ErsatzTV.Infrastructure.MySql/Migrations/20250809130844_Populate_ProgramScheduleItemWatermarks.Designer.cs generated

File diff suppressed because it is too large Load Diff

23
ErsatzTV.Infrastructure.MySql/Migrations/20250809130844_Populate_ProgramScheduleItemWatermarks.cs

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Populate_ProgramScheduleItemWatermarks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"INSERT INTO `ProgramScheduleItemWatermark` (`ProgramScheduleItemId`, `WatermarkId`)
SELECT `Id`, `WatermarkId` FROM `ProgramScheduleItem` WHERE `WatermarkId` IS NOT NULL");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

6306
ErsatzTV.Infrastructure.MySql/Migrations/20250809133650_Remove_ProgramScheduleItemWatermark.Designer.cs generated

File diff suppressed because it is too large Load Diff

49
ErsatzTV.Infrastructure.MySql/Migrations/20250809133650_Remove_ProgramScheduleItemWatermark.cs

@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Remove_ProgramScheduleItemWatermark : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_ChannelWatermark_WatermarkId",
table: "ProgramScheduleItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleItem_WatermarkId",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "WatermarkId",
table: "ProgramScheduleItem");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "WatermarkId",
table: "ProgramScheduleItem",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleItem_WatermarkId",
table: "ProgramScheduleItem",
column: "WatermarkId");
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_ChannelWatermark_WatermarkId",
table: "ProgramScheduleItem",
column: "WatermarkId",
principalTable: "ChannelWatermark",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
}
}

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

@ -2240,9 +2240,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int?>("TailFillerId") b.Property<int?>("TailFillerId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int?>("WatermarkId")
.HasColumnType("int");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("CollectionId"); b.HasIndex("CollectionId");
@ -2267,13 +2264,26 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasIndex("TailFillerId"); b.HasIndex("TailFillerId");
b.HasIndex("WatermarkId");
b.ToTable("ProgramScheduleItem", (string)null); b.ToTable("ProgramScheduleItem", (string)null);
b.UseTptMappingStrategy(); b.UseTptMappingStrategy();
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemWatermark", b =>
{
b.Property<int>("ProgramScheduleItemId")
.HasColumnType("int");
b.Property<int>("WatermarkId")
.HasColumnType("int");
b.HasKey("ProgramScheduleItemId", "WatermarkId");
b.HasIndex("WatermarkId");
b.ToTable("ProgramScheduleItemWatermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b => modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -4915,11 +4925,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.HasForeignKey("TailFillerId") .HasForeignKey("TailFillerId")
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Collection"); b.Navigation("Collection");
b.Navigation("FallbackFiller"); b.Navigation("FallbackFiller");
@ -4941,6 +4946,23 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("SmartCollection"); b.Navigation("SmartCollection");
b.Navigation("TailFiller"); b.Navigation("TailFiller");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemWatermark", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "ProgramScheduleItem")
.WithMany("ProgramScheduleItemWatermarks")
.HasForeignKey("ProgramScheduleItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany("ProgramScheduleItemWatermarks")
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProgramScheduleItem");
b.Navigation("Watermark"); b.Navigation("Watermark");
}); });
@ -5832,6 +5854,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b => modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b =>
{ {
b.Navigation("PlayoutItemWatermarks"); b.Navigation("PlayoutItemWatermarks");
b.Navigation("ProgramScheduleItemWatermarks");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
@ -6037,6 +6061,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("ProgramScheduleAlternates"); b.Navigation("ProgramScheduleAlternates");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
{
b.Navigation("ProgramScheduleItemWatermarks");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b => modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b =>
{ {
b.Navigation("Actors"); b.Navigation("Actors");

6153
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809130350_Add_ProgramScheduleItemWatermarks.Designer.cs generated

File diff suppressed because it is too large Load Diff

50
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809130350_Add_ProgramScheduleItemWatermarks.cs

@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_ProgramScheduleItemWatermarks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ProgramScheduleItemWatermark",
columns: table => new
{
ProgramScheduleItemId = table.Column<int>(type: "INTEGER", nullable: false),
WatermarkId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProgramScheduleItemWatermark", x => new { x.ProgramScheduleItemId, x.WatermarkId });
table.ForeignKey(
name: "FK_ProgramScheduleItemWatermark_ChannelWatermark_WatermarkId",
column: x => x.WatermarkId,
principalTable: "ChannelWatermark",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ProgramScheduleItemWatermark_ProgramScheduleItem_ProgramScheduleItemId",
column: x => x.ProgramScheduleItemId,
principalTable: "ProgramScheduleItem",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleItemWatermark_WatermarkId",
table: "ProgramScheduleItemWatermark",
column: "WatermarkId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ProgramScheduleItemWatermark");
}
}
}

6153
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809130815_Populate_ProgramScheduleItemWatermarks.Designer.cs generated

File diff suppressed because it is too large Load Diff

23
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809130815_Populate_ProgramScheduleItemWatermarks.cs

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Populate_ProgramScheduleItemWatermarks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"INSERT INTO `ProgramScheduleItemWatermark` (`ProgramScheduleItemId`, `WatermarkId`)
SELECT `Id`, `WatermarkId` FROM `ProgramScheduleItem` WHERE `WatermarkId` IS NOT NULL");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

6141
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809133712_Remove_ProgramScheduleItemWatermark.Designer.cs generated

File diff suppressed because it is too large Load Diff

49
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809133712_Remove_ProgramScheduleItemWatermark.cs

@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Remove_ProgramScheduleItemWatermark : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_ChannelWatermark_WatermarkId",
table: "ProgramScheduleItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleItem_WatermarkId",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "WatermarkId",
table: "ProgramScheduleItem");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "WatermarkId",
table: "ProgramScheduleItem",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleItem_WatermarkId",
table: "ProgramScheduleItem",
column: "WatermarkId");
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_ChannelWatermark_WatermarkId",
table: "ProgramScheduleItem",
column: "WatermarkId",
principalTable: "ChannelWatermark",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
}
}

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

@ -2133,9 +2133,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int?>("TailFillerId") b.Property<int?>("TailFillerId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int?>("WatermarkId")
.HasColumnType("INTEGER");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("CollectionId"); b.HasIndex("CollectionId");
@ -2160,13 +2157,26 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("TailFillerId"); b.HasIndex("TailFillerId");
b.HasIndex("WatermarkId");
b.ToTable("ProgramScheduleItem", (string)null); b.ToTable("ProgramScheduleItem", (string)null);
b.UseTptMappingStrategy(); b.UseTptMappingStrategy();
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemWatermark", b =>
{
b.Property<int>("ProgramScheduleItemId")
.HasColumnType("INTEGER");
b.Property<int>("WatermarkId")
.HasColumnType("INTEGER");
b.HasKey("ProgramScheduleItemId", "WatermarkId");
b.HasIndex("WatermarkId");
b.ToTable("ProgramScheduleItemWatermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b => modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -4750,11 +4760,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.HasForeignKey("TailFillerId") .HasForeignKey("TailFillerId")
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Collection"); b.Navigation("Collection");
b.Navigation("FallbackFiller"); b.Navigation("FallbackFiller");
@ -4776,6 +4781,23 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("SmartCollection"); b.Navigation("SmartCollection");
b.Navigation("TailFiller"); b.Navigation("TailFiller");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemWatermark", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "ProgramScheduleItem")
.WithMany("ProgramScheduleItemWatermarks")
.HasForeignKey("ProgramScheduleItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany("ProgramScheduleItemWatermarks")
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProgramScheduleItem");
b.Navigation("Watermark"); b.Navigation("Watermark");
}); });
@ -5667,6 +5689,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b => modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b =>
{ {
b.Navigation("PlayoutItemWatermarks"); b.Navigation("PlayoutItemWatermarks");
b.Navigation("ProgramScheduleItemWatermarks");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
@ -5872,6 +5896,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("ProgramScheduleAlternates"); b.Navigation("ProgramScheduleAlternates");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
{
b.Navigation("ProgramScheduleItemWatermarks");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b => modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b =>
{ {
b.Navigation("Actors"); b.Navigation("Actors");

17
ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs

@ -70,10 +70,17 @@ public class ProgramScheduleItemConfiguration : IEntityTypeConfiguration<Program
.OnDelete(DeleteBehavior.SetNull) .OnDelete(DeleteBehavior.SetNull)
.IsRequired(false); .IsRequired(false);
builder.HasOne(i => i.Watermark) builder.HasMany(c => c.Watermarks)
.WithMany() .WithMany(m => m.ProgramScheduleItems)
.HasForeignKey(i => i.WatermarkId) .UsingEntity<ProgramScheduleItemWatermark>(
.OnDelete(DeleteBehavior.SetNull) j => j.HasOne(ci => ci.Watermark)
.IsRequired(false); .WithMany(mi => mi.ProgramScheduleItemWatermarks)
.HasForeignKey(ci => ci.WatermarkId)
.OnDelete(DeleteBehavior.Cascade),
j => j.HasOne(ci => ci.ProgramScheduleItem)
.WithMany(c => c.ProgramScheduleItemWatermarks)
.HasForeignKey(ci => ci.ProgramScheduleItemId)
.OnDelete(DeleteBehavior.Cascade),
j => j.HasKey(ci => new { ci.ProgramScheduleItemId, ci.WatermarkId }));
} }
} }

2
ErsatzTV/Pages/Artist.razor

@ -299,7 +299,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (result is { Canceled: false, Data: ProgramScheduleViewModel schedule }) if (result is { Canceled: false, Data: ProgramScheduleViewModel schedule })
{ {
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, MultipleMode.Count, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, MultipleMode.Count, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, [], null, null, null, null), _cts.Token);
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items"); NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
} }
} }

29
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -50,18 +50,18 @@
} }
</div> </div>
<div style="margin-left: auto" class="d-none d-md-flex"> <div style="margin-left: auto" class="d-none d-md-flex">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveChanges" StartIcon="@Icons.Material.Filled.Save"> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@SaveChanges" StartIcon="@Icons.Material.Filled.Save">
Save Schedule Items Save Schedule Items
</MudButton> </MudButton>
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Default" OnClick="AddScheduleItem" StartIcon="@Icons.Material.Filled.PlaylistAdd"> <MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Default" OnClick="@AddScheduleItem" StartIcon="@Icons.Material.Filled.PlaylistAdd">
Add Schedule Item Add Schedule Item
</MudButton> </MudButton>
</div> </div>
<div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none"> <div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none">
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<MudMenu Icon="@Icons.Material.Filled.MoreVert"> <MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem Icon="@Icons.Material.Filled.Save" Label="Save Schedule Items" OnClick="SaveChanges"/> <MudMenuItem Icon="@Icons.Material.Filled.Save" Label="Save Schedule Items" OnClick="@SaveChanges"/>
<MudMenuItem Icon="@Icons.Material.Filled.PlaylistAdd" Label="Add Schedule Item" OnClick="AddScheduleItem"/> <MudMenuItem Icon="@Icons.Material.Filled.PlaylistAdd" Label="Add Schedule Item" OnClick="@AddScheduleItem"/>
</MudMenu> </MudMenu>
</div> </div>
</div> </div>
@ -563,6 +563,21 @@
} }
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Watermarks</MudText>
</div>
<MudSelect T="WatermarkViewModel"
@bind-SelectedValues="_selectedItem.Watermarks"
ToStringFunc="@(wm => wm?.Name)"
Clearable="true"
MultiSelection="true">
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem Value="@watermark">@watermark.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"> <div class="d-flex">
<MudText>Preferred Audio Language</MudText> <MudText>Preferred Audio Language</MudText>
@ -770,11 +785,11 @@
PostRollFiller = item.PostRollFiller, PostRollFiller = item.PostRollFiller,
TailFiller = item.TailFiller, TailFiller = item.TailFiller,
FallbackFiller = item.FallbackFiller, FallbackFiller = item.FallbackFiller,
Watermark = item.Watermark,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode, PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle, PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode SubtitleMode = item.SubtitleMode,
Watermarks = item.Watermarks
}; };
switch (item) switch (item)
@ -839,6 +854,7 @@
TailFiller = item.TailFiller, TailFiller = item.TailFiller,
FallbackFiller = item.FallbackFiller, FallbackFiller = item.FallbackFiller,
Watermark = item.Watermark, Watermark = item.Watermark,
Watermarks = item.Watermarks.ToList(),
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode, PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle, PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
@ -913,6 +929,7 @@
item.TailFiller?.Id, item.TailFiller?.Id,
item.FallbackFiller?.Id, item.FallbackFiller?.Id,
item.Watermark?.Id, item.Watermark?.Id,
item.Watermarks.Map(wm => wm.Id).ToList(),
item.PreferredAudioLanguageCode, item.PreferredAudioLanguageCode,
item.PreferredAudioTitle, item.PreferredAudioTitle,
item.PreferredSubtitleLanguageCode, item.PreferredSubtitleLanguageCode,

2
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -259,7 +259,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (result is { Canceled: false, Data: ProgramScheduleViewModel schedule }) if (result is { Canceled: false, Data: ProgramScheduleViewModel schedule })
{ {
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, MultipleMode.Count, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, MultipleMode.Count, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, [], null, null, null, null), _cts.Token);
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items"); NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
} }
} }

2
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -251,7 +251,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (result is { Canceled: false, Data: ProgramScheduleViewModel schedule }) if (result is { Canceled: false, Data: ProgramScheduleViewModel schedule })
{ {
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, MultipleMode.Count, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, MultipleMode.Count, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, [], null, null, null, null), _cts.Token);
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items"); NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
} }
} }

1
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -93,6 +93,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
public FillerPresetViewModel TailFiller { get; set; } public FillerPresetViewModel TailFiller { get; set; }
public FillerPresetViewModel FallbackFiller { get; set; } public FillerPresetViewModel FallbackFiller { get; set; }
public WatermarkViewModel Watermark { get; set; } public WatermarkViewModel Watermark { get; set; }
public IEnumerable<WatermarkViewModel> Watermarks { get; set; }
public string PreferredAudioLanguageCode { get; set; } public string PreferredAudioLanguageCode { get; set; }
public string PreferredAudioTitle { get; set; } public string PreferredAudioTitle { get; set; }
public string PreferredSubtitleLanguageCode { get; set; } public string PreferredSubtitleLanguageCode { get; set; }

Loading…
Cancel
Save