Browse Source

add block playout troubleshooting tool (#2512)

* rename yaml validation to sequential schedule validation

* some better exception handling

* add block playout troubleshooting page

* add paged block playout history

* add history details

* update changelog
pull/2513/head
Jason Dove 3 months ago committed by GitHub
parent
commit
c03f81a465
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 9
      ErsatzTV.Application/Playouts/Mapper.cs
  3. 3
      ErsatzTV.Application/Playouts/PagedPlayoutHistoryViewModel.cs
  4. 3
      ErsatzTV.Application/Playouts/PlayoutHistoryViewModel.cs
  5. 3
      ErsatzTV.Application/Playouts/Queries/GetAllBlockPlayouts.cs
  6. 26
      ErsatzTV.Application/Playouts/Queries/GetAllBlockPlayoutsHandler.cs
  7. 4
      ErsatzTV.Application/Playouts/Queries/GetBlockPlayoutHistory.cs
  8. 30
      ErsatzTV.Application/Playouts/Queries/GetBlockPlayoutHistoryHandler.cs
  9. 3
      ErsatzTV.Application/Scheduling/Queries/GetAllBlocksForPlayout.cs
  10. 49
      ErsatzTV.Application/Scheduling/Queries/GetAllBlocksForPlayoutHandler.cs
  11. 10
      ErsatzTV.Application/Troubleshooting/PlayoutHistoryDetailsViewModel.cs
  12. 5
      ErsatzTV.Application/Troubleshooting/Queries/DecodePlayoutHistory.cs
  13. 97
      ErsatzTV.Application/Troubleshooting/Queries/DecodePlayoutHistoryHandler.cs
  14. 3
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  15. 13
      ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs
  16. 159
      ErsatzTV/Pages/Troubleshooting/BlockPlayoutHistory.razor
  17. 162
      ErsatzTV/Pages/Troubleshooting/BlockPlayoutTroubleshooting.razor
  18. 0
      ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor
  19. 12
      ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor
  20. 4
      ErsatzTV/Pages/Troubleshooting/YamlValidator.razor
  21. 2
      ErsatzTV/Services/ScannerService.cs

2
CHANGELOG.md

@ -9,12 +9,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -9,12 +9,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add template data (like `MediaItem_Title`) for other video files
- Add `MediaItem_Path` for movies, episodes, music videos and other videos
- Add `get_directory_name` and `get_filename_without_extension` functions for path processing
- Add `Block Playout Troubleshooting` tool to help investigate block playout history
### Fixed
- Fix NVIDIA startup errors on arm64
### Changed
- Do not use graphics engine for single, permanent watermark
- Rename `YAML Validation` tool to `Sequential Schedule Validation`
## [25.7.1] - 2025-10-09
### Added

9
ErsatzTV.Application/Playouts/Mapper.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Application.Playouts;
@ -34,6 +35,14 @@ internal static class Mapper @@ -34,6 +35,14 @@ internal static class Mapper
programScheduleAlternate.DaysOfMonth,
programScheduleAlternate.MonthsOfYear);
internal static PlayoutHistoryViewModel ProjectToViewModel(PlayoutHistory playoutHistory) =>
new(
playoutHistory.Id,
new DateTimeOffset(playoutHistory.When, TimeSpan.Zero).ToLocalTime(),
new DateTimeOffset(playoutHistory.Finish, TimeSpan.Zero).ToLocalTime(),
playoutHistory.Key,
playoutHistory.Details);
internal static string GetDisplayTitle(MediaItem mediaItem, Option<string> maybeChapterTitle)
{
string chapterTitle = maybeChapterTitle.IfNone(string.Empty);

3
ErsatzTV.Application/Playouts/PagedPlayoutHistoryViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Playouts;
public record PagedPlayoutHistoryViewModel(int TotalCount, List<PlayoutHistoryViewModel> Page);

3
ErsatzTV.Application/Playouts/PlayoutHistoryViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Playouts;
public record PlayoutHistoryViewModel(int Id, DateTimeOffset When, DateTimeOffset Finish, string Key, string Details);

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

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

26
ErsatzTV.Application/Playouts/Queries/GetAllBlockPlayoutsHandler.cs

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts;
public class GetAllBlockPlayoutsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllBlockPlayouts, List<PlayoutNameViewModel>>
{
public async Task<List<PlayoutNameViewModel>> Handle(
GetAllBlockPlayouts request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Playout> playouts = await dbContext.Playouts
.AsNoTracking()
.Include(p => p.Channel)
.Include(p => p.ProgramSchedule)
.Include(p => p.BuildStatus)
.Where(p => p.Channel != null && p.ScheduleKind == PlayoutScheduleKind.Block)
.ToListAsync(cancellationToken);
return playouts.Map(Mapper.ProjectToViewModel).ToList();
}
}

4
ErsatzTV.Application/Playouts/Queries/GetBlockPlayoutHistory.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Playouts;
public record GetBlockPlayoutHistory(int PlayoutId, int BlockId, int PageNum, int PageSize)
: IRequest<PagedPlayoutHistoryViewModel>;

30
ErsatzTV.Application/Playouts/Queries/GetBlockPlayoutHistoryHandler.cs

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts;
public class GetBlockPlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetBlockPlayoutHistory, PagedPlayoutHistoryViewModel>
{
public async Task<PagedPlayoutHistoryViewModel> Handle(
GetBlockPlayoutHistory request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
IQueryable<PlayoutHistory> query = dbContext.PlayoutHistory
.AsNoTracking()
.Where(ph => ph.PlayoutId == request.PlayoutId && ph.BlockId == request.BlockId);
int totalCount = await query.CountAsync(cancellationToken);
List<PlayoutHistory> allHistory = await query
.OrderBy(ph => ph.Id)
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken);
return new PagedPlayoutHistoryViewModel(totalCount, allHistory.Map(Mapper.ProjectToViewModel).ToList());
}
}

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

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetAllBlocksForPlayout(int PlayoutId) : IRequest<List<BlockViewModel>>;

49
ErsatzTV.Application/Scheduling/Queries/GetAllBlocksForPlayoutHandler.cs

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
using Dapper;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetAllBlocksForPlayoutHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllBlocksForPlayout, List<BlockViewModel>>
{
public async Task<List<BlockViewModel>> Handle(GetAllBlocksForPlayout request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> blockIds = await dbContext.Connection.QueryAsync<int>(
"""
SELECT DISTINCT TI.BlockId
FROM TemplateItem TI
INNER JOIN PlayoutTemplate PT ON PT.TemplateId = TI.TemplateId
WHERE PT.PlayoutId = @PlayoutId
""",
new { request.PlayoutId })
.Map(result => result.ToList());
List<Block> blocks = await dbContext.Blocks
.AsNoTracking()
.Where(b => blockIds.Contains(b.Id))
.ToListAsync(cancellationToken);
List<BlockGroup> blockGroups = await dbContext.BlockGroups
.AsNoTracking()
.ToListAsync(cancellationToken);
// match blocks to block groups
foreach (var block in blocks)
{
var maybeBlockGroup = blockGroups.FirstOrDefault(bg => bg.Id == block.BlockGroupId);
if (maybeBlockGroup != null)
{
block.BlockGroup = maybeBlockGroup;
}
}
return blocks.Map(Mapper.ProjectToViewModel)
.OrderBy(b => b.GroupName)
.ThenBy(b => b.Name)
.ToList();
}
}

10
ErsatzTV.Application/Troubleshooting/PlayoutHistoryDetailsViewModel.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Troubleshooting;
public record PlayoutHistoryDetailsViewModel(
PlaybackOrder PlaybackOrder,
CollectionType CollectionType,
string Name,
string MediaItemType,
string MediaItemTitle);

5
ErsatzTV.Application/Troubleshooting/Queries/DecodePlayoutHistory.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Application.Playouts;
namespace ErsatzTV.Application.Troubleshooting.Queries;
public record DecodePlayoutHistory(PlayoutHistoryViewModel PlayoutHistory) : IRequest<PlayoutHistoryDetailsViewModel>;

97
ErsatzTV.Application/Troubleshooting/Queries/DecodePlayoutHistoryHandler.cs

@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace ErsatzTV.Application.Troubleshooting.Queries;
public class DecodePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DecodePlayoutHistory, PlayoutHistoryDetailsViewModel>
{
public async Task<PlayoutHistoryDetailsViewModel> Handle(
DecodePlayoutHistory request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
var decodedKey = JsonConvert.DeserializeObject<BlockItemHistoryKey>(request.PlayoutHistory.Key);
PlaybackOrder playbackOrder = decodedKey.PlaybackOrder ?? PlaybackOrder.None;
CollectionType collectionType = decodedKey.CollectionType ?? CollectionType.Collection;
string name = string.Empty;
switch (collectionType)
{
case CollectionType.Collection:
name = await dbContext.Collections
.AsNoTracking()
.Where(c => c.Id == (decodedKey.CollectionId ?? 0))
.Map(c => c.Name)
.FirstOrDefaultAsync(cancellationToken);
break;
case CollectionType.SmartCollection:
name = await dbContext.SmartCollections
.AsNoTracking()
.Where(c => c.Id == (decodedKey.SmartCollectionId ?? 0))
.Map(c => c.Name)
.FirstOrDefaultAsync(cancellationToken);
break;
}
string mediaItemType = string.Empty;
string mediaItemTitle = string.Empty;
Details details = JsonConvert.DeserializeObject<Details>(request.PlayoutHistory.Details);
if (details?.MediaItemId != null)
{
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
.AsNoTracking()
.Include(i => i.LibraryPath)
.ThenInclude(lp => lp.Library)
.ThenInclude(l => l.MediaSource)
.Include(i => (i as Movie).MovieMetadata)
.Include(i => (i as Episode).EpisodeMetadata)
.Include(i => (i as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.Include(i => (i as OtherVideo).OtherVideoMetadata)
.Include(i => (i as Image).ImageMetadata)
.Include(i => (i as RemoteStream).RemoteStreamMetadata)
.Include(i => (i as Song).SongMetadata)
.Include(i => (i as MusicVideo).MusicVideoMetadata)
.Include(i => (i as MusicVideo).Artist)
.ThenInclude(a => a.ArtistMetadata)
.SelectOneAsync(i => i.Id, i => i.Id == details.MediaItemId, cancellationToken);
foreach (var mediaItem in maybeMediaItem)
{
mediaItemType = mediaItem switch
{
Episode => "Episode",
Movie => "Movie",
MusicVideo => "Music Video",
OtherVideo => "Other Video",
Song => "Song",
Image => "Image",
RemoteStream => "Remote Stream",
_ => $"Unknown ({mediaItem.GetType().Name})"
};
mediaItemTitle = Playouts.Mapper.GetDisplayTitle(mediaItem, Option<string>.None);
}
}
return new PlayoutHistoryDetailsViewModel(playbackOrder, collectionType, name, mediaItemType, mediaItemTitle);
}
private sealed record BlockItemHistoryKey(
int? BlockId,
PlaybackOrder? PlaybackOrder,
CollectionType? CollectionType,
int? CollectionId,
int? SmartCollectionId);
private sealed record Details(int? MediaItemId);
}

3
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -47,6 +47,9 @@ public class ConfigElementKey @@ -47,6 +47,9 @@ public class ConfigElementKey
public static ConfigElementKey PlayoutDaysToBuild => new("playout.days_to_build");
public static ConfigElementKey PlayoutSkipMissingItems => new("playout.skip_missing_items");
public static ConfigElementKey TroubleshootingBlockPlayoutHistoryPageSize =>
new("pages.troubleshooting.block_playout_history.page_size");
public static ConfigElementKey PlayoutScriptedScheduleTimeoutSeconds =>
new("playout.scripted_schedule_timeout_seconds");

13
ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs

@ -58,7 +58,11 @@ public class PlexNetworkScanner( @@ -58,7 +58,11 @@ public class PlexNetworkScanner(
var keepIds = new System.Collections.Generic.HashSet<int>();
await foreach ((PlexShow item, int _) in items)
{
PlexShowAddTagResult result = await plexTelevisionRepository.AddTag(library, item, tag, cancellationToken);
PlexShowAddTagResult result = await plexTelevisionRepository.AddTag(
library,
item,
tag,
cancellationToken);
foreach (int existing in result.Existing)
{
@ -74,7 +78,8 @@ public class PlexNetworkScanner( @@ -74,7 +78,8 @@ public class PlexNetworkScanner(
cancellationToken.ThrowIfCancellationRequested();
}
List<int> removedIds = await plexTelevisionRepository.RemoveAllTags(library, tag, keepIds, cancellationToken);
List<int> removedIds =
await plexTelevisionRepository.RemoveAllTags(library, tag, keepIds, cancellationToken);
var changedIds = removedIds.Concat(addedIds).Distinct().ToList();
if (changedIds.Count > 0)
@ -91,6 +96,10 @@ public class PlexNetworkScanner( @@ -91,6 +96,10 @@ public class PlexNetworkScanner(
new ScannerProgressUpdate(0, null, null, changedIds.ToArray(), []),
CancellationToken.None);
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{
// do nothing
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to synchronize Plex network {Name}", tag.Tag);

159
ErsatzTV/Pages/Troubleshooting/BlockPlayoutHistory.razor

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
@page "/system/troubleshooting/block-playout/history"
@using System.Globalization
@using ErsatzTV.Application.Configuration
@using ErsatzTV.Application.Playouts
@using ErsatzTV.Application.Troubleshooting
@using ErsatzTV.Application.Troubleshooting.Queries
@implements IDisposable
@inject IMediator Mediator
@inject NavigationManager NavigationManager
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Block Playout History</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
Dense="true"
Class="mt-8"
SelectedItemChanged="@(async (PlayoutHistoryViewModel x) => await PlayoutHistorySelected(x))"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutHistoryViewModel>>>(ServerReload))"
RowClassFunc="@SelectedRowClassFunc">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col/>
<col/>
<col/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>Start</MudTh>
<MudTh>Finish</MudTh>
<MudTh>Key</MudTh>
<MudTh>Details</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.When.ToString("G", _dtf)</MudTd>
<MudTd>@context.Finish.ToString("G", _dtf)</MudTd>
<MudTd>@context.Key</MudTd>
<MudTd>@context.Details</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">History Details</MudText>
<MudDivider Class="mb-6"/>
@if (_decodedHistory != null)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Playback Order</MudText>
</div>
<MudTextField Value="@_decodedHistory.PlaybackOrder.ToString()" ReadOnly="true" />
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Collection Type</MudText>
</div>
<MudTextField Value="@_decodedHistory.CollectionType.ToString()" ReadOnly="true" />
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Collection Name</MudText>
</div>
<MudTextField Value="@_decodedHistory.Name" ReadOnly="true" />
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Media Item Type</MudText>
</div>
<MudTextField Value="@_decodedHistory.MediaItemType" ReadOnly="true" />
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Media Item Title</MudText>
</div>
<MudTextField Value="@_decodedHistory.MediaItemTitle" ReadOnly="true" />
</MudStack>
}
</MudContainer>
</div>
</MudForm>
@code {
[SupplyParameterFromQuery(Name = "playoutId")]
public int? PlayoutId { get; set; }
[SupplyParameterFromQuery(Name = "blockId")]
public int? BlockId { get; set; }
private CancellationTokenSource _cts;
private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat;
private int _rowsPerPage = 10;
private int? _selectedPlayoutHistoryId;
private PlayoutHistoryDetailsViewModel _decodedHistory;
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
if (PlayoutId is null || BlockId is null)
{
NavigationManager.NavigateTo("system/troubleshooting");
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_rowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.TroubleshootingBlockPlayoutHistoryPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task<TableData<PlayoutHistoryViewModel>> ServerReload(TableState state, CancellationToken cancellationToken)
{
_selectedPlayoutHistoryId = null;
_decodedHistory = null;
await InvokeAsync(StateHasChanged);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.TroubleshootingBlockPlayoutHistoryPageSize, state.PageSize.ToString()), cancellationToken);
PagedPlayoutHistoryViewModel data = await Mediator.Send(new GetBlockPlayoutHistory(PlayoutId!.Value, BlockId!.Value, state.Page, state.PageSize), cancellationToken);
return new TableData<PlayoutHistoryViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}
private async Task PlayoutHistorySelected(PlayoutHistoryViewModel playoutHistory)
{
_selectedPlayoutHistoryId = playoutHistory.Id;
await Task.Delay(100);
_decodedHistory = await Mediator.Send(new DecodePlayoutHistory(playoutHistory), _cts?.Token ?? CancellationToken.None);
await InvokeAsync(StateHasChanged);
}
private string SelectedRowClassFunc(PlayoutHistoryViewModel element, int rowNumber)
{
if (_selectedPlayoutHistoryId != null && _selectedPlayoutHistoryId == element.Id)
{
return "selected";
}
return string.Empty;
}
}

162
ErsatzTV/Pages/Troubleshooting/BlockPlayoutTroubleshooting.razor

@ -0,0 +1,162 @@ @@ -0,0 +1,162 @@
@page "/system/troubleshooting/block-playout"
@using ErsatzTV.Application.Playouts
@using ErsatzTV.Application.Scheduling
@implements IDisposable
@inject IMediator Mediator
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Block Playout Troubleshooting</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Block Playout</MudText>
</div>
<MudSelect T="PlayoutNameViewModel" SelectedValuesChanged="OnSelectedPlayoutChanged">
@foreach (PlayoutNameViewModel playout in _playouts)
{
<MudSelectItem Value="@playout">@playout.ChannelNumber - @playout.ChannelName</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Blocks</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
Dense="true"
Class="mt-8"
Items="@_blocks"
GroupBy="@_groupDefinition"
GroupHeaderStyle="background-color:var(--mud-palette-appbarbackground)"
RowStyle="background-color:var(--mud-palette-background-gray)"
Filter="new Func<BlockViewModel,bool>(FilterBlocks)">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col style="width: 60px;"/>
<col/>
<col style="width: 120px;"/>
</MudHidden>
</ColGroup>
<ToolBarContent>
<MudTextField T="string"
ValueChanged="@(s => OnSearch(s))"
Placeholder="Search for blocks"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.FilterList"
Clearable="true">
</MudTextField>
</ToolBarContent>
<GroupHeaderTemplate>
<MudTd Class="mud-table-cell-custom-group">
@($"{context.Key}")
</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<div style="width: 48px;"></div>
<div style="width: 48px;"></div>
</div>
</MudTd>
</GroupHeaderTemplate>
<RowTemplate>
<MudTd>@context.Name</MudTd>
<MudTd>
<div class="d-flex">
@if (context.Id >= 0)
{
<MudIconButton Icon="@Icons.Material.Filled.Search"
Href="@($"system/troubleshooting/block-playout/history?playoutId={_selectedPlayout?.PlayoutId}&blockId={context.Id}")">
</MudIconButton>
<div style="width: 48px;"></div>
}
else
{
<div style="height: 48px; width: 48px"></div>
}
</div>
</MudTd>
</RowTemplate>
</MudTable>
</MudContainer>
</div>
</MudForm>
@code {
private CancellationTokenSource _cts;
private readonly List<PlayoutNameViewModel> _playouts = [];
private PlayoutNameViewModel _selectedPlayout;
private readonly List<BlockViewModel> _blocks = [];
private string _searchString;
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_playouts.Clear();
_playouts.AddRange(await Mediator.Send(new GetAllBlockPlayouts(), token));
_blocks.Clear();
if (_selectedPlayout is not null)
{
_blocks.AddRange(await Mediator.Send(new GetAllBlocksForPlayout(_selectedPlayout.PlayoutId), token));
}
}
catch (OperationCanceledException)
{
// do nothing
}
}
private readonly TableGroupDefinition<BlockViewModel> _groupDefinition = new()
{
GroupName = "Group",
Indentation = false,
Expandable = true,
Selector = (e) => e.GroupName
};
private void OnSearch(string query)
{
_searchString = query;
}
private bool FilterBlocks(BlockViewModel block) => FilterBlocks(block, _searchString);
private bool FilterBlocks(BlockViewModel block, string searchString)
{
if (string.IsNullOrWhiteSpace(searchString))
{
return true;
}
if (block.Name.Contains(searchString, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
private async Task OnSelectedPlayoutChanged(IEnumerable<PlayoutNameViewModel> selectedPlayouts)
{
_selectedPlayout = null;
_blocks.Clear();
foreach (var playout in selectedPlayouts.HeadOrNone())
{
_selectedPlayout = playout;
_blocks.AddRange(await Mediator.Send(new GetAllBlocksForPlayout(playout.PlayoutId), _cts?.Token ?? CancellationToken.None));
await InvokeAsync(StateHasChanged);
}
}
}

0
ErsatzTV/Pages/PlaybackTroubleshooting.razor → ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor

12
ErsatzTV/Pages/Troubleshooting.razor → ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor

@ -28,10 +28,18 @@ @@ -28,10 +28,18 @@
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Checklist"
Href="system/troubleshooting/yaml"
Href="system/troubleshooting/sequential-schedule"
Class="mt-6"
Style="margin-right: auto">
YAML Validation
Sequential Schedule Validation
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Troubleshoot"
Href="system/troubleshooting/block-playout"
Class="mt-6"
Style="margin-right: auto">
Block Playout Troubleshooting
</MudButton>
</MudStack>
</MudPaper>

4
ErsatzTV/Pages/YamlValidator.razor → ErsatzTV/Pages/Troubleshooting/YamlValidator.razor

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
@page "/system/troubleshooting/yaml"
@page "/system/troubleshooting/sequential-schedule"
@using ErsatzTV.Core.Interfaces.Scheduling
@implements IDisposable
@inject ISequentialScheduleValidator SequentialScheduleValidator
@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">YAML Validation</MudText>
<MudText Typo="Typo.h5" Class="mb-2">Sequential Schedule Validation</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">

2
ErsatzTV/Services/ScannerService.cs

@ -87,7 +87,7 @@ public class ScannerService : BackgroundService @@ -87,7 +87,7 @@ public class ScannerService : BackgroundService
await requestTask;
}
catch (Exception ex)
catch (Exception ex) when (ex is not (TaskCanceledException or OperationCanceledException))
{
_logger.LogWarning(ex, "Failed to process scanner background service request");

Loading…
Cancel
Save