Browse Source

allow selecting multiple watermarks on decos (#2287)

* load fonts on demand

* add new table

* populate new table

* edit and use multiple watermarks in deco

* remove old field

* update changelog
pull/2288/head
Jason Dove 1 week ago committed by GitHub
parent
commit
908125f8a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs
  3. 4
      ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs
  4. 33
      ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs
  5. 3
      ErsatzTV.Application/Scheduling/DecoViewModel.cs
  6. 2
      ErsatzTV.Application/Scheduling/Mapper.cs
  7. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecoByIdHandler.cs
  8. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutIdHandler.cs
  9. 4
      ErsatzTV.Application/Scheduling/Queries/GetDecosByDecoGroupIdHandler.cs
  10. 14
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  11. 4
      ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs
  12. 5
      ErsatzTV.Core/Domain/ChannelWatermark.cs
  13. 11
      ErsatzTV.Core/Domain/DecoWatermark.cs
  14. 2
      ErsatzTV.Core/Domain/PlayoutItemWatermark.cs
  15. 2
      ErsatzTV.Core/Domain/ProgramScheduleItemWatermark.cs
  16. 4
      ErsatzTV.Core/Domain/Scheduling/Deco.cs
  17. 6344
      ErsatzTV.Infrastructure.MySql/Migrations/20250809155249_Add_DecoWatermarks.Designer.cs
  18. 51
      ErsatzTV.Infrastructure.MySql/Migrations/20250809155249_Add_DecoWatermarks.cs
  19. 6344
      ErsatzTV.Infrastructure.MySql/Migrations/20250809160002_Populate_DecoWatermarks.Designer.cs
  20. 23
      ErsatzTV.Infrastructure.MySql/Migrations/20250809160002_Populate_DecoWatermarks.cs
  21. 6332
      ErsatzTV.Infrastructure.MySql/Migrations/20250809165720_Remove_DecoWatermark.Designer.cs
  22. 49
      ErsatzTV.Infrastructure.MySql/Migrations/20250809165720_Remove_DecoWatermark.cs
  23. 50
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  24. 6179
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809155342_Add_DecoWatermarks.Designer.cs
  25. 50
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809155342_Add_DecoWatermarks.cs
  26. 6179
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809155927_Populate_DecoWatermarks.Designer.cs
  27. 23
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809155927_Populate_DecoWatermarks.cs
  28. 6167
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809165652_Remove_DecoWatermark.Designer.cs
  29. 49
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250809165652_Remove_DecoWatermark.cs
  30. 50
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  31. 20
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoConfiguration.cs
  32. 29
      ErsatzTV.Infrastructure/Streaming/GraphicsEngineFonts.cs
  33. 6
      ErsatzTV/Pages/BlockPlayoutEditor.razor
  34. 16
      ErsatzTV/Pages/DecoEditor.razor
  35. 3
      ErsatzTV/ViewModels/DecoEditViewModel.cs

2
CHANGELOG.md

@ -36,9 +36,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -36,9 +36,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- YAML playouts in particular should build significantly faster
### Changed
- Allow multiple watermarks on a single playout item
- Allow multiple watermarks in playback troubleshooting
- Classic schedules: allow selecting multiple watermarks on schedule items
- Block schedules: allow selecting multiple watermarks on decos
- YAML playout: `watermark` instruction changes:
- 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

3
ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs

@ -28,7 +28,8 @@ public class CreateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -28,7 +28,8 @@ public class CreateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
await ValidateDecoName(dbContext, request).MapT(name => new Deco
{
DecoGroupId = request.DecoGroupId,
Name = name
Name = name,
DecoWatermarks = []
});
private static async Task<Validation<BaseError, string>> ValidateDecoName(

4
ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs

@ -9,7 +9,7 @@ public record UpdateDeco( @@ -9,7 +9,7 @@ public record UpdateDeco(
int DecoGroupId,
string Name,
DecoMode WatermarkMode,
int? WatermarkId,
List<int> WatermarkIds,
bool UseWatermarkDuringFiller,
DecoMode DefaultFillerMode,
ProgramScheduleItemCollectionType DefaultFillerCollectionType,
@ -24,4 +24,4 @@ public record UpdateDeco( @@ -24,4 +24,4 @@ public record UpdateDeco(
int? DeadAirFallbackMediaItemId,
int? DeadAirFallbackMultiCollectionId,
int? DeadAirFallbackSmartCollectionId)
: IRequest<Either<BaseError, DecoViewModel>>;
: IRequest<Either<BaseError, Unit>>;

33
ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -7,16 +8,16 @@ using Microsoft.EntityFrameworkCore; @@ -7,16 +8,16 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<UpdateDeco, Either<BaseError, DecoViewModel>>
: IRequestHandler<UpdateDeco, Either<BaseError, Unit>>
{
public async Task<Either<BaseError, DecoViewModel>> Handle(UpdateDeco request, CancellationToken cancellationToken)
public async Task<Either<BaseError, Unit>> Handle(UpdateDeco request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Deco> validation = await Validate(dbContext, request);
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
}
private static async Task<DecoViewModel> ApplyUpdateRequest(
private static async Task<Unit> ApplyUpdateRequest(
TvContext dbContext,
Deco existing,
UpdateDeco request)
@ -25,10 +26,30 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -25,10 +26,30 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
// watermark
existing.WatermarkMode = request.WatermarkMode;
existing.WatermarkId = request.WatermarkMode is DecoMode.Override ? request.WatermarkId : null;
existing.UseWatermarkDuringFiller =
request.WatermarkMode is DecoMode.Override && request.UseWatermarkDuringFiller;
if (request.WatermarkMode is DecoMode.Override)
{
// this is different than schedule item/playout item because we have to merge watermark ids
var toAdd = request.WatermarkIds.Where(id => existing.DecoWatermarks.All(wm => wm.WatermarkId != id));
var toRemove = existing.DecoWatermarks.Where(wm => !request.WatermarkIds.Contains(wm.WatermarkId));
existing.DecoWatermarks.RemoveAll(toRemove.Contains);
foreach (var watermarkId in toAdd)
{
existing.DecoWatermarks.Add(
new DecoWatermark
{
DecoId = existing.Id,
WatermarkId = watermarkId
});
}
}
else
{
existing.DecoWatermarks.Clear();
}
// default filler
existing.DefaultFillerMode = request.DefaultFillerMode;
existing.DefaultFillerCollectionType = request.DefaultFillerCollectionType;
@ -64,7 +85,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -64,7 +85,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(existing);
return Unit.Default;
}
private static async Task<Validation<BaseError, Deco>> Validate(TvContext dbContext, UpdateDeco request) =>
@ -75,6 +96,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -75,6 +96,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
TvContext dbContext,
UpdateDeco request) =>
dbContext.Decos
.Include(d => d.DecoWatermarks)
.SelectOneAsync(d => d.Id, d => d.Id == request.DecoId)
.Map(o => o.ToValidation<BaseError>("Deco does not exist"));
@ -88,6 +110,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -88,6 +110,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
}
Option<Deco> maybeExisting = await dbContext.Decos
.AsNoTracking()
.FirstOrDefaultAsync(d =>
d.Id != request.DecoId && d.DecoGroupId == request.DecoGroupId && d.Name == request.Name)
.Map(Optional);

3
ErsatzTV.Application/Scheduling/DecoViewModel.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
@ -8,7 +9,7 @@ public record DecoViewModel( @@ -8,7 +9,7 @@ public record DecoViewModel(
int DecoGroupId,
string Name,
DecoMode WatermarkMode,
int? WatermarkId,
List<WatermarkViewModel> Watermarks,
bool UseWatermarkDuringFiller,
DecoMode DefaultFillerMode,
ProgramScheduleItemCollectionType DefaultFillerCollectionType,

2
ErsatzTV.Application/Scheduling/Mapper.cs

@ -85,7 +85,7 @@ internal static class Mapper @@ -85,7 +85,7 @@ internal static class Mapper
deco.DecoGroupId,
deco.Name,
deco.WatermarkMode,
deco.WatermarkId,
deco.DecoWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(),
deco.UseWatermarkDuringFiller,
deco.DefaultFillerMode,
deco.DefaultFillerCollectionType,

3
ErsatzTV.Application/Scheduling/Queries/GetDecoByIdHandler.cs

@ -11,6 +11,9 @@ public class GetDecoByIdHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -11,6 +11,9 @@ public class GetDecoByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Decos
.AsNoTracking()
.Include(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.SelectOneAsync(b => b.Id, b => b.Id == request.DecoId)
.MapT(Mapper.ProjectToViewModel);
}

3
ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutIdHandler.cs

@ -11,7 +11,10 @@ public class GetDecoByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFac @@ -11,7 +11,10 @@ public class GetDecoByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFac
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Playouts
.AsNoTracking()
.Include(p => p.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId && p.DecoId != null)
.MapT(p => Mapper.ProjectToViewModel(p.Deco));
}

4
ErsatzTV.Application/Scheduling/Queries/GetDecosByDecoGroupIdHandler.cs

@ -12,8 +12,10 @@ public class GetDecosByDecoGroupIdHandler(IDbContextFactory<TvContext> dbContext @@ -12,8 +12,10 @@ public class GetDecosByDecoGroupIdHandler(IDbContextFactory<TvContext> dbContext
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Deco> decos = await dbContext.Decos
.Filter(b => b.DecoGroupId == request.DecoGroupId)
.AsNoTracking()
.Include(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.Filter(b => b.DecoGroupId == request.DecoGroupId)
.ToListAsync(cancellationToken);
return decos.Map(Mapper.ProjectToViewModel).ToList();

14
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -86,6 +86,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -86,6 +86,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
// get playout deco
.Include(i => i.Playout)
.ThenInclude(p => p.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
// get graphics elements
@ -98,6 +99,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -98,6 +99,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.ThenInclude(t => t.DecoTemplate)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
@ -194,6 +196,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -194,6 +196,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
// get playout deco
.Include(p => p.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
// get playout templates (and deco templates/decos)
@ -201,6 +204,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -201,6 +204,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.ThenInclude(t => t.DecoTemplate)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.SelectOneAsync(p => p.ChannelId, p => p.ChannelId == channel.Id);
@ -259,9 +263,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -259,9 +263,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
case DisableWatermark:
disableWatermarks = true;
break;
case CustomWatermark watermark:
case CustomWatermarks watermarks:
playoutItemWatermarks.Clear();
playoutItemWatermarks.Add(watermark.Watermark);
playoutItemWatermarks.AddRange(watermarks.Watermarks);
break;
}
@ -765,7 +769,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -765,7 +769,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
if (playoutItem.FillerKind is FillerKind.None || templateDeco.UseWatermarkDuringFiller)
{
_logger.LogDebug("Watermark will come from template deco (override)");
return new CustomWatermark(templateDeco.Watermark);
return new CustomWatermarks(templateDeco.DecoWatermarks.Map(dwm => dwm.Watermark).ToList());
}
_logger.LogDebug("Watermark is disabled by template deco during filler");
@ -788,7 +792,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -788,7 +792,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
if (playoutItem.FillerKind is FillerKind.None || playoutDeco.UseWatermarkDuringFiller)
{
_logger.LogDebug("Watermark will come from playout deco (override)");
return new CustomWatermark(playoutDeco.Watermark);
return new CustomWatermarks(playoutDeco.DecoWatermarks.Map(dwm => dwm.Watermark).ToList());
}
_logger.LogDebug("Watermark is disabled by playout deco during filler");
@ -891,7 +895,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -891,7 +895,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
private sealed record DisableWatermark : WatermarkResult;
private sealed record CustomWatermark(ChannelWatermark Watermark) : WatermarkResult;
private sealed record CustomWatermarks(List<ChannelWatermark> Watermarks) : WatermarkResult;
private abstract record DeadAirFallbackResult;

4
ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs

@ -126,6 +126,10 @@ public class StartTroubleshootingPlaybackHandler( @@ -126,6 +126,10 @@ public class StartTroubleshootingPlaybackHandler(
notifier.NotifyFailed(request.SessionId);
}
}
catch (TaskCanceledException)
{
// do nothing
}
catch (Exception e)
{
Console.WriteLine(e);

5
ErsatzTV.Core/Domain/ChannelWatermark.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.FFmpeg.State;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.FFmpeg.State;
namespace ErsatzTV.Core.Domain;
@ -24,6 +25,8 @@ public class ChannelWatermark @@ -24,6 +25,8 @@ public class ChannelWatermark
public List<PlayoutItemWatermark> PlayoutItemWatermarks { get; set; }
public List<ProgramScheduleItem> ProgramScheduleItems { get; set; }
public List<ProgramScheduleItemWatermark> ProgramScheduleItemWatermarks { get; set; }
public List<Deco> Decos { get; set; }
public List<DecoWatermark> DecoWatermarks { get; set; }
public int ZIndex { get; set; }
}

11
ErsatzTV.Core/Domain/DecoWatermark.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Domain;
public class DecoWatermark
{
public int DecoId { get; set; }
public Deco Deco { get; set; }
public int WatermarkId { get; set; }
public ChannelWatermark Watermark { get; set; }
}

2
ErsatzTV.Core/Domain/PlayoutItemWatermark.cs

@ -4,6 +4,6 @@ public class PlayoutItemWatermark @@ -4,6 +4,6 @@ public class PlayoutItemWatermark
{
public int PlayoutItemId { get; set; }
public PlayoutItem PlayoutItem { get; set; }
public int? WatermarkId { get; set; }
public int WatermarkId { get; set; }
public ChannelWatermark Watermark { get; set; }
}

2
ErsatzTV.Core/Domain/ProgramScheduleItemWatermark.cs

@ -4,6 +4,6 @@ public class ProgramScheduleItemWatermark @@ -4,6 +4,6 @@ public class ProgramScheduleItemWatermark
{
public int ProgramScheduleItemId { get; set; }
public ProgramScheduleItem ProgramScheduleItem { get; set; }
public int? WatermarkId { get; set; }
public int WatermarkId { get; set; }
public ChannelWatermark Watermark { get; set; }
}

4
ErsatzTV.Core/Domain/Scheduling/Deco.cs

@ -10,8 +10,8 @@ public class Deco @@ -10,8 +10,8 @@ public class Deco
// watermark
public DecoMode WatermarkMode { get; set; }
public int? WatermarkId { get; set; }
public ChannelWatermark Watermark { get; set; }
public List<ChannelWatermark> Watermarks { get; set; }
public List<DecoWatermark> DecoWatermarks { get; set; }
public bool UseWatermarkDuringFiller { get; set; }
// default filler

6344
ErsatzTV.Infrastructure.MySql/Migrations/20250809155249_Add_DecoWatermarks.Designer.cs generated

File diff suppressed because it is too large Load Diff

51
ErsatzTV.Infrastructure.MySql/Migrations/20250809155249_Add_DecoWatermarks.cs

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

6344
ErsatzTV.Infrastructure.MySql/Migrations/20250809160002_Populate_DecoWatermarks.Designer.cs generated

File diff suppressed because it is too large Load Diff

23
ErsatzTV.Infrastructure.MySql/Migrations/20250809160002_Populate_DecoWatermarks.cs

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

6332
ErsatzTV.Infrastructure.MySql/Migrations/20250809165720_Remove_DecoWatermark.Designer.cs generated

File diff suppressed because it is too large Load Diff

49
ErsatzTV.Infrastructure.MySql/Migrations/20250809165720_Remove_DecoWatermark.cs

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

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

@ -464,6 +464,21 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -464,6 +464,21 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.ToTable("ConfigElement", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b =>
{
b.Property<int>("DecoId")
.HasColumnType("int");
b.Property<int>("WatermarkId")
.HasColumnType("int");
b.HasKey("DecoId", "WatermarkId");
b.HasIndex("WatermarkId");
b.ToTable("DecoWatermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b =>
{
b.Property<int>("Id")
@ -2512,9 +2527,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2512,9 +2527,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<bool>("UseWatermarkDuringFiller")
.HasColumnType("tinyint(1)");
b.Property<int?>("WatermarkId")
.HasColumnType("int");
b.Property<int>("WatermarkMode")
.HasColumnType("int");
@ -2536,8 +2548,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2536,8 +2548,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasIndex("DefaultFillerSmartCollectionId");
b.HasIndex("WatermarkId");
b.HasIndex("DecoGroupId", "Name")
.IsUnique();
@ -4058,6 +4068,25 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4058,6 +4068,25 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("MediaItem");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
.WithMany("DecoWatermarks")
.HasForeignKey("DecoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany("DecoWatermarks")
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Deco");
b.Navigation("Watermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b =>
{
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
@ -5072,11 +5101,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -5072,11 +5101,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.WithMany()
.HasForeignKey("DefaultFillerSmartCollectionId");
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DeadAirFallbackCollection");
b.Navigation("DeadAirFallbackMediaItem");
@ -5094,8 +5118,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -5094,8 +5118,6 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("DefaultFillerMultiCollection");
b.Navigation("DefaultFillerSmartCollection");
b.Navigation("Watermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
@ -5853,6 +5875,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -5853,6 +5875,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b =>
{
b.Navigation("DecoWatermarks");
b.Navigation("PlayoutItemWatermarks");
b.Navigation("ProgramScheduleItemWatermarks");
@ -6099,6 +6123,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -6099,6 +6123,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Navigation("DecoWatermarks");
b.Navigation("Playouts");
});

6179
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809155342_Add_DecoWatermarks.Designer.cs generated

File diff suppressed because it is too large Load Diff

50
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809155342_Add_DecoWatermarks.cs

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

6179
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809155927_Populate_DecoWatermarks.Designer.cs generated

File diff suppressed because it is too large Load Diff

23
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809155927_Populate_DecoWatermarks.cs

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

6167
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809165652_Remove_DecoWatermark.Designer.cs generated

File diff suppressed because it is too large Load Diff

49
ErsatzTV.Infrastructure.Sqlite/Migrations/20250809165652_Remove_DecoWatermark.cs

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

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

@ -445,6 +445,21 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -445,6 +445,21 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.ToTable("ConfigElement", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b =>
{
b.Property<int>("DecoId")
.HasColumnType("INTEGER");
b.Property<int>("WatermarkId")
.HasColumnType("INTEGER");
b.HasKey("DecoId", "WatermarkId");
b.HasIndex("WatermarkId");
b.ToTable("DecoWatermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b =>
{
b.Property<int>("Id")
@ -2393,9 +2408,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2393,9 +2408,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<bool>("UseWatermarkDuringFiller")
.HasColumnType("INTEGER");
b.Property<int?>("WatermarkId")
.HasColumnType("INTEGER");
b.Property<int>("WatermarkMode")
.HasColumnType("INTEGER");
@ -2417,8 +2429,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2417,8 +2429,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("DefaultFillerSmartCollectionId");
b.HasIndex("WatermarkId");
b.HasIndex("DecoGroupId", "Name")
.IsUnique();
@ -3893,6 +3903,25 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3893,6 +3903,25 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("MediaItem");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
.WithMany("DecoWatermarks")
.HasForeignKey("DecoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany("DecoWatermarks")
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Deco");
b.Navigation("Watermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b =>
{
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
@ -4907,11 +4936,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4907,11 +4936,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.WithMany()
.HasForeignKey("DefaultFillerSmartCollectionId");
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DeadAirFallbackCollection");
b.Navigation("DeadAirFallbackMediaItem");
@ -4929,8 +4953,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4929,8 +4953,6 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("DefaultFillerMultiCollection");
b.Navigation("DefaultFillerSmartCollection");
b.Navigation("Watermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
@ -5688,6 +5710,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -5688,6 +5710,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b =>
{
b.Navigation("DecoWatermarks");
b.Navigation("PlayoutItemWatermarks");
b.Navigation("ProgramScheduleItemWatermarks");
@ -5934,6 +5958,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -5934,6 +5958,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Navigation("DecoWatermarks");
b.Navigation("Playouts");
});

20
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoConfiguration.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
@ -19,12 +20,6 @@ public class DecoConfiguration : IEntityTypeConfiguration<Deco> @@ -19,12 +20,6 @@ public class DecoConfiguration : IEntityTypeConfiguration<Deco>
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
builder.HasOne(d => d.Watermark)
.WithMany()
.HasForeignKey(d => d.WatermarkId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
builder.HasOne(d => d.DeadAirFallbackCollection)
.WithMany()
.HasForeignKey(d => d.DeadAirFallbackCollectionId)
@ -48,5 +43,18 @@ public class DecoConfiguration : IEntityTypeConfiguration<Deco> @@ -48,5 +43,18 @@ public class DecoConfiguration : IEntityTypeConfiguration<Deco>
.HasForeignKey(d => d.DeadAirFallbackSmartCollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasMany(c => c.Watermarks)
.WithMany(m => m.Decos)
.UsingEntity<DecoWatermark>(
j => j.HasOne(ci => ci.Watermark)
.WithMany(mi => mi.DecoWatermarks)
.HasForeignKey(ci => ci.WatermarkId)
.OnDelete(DeleteBehavior.Cascade),
j => j.HasOne(ci => ci.Deco)
.WithMany(c => c.DecoWatermarks)
.HasForeignKey(ci => ci.DecoId)
.OnDelete(DeleteBehavior.Cascade),
j => j.HasKey(ci => new { ci.DecoId, ci.WatermarkId }));
}
}

29
ErsatzTV.Infrastructure/Streaming/GraphicsEngineFonts.cs

@ -7,28 +7,29 @@ namespace ErsatzTV.Infrastructure.Streaming; @@ -7,28 +7,29 @@ namespace ErsatzTV.Infrastructure.Streaming;
public static class GraphicsEngineFonts
{
private static readonly FontCollection CustomFontCollection = new();
private static readonly ConcurrentDictionary<string, FontFamily> CustomFontFamilies = new();
private static bool _fontsLoaded;
private static readonly ConcurrentDictionary<string, FontFamily> CustomFontFamilies
= new(StringComparer.OrdinalIgnoreCase);
private static readonly System.Collections.Generic.HashSet<string> LoadedFontFiles
= new(StringComparer.OrdinalIgnoreCase);
public static void LoadFonts(string fontsFolder)
{
if (_fontsLoaded)
foreach (var file in Directory.EnumerateFiles(fontsFolder, "*.*", SearchOption.AllDirectories))
{
return;
}
if (!file.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) &&
!file.EndsWith(".otf", StringComparison.OrdinalIgnoreCase))
{
continue;
}
foreach (var file in Directory.GetFiles(fontsFolder, "*.*", SearchOption.AllDirectories))
{
if (file.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) ||
file.EndsWith(".otf", StringComparison.OrdinalIgnoreCase))
if (!LoadedFontFiles.Add(file))
{
var fontFamily = CustomFontCollection.Add(file, CultureInfo.CurrentCulture);
CustomFontFamilies.TryAdd(fontFamily.Name, fontFamily);
continue;
}
}
_fontsLoaded = true;
var fontFamily = CustomFontCollection.Add(file, CultureInfo.CurrentCulture);
CustomFontFamilies.TryAdd(fontFamily.Name, fontFamily);
}
}
public static Font GetFont(string fontFamilyName, float fontSize, FontStyle style)

6
ErsatzTV/Pages/BlockPlayoutEditor.razor

@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
<div class="d-flex">
<MudText>Enable Default Deco</MudText>
</div>
<MudCheckBox @bind-Value="_enableDefaultDeco" Color="Color.Primary" Dense="true"/>
<MudCheckBox @bind-Value="_enableDefaultDeco" Dense="true"/>
</MudStack>
@if (_enableDefaultDeco)
{
@ -53,7 +53,9 @@ @@ -53,7 +53,9 @@
<div class="d-flex">
<MudText>Deco</MudText>
</div>
<MudSelect @bind-Value="_defaultDeco" For="@(() => _defaultDeco)">
<MudSelect @bind-Value="_defaultDeco"
For="@(() => _defaultDeco)"
ToStringFunc="@(d => d?.Name)">
@foreach (DecoViewModel deco in _decos)
{
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem>

16
ErsatzTV/Pages/DecoEditor.razor

@ -44,12 +44,15 @@ @@ -44,12 +44,15 @@
<div class="d-flex">
<MudText>Watermark Override</MudText>
</div>
<MudSelect Disabled="@(_deco.WatermarkMode != DecoMode.Override)" @bind-Value="_deco.WatermarkId" For="@(() => _deco.WatermarkId)"
Clearable="true">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
<MudSelect T="WatermarkViewModel"
@bind-SelectedValues="_deco.Watermarks"
Disabled="@(_deco.WatermarkMode != DecoMode.Override)"
ToStringFunc="@(wm => wm?.Name)"
Clearable="true"
MultiSelection="true">
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
<MudSelectItem Value="@watermark">@watermark.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
@ -60,7 +63,6 @@ @@ -60,7 +63,6 @@
<MudCheckBox T="bool"
Disabled="@(_deco.WatermarkMode != DecoMode.Override)"
@bind-Value="_deco.UseWatermarkDuringFiller"
Color="Color.Primary"
Dense="true" />
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Default Filler</MudText>
@ -389,7 +391,7 @@ @@ -389,7 +391,7 @@
Name = deco.Name,
DecoGroupId = deco.DecoGroupId,
WatermarkMode = deco.WatermarkMode,
WatermarkId = deco.WatermarkId,
Watermarks = deco.Watermarks,
UseWatermarkDuringFiller = deco.UseWatermarkDuringFiller,
DefaultFillerMode = deco.DefaultFillerMode,
@ -435,7 +437,7 @@ @@ -435,7 +437,7 @@
_deco.DecoGroupId,
_deco.Name,
_deco.WatermarkMode,
_deco.WatermarkId,
_deco.Watermarks.Map(wm => wm.Id).ToList(),
_deco.UseWatermarkDuringFiller,
_deco.DefaultFillerMode,
_deco.DefaultFillerCollectionType,

3
ErsatzTV/ViewModels/DecoEditViewModel.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
@ -10,7 +11,7 @@ public class DecoEditViewModel @@ -10,7 +11,7 @@ public class DecoEditViewModel
public int DecoGroupId { get; set; }
public string Name { get; set; }
public DecoMode WatermarkMode { get; set; }
public int? WatermarkId { get; set; }
public IEnumerable<WatermarkViewModel> Watermarks { get; set; }
public bool UseWatermarkDuringFiller { get; set; }
public DecoMode DefaultFillerMode { get; set; }

Loading…
Cancel
Save