Browse Source

add configurable library refresh interval (#184)

* add configurable library refresh interval

* code cleanup
pull/186/head
Jason Dove 5 years ago committed by GitHub
parent
commit
27e0a70d93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshInterval.cs
  2. 47
      ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshIntervalHandler.cs
  3. 6
      ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshInterval.cs
  4. 21
      ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshIntervalHandler.cs
  5. 45
      ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs
  6. 22
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs
  7. 1
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  8. 2269
      ErsatzTV.Infrastructure/Migrations/20210514110050_Add_LibraryRefreshInterval.Designer.cs
  9. 15
      ErsatzTV.Infrastructure/Migrations/20210514110050_Add_LibraryRefreshInterval.cs
  10. 74
      ErsatzTV/Pages/Settings.razor

7
ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshInterval.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>;
}

47
ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshIntervalHandler.cs

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands
{
public class
UpdateLibraryRefreshIntervalHandler : MediatR.IRequestHandler<UpdateLibraryRefreshInterval,
Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<Either<BaseError, Unit>> Handle(
UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval.ToString()))
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval)
.Filter(lri => lri > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask();
private Task<Unit> Upsert(ConfigElementKey key, string value) =>
_configElementRepository.Get(key).Match(
ce =>
{
ce.Value = value;
return _configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement { Key = key.Key, Value = value };
return _configElementRepository.Add(ce);
}).ToUnit();
}
}

6
ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshInterval.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetLibraryRefreshInterval : IRequest<int>;
}

21
ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshIntervalHandler.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefreshInterval, int>
{
private readonly IConfigElementRepository _configElementRepository;
public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.Map(result => result.IfNone(6));
}
}

45
ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs

@ -64,11 +64,13 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -64,11 +64,13 @@ namespace ErsatzTV.Application.MediaSources.Commands
private async Task<Unit> PerformScan(RequestParameters parameters)
{
(LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters;
(LocalLibrary localLibrary, string ffprobePath, bool forceScan, int libraryRefreshInterval) = parameters;
var sw = new Stopwatch();
sw.Start();
var scanned = false;
for (var i = 0; i < localLibrary.Paths.Count; i++)
{
LibraryPath libraryPath = localLibrary.Paths[i];
@ -77,8 +79,11 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -77,8 +79,11 @@ namespace ErsatzTV.Application.MediaSources.Commands
decimal progressMax = (decimal) (i + 1) / localLibrary.Paths.Count;
var lastScan = new DateTimeOffset(libraryPath.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
if (forceScan || nextScan < DateTimeOffset.Now)
{
scanned = true;
switch (localLibrary.MediaKind)
{
case LibraryMediaKind.Movies:
@ -115,10 +120,20 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -115,10 +120,20 @@ namespace ErsatzTV.Application.MediaSources.Commands
}
sw.Stop();
_logger.LogDebug(
"Scan of library {Name} completed in {Duration}",
localLibrary.Name,
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
if (scanned)
{
_logger.LogDebug(
"Scan of library {Name} completed in {Duration}",
localLibrary.Name,
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
}
else
{
_logger.LogDebug(
"Skipping unforced scan of local media library {Name}",
localLibrary.Name);
}
await _mediator.Publish(new LibraryScanProgress(localLibrary.Id, 0));
@ -127,12 +142,13 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -127,12 +142,13 @@ namespace ErsatzTV.Application.MediaSources.Commands
}
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalLibrary request) =>
(await LocalLibraryMustExist(request), await ValidateFFprobePath())
(await LocalLibraryMustExist(request), await ValidateFFprobePath(), await ValidateLibraryRefreshInterval())
.Apply(
(library, ffprobePath) => new RequestParameters(
(library, ffprobePath, libraryRefreshInterval) => new RequestParameters(
library,
ffprobePath,
request.ForceScan));
request.ForceScan,
libraryRefreshInterval));
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
IScanLocalLibrary request) =>
@ -147,6 +163,15 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -147,6 +163,15 @@ namespace ErsatzTV.Application.MediaSources.Commands
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan);
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private record RequestParameters(
LocalLibrary LocalLibrary,
string FFprobePath,
bool ForceScan,
int LibraryRefreshInterval);
}
}

22
ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs

@ -19,6 +19,7 @@ namespace ErsatzTV.Application.Plex.Commands @@ -19,6 +19,7 @@ namespace ErsatzTV.Application.Plex.Commands
SynchronizePlexLibraryByIdHandler : IRequestHandler<ForceSynchronizePlexLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizePlexLibraryByIdHandler> _logger;
@ -29,6 +30,7 @@ namespace ErsatzTV.Application.Plex.Commands @@ -29,6 +30,7 @@ namespace ErsatzTV.Application.Plex.Commands
public SynchronizePlexLibraryByIdHandler(
IMediaSourceRepository mediaSourceRepository,
IConfigElementRepository configElementRepository,
IPlexSecretStore plexSecretStore,
IPlexMovieLibraryScanner plexMovieLibraryScanner,
IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner,
@ -37,6 +39,7 @@ namespace ErsatzTV.Application.Plex.Commands @@ -37,6 +39,7 @@ namespace ErsatzTV.Application.Plex.Commands
ILogger<SynchronizePlexLibraryByIdHandler> logger)
{
_mediaSourceRepository = mediaSourceRepository;
_configElementRepository = configElementRepository;
_plexSecretStore = plexSecretStore;
_plexMovieLibraryScanner = plexMovieLibraryScanner;
_plexTelevisionLibraryScanner = plexTelevisionLibraryScanner;
@ -62,7 +65,8 @@ namespace ErsatzTV.Application.Plex.Commands @@ -62,7 +65,8 @@ namespace ErsatzTV.Application.Plex.Commands
private async Task<Unit> Synchronize(RequestParameters parameters)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{
switch (parameters.Library.MediaKind)
{
@ -95,12 +99,14 @@ namespace ErsatzTV.Application.Plex.Commands @@ -95,12 +99,14 @@ namespace ErsatzTV.Application.Plex.Commands
}
private async Task<Validation<BaseError, RequestParameters>> Validate(ISynchronizePlexLibraryById request) =>
(await ValidateConnection(request), await PlexLibraryMustExist(request))
(await ValidateConnection(request), await PlexLibraryMustExist(request),
await ValidateLibraryRefreshInterval())
.Apply(
(connectionParameters, plexLibrary) => new RequestParameters(
(connectionParameters, plexLibrary, libraryRefreshInterval) => new RequestParameters(
connectionParameters,
plexLibrary,
request.ForceScan
request.ForceScan,
libraryRefreshInterval
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -139,10 +145,16 @@ namespace ErsatzTV.Application.Plex.Commands @@ -139,10 +145,16 @@ namespace ErsatzTV.Application.Plex.Commands
_mediaSourceRepository.GetPlexLibrary(request.PlexLibraryId)
.Map(v => v.ToValidation<BaseError>($"Plex library {request.PlexLibraryId} does not exist."));
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private record RequestParameters(
ConnectionParameters ConnectionParameters,
PlexLibrary Library,
bool ForceScan);
bool ForceScan,
int LibraryRefreshInterval);
private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection)
{

1
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -15,5 +15,6 @@ @@ -15,5 +15,6 @@
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
public static ConfigElementKey CollectionsPageSize => new("pages.collections.page_size");
public static ConfigElementKey LibraryRefreshInterval => new("scanner.library_refresh_interval");
}
}

2269
ErsatzTV.Infrastructure/Migrations/20210514110050_Add_LibraryRefreshInterval.Designer.cs generated

File diff suppressed because it is too large Load Diff

15
ErsatzTV.Infrastructure/Migrations/20210514110050_Add_LibraryRefreshInterval.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_LibraryRefreshInterval : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.Sql(
"INSERT INTO ConfigElement (Key, Value) VALUES ('scanner.library_refresh_interval', '6')");
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

74
ErsatzTV/Pages/Settings.razor

@ -6,10 +6,12 @@ @@ -6,10 +6,12 @@
@using ErsatzTV.Application.HDHR.Queries
@using ErsatzTV.Application.MediaItems.Queries
@using System.Globalization
@using ErsatzTV.Application.Configuration.Queries
@using Unit = LanguageExt.Unit
@inject IMediator Mediator
@inject ISnackbar Snackbar
@inject ILogger<Settings> Logger
@using ErsatzTV.Application.Configuration.Commands
@inject IMediator _mediator
@inject ISnackbar _snackbar
@inject ILogger<Settings> _logger
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="display: flex; flex-direction: row">
<MudCard Class="mr-6" Style="max-width: 400px">
@ -50,7 +52,7 @@ @@ -50,7 +52,7 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_success)" OnClick="@(_ => SaveFFmpegSettings())">Save Settings</MudButton>
</MudCardActions>
</MudCard>
<MudCard Style="width: 350px">
<MudCard Class="mr-6" Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">HDHomeRun Settings</MudText>
@ -65,56 +67,96 @@ @@ -65,56 +67,96 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_hdhrSuccess)" OnClick="@(_ => SaveHDHRSettings())">Save Settings</MudButton>
</MudCardActions>
</MudCard>
<MudCard Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Scanner Settings</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm @bind-IsValid="@_scannerSuccess">
<MudTextField T="int"
Label="Library Refresh Interval"
@bind-Value="_libraryRefreshInterval"
Validation="@(new Func<int, string>(ValidateLibraryRefreshInterval))"
Required="true"
RequiredError="Library refresh interval is required!"
Adornment="Adornment.End"
AdornmentText="Hours"/>
</MudForm>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!_scannerSuccess)" OnClick="@(_ => SaveScannerSettings())">Save Settings</MudButton>
</MudCardActions>
</MudCard>
</MudContainer>
@code {
private bool _success;
private bool _hdhrSuccess;
private bool _scannerSuccess;
private List<FFmpegProfileViewModel> _ffmpegProfiles;
private FFmpegSettingsViewModel _ffmpegSettings;
private List<CultureInfo> _availableCultures;
private int _tunerCount;
private int _libraryRefreshInterval;
protected override async Task OnParametersSetAsync()
{
await LoadFFmpegProfilesAsync();
_ffmpegSettings = await Mediator.Send(new GetFFmpegSettings());
_ffmpegSettings = await _mediator.Send(new GetFFmpegSettings());
_success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath);
_availableCultures = await Mediator.Send(new GetAllLanguageCodes());
_tunerCount = await Mediator.Send(new GetHDHRTunerCount());
_availableCultures = await _mediator.Send(new GetAllLanguageCodes());
_tunerCount = await _mediator.Send(new GetHDHRTunerCount());
_hdhrSuccess = string.IsNullOrWhiteSpace(ValidateTunerCount(_tunerCount));
_libraryRefreshInterval = await _mediator.Send(new GetLibraryRefreshInterval());
_scannerSuccess = _libraryRefreshInterval > 0;
}
private static string ValidatePathExists(string path) => !File.Exists(path) ? "Path does not exist" : null;
private static string ValidateTunerCount(int tunerCount) => tunerCount <= 0 ? "Tuner count must be greater than zero" : null;
private static string ValidateLibraryRefreshInterval(int libraryRefreshInterval) => libraryRefreshInterval <= 0 ? "Library refresh interval must be greater than zero" : null;
private async Task LoadFFmpegProfilesAsync() =>
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles());
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles());
private async Task SaveFFmpegSettings()
{
Either<BaseError, Unit> result = await Mediator.Send(new UpdateFFmpegSettings(_ffmpegSettings));
Either<BaseError, Unit> result = await _mediator.Send(new UpdateFFmpegSettings(_ffmpegSettings));
result.Match(
Left: error =>
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error saving FFmpeg settings: {Error}", error.Value);
_snackbar.Add(error.Value, Severity.Error);
_logger.LogError("Unexpected error saving FFmpeg settings: {Error}", error.Value);
},
Right: _ => Snackbar.Add("Successfully saved FFmpeg settings", Severity.Success));
Right: _ => _snackbar.Add("Successfully saved FFmpeg settings", Severity.Success));
}
private async Task SaveHDHRSettings()
{
Either<BaseError, Unit> result = await Mediator.Send(new UpdateHDHRTunerCount(_tunerCount));
Either<BaseError, Unit> result = await _mediator.Send(new UpdateHDHRTunerCount(_tunerCount));
result.Match(
Left: error =>
{
_snackbar.Add(error.Value, Severity.Error);
_logger.LogError("Unexpected error saving HDHomeRun settings: {Error}", error.Value);
},
Right: _ => _snackbar.Add("Successfully saved HDHomeRun settings", Severity.Success));
}
private async Task SaveScannerSettings()
{
Either<BaseError, Unit> result = await _mediator.Send(new UpdateLibraryRefreshInterval(_libraryRefreshInterval));
result.Match(
Left: error =>
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error saving HDHomeRun settings: {Error}", error.Value);
_snackbar.Add(error.Value, Severity.Error);
_logger.LogError("Unexpected error saving scanner settings: {Error}", error.Value);
},
Right: _ => Snackbar.Add("Successfully saved HDHomeRun settings", Severity.Success));
Right: _ => _snackbar.Add("Successfully saved scanner settings", Severity.Success));
}
}
Loading…
Cancel
Save