Browse Source

add playout build status (#2476)

* add playout build status

* show build status in playout list

* update changelog
pull/2477/head
Jason Dove 3 months ago committed by GitHub
parent
commit
598de5d5d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 40
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 3
      ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayoutHandler.cs
  4. 3
      ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs
  5. 3
      ErsatzTV.Application/Playouts/Commands/UpdateScriptedPlayoutHandler.cs
  6. 3
      ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayoutHandler.cs
  7. 3
      ErsatzTV.Application/Playouts/Mapper.cs
  8. 3
      ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs
  9. 3
      ErsatzTV.Application/Playouts/Queries/GetAllPlayouts.cs
  10. 33
      ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs
  11. 1
      ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs
  12. 3
      ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs
  13. 1
      ErsatzTV.Core/Domain/Playout.cs
  14. 10
      ErsatzTV.Core/Domain/PlayoutBuildStatus.cs
  15. 8
      ErsatzTV.Core/Scheduling/PlayoutBuildException.cs
  16. 9
      ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs
  17. 6855
      ErsatzTV.Infrastructure.MySql/Migrations/20251002004133_Add_PlayoutBuildStatus.Designer.cs
  18. 44
      ErsatzTV.Infrastructure.MySql/Migrations/20251002004133_Add_PlayoutBuildStatus.cs
  19. 32
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  20. 6682
      ErsatzTV.Infrastructure.Sqlite/Migrations/20251002001556_Add_PlayoutBuildStatus.Designer.cs
  21. 42
      ErsatzTV.Infrastructure.Sqlite/Migrations/20251002001556_Add_PlayoutBuildStatus.cs
  22. 32
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  23. 15
      ErsatzTV.Infrastructure/Data/Configurations/PlayoutBuildStatusConfiguration.cs
  24. 5
      ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs
  25. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  26. 100
      ErsatzTV/Pages/Playouts.razor
  27. 8
      ErsatzTV/wwwroot/css/site.css

3
CHANGELOG.md

@ -68,6 +68,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -68,6 +68,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add basic log viewer to playback troubleshooting tool
- Streaming log level will be forced to `Debug` during troubleshooting
- Streaming log level will be restored to its previous value after troubleshooting completes
- Add playout build status to UI
- Playouts that fail to build will be highlighted yellow in the playouts table
- Clicking on the failed playout will display the warning or error that caused the playout build to fail
### Fixed
- Fix green output when libplacebo tonemapping is used with NVIDIA acceleration and 10-bit output in FFmpeg Profile

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

@ -28,7 +28,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -28,7 +28,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IPlayoutBuilder _playoutBuilder;
private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly IRerunHelper _rerunHelper;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly ISequentialPlayoutBuilder _sequentialPlayoutBuilder;
private readonly IScriptedPlayoutBuilder _scriptedPlayoutBuilder;
@ -45,7 +44,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -45,7 +44,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
IFFmpegSegmenterService ffmpegSegmenterService,
IEntityLocker entityLocker,
IPlayoutTimeShifter playoutTimeShifter,
IRerunHelper rerunHelper,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_client = client;
@ -59,7 +57,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -59,7 +57,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
_ffmpegSegmenterService = ffmpegSegmenterService;
_entityLocker = entityLocker;
_playoutTimeShifter = playoutTimeShifter;
_rerunHelper = rerunHelper;
_workerChannel = workerChannel;
}
@ -109,6 +106,16 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -109,6 +106,16 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{
var channelName = "[unknown]";
await dbContext.PlayoutBuildStatus
.Where(pbs => pbs.PlayoutId == playout.Id)
.ExecuteDeleteAsync(cancellationToken);
var newBuildStatus = new PlayoutBuildStatus
{
PlayoutId = playout.Id,
LastBuild = DateTimeOffset.Now
};
try
{
PlayoutReferenceData referenceData = await GetReferenceData(
@ -157,7 +164,12 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -157,7 +164,12 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
case PlayoutScheduleKind.None:
case PlayoutScheduleKind.Classic:
default:
result = await _playoutBuilder.Build(request.Start, playout, referenceData, request.Mode, cancellationToken);
result = await _playoutBuilder.Build(
request.Start,
playout,
referenceData,
request.Mode,
cancellationToken);
break;
}
@ -275,10 +287,15 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -275,10 +287,15 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id), cancellationToken);
newBuildStatus.Success = true;
return result;
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{
newBuildStatus.Success = false;
newBuildStatus.Message = $"Timeout building playout for channel {channelName}";
_client.Notify(ex);
return BaseError.New(
$"Timeout building playout for channel {channelName}; this may be a bug!");
@ -287,10 +304,25 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -287,10 +304,25 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{
DebugBreak.Break();
newBuildStatus.Success = false;
newBuildStatus.Message = $"Unexpected error building playout for channel {channelName}: {ex}";
_client.Notify(ex);
return BaseError.New(
$"Unexpected error building playout for channel {channelName}: {ex.Message}");
}
finally
{
try
{
await dbContext.PlayoutBuildStatus.AddAsync(newBuildStatus, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (Exception)
{
// do nothing
}
}
}
private static Task<Validation<BaseError, Playout>> Validate(

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

@ -53,7 +53,8 @@ public class @@ -53,7 +53,8 @@ public class
playout.Channel.PlayoutMode,
playout.ProgramSchedule?.Name ?? string.Empty,
playout.ScheduleFile,
playout.DailyRebuildTime);
playout.DailyRebuildTime,
playout.BuildStatus);
}
private static Task<Validation<BaseError, Playout>> Validate(

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

@ -45,7 +45,8 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr @@ -45,7 +45,8 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr
playout.Channel.PlayoutMode,
playout.ProgramSchedule?.Name ?? string.Empty,
playout.ScheduleFile,
playout.DailyRebuildTime);
playout.DailyRebuildTime,
playout.BuildStatus);
}
private static Task<Validation<BaseError, Playout>> Validate(

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

@ -48,7 +48,8 @@ public class @@ -48,7 +48,8 @@ public class
playout.Channel.PlayoutMode,
playout.ProgramSchedule?.Name ?? string.Empty,
playout.ScheduleFile,
playout.DailyRebuildTime);
playout.DailyRebuildTime,
playout.BuildStatus);
}
private async Task<Validation<BaseError, Playout>> Validate(

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

@ -53,7 +53,8 @@ public class @@ -53,7 +53,8 @@ public class
playout.Channel.PlayoutMode,
playout.ProgramSchedule?.Name ?? string.Empty,
playout.ScheduleFile,
playout.DailyRebuildTime);
playout.DailyRebuildTime,
playout.BuildStatus);
}
private static Task<Validation<BaseError, Playout>> Validate(

3
ErsatzTV.Application/Playouts/Mapper.cs

@ -13,7 +13,8 @@ internal static class Mapper @@ -13,7 +13,8 @@ internal static class Mapper
playout.Channel.PlayoutMode,
playout.ProgramScheduleId == null ? string.Empty : playout.ProgramSchedule.Name,
playout.ScheduleFile,
playout.DailyRebuildTime);
playout.DailyRebuildTime,
playout.BuildStatus);
internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
new(

3
ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs

@ -10,7 +10,8 @@ public record PlayoutNameViewModel( @@ -10,7 +10,8 @@ public record PlayoutNameViewModel(
ChannelPlayoutMode PlayoutMode,
string ScheduleName,
string ScheduleFile,
TimeSpan? DbDailyRebuildTime)
TimeSpan? DbDailyRebuildTime,
PlayoutBuildStatus BuildStatus)
{
public Option<TimeSpan> DailyRebuildTime => Optional(DbDailyRebuildTime);

3
ErsatzTV.Application/Playouts/Queries/GetAllPlayouts.cs

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
namespace ErsatzTV.Application.Playouts;
public record GetAllPlayouts : IRequest<List<PlayoutNameViewModel>>;

33
ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs

@ -1,33 +0,0 @@ @@ -1,33 +0,0 @@
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts;
public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<PlayoutNameViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllPlayoutsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<PlayoutNameViewModel>> Handle(
GetAllPlayouts request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Playouts
.AsNoTracking()
.Include(p => p.ProgramSchedule)
.Filter(p => p.Channel != null)
.Map(p => new PlayoutNameViewModel(
p.Id,
p.ScheduleKind,
p.Channel.Name,
p.Channel.Number,
p.Channel.PlayoutMode,
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
p.ScheduleFile,
p.DailyRebuildTime))
.ToListAsync(cancellationToken);
}
}

1
ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs

@ -19,6 +19,7 @@ public class GetPagedPlayoutsHandler(IDbContextFactory<TvContext> dbContextFacto @@ -19,6 +19,7 @@ public class GetPagedPlayoutsHandler(IDbContextFactory<TvContext> dbContextFacto
.AsNoTracking()
.Include(p => p.Channel)
.Include(p => p.ProgramSchedule)
.Include(p => p.BuildStatus)
.Filter(p => p.Channel != null);
if (!string.IsNullOrWhiteSpace(request.Query))

3
ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs

@ -25,6 +25,7 @@ public class GetPlayoutByIdHandler(IDbContextFactory<TvContext> dbContextFactory @@ -25,6 +25,7 @@ public class GetPlayoutByIdHandler(IDbContextFactory<TvContext> dbContextFactory
p.Channel.PlayoutMode,
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
p.ScheduleFile,
p.DailyRebuildTime));
p.DailyRebuildTime,
p.BuildStatus));
}
}

1
ErsatzTV.Core/Domain/Playout.cs

@ -24,4 +24,5 @@ public class Playout @@ -24,4 +24,5 @@ public class Playout
public int? DecoId { get; set; }
public Deco Deco { get; set; }
public DateTimeOffset? OnDemandCheckpoint { get; set; }
public PlayoutBuildStatus BuildStatus { get; set; }
}

10
ErsatzTV.Core/Domain/PlayoutBuildStatus.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Domain;
public class PlayoutBuildStatus
{
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public DateTimeOffset LastBuild { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
}

8
ErsatzTV.Core/Scheduling/PlayoutBuildException.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Scheduling;
public class PlayoutBuildException : Exception
{
public PlayoutBuildException() : base() { }
public PlayoutBuildException(string message) : base(message) { }
public PlayoutBuildException(string message, Exception innerException) : base(message, innerException) { }
}

9
ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs

@ -41,7 +41,7 @@ public class SequentialPlayoutBuilder( @@ -41,7 +41,7 @@ public class SequentialPlayoutBuilder(
if (!localFileSystem.FileExists(playout.ScheduleFile))
{
logger.LogWarning("Sequential schedule file {File} does not exist; aborting.", playout.ScheduleFile);
return result;
throw new PlayoutBuildException($"Sequential schedule file {playout.ScheduleFile} does not exist");
}
Option<YamlPlayoutDefinition> maybePlayoutDefinition =
@ -49,7 +49,7 @@ public class SequentialPlayoutBuilder( @@ -49,7 +49,7 @@ public class SequentialPlayoutBuilder(
if (maybePlayoutDefinition.IsNone)
{
logger.LogWarning("Sequential schedule file {File} is invalid; aborting.", playout.ScheduleFile);
return result;
throw new PlayoutBuildException($"Sequential schedule file {playout.ScheduleFile} is invalid");
}
// using ValueUnsafe to avoid nesting
@ -96,12 +96,13 @@ public class SequentialPlayoutBuilder( @@ -96,12 +96,13 @@ public class SequentialPlayoutBuilder(
if (maybeImportedDefinition.IsNone)
{
logger.LogWarning("YAML playout import {File} is invalid; aborting.", import);
return result;
throw new PlayoutBuildException($"YAML playout import {import} is invalid");
}
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected exception loading YAML playout import");
throw new PlayoutBuildException("Unexpected exception loading YAML playout import", ex);
}
}
@ -235,7 +236,7 @@ public class SequentialPlayoutBuilder( @@ -235,7 +236,7 @@ public class SequentialPlayoutBuilder(
if (DetectCycle(context.Definition))
{
logger.LogError("YAML sequence contains a cycle; unable to build playout");
return result;
throw new PlayoutBuildException("YAML sequence contains a cycle; unable to build playout");
}
var flattenCount = 0;

6855
ErsatzTV.Infrastructure.MySql/Migrations/20251002004133_Add_PlayoutBuildStatus.Designer.cs generated

File diff suppressed because it is too large Load Diff

44
ErsatzTV.Infrastructure.MySql/Migrations/20251002004133_Add_PlayoutBuildStatus.cs

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutBuildStatus : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PlayoutBuildStatus",
columns: table => new
{
PlayoutId = table.Column<int>(type: "int", nullable: false),
LastBuild = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false),
Success = table.Column<bool>(type: "tinyint(1)", nullable: false),
Message = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_PlayoutBuildStatus", x => x.PlayoutId);
table.ForeignKey(
name: "FK_PlayoutBuildStatus_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PlayoutBuildStatus");
}
}
}

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

@ -1910,6 +1910,25 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -1910,6 +1910,25 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.ToTable("Playout", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutBuildStatus", b =>
{
b.Property<int>("PlayoutId")
.HasColumnType("int");
b.Property<DateTimeOffset>("LastBuild")
.HasColumnType("datetime(6)");
b.Property<string>("Message")
.HasColumnType("longtext");
b.Property<bool>("Success")
.HasColumnType("tinyint(1)");
b.HasKey("PlayoutId");
b.ToTable("PlayoutBuildStatus", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b =>
{
b.Property<int>("Id")
@ -5012,6 +5031,17 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -5012,6 +5031,17 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutBuildStatus", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithOne("BuildStatus")
.HasForeignKey("ErsatzTV.Core.Domain.PlayoutBuildStatus", "PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playout");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
@ -6532,6 +6562,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -6532,6 +6562,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
{
b.Navigation("BuildStatus");
b.Navigation("FillGroupIndices");
b.Navigation("Gaps");

6682
ErsatzTV.Infrastructure.Sqlite/Migrations/20251002001556_Add_PlayoutBuildStatus.Designer.cs generated

File diff suppressed because it is too large Load Diff

42
ErsatzTV.Infrastructure.Sqlite/Migrations/20251002001556_Add_PlayoutBuildStatus.cs

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutBuildStatus : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PlayoutBuildStatus",
columns: table => new
{
PlayoutId = table.Column<int>(type: "INTEGER", nullable: false),
LastBuild = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Success = table.Column<bool>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlayoutBuildStatus", x => x.PlayoutId);
table.ForeignKey(
name: "FK_PlayoutBuildStatus_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PlayoutBuildStatus");
}
}
}

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

@ -1821,6 +1821,25 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1821,6 +1821,25 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.ToTable("Playout", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutBuildStatus", b =>
{
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("LastBuild")
.HasColumnType("TEXT");
b.Property<string>("Message")
.HasColumnType("TEXT");
b.Property<bool>("Success")
.HasColumnType("INTEGER");
b.HasKey("PlayoutId");
b.ToTable("PlayoutBuildStatus", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b =>
{
b.Property<int>("Id")
@ -4839,6 +4858,17 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4839,6 +4858,17 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutBuildStatus", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithOne("BuildStatus")
.HasForeignKey("ErsatzTV.Core.Domain.PlayoutBuildStatus", "PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playout");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
@ -6359,6 +6389,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -6359,6 +6389,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
{
b.Navigation("BuildStatus");
b.Navigation("FillGroupIndices");
b.Navigation("Gaps");

15
ErsatzTV.Infrastructure/Data/Configurations/PlayoutBuildStatusConfiguration.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations;
public class PlayoutBuildStatusConfiguration : IEntityTypeConfiguration<PlayoutBuildStatus>
{
public void Configure(EntityTypeBuilder<PlayoutBuildStatus> builder)
{
builder.ToTable("PlayoutBuildStatus");
builder.HasKey(p => p.PlayoutId);
}
}

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

@ -49,5 +49,10 @@ public class PlayoutConfiguration : IEntityTypeConfiguration<Playout> @@ -49,5 +49,10 @@ public class PlayoutConfiguration : IEntityTypeConfiguration<Playout>
.WithOne(h => h.Playout)
.HasForeignKey(h => h.PlayoutId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(p => p.BuildStatus)
.WithOne(pbr => pbr.Playout)
.HasForeignKey<PlayoutBuildStatus>(p => p.PlayoutId)
.OnDelete(DeleteBehavior.Cascade);
}
}

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -96,6 +96,7 @@ public class TvContext : DbContext @@ -96,6 +96,7 @@ public class TvContext : DbContext
public DbSet<PlayoutGap> PlayoutGaps { get; set; }
public DbSet<PlayoutProgramScheduleAnchor> PlayoutProgramScheduleItemAnchors { get; set; }
public DbSet<PlayoutTemplate> PlayoutTemplates { get; set; }
public DbSet<PlayoutBuildStatus> PlayoutBuildStatus { get; set; }
public DbSet<BlockGroup> BlockGroups { get; set; }
public DbSet<Block> Blocks { get; set; }
public DbSet<BlockItem> BlockItems { get; set; }

100
ErsatzTV/Pages/Playouts.razor

@ -224,31 +224,38 @@ @@ -224,31 +224,38 @@
{
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Playout Detail</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
Dense="true"
@bind-RowsPerPage="@_detailRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutItemViewModel>>>(DetailServerReload))"
@ref="_detailTable"
RowClassFunc="PlayoutItemViewRowClassFunc">
<ToolBarContent>
<MudSwitch T="bool" Class="ml-6" @bind-Value="@ShowFiller" Color="Color.Secondary" Label="Show Filler"/>
</ToolBarContent>
<HeaderContent>
<MudTh>Start</MudTh>
<MudTh>Finish</MudTh>
<MudTh>Media Item</MudTh>
<MudTh>Duration</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Start">@context.Start.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Finish">@context.Finish.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Media Item">@context.Title</MudTd>
<MudTd DataLabel="Duration">@context.Duration</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
@if (!string.IsNullOrWhiteSpace(_buildMessage))
{
<MudAlert Severity="Severity.Warning">@_buildMessage</MudAlert>
}
else
{
<MudTable Hover="true"
Dense="true"
@bind-RowsPerPage="@_detailRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutItemViewModel>>>(DetailServerReload))"
@ref="_detailTable"
RowClassFunc="PlayoutItemViewRowClassFunc">
<ToolBarContent>
<MudSwitch T="bool" Class="ml-6" @bind-Value="@ShowFiller" Color="Color.Secondary" Label="Show Filler"/>
</ToolBarContent>
<HeaderContent>
<MudTh>Start</MudTh>
<MudTh>Finish</MudTh>
<MudTh>Media Item</MudTh>
<MudTh>Duration</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Start">@context.Start.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Finish">@context.Finish.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Media Item">@context.Title</MudTd>
<MudTd DataLabel="Duration">@context.Duration</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
}
}
</MudContainer>
</div>
@ -259,6 +266,7 @@ @@ -259,6 +266,7 @@
private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat;
private readonly List<PlayoutNameViewModel> _currentPage = [];
private MudTable<PlayoutNameViewModel> _table;
private MudTable<PlayoutItemViewModel> _detailTable;
private int _rowsPerPage = 10;
@ -266,6 +274,7 @@ @@ -266,6 +274,7 @@
private string _searchString;
private int? _selectedPlayoutId;
private bool _showFiller;
private string _buildMessage;
private bool ShowFiller
{
@ -298,9 +307,19 @@ @@ -298,9 +307,19 @@
// only refresh detail table on unlock operations (after playout has been modified)
if (!notification.IsLocked)
{
if (notification.PlayoutId == _selectedPlayoutId && _detailTable is not null)
int? previouslySelected = _selectedPlayoutId;
await InvokeAsync(() => _table.ReloadServerData());
_buildMessage = string.Empty;
_selectedPlayoutId = null;
if (notification.PlayoutId == previouslySelected)
{
await InvokeAsync(() => _detailTable.ReloadServerData());
Option<PlayoutNameViewModel> maybeItem = _currentPage.Find(i => i.PlayoutId == previouslySelected);
foreach (var item in maybeItem)
{
await InvokeAsync(() => PlayoutSelected(item));
}
}
}
@ -336,6 +355,10 @@ @@ -336,6 +355,10 @@
? playout.PlayoutId
: null;
_buildMessage = !playout.BuildStatus.Success && !string.IsNullOrWhiteSpace(playout.BuildStatus.Message)
? playout.BuildStatus.Message
: string.Empty;
if (_detailTable != null)
{
await _detailTable.ReloadServerData();
@ -416,6 +439,8 @@ @@ -416,6 +439,8 @@
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsPageSize, state.PageSize.ToString()), cancellationToken);
PagedPlayoutsViewModel data = await Mediator.Send(new GetPagedPlayouts(_searchString, state.Page, state.PageSize), cancellationToken);
_currentPage.Clear();
_currentPage.AddRange(data.Page);
return new TableData<PlayoutNameViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}
@ -453,5 +478,24 @@ @@ -453,5 +478,24 @@
() => "unscheduled");
}
private string SelectedRowClassFunc(PlayoutNameViewModel element, int rowNumber) => _selectedPlayoutId != null && _selectedPlayoutId == element.PlayoutId ? "selected" : string.Empty;
private string SelectedRowClassFunc(PlayoutNameViewModel element, int rowNumber)
{
if (_selectedPlayoutId != null && _selectedPlayoutId == element.PlayoutId)
{
if (element.BuildStatus is { Success: false })
{
return "playout-build-failure-selected";
}
return "selected";
}
if (element.BuildStatus is { Success: false })
{
return "playout-build-failure";
}
return string.Empty;
}
}

8
ErsatzTV/wwwroot/css/site.css

@ -193,6 +193,14 @@ div.ersatztv-light { @@ -193,6 +193,14 @@ div.ersatztv-light {
background-color: var(--selected-row-color) !important;
}
.playout-build-failure {
background-color: #573a00 !important;
}
.playout-build-failure-selected {
background-color: var(--mud-palette-warning-darken) !important;
}
.wrap-pre {
white-space: pre-wrap;
word-wrap: break-word;

Loading…
Cancel
Save