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>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Playouts;
|
||||
|
||||
public record InsertPlayoutGaps(int PlayoutId) : IRequest, IBackgroundServiceRequest;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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
|
||||
{
|
||||
b.Navigation("FillGroupIndices");
|
||||
|
||||
b.Navigation("Gaps");
|
||||
|
||||
b.Navigation("Items");
|
||||
|
||||
b.Navigation("PlayoutHistory");
|
||||
|
||||
+6272
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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
|
||||
{
|
||||
b.Navigation("FillGroupIndices");
|
||||
|
||||
b.Navigation("Gaps");
|
||||
|
||||
b.Navigation("Items");
|
||||
|
||||
b.Navigation("PlayoutHistory");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 @@
|
||||
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 @@
|
||||
{
|
||||
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 @@
|
||||
return new TableData<PlayoutItemViewModel> { TotalItems = 0 };
|
||||
}
|
||||
|
||||
private string PlayoutItemViewRowClassFunc(PlayoutItemViewModel item, int index)
|
||||
{
|
||||
return "playout-filler-" + item.FillerKind.Match(
|
||||
fk => fk.ToString().ToLower(),
|
||||
() => "unscheduled");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user