Browse Source

lock media source during refresh

pull/24/head
Jason Dove 6 years ago
parent
commit
a0e80f881d
  1. 3
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs
  2. 23
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs
  3. 12
      ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs
  4. 40
      ErsatzTV.Infrastructure/Locking/EntityLocker.cs
  5. 8
      ErsatzTV/Pages/LocalMediaSourceEditor.razor
  6. 11
      ErsatzTV/Services/SchedulerService.cs
  7. 45
      ErsatzTV/Shared/LocalMediaSources.razor
  8. 3
      ErsatzTV/Startup.cs
  9. 1
      ErsatzTV/_Imports.razor

3
ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs

@ -4,6 +4,7 @@ using MediatR; @@ -4,6 +4,7 @@ using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
{
public record ScanLocalMediaSource(int MediaSourceId, bool RefreshAllMetadata) : IRequest<Either<BaseError, string>>,
public record ScanLocalMediaSource(int MediaSourceId, bool RefreshAllMetadata) :
IRequest<Either<BaseError, string>>,
IBackgroundServiceRequest;
}

23
ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs

@ -3,37 +3,52 @@ using System.Threading; @@ -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<ScanLocalMediaSource, Either<BaseError, string>>
{
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<Either<BaseError, string>>
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<Unit> 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<Validation<BaseError, RequestParameters>> Validate(ScanLocalMediaSource request) =>
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath())
.Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath));

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

@ -0,0 +1,12 @@ @@ -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);
}
}

40
ErsatzTV.Infrastructure/Locking/EntityLocker.cs

@ -0,0 +1,40 @@ @@ -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<int, byte> _lockedMediaSources;
public EntityLocker() => _lockedMediaSources = new ConcurrentDictionary<int, byte>();
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);
}
}

8
ErsatzTV/Pages/LocalMediaSourceEditor.razor

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
@inject ILogger<LocalMediaSourceEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IEntityLocker Locker
@inject ChannelWriter<IBackgroundServiceRequest> Channel
<div style="max-width: 400px;">
@ -84,8 +85,11 @@ @@ -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");
}
});
}
}

11
ErsatzTV/Services/SchedulerService.cs

@ -6,6 +6,7 @@ using System.Threading.Tasks; @@ -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 @@ -16,15 +17,18 @@ namespace ErsatzTV.Services
public class SchedulerService : IHostedService
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IEntityLocker _entityLocker;
private readonly IServiceScopeFactory _serviceScopeFactory;
private Timer _timer;
public SchedulerService(
IServiceScopeFactory serviceScopeFactory,
ChannelWriter<IBackgroundServiceRequest> channel)
ChannelWriter<IBackgroundServiceRequest> channel,
IEntityLocker entityLocker)
{
_serviceScopeFactory = serviceScopeFactory;
_channel = channel;
_entityLocker = entityLocker;
}
public Task StartAsync(CancellationToken cancellationToken)
@ -75,7 +79,10 @@ namespace ErsatzTV.Services @@ -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);
}
}
}
}

45
ErsatzTV/Shared/LocalMediaSources.razor

@ -1,11 +1,13 @@ @@ -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<IBackgroundServiceRequest> Channel
<MudTable Hover="true" Items="_mediaSources">
<MudTable Hover="true" Items="_mediaSources" Dense="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Local Media Sources</MudText>
</ToolBarContent>
@ -20,12 +22,27 @@ @@ -20,12 +22,27 @@
<RowTemplate>
<MudTd DataLabel="Folder">@context.Folder</MudTd>
<MudTd>
<div style="display: flex">
<MudTooltip Text="Refresh All Metadata">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Disabled="@_refreshing" OnClick="@(_ => RefreshAllMetadata(context))"></MudIconButton>
</MudTooltip>
<div style="align-items: center; display: flex;">
@if (Locker.IsMediaSourceLocked(context.Id))
{
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
</div>
}
else
{
<MudTooltip Text="Refresh All Metadata">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@Locker.IsMediaSourceLocked(context.Id)"
OnClick="@(_ => RefreshAllMetadata(context))">
</MudIconButton>
</MudTooltip>
}
<MudTooltip Text="Delete Media Source">
<MudIconButton Icon="@Icons.Material.Filled.Delete" OnClick="@(_ => DeleteMediaSource(context))"></MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Disabled="@Locker.IsMediaSourceLocked(context.Id)"
OnClick="@(_ => DeleteMediaSource(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
@ -37,7 +54,9 @@ @@ -37,7 +54,9 @@
@code {
private IList<LocalMediaSourceViewModel> _mediaSources;
private bool _refreshing = false;
protected override void OnInitialized() =>
Locker.OnMediaSourceChanged += LockChanged;
protected override async Task OnParametersSetAsync() => await LoadMediaSources();
@ -68,8 +87,16 @@ @@ -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;
}

3
ErsatzTV/Startup.cs

@ -8,6 +8,7 @@ using ErsatzTV.Core; @@ -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; @@ -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 @@ -155,6 +157,7 @@ namespace ErsatzTV
services.AddSingleton<IPlexSecretStore, PlexSecretStore>();
services.AddSingleton<IPlexTvApiClient, PlexTvApiClient>(); // TODO: does this need to be singleton?
services.AddSingleton<IPlexServerApiClient, PlexServerApiClient>();
services.AddSingleton<IEntityLocker, EntityLocker>();
AddChannel<IBackgroundServiceRequest>(services);
AddChannel<IPlexBackgroundServiceRequest>(services);

1
ErsatzTV/_Imports.razor

@ -20,6 +20,7 @@ @@ -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
Loading…
Cancel
Save