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;
namespace ErsatzTV.Application.MediaSources.Commands 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; IBackgroundServiceRequest;
} }

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

@ -3,37 +3,52 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaSources.Commands namespace ErsatzTV.Application.MediaSources.Commands
{ {
public class ScanLocalMediaSourceHandler : IRequestHandler<ScanLocalMediaSource, Either<BaseError, string>> public class ScanLocalMediaSourceHandler : IRequestHandler<ScanLocalMediaSource, Either<BaseError, string>>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker;
private readonly ILocalMediaScanner _localMediaScanner; private readonly ILocalMediaScanner _localMediaScanner;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
public ScanLocalMediaSourceHandler( public ScanLocalMediaSourceHandler(
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
ILocalMediaScanner localMediaScanner) ILocalMediaScanner localMediaScanner,
IEntityLocker entityLocker)
{ {
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_localMediaScanner = localMediaScanner; _localMediaScanner = localMediaScanner;
_entityLocker = entityLocker;
} }
public Task<Either<BaseError, string>> public Task<Either<BaseError, string>>
Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) => Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT( .MapT(parameters => PerformScan(request, parameters).Map(_ => parameters.LocalMediaSource.Folder))
p => _localMediaScanner.ScanLocalMediaSource(p.LocalMediaSource, p.FFprobePath, request.RefreshAllMetadata)
.Map(_ => p.LocalMediaSource.Folder))
.Bind(v => v.ToEitherAsync()); .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) => private async Task<Validation<BaseError, RequestParameters>> Validate(ScanLocalMediaSource request) =>
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath()) (await LocalMediaSourceMustExist(request), await ValidateFFprobePath())
.Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath)); .Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath));

12
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);
}
}

40
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<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 @@
@inject ILogger<LocalMediaSourceEditor> Logger @inject ILogger<LocalMediaSourceEditor> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
@inject IEntityLocker Locker
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<div style="max-width: 400px;"> <div style="max-width: 400px;">
@ -84,8 +85,11 @@
}, },
Right: async vm => Right: async vm =>
{ {
await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id, false)); if (Locker.LockMediaSource(vm.Id))
NavigationManager.NavigateTo("/media/sources"); {
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;
using ErsatzTV.Application; using ErsatzTV.Application;
using ErsatzTV.Application.MediaSources.Commands; using ErsatzTV.Application.MediaSources.Commands;
using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -16,15 +17,18 @@ namespace ErsatzTV.Services
public class SchedulerService : IHostedService public class SchedulerService : IHostedService
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IEntityLocker _entityLocker;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private Timer _timer; private Timer _timer;
public SchedulerService( public SchedulerService(
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel,
IEntityLocker entityLocker)
{ {
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_channel = channel; _channel = channel;
_entityLocker = entityLocker;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@ -75,7 +79,10 @@ namespace ErsatzTV.Services
foreach (int mediaSourceId in localMediaSourceIds) 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 @@
@using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.MediaSources
@using ErsatzTV.Application.MediaSources.Commands @using ErsatzTV.Application.MediaSources.Commands
@using ErsatzTV.Application.MediaSources.Queries @using ErsatzTV.Application.MediaSources.Queries
@implements IDisposable
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IMediator Mediator @inject IMediator Mediator
@inject IEntityLocker Locker
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudTable Hover="true" Items="_mediaSources"> <MudTable Hover="true" Items="_mediaSources" Dense="true">
<ToolBarContent> <ToolBarContent>
<MudText Typo="Typo.h6">Local Media Sources</MudText> <MudText Typo="Typo.h6">Local Media Sources</MudText>
</ToolBarContent> </ToolBarContent>
@ -20,12 +22,27 @@
<RowTemplate> <RowTemplate>
<MudTd DataLabel="Folder">@context.Folder</MudTd> <MudTd DataLabel="Folder">@context.Folder</MudTd>
<MudTd> <MudTd>
<div style="display: flex"> <div style="align-items: center; display: flex;">
<MudTooltip Text="Refresh All Metadata"> @if (Locker.IsMediaSourceLocked(context.Id))
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Disabled="@_refreshing" OnClick="@(_ => RefreshAllMetadata(context))"></MudIconButton> {
</MudTooltip> <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"> <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> </MudTooltip>
</div> </div>
</MudTd> </MudTd>
@ -37,7 +54,9 @@
@code { @code {
private IList<LocalMediaSourceViewModel> _mediaSources; private IList<LocalMediaSourceViewModel> _mediaSources;
private bool _refreshing = false;
protected override void OnInitialized() =>
Locker.OnMediaSourceChanged += LockChanged;
protected override async Task OnParametersSetAsync() => await LoadMediaSources(); protected override async Task OnParametersSetAsync() => await LoadMediaSources();
@ -68,8 +87,16 @@
private async Task RefreshAllMetadata(LocalMediaSourceViewModel mediaSource) private async Task RefreshAllMetadata(LocalMediaSourceViewModel mediaSource)
{ {
_refreshing = true; if (Locker.LockMediaSource(mediaSource.Id))
await Channel.WriteAsync(new ScanLocalMediaSource(mediaSource.Id, true)); {
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;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
@ -18,6 +19,7 @@ using ErsatzTV.Formatters;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Images; using ErsatzTV.Infrastructure.Images;
using ErsatzTV.Infrastructure.Locking;
using ErsatzTV.Infrastructure.Plex; using ErsatzTV.Infrastructure.Plex;
using ErsatzTV.Serialization; using ErsatzTV.Serialization;
using ErsatzTV.Services; using ErsatzTV.Services;
@ -155,6 +157,7 @@ namespace ErsatzTV
services.AddSingleton<IPlexSecretStore, PlexSecretStore>(); services.AddSingleton<IPlexSecretStore, PlexSecretStore>();
services.AddSingleton<IPlexTvApiClient, PlexTvApiClient>(); // TODO: does this need to be singleton? services.AddSingleton<IPlexTvApiClient, PlexTvApiClient>(); // TODO: does this need to be singleton?
services.AddSingleton<IPlexServerApiClient, PlexServerApiClient>(); services.AddSingleton<IPlexServerApiClient, PlexServerApiClient>();
services.AddSingleton<IEntityLocker, EntityLocker>();
AddChannel<IBackgroundServiceRequest>(services); AddChannel<IBackgroundServiceRequest>(services);
AddChannel<IPlexBackgroundServiceRequest>(services); AddChannel<IPlexBackgroundServiceRequest>(services);

1
ErsatzTV/_Imports.razor

@ -20,6 +20,7 @@
@using ErsatzTV.Application @using ErsatzTV.Application
@using ErsatzTV.Core @using ErsatzTV.Core
@using ErsatzTV.Core.Domain @using ErsatzTV.Core.Domain
@using ErsatzTV.Core.Interfaces.Locking
@using ErsatzTV.Infrastructure.Data @using ErsatzTV.Infrastructure.Data
@using ErsatzTV.Shared @using ErsatzTV.Shared
@using ErsatzTV.ViewModels @using ErsatzTV.ViewModels
Loading…
Cancel
Save