diff --git a/CHANGELOG.md b/CHANGELOG.md index 856d6a06d..66e9c9255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs index fc8e25b0d..4d84d43a8 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs @@ -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 _dbContextFactory; private readonly IFFmpegSegmenterService _ffmpegSegmenterService; + private readonly IEntityLocker _entityLocker; private readonly IPlayoutBuilder _playoutBuilder; private readonly ChannelWriter _workerChannel; @@ -27,12 +29,14 @@ public class BuildPlayoutHandler : IRequestHandler dbContextFactory, IPlayoutBuilder playoutBuilder, IFFmpegSegmenterService ffmpegSegmenterService, + IEntityLocker entityLocker, ChannelWriter workerChannel) { _client = client; _dbContextFactory = dbContextFactory; _playoutBuilder = playoutBuilder; _ffmpegSegmenterService = ffmpegSegmenterService; + _entityLocker = entityLocker; _workerChannel = workerChannel; } @@ -64,6 +68,8 @@ public class BuildPlayoutHandler : IRequestHandler maybeChannelNumber = await dbContext.Connection .QuerySingleOrDefaultAsync( @"select C.Number from Channel C diff --git a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs index 3f4575f8f..b83299cb5 100644 --- a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs +++ b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs @@ -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 _dbContextFactory; private readonly ILocalFileSystem _localFileSystem; + private readonly IEntityLocker _entityLocker; private readonly ILogger _logger; private readonly ChannelWriter _workerChannel; public ExtractEmbeddedSubtitlesHandler( IDbContextFactory dbContextFactory, ILocalFileSystem localFileSystem, + IEntityLocker entityLocker, ChannelWriter workerChannel, ILogger logger) { _dbContextFactory = dbContextFactory; _localFileSystem = localFileSystem; + _entityLocker = entityLocker; _workerChannel = workerChannel; _logger = logger; } @@ -70,7 +74,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler 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 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 OnRemoteMediaSourceChanged; event EventHandler OnTraktChanged; event EventHandler OnEmbyCollectionsChanged; + event EventHandler OnPlayoutChanged; bool LockLibrary(int libraryId); bool UnlockLibrary(int libraryId); bool IsLibraryLocked(int libraryId); @@ -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); } diff --git a/ErsatzTV.Infrastructure/Locking/EntityLocker.cs b/ErsatzTV.Infrastructure/Locking/EntityLocker.cs index 1e11c5d56..2cac6452f 100644 --- a/ErsatzTV.Infrastructure/Locking/EntityLocker.cs +++ b/ErsatzTV.Infrastructure/Locking/EntityLocker.cs @@ -6,6 +6,7 @@ namespace ErsatzTV.Infrastructure.Locking; public class EntityLocker : IEntityLocker { private readonly ConcurrentDictionary _lockedLibraries; + private readonly ConcurrentDictionary _lockedPlayouts; private readonly ConcurrentDictionary _lockedRemoteMediaSourceTypes; private bool _embyCollections; private bool _plex; @@ -14,6 +15,7 @@ public class EntityLocker : IEntityLocker public EntityLocker() { _lockedLibraries = new ConcurrentDictionary(); + _lockedPlayouts = new ConcurrentDictionary(); _lockedRemoteMediaSourceTypes = new ConcurrentDictionary(); } @@ -22,6 +24,7 @@ public class EntityLocker : IEntityLocker public event EventHandler OnRemoteMediaSourceChanged; public event EventHandler OnTraktChanged; public event EventHandler OnEmbyCollectionsChanged; + public event EventHandler OnPlayoutChanged; public bool LockLibrary(int libraryId) { @@ -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); } diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index 88a2b8eb9..f04a5e72d 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -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 WorkerChannel; +@inject IEntityLocker EntityLocker; @@ -46,23 +47,33 @@ @* @context.ProgramSchedulePlayoutType *@
+ @if (EntityLocker.IsPlayoutLocked(context.PlayoutId)) + { +
+ +
+ } @@ -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 @@ 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("Delete Playout", parameters, options); + IDialogReference dialog = await Dialog.ShowAsync("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 @@ }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; - IDialogReference dialog = await _dialog.ShowAsync("Schedule Playout Reset", parameters, options); + IDialogReference dialog = await Dialog.ShowAsync("Schedule Playout Reset", parameters, options); await dialog.Result; if (_table != null) @@ -210,9 +225,9 @@ private async Task> 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 playouts = await _mediator.Send(new GetAllPlayouts(), _cts.Token); + List playouts = await Mediator.Send(new GetAllPlayouts(), _cts.Token); IOrderedEnumerable sorted = playouts.OrderBy(p => decimal.Parse(p.ChannelNumber)); // TODO: properly page this data @@ -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> 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 { TotalItems = data.TotalCount,