From a0e80f881d747effb5cbcb1025eb27f4db01c779 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 14 Feb 2021 06:29:38 -0600 Subject: [PATCH] lock media source during refresh --- .../Commands/ScanLocalMediaSource.cs | 3 +- .../Commands/ScanLocalMediaSourceHandler.cs | 23 ++++++++-- .../Interfaces/Locking/IEntityLocker.cs | 12 +++++ .../Locking/EntityLocker.cs | 40 +++++++++++++++++ ErsatzTV/Pages/LocalMediaSourceEditor.razor | 8 +++- ErsatzTV/Services/SchedulerService.cs | 11 ++++- ErsatzTV/Shared/LocalMediaSources.razor | 45 +++++++++++++++---- ErsatzTV/Startup.cs | 3 ++ ErsatzTV/_Imports.razor | 1 + 9 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs create mode 100644 ErsatzTV.Infrastructure/Locking/EntityLocker.cs diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs index d04458786..e4d5debd5 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs @@ -4,6 +4,7 @@ using MediatR; namespace ErsatzTV.Application.MediaSources.Commands { - public record ScanLocalMediaSource(int MediaSourceId, bool RefreshAllMetadata) : IRequest>, + public record ScanLocalMediaSource(int MediaSourceId, bool RefreshAllMetadata) : + IRequest>, IBackgroundServiceRequest; } diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs index e6d67e6d1..76ff69ac3 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs @@ -3,37 +3,52 @@ using System.Threading; using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using LanguageExt; using MediatR; +using Unit = LanguageExt.Unit; namespace ErsatzTV.Application.MediaSources.Commands { public class ScanLocalMediaSourceHandler : IRequestHandler> { private readonly IConfigElementRepository _configElementRepository; + private readonly IEntityLocker _entityLocker; private readonly ILocalMediaScanner _localMediaScanner; private readonly IMediaSourceRepository _mediaSourceRepository; public ScanLocalMediaSourceHandler( IMediaSourceRepository mediaSourceRepository, IConfigElementRepository configElementRepository, - ILocalMediaScanner localMediaScanner) + ILocalMediaScanner localMediaScanner, + IEntityLocker entityLocker) { _mediaSourceRepository = mediaSourceRepository; _configElementRepository = configElementRepository; _localMediaScanner = localMediaScanner; + _entityLocker = entityLocker; } public Task> Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) => Validate(request) - .MapT( - p => _localMediaScanner.ScanLocalMediaSource(p.LocalMediaSource, p.FFprobePath, request.RefreshAllMetadata) - .Map(_ => p.LocalMediaSource.Folder)) + .MapT(parameters => PerformScan(request, parameters).Map(_ => parameters.LocalMediaSource.Folder)) .Bind(v => v.ToEitherAsync()); + private async Task PerformScan(ScanLocalMediaSource request, RequestParameters parameters) + { + await _localMediaScanner.ScanLocalMediaSource( + parameters.LocalMediaSource, + parameters.FFprobePath, + request.RefreshAllMetadata); + + _entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id); + + return Unit.Default; + } + private async Task> Validate(ScanLocalMediaSource request) => (await LocalMediaSourceMustExist(request), await ValidateFFprobePath()) .Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath)); diff --git a/ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs b/ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs new file mode 100644 index 000000000..f750db1b7 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs @@ -0,0 +1,12 @@ +using System; + +namespace ErsatzTV.Core.Interfaces.Locking +{ + public interface IEntityLocker + { + public event EventHandler OnMediaSourceChanged; + public bool LockMediaSource(int mediaSourceId); + public bool UnlockMediaSource(int mediaSourceId); + public bool IsMediaSourceLocked(int mediaSourceId); + } +} diff --git a/ErsatzTV.Infrastructure/Locking/EntityLocker.cs b/ErsatzTV.Infrastructure/Locking/EntityLocker.cs new file mode 100644 index 000000000..6cfcd2200 --- /dev/null +++ b/ErsatzTV.Infrastructure/Locking/EntityLocker.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Concurrent; +using ErsatzTV.Core.Interfaces.Locking; + +namespace ErsatzTV.Infrastructure.Locking +{ + public class EntityLocker : IEntityLocker + { + private readonly ConcurrentDictionary _lockedMediaSources; + + public EntityLocker() => _lockedMediaSources = new ConcurrentDictionary(); + + public event EventHandler OnMediaSourceChanged; + + public bool LockMediaSource(int mediaSourceId) + { + if (_lockedMediaSources.TryAdd(mediaSourceId, 0)) + { + OnMediaSourceChanged?.Invoke(this, EventArgs.Empty); + return true; + } + + return false; + } + + public bool UnlockMediaSource(int mediaSourceId) + { + if (_lockedMediaSources.TryRemove(mediaSourceId, out byte _)) + { + OnMediaSourceChanged?.Invoke(this, EventArgs.Empty); + return true; + } + + return false; + } + + public bool IsMediaSourceLocked(int mediaSourceId) => + _lockedMediaSources.ContainsKey(mediaSourceId); + } +} diff --git a/ErsatzTV/Pages/LocalMediaSourceEditor.razor b/ErsatzTV/Pages/LocalMediaSourceEditor.razor index ad72d8fd9..a36599d29 100644 --- a/ErsatzTV/Pages/LocalMediaSourceEditor.razor +++ b/ErsatzTV/Pages/LocalMediaSourceEditor.razor @@ -5,6 +5,7 @@ @inject ILogger Logger @inject ISnackbar Snackbar @inject IMediator Mediator +@inject IEntityLocker Locker @inject ChannelWriter Channel
@@ -84,8 +85,11 @@ }, Right: async vm => { - await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id, false)); - NavigationManager.NavigateTo("/media/sources"); + if (Locker.LockMediaSource(vm.Id)) + { + await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id, false)); + NavigationManager.NavigateTo("/media/sources"); + } }); } } diff --git a/ErsatzTV/Services/SchedulerService.cs b/ErsatzTV/Services/SchedulerService.cs index 57d00e13c..3ea11ebb7 100644 --- a/ErsatzTV/Services/SchedulerService.cs +++ b/ErsatzTV/Services/SchedulerService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using ErsatzTV.Application; using ErsatzTV.Application.MediaSources.Commands; using ErsatzTV.Application.Playouts.Commands; +using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -16,15 +17,18 @@ namespace ErsatzTV.Services public class SchedulerService : IHostedService { private readonly ChannelWriter _channel; + private readonly IEntityLocker _entityLocker; private readonly IServiceScopeFactory _serviceScopeFactory; private Timer _timer; public SchedulerService( IServiceScopeFactory serviceScopeFactory, - ChannelWriter channel) + ChannelWriter channel, + IEntityLocker entityLocker) { _serviceScopeFactory = serviceScopeFactory; _channel = channel; + _entityLocker = entityLocker; } public Task StartAsync(CancellationToken cancellationToken) @@ -75,7 +79,10 @@ namespace ErsatzTV.Services foreach (int mediaSourceId in localMediaSourceIds) { - await _channel.WriteAsync(new ScanLocalMediaSource(mediaSourceId, false), cancellationToken); + if (_entityLocker.LockMediaSource(mediaSourceId)) + { + await _channel.WriteAsync(new ScanLocalMediaSource(mediaSourceId, false), cancellationToken); + } } } } diff --git a/ErsatzTV/Shared/LocalMediaSources.razor b/ErsatzTV/Shared/LocalMediaSources.razor index b9820c7f1..39ba78d69 100644 --- a/ErsatzTV/Shared/LocalMediaSources.razor +++ b/ErsatzTV/Shared/LocalMediaSources.razor @@ -1,11 +1,13 @@ @using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.MediaSources.Commands @using ErsatzTV.Application.MediaSources.Queries +@implements IDisposable @inject IDialogService Dialog @inject IMediator Mediator +@inject IEntityLocker Locker @inject ChannelWriter Channel - + Local Media Sources @@ -20,12 +22,27 @@ @context.Folder -
- - - +
+ @if (Locker.IsMediaSourceLocked(context.Id)) + { +
+ +
+ } + else + { + + + + + } - + +
@@ -37,7 +54,9 @@ @code { private IList _mediaSources; - private bool _refreshing = false; + + protected override void OnInitialized() => + Locker.OnMediaSourceChanged += LockChanged; protected override async Task OnParametersSetAsync() => await LoadMediaSources(); @@ -68,8 +87,16 @@ private async Task RefreshAllMetadata(LocalMediaSourceViewModel mediaSource) { - _refreshing = true; - await Channel.WriteAsync(new ScanLocalMediaSource(mediaSource.Id, true)); + if (Locker.LockMediaSource(mediaSource.Id)) + { + await Channel.WriteAsync(new ScanLocalMediaSource(mediaSource.Id, true)); + StateHasChanged(); + } } + private void LockChanged(object sender, EventArgs e) => + InvokeAsync(StateHasChanged); + + void IDisposable.Dispose() => Locker.OnMediaSourceChanged -= LockChanged; + } \ No newline at end of file diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index eea3a74ff..e6f92b097 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -8,6 +8,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; +using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; @@ -18,6 +19,7 @@ using ErsatzTV.Formatters; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Images; +using ErsatzTV.Infrastructure.Locking; using ErsatzTV.Infrastructure.Plex; using ErsatzTV.Serialization; using ErsatzTV.Services; @@ -155,6 +157,7 @@ namespace ErsatzTV services.AddSingleton(); services.AddSingleton(); // TODO: does this need to be singleton? services.AddSingleton(); + services.AddSingleton(); AddChannel(services); AddChannel(services); diff --git a/ErsatzTV/_Imports.razor b/ErsatzTV/_Imports.razor index fad2bdf07..f6dc56d31 100644 --- a/ErsatzTV/_Imports.razor +++ b/ErsatzTV/_Imports.razor @@ -20,6 +20,7 @@ @using ErsatzTV.Application @using ErsatzTV.Core @using ErsatzTV.Core.Domain +@using ErsatzTV.Core.Interfaces.Locking @using ErsatzTV.Infrastructure.Data @using ErsatzTV.Shared @using ErsatzTV.ViewModels \ No newline at end of file