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
This commit is contained in:
Jason Dove
2025-08-09 17:00:12 +00:00
committed by GitHub
parent 942cf9e225
commit 908125f8a9
35 changed files with 37988 additions and 77 deletions
+1 -1
View File
@@ -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
@@ -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(
@@ -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(
int? DeadAirFallbackMediaItemId,
int? DeadAirFallbackMultiCollectionId,
int? DeadAirFallbackSmartCollectionId)
: IRequest<Either<BaseError, DecoViewModel>>;
: IRequest<Either<BaseError, Unit>>;
@@ -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;
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)
// 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)
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)
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)
}
Option<Deco> maybeExisting = await dbContext.Decos
.AsNoTracking()
.FirstOrDefaultAsync(d =>
d.Id != request.DecoId && d.DecoGroupId == request.DecoGroupId && d.Name == request.Name)
.Map(Optional);
@@ -1,3 +1,4 @@
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
@@ -8,7 +9,7 @@ public record DecoViewModel(
int DecoGroupId,
string Name,
DecoMode WatermarkMode,
int? WatermarkId,
List<WatermarkViewModel> Watermarks,
bool UseWatermarkDuringFiller,
DecoMode DefaultFillerMode,
ProgramScheduleItemCollectionType DefaultFillerCollectionType,
+1 -1
View File
@@ -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,
@@ -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);
}
@@ -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));
}
@@ -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();
@@ -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<
.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<
// 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<
.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<
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<
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<
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<
private sealed record DisableWatermark : WatermarkResult;
private sealed record CustomWatermark(ChannelWatermark Watermark) : WatermarkResult;
private sealed record CustomWatermarks(List<ChannelWatermark> Watermarks) : WatermarkResult;
private abstract record DeadAirFallbackResult;
@@ -126,6 +126,10 @@ public class StartTroubleshootingPlaybackHandler(
notifier.NotifyFailed(request.SessionId);
}
}
catch (TaskCanceledException)
{
// do nothing
}
catch (Exception e)
{
Console.WriteLine(e);
+4 -1
View File
@@ -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
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
View File
@@ -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; }
}
+1 -1
View File
@@ -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; }
}
@@ -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; }
}
+2 -2
View File
@@ -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
File diff suppressed because it is too large Load Diff
@@ -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");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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)
{
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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);
}
}
}
@@ -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
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
b.HasIndex("DefaultFillerSmartCollectionId");
b.HasIndex("WatermarkId");
b.HasIndex("DecoGroupId", "Name")
.IsUnique();
@@ -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
.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
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
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
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Navigation("DecoWatermarks");
b.Navigation("Playouts");
});
File diff suppressed because it is too large Load Diff
@@ -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");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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)
{
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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);
}
}
}
@@ -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
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
b.HasIndex("DefaultFillerSmartCollectionId");
b.HasIndex("WatermarkId");
b.HasIndex("DecoGroupId", "Name")
.IsUnique();
@@ -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
.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
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
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
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Navigation("DecoWatermarks");
b.Navigation("Playouts");
});
@@ -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>
.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>
.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 }));
}
}
@@ -7,30 +7,31 @@ 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))
{
continue;
}
var fontFamily = CustomFontCollection.Add(file, CultureInfo.CurrentCulture);
CustomFontFamilies.TryAdd(fontFamily.Name, fontFamily);
}
}
_fontsLoaded = true;
}
public static Font GetFont(string fontFamilyName, float fontSize, FontStyle style)
{
// try custom fonts
+4 -2
View File
@@ -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 @@
<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>
+9 -7
View File
@@ -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 @@
<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 @@
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 @@
_deco.DecoGroupId,
_deco.Name,
_deco.WatermarkMode,
_deco.WatermarkId,
_deco.Watermarks.Map(wm => wm.Id).ToList(),
_deco.UseWatermarkDuringFiller,
_deco.DefaultFillerMode,
_deco.DefaultFillerCollectionType,
+2 -1
View File
@@ -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
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; }