Browse Source

Show fillers in the playout view in alternative shading (#2405)

* Add shading to filler rows in the playout view

* Insert rows in Playout listing for gaps in the playout (station offline)

* Make FillerKind in PlayoutItemViewModel optional.
Remove Unscheduled enum in FillerKind.

* Correctly handle "Show Filler" also for Unscheduled fillers.
* Moved the Unscheduled item generation for the playout view to GetFuturePlayoutItemsByIdHandle to handle ShowFiller
* Includes for the PlayoutItemDetails moved to an extension for maintainability.
* Bugfix: Page size was more than the desired for pagination because of the inserted unscheduled items.

* Add specified colours for playout fillers to make them less intense.

* use common queryable

* add playout gap model and migrations

* insert playout gaps after playout build

* optimize get future playout items handler

* update changelog

---------

Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
pull/2409/head
Peter Dey 4 months ago committed by GitHub
parent
commit
87bc779d48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 3
      ErsatzTV.Application/Playouts/Commands/InsertPlayoutGaps.cs
  4. 53
      ErsatzTV.Application/Playouts/Commands/InsertPlayoutGapsHandler.cs
  5. 3
      ErsatzTV.Application/Playouts/Mapper.cs
  6. 6
      ErsatzTV.Application/Playouts/PlayoutItemViewModel.cs
  7. 152
      ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs
  8. 1
      ErsatzTV.Core/Domain/Playout.cs
  9. 12
      ErsatzTV.Core/Domain/PlayoutGap.cs
  10. 6439
      ErsatzTV.Infrastructure.MySql/Migrations/20250912212314_Add_PlayoutGap.Designer.cs
  11. 55
      ErsatzTV.Infrastructure.MySql/Migrations/20250912212314_Add_PlayoutGap.cs
  12. 40
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  13. 6272
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250912212243_Add_PlayoutGap.Designer.cs
  14. 53
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250912212243_Add_PlayoutGap.cs
  15. 38
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  16. 5
      ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs
  17. 17
      ErsatzTV.Infrastructure/Data/Configurations/PlayoutGapConfiguration.cs
  18. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  19. 11
      ErsatzTV/Pages/Playouts.razor
  20. 3
      ErsatzTV/Services/WorkerService.cs
  21. 20
      ErsatzTV/wwwroot/css/site.css

3
CHANGELOG.md

@ -27,6 +27,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -27,6 +27,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Now, the playlist can play a specific number of items from the collection before moving to the next playlist item
- Classic schedules: add `Shuffle Playlist Items` setting to shuffle the order of playlist items
- Shuffling happens initially (on playout reset), and after all items from the *entire playlist* have been played
- Add playout detail row coloring by @peterdey
- Filler has unique row colors
- Unscheduled gaps are now displayed and have a unique row color
### Fixed
- Fix transcoding content with bt709/pc color metadata

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

@ -237,6 +237,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -237,6 +237,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
new CheckForOverlappingPlayoutItems(request.PlayoutId),
cancellationToken);
await _workerChannel.WriteAsync(new InsertPlayoutGaps(request.PlayoutId), cancellationToken);
string fileName = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml");
if (hasChanges || !File.Exists(fileName) ||
playout.ScheduleKind is PlayoutScheduleKind.ExternalJson)

3
ErsatzTV.Application/Playouts/Commands/InsertPlayoutGaps.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Playouts;
public record InsertPlayoutGaps(int PlayoutId) : IRequest, IBackgroundServiceRequest;

53
ErsatzTV.Application/Playouts/Commands/InsertPlayoutGapsHandler.cs

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
using EFCore.BulkExtensions;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts;
public class InsertPlayoutGapsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<InsertPlayoutGaps>
{
public async Task Handle(InsertPlayoutGaps request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
var toAdd = new List<PlayoutGap>();
IOrderedQueryable<PlayoutItem> query = dbContext.PlayoutItems
.Filter(pi => pi.PlayoutId == request.PlayoutId)
.OrderBy(i => i.Start);
var queue = new Queue<PlayoutItem>(query);
while (queue.Count > 1)
{
PlayoutItem one = queue.Dequeue();
PlayoutItem two = queue.Peek();
DateTimeOffset start = one.FinishOffset;
DateTimeOffset finish = two.StartOffset;
if (start == finish)
{
continue;
}
var gap = new PlayoutGap
{
PlayoutId = request.PlayoutId,
Start = start.UtcDateTime,
Finish = finish.UtcDateTime
};
toAdd.Add(gap);
}
// delete all existing gaps
await dbContext.PlayoutGaps
.Where(pg => pg.PlayoutId == request.PlayoutId)
.ExecuteDeleteAsync(cancellationToken);
// insert new gaps
await dbContext.BulkInsertAsync(toAdd, cancellationToken: cancellationToken);
}
}

3
ErsatzTV.Application/Playouts/Mapper.cs

@ -9,7 +9,8 @@ internal static class Mapper @@ -9,7 +9,8 @@ internal static class Mapper
GetDisplayTitle(playoutItem.MediaItem, playoutItem.ChapterTitle),
playoutItem.StartOffset,
playoutItem.FinishOffset,
playoutItem.GetDisplayDuration());
playoutItem.GetDisplayDuration(),
Some(playoutItem.FillerKind));
internal static PlayoutAlternateScheduleViewModel ProjectToViewModel(
ProgramScheduleAlternate programScheduleAlternate) =>

6
ErsatzTV.Application/Playouts/PlayoutItemViewModel.cs

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

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain;
using System.Globalization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
@ -6,73 +7,116 @@ using static ErsatzTV.Application.Playouts.Mapper; @@ -6,73 +7,116 @@ using static ErsatzTV.Application.Playouts.Mapper;
namespace ErsatzTV.Application.Playouts;
public class GetFuturePlayoutItemsByIdHandler : IRequestHandler<GetFuturePlayoutItemsById, PagedPlayoutItemsViewModel>
public class GetFuturePlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetFuturePlayoutItemsById, PagedPlayoutItemsViewModel>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetFuturePlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<PagedPlayoutItemsViewModel> Handle(
GetFuturePlayoutItemsById request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
DateTime now = DateTimeOffset.Now.UtcDateTime;
int totalCount = await dbContext.PlayoutItems
.CountAsync(
i => i.Finish >= now && i.PlayoutId == request.PlayoutId &&
(request.ShowFiller || i.FillerKind == FillerKind.None),
cancellationToken);
List<PlayoutItemViewModel> page = await dbContext.PlayoutItems
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MovieMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).Artist)
.ThenInclude(mm => mm.ArtistMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).Season)
.ThenInclude(s => s.SeasonMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).Season.Show)
.ThenInclude(s => s.ShowMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).OtherVideoMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Song).SongMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Song).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Image).ImageMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Image).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as RemoteStream).RemoteStreamMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as RemoteStream).MediaVersions)
// items for total count
var items = dbContext.PlayoutItems
.AsNoTracking()
.Filter(i => i.PlayoutId == request.PlayoutId)
.Filter(i => i.Finish >= now)
.Filter(i => request.ShowFiller || i.FillerKind == FillerKind.None)
.OrderBy(i => i.Start)
.Select(i => new { i.Id, Type = "Item", i.Start, i.Finish });
// gaps for total count
var gaps = dbContext.PlayoutGaps
.AsNoTracking()
.Filter(g => g.PlayoutId == request.PlayoutId)
.Filter(g => g.Finish >= now)
.Select(g => new { g.Id, Type = "Gap", g.Start, g.Finish });
var combined = items.Concat(gaps);
int totalCount = await combined.CountAsync(cancellationToken);
List<PlayoutItemViewModel> page = await combined
.OrderBy(c => c.Start)
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
.Bind(async pageOfCombined =>
{
var itemIds = pageOfCombined.Where(i => i.Type == "Item").Select(i => i.Id).ToList();
var gapIds = pageOfCombined.Where(i => i.Type == "Gap").Select(i => i.Id).ToList();
// full playout items
List<PlayoutItem> playoutItems = await dbContext.PlayoutItems
.AsNoTracking()
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MovieMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).Artist)
.ThenInclude(mm => mm.ArtistMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).Season)
.ThenInclude(s => s.SeasonMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).Season.Show)
.ThenInclude(s => s.ShowMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).OtherVideoMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Song).SongMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Song).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Image).ImageMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Image).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as RemoteStream).RemoteStreamMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as RemoteStream).MediaVersions)
.Where(i => itemIds.Contains(i.Id))
.ToListAsync(cancellationToken);
// full gaps
List<PlayoutGap> playoutGaps = await dbContext.PlayoutGaps
.AsNoTracking()
.Where(g => gapIds.Contains(g.Id))
.ToListAsync(cancellationToken);
return pageOfCombined.Select(c =>
{
if (c.Type == "Item")
{
var item = playoutItems.Single(i => i.Id == c.Id);
return ProjectToViewModel(item);
}
var gap = playoutGaps.Single(g => g.Id == c.Id);
TimeSpan gapDuration = gap.Finish - gap.Start;
return new PlayoutItemViewModel(
"UNSCHEDULED",
gap.Start,
gap.Finish,
TimeSpan.FromSeconds(Math.Round(gapDuration.TotalSeconds)).ToString(
gapDuration.TotalHours >= 1 ? @"h\:mm\:ss" : @"mm\:ss",
CultureInfo.CurrentUICulture.DateTimeFormat),
None
);
}).ToList();
});
return new PagedPlayoutItemsViewModel(totalCount, page);
}

1
ErsatzTV.Core/Domain/Playout.cs

@ -13,6 +13,7 @@ public class Playout @@ -13,6 +13,7 @@ public class Playout
public List<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }
public PlayoutScheduleKind ScheduleKind { get; set; }
public List<PlayoutItem> Items { get; set; }
public List<PlayoutGap> Gaps { get; set; }
public PlayoutAnchor Anchor { get; set; }
public List<PlayoutProgramScheduleAnchor> ProgramScheduleAnchors { get; set; }
public List<PlayoutScheduleItemFillGroupIndex> FillGroupIndices { get; set; }

12
ErsatzTV.Core/Domain/PlayoutGap.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
namespace ErsatzTV.Core.Domain;
public class PlayoutGap
{
public int Id { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public DateTime Start { get; set; }
public DateTime Finish { get; set; }
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();
}

6439
ErsatzTV.Infrastructure.MySql/Migrations/20250912212314_Add_PlayoutGap.Designer.cs generated

File diff suppressed because it is too large Load Diff

55
ErsatzTV.Infrastructure.MySql/Migrations/20250912212314_Add_PlayoutGap.cs

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutGap : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PlayoutGap",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
PlayoutId = table.Column<int>(type: "int", nullable: false),
Start = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Finish = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PlayoutGap", x => x.Id);
table.ForeignKey(
name: "FK_PlayoutGap_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_PlayoutGap_PlayoutId",
table: "PlayoutGap",
column: "PlayoutId");
migrationBuilder.CreateIndex(
name: "IX_PlayoutGap_Start_Finish",
table: "PlayoutGap",
columns: new[] { "Start", "Finish" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PlayoutGap");
}
}
}

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

@ -1862,6 +1862,33 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -1862,6 +1862,33 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.ToTable("Playout", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("Finish")
.HasColumnType("datetime(6)");
b.Property<int>("PlayoutId")
.HasColumnType("int");
b.Property<DateTime>("Start")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("PlayoutId");
b.HasIndex("Start", "Finish")
.HasDatabaseName("IX_PlayoutGap_Start_Finish");
b.ToTable("PlayoutGap", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
{
b.Property<int>("Id")
@ -4736,6 +4763,17 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4736,6 +4763,17 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("Gaps")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playout");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem")
@ -6123,6 +6161,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -6123,6 +6161,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
{
b.Navigation("FillGroupIndices");
b.Navigation("Gaps");
b.Navigation("Items");
b.Navigation("PlayoutHistory");

6272
ErsatzTV.Infrastructure.Sqlite/Migrations/20250912212243_Add_PlayoutGap.Designer.cs generated

File diff suppressed because it is too large Load Diff

53
ErsatzTV.Infrastructure.Sqlite/Migrations/20250912212243_Add_PlayoutGap.cs

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutGap : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PlayoutGap",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PlayoutId = table.Column<int>(type: "INTEGER", nullable: false),
Start = table.Column<DateTime>(type: "TEXT", nullable: false),
Finish = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PlayoutGap", x => x.Id);
table.ForeignKey(
name: "FK_PlayoutGap_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PlayoutGap_PlayoutId",
table: "PlayoutGap",
column: "PlayoutId");
migrationBuilder.CreateIndex(
name: "IX_PlayoutGap_Start_Finish",
table: "PlayoutGap",
columns: new[] { "Start", "Finish" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PlayoutGap");
}
}
}

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

@ -1773,6 +1773,31 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1773,6 +1773,31 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.ToTable("Playout", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Finish")
.HasColumnType("TEXT");
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<DateTime>("Start")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("PlayoutId");
b.HasIndex("Start", "Finish")
.HasDatabaseName("IX_PlayoutGap_Start_Finish");
b.ToTable("PlayoutGap", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
{
b.Property<int>("Id")
@ -4571,6 +4596,17 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4571,6 +4596,17 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("Gaps")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playout");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem")
@ -5958,6 +5994,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -5958,6 +5994,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
b.Navigation("FillGroupIndices");
b.Navigation("Gaps");
b.Navigation("Items");
b.Navigation("PlayoutHistory");

5
ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs

@ -20,6 +20,11 @@ public class PlayoutConfiguration : IEntityTypeConfiguration<Playout> @@ -20,6 +20,11 @@ public class PlayoutConfiguration : IEntityTypeConfiguration<Playout>
.HasForeignKey(pi => pi.PlayoutId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(p => p.Gaps)
.WithOne(pi => pi.Playout)
.HasForeignKey(pi => pi.PlayoutId)
.OnDelete(DeleteBehavior.Cascade);
builder.OwnsOne(p => p.Anchor)
.ToTable("PlayoutAnchor")
.OwnsOne(a => a.ScheduleItemsEnumeratorState)

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

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations;
public class PlayoutGapConfiguration : IEntityTypeConfiguration<PlayoutGap>
{
public void Configure(EntityTypeBuilder<PlayoutGap> builder)
{
builder.ToTable("PlayoutGap");
builder.HasIndex(p => new { p.Start, p.Finish })
.HasDatabaseName("IX_PlayoutGap_Start_Finish");
}
}

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -91,6 +91,7 @@ public class TvContext : DbContext @@ -91,6 +91,7 @@ public class TvContext : DbContext
public DbSet<PlayoutHistory> PlayoutHistory { get; set; }
public DbSet<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }
public DbSet<PlayoutItem> PlayoutItems { get; set; }
public DbSet<PlayoutGap> PlayoutGaps { get; set; }
public DbSet<PlayoutProgramScheduleAnchor> PlayoutProgramScheduleItemAnchors { get; set; }
public DbSet<PlayoutTemplate> PlayoutTemplates { get; set; }
public DbSet<BlockGroup> BlockGroups { get; set; }

11
ErsatzTV/Pages/Playouts.razor

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
@using ErsatzTV.Application.Playouts
@using ErsatzTV.Core.Notifications
@using ErsatzTV.Core.Scheduling
@using ErsatzTV.Core.Domain.Filler
@using MediatR.Courier
@implements IDisposable
@inject IDialogService Dialog
@ -222,7 +223,8 @@ @@ -222,7 +223,8 @@
Dense="true"
@bind-RowsPerPage="@_detailRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutItemViewModel>>>(DetailServerReload))"
@ref="_detailTable">
@ref="_detailTable"
RowClassFunc="PlayoutItemViewRowClassFunc">
<ToolBarContent>
<MudSwitch T="bool" Class="ml-6" @bind-Value="@ShowFiller" Color="Color.Secondary" Label="Show Filler"/>
</ToolBarContent>
@ -427,6 +429,7 @@ @@ -427,6 +429,7 @@
{
PagedPlayoutItemsViewModel data =
await Mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, _showFiller, state.Page, state.PageSize), cancellationToken);
return new TableData<PlayoutItemViewModel>
{
TotalItems = data.TotalCount,
@ -437,4 +440,10 @@ @@ -437,4 +440,10 @@
return new TableData<PlayoutItemViewModel> { TotalItems = 0 };
}
private string PlayoutItemViewRowClassFunc(PlayoutItemViewModel item, int index)
{
return "playout-filler-" + item.FillerKind.Match(
fk => fk.ToString().ToLower(),
() => "unscheduled");
}
}

3
ErsatzTV/Services/WorkerService.cs

@ -81,6 +81,9 @@ public class WorkerService : BackgroundService @@ -81,6 +81,9 @@ public class WorkerService : BackgroundService
case CheckForOverlappingPlayoutItems checkForOverlappingPlayoutItems:
await mediator.Send(checkForOverlappingPlayoutItems, stoppingToken);
break;
case InsertPlayoutGaps insertPlayoutGaps:
await mediator.Send(insertPlayoutGaps, stoppingToken);
break;
case TimeShiftOnDemandPlayout timeShiftOnDemandPlayout:
await mediator.Send(timeShiftOnDemandPlayout, stoppingToken);
break;

20
ErsatzTV/wwwroot/css/site.css

@ -197,3 +197,23 @@ div.ersatztv-light { @@ -197,3 +197,23 @@ div.ersatztv-light {
word-wrap: break-word;
overflow-wrap: break-word;
}
.playout-filler-guidemode {
background-color: #002e00;
}
.playout-filler-preroll, .playout-filler-midroll, .playout-filler-postroll {
background-color: #27272f
}
.playout-filler-tail {
background-color: #383843;
}
.playout-filler-fallback {
background-color: #573a00;
}
.playout-filler-unscheduled {
background-color: #52040d;
}
Loading…
Cancel
Save