Browse Source

playout management ui improvements (#1298)

pull/1299/head
Jason Dove 2 years ago committed by GitHub
parent
commit
c554d83d60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 6
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 18
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs
  4. 4
      ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs
  5. 27
      ErsatzTV.Infrastructure/Locking/EntityLocker.cs
  6. 51
      ErsatzTV/Pages/Playouts.razor

6
CHANGELOG.md

@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Disable playout buttons and show spinning indicator when a playout is being modified (built/extended, or subtitles are being extracted)
- Automatically reload playout details table when playout build is complete
### Fixed
- Skip checking for subtitles to extract when subtitles are not enabled on a channel/schedule item
## [0.7.9-beta] - 2023-06-10
### Added

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

@ -6,6 +6,7 @@ using ErsatzTV.Application.Subtitles; @@ -6,6 +6,7 @@ using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
@ -19,6 +20,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -19,6 +20,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IEntityLocker _entityLocker;
private readonly IPlayoutBuilder _playoutBuilder;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
@ -27,12 +29,14 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -27,12 +29,14 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
IDbContextFactory<TvContext> dbContextFactory,
IPlayoutBuilder playoutBuilder,
IFFmpegSegmenterService ffmpegSegmenterService,
IEntityLocker entityLocker,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_client = client;
_dbContextFactory = dbContextFactory;
_playoutBuilder = playoutBuilder;
_ffmpegSegmenterService = ffmpegSegmenterService;
_entityLocker = entityLocker;
_workerChannel = workerChannel;
}
@ -64,6 +68,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -64,6 +68,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
_ffmpegSegmenterService.PlayoutUpdated(playout.Channel.Number);
}
_entityLocker.UnlockPlayout(playout.Id);
Option<string> maybeChannelNumber = await dbContext.Connection
.QuerySingleOrDefaultAsync<string>(
@"select C.Number from Channel C

18
ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs

@ -9,6 +9,7 @@ using ErsatzTV.Application.Maintenance; @@ -9,6 +9,7 @@ using ErsatzTV.Application.Maintenance;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -21,17 +22,20 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -21,17 +22,20 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly IEntityLocker _entityLocker;
private readonly ILogger<ExtractEmbeddedSubtitlesHandler> _logger;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public ExtractEmbeddedSubtitlesHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem,
IEntityLocker entityLocker,
ChannelWriter<IBackgroundServiceRequest> workerChannel,
ILogger<ExtractEmbeddedSubtitlesHandler> logger)
{
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
_entityLocker = entityLocker;
_workerChannel = workerChannel;
_logger = logger;
}
@ -70,7 +74,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -70,7 +74,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.AsNoTracking()
.Filter(
p => p.Channel.SubtitleMode != ChannelSubtitleMode.None ||
p.ProgramSchedule.Items.Any(psi => psi.SubtitleMode != ChannelSubtitleMode.None))
p.ProgramSchedule.Items.Any(psi => psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None))
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId.IfNone(-1));
playoutIdsToCheck.AddRange(requestedPlayout.Map(p => p.Id));
@ -82,7 +86,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -82,7 +86,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.AsNoTracking()
.Filter(
p => p.Channel.SubtitleMode != ChannelSubtitleMode.None ||
p.ProgramSchedule.Items.Any(psi => psi.SubtitleMode != ChannelSubtitleMode.None))
p.ProgramSchedule.Items.Any(psi => psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None))
.Map(p => p.Id)
.ToList();
}
@ -101,6 +105,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -101,6 +105,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
return Unit.Default;
}
foreach (int playoutId in playoutIdsToCheck)
{
_entityLocker.LockPlayout(playoutId);
}
_logger.LogDebug("Checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck);
// find all playout items in the next hour
@ -154,6 +163,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -154,6 +163,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
_logger.LogDebug("Done checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck);
foreach (int playoutId in playoutIdsToCheck)
{
_entityLocker.UnlockPlayout(playoutId);
}
return Unit.Default;
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)

4
ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs

@ -7,6 +7,7 @@ public interface IEntityLocker @@ -7,6 +7,7 @@ public interface IEntityLocker
event EventHandler<Type> OnRemoteMediaSourceChanged;
event EventHandler OnTraktChanged;
event EventHandler OnEmbyCollectionsChanged;
event EventHandler<int> OnPlayoutChanged;
bool LockLibrary(int libraryId);
bool UnlockLibrary(int libraryId);
bool IsLibraryLocked(int libraryId);
@ -22,4 +23,7 @@ public interface IEntityLocker @@ -22,4 +23,7 @@ public interface IEntityLocker
bool LockEmbyCollections();
bool UnlockEmbyCollections();
bool AreEmbyCollectionsLocked();
bool LockPlayout(int playoutId);
bool UnlockPlayout(int playoutId);
bool IsPlayoutLocked(int playoutId);
}

27
ErsatzTV.Infrastructure/Locking/EntityLocker.cs

@ -6,6 +6,7 @@ namespace ErsatzTV.Infrastructure.Locking; @@ -6,6 +6,7 @@ namespace ErsatzTV.Infrastructure.Locking;
public class EntityLocker : IEntityLocker
{
private readonly ConcurrentDictionary<int, byte> _lockedLibraries;
private readonly ConcurrentDictionary<int, byte> _lockedPlayouts;
private readonly ConcurrentDictionary<Type, byte> _lockedRemoteMediaSourceTypes;
private bool _embyCollections;
private bool _plex;
@ -14,6 +15,7 @@ public class EntityLocker : IEntityLocker @@ -14,6 +15,7 @@ public class EntityLocker : IEntityLocker
public EntityLocker()
{
_lockedLibraries = new ConcurrentDictionary<int, byte>();
_lockedPlayouts = new ConcurrentDictionary<int, byte>();
_lockedRemoteMediaSourceTypes = new ConcurrentDictionary<Type, byte>();
}
@ -22,6 +24,7 @@ public class EntityLocker : IEntityLocker @@ -22,6 +24,7 @@ public class EntityLocker : IEntityLocker
public event EventHandler<Type> OnRemoteMediaSourceChanged;
public event EventHandler OnTraktChanged;
public event EventHandler OnEmbyCollectionsChanged;
public event EventHandler<int> OnPlayoutChanged;
public bool LockLibrary(int libraryId)
{
@ -155,4 +158,28 @@ public class EntityLocker : IEntityLocker @@ -155,4 +158,28 @@ public class EntityLocker : IEntityLocker
}
public bool AreEmbyCollectionsLocked() => _embyCollections;
public bool LockPlayout(int playoutId)
{
if (!_lockedPlayouts.ContainsKey(playoutId) && _lockedPlayouts.TryAdd(playoutId, 0))
{
OnPlayoutChanged?.Invoke(this, playoutId);
return true;
}
return false;
}
public bool UnlockPlayout(int playoutId)
{
if (_lockedPlayouts.TryRemove(playoutId, out byte _))
{
OnPlayoutChanged?.Invoke(this, playoutId);
return true;
}
return false;
}
public bool IsPlayoutLocked(int playoutId) => _lockedPlayouts.ContainsKey(playoutId);
}

51
ErsatzTV/Pages/Playouts.razor

@ -3,9 +3,10 @@ @@ -3,9 +3,10 @@
@using ErsatzTV.Application.Configuration
@using ErsatzTV.Core.Scheduling
@implements IDisposable
@inject IDialogService _dialog
@inject IMediator _mediator
@inject IDialogService Dialog
@inject IMediator Mediator
@inject ChannelWriter<IBackgroundServiceRequest> WorkerChannel;
@inject IEntityLocker EntityLocker;
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="playouts/add">
@ -46,23 +47,33 @@ @@ -46,23 +47,33 @@
@* <MudTd DataLabel="Playout Type">@context.ProgramSchedulePlayoutType</MudTd> *@
<MudTd>
<div style="align-items: center; display: flex;">
@if (EntityLocker.IsPlayoutLocked(context.PlayoutId))
{
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
</div>
}
<MudTooltip Text="Edit Alternate Schedules">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Link="@($"playouts/{context.PlayoutId}/alternate-schedules")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ResetPlayout(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Schedule Reset">
<MudIconButton Icon="@Icons.Material.Filled.Update"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ScheduleReset(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Playout">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => DeletePlayout(context))">
</MudIconButton>
</MudTooltip>
@ -131,17 +142,21 @@ @@ -131,17 +142,21 @@
public void Dispose()
{
EntityLocker.OnPlayoutChanged -= ReloadDetailsIfNeeded;
_cts.Cancel();
_cts.Dispose();
}
protected override void OnInitialized() => EntityLocker.OnPlayoutChanged += ReloadDetailsIfNeeded;
protected override async Task OnParametersSetAsync()
{
_rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsPageSize), _cts.Token)
_rowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_detailRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize), _cts.Token)
_detailRowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_showFiller = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller), _cts.Token)
_showFiller = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller), _cts.Token)
.Map(maybeShow => maybeShow.Match(ce => bool.TryParse(ce.Value, out bool show) && show, () => false));
}
@ -159,11 +174,11 @@ @@ -159,11 +174,11 @@
var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ScheduleName} on {playout.ChannelNumber} - {playout.ChannelName}" } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await _dialog.ShowAsync<DeleteDialog>("Delete Playout", parameters, options);
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Playout", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
{
await _mediator.Send(new DeletePlayout(playout.PlayoutId), _cts.Token);
await Mediator.Send(new DeletePlayout(playout.PlayoutId), _cts.Token);
if (_table != null)
{
await _table.ReloadServerData();
@ -199,7 +214,7 @@ @@ -199,7 +214,7 @@
};
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await _dialog.ShowAsync<SchedulePlayoutReset>("Schedule Playout Reset", parameters, options);
IDialogReference dialog = await Dialog.ShowAsync<SchedulePlayoutReset>("Schedule Playout Reset", parameters, options);
await dialog.Result;
if (_table != null)
@ -210,9 +225,9 @@ @@ -210,9 +225,9 @@
private async Task<TableData<PlayoutNameViewModel>> ServerReload(TableState state)
{
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsPageSize, state.PageSize.ToString()), _cts.Token);
List<PlayoutNameViewModel> playouts = await _mediator.Send(new GetAllPlayouts(), _cts.Token);
List<PlayoutNameViewModel> playouts = await Mediator.Send(new GetAllPlayouts(), _cts.Token);
IOrderedEnumerable<PlayoutNameViewModel> sorted = playouts.OrderBy(p => decimal.Parse(p.ChannelNumber));
// TODO: properly page this data
@ -223,15 +238,25 @@ @@ -223,15 +238,25 @@
};
}
private async void ReloadDetailsIfNeeded(object sender, int playoutId)
{
if (playoutId == _selectedPlayoutId && _detailTable is not null)
{
await InvokeAsync(() => _detailTable.ReloadServerData());
}
await InvokeAsync(StateHasChanged);
}
private async Task<TableData<PlayoutItemViewModel>> DetailServerReload(TableState state)
{
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString()), _cts.Token);
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller, _showFiller.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller, _showFiller.ToString()), _cts.Token);
if (_selectedPlayoutId.HasValue)
{
PagedPlayoutItemsViewModel data =
await _mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, _showFiller, state.Page, state.PageSize), _cts.Token);
await Mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, _showFiller, state.Page, state.PageSize), _cts.Token);
return new TableData<PlayoutItemViewModel>
{
TotalItems = data.TotalCount,

Loading…
Cancel
Save