scanner improvements (#1122)
* optimize periodic scanning * set scanner process priority * update dependencies
This commit is contained in:
@@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
### Changed
|
### Changed
|
||||||
- Always use software pipeline for error display
|
- Always use software pipeline for error display
|
||||||
- This ensures errors will display even when hardware acceleration is misconfigured
|
- This ensures errors will display even when hardware acceleration is misconfigured
|
||||||
|
- Call scanner process only when scanning is required based on library refresh interval
|
||||||
|
- Use idle priority for scanner process with unforced (automatic) library scans
|
||||||
|
|
||||||
## [0.7.2-beta] - 2023-01-05
|
## [0.7.2-beta] - 2023-01-05
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Application.Libraries;
|
using ErsatzTV.Application.Libraries;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.FFmpeg.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Emby;
|
namespace ErsatzTV.Application.Emby;
|
||||||
|
|
||||||
public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler,
|
public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchronizeEmbyLibraryById>,
|
||||||
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
|
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
|
||||||
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
|
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
|
||||||
{
|
{
|
||||||
public CallEmbyLibraryScannerHandler(
|
public CallEmbyLibraryScannerHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IRuntimeInfo runtimeInfo)
|
IRuntimeInfo runtimeInfo)
|
||||||
: base(channel, mediator, runtimeInfo)
|
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,10 +36,18 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler,
|
|||||||
ISynchronizeEmbyLibraryById request,
|
ISynchronizeEmbyLibraryById request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Validation<BaseError, string> validation = Validate();
|
Validation<BaseError, string> validation = await Validate(request);
|
||||||
return await validation.Match(
|
return await validation.Match(
|
||||||
scanner => PerformScan(scanner, request, cancellationToken),
|
scanner => PerformScan(scanner, request, cancellationToken),
|
||||||
error => Task.FromResult<Either<BaseError, string>>(error.Join()));
|
error =>
|
||||||
|
{
|
||||||
|
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
|
||||||
|
{
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(scanIsNotRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(error.Join());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Either<BaseError, string>> PerformScan(
|
private async Task<Either<BaseError, string>> PerformScan(
|
||||||
@@ -52,4 +67,22 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler,
|
|||||||
|
|
||||||
return await base.PerformScan(scanner, arguments, cancellationToken);
|
return await base.PerformScan(scanner, arguments, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task<DateTimeOffset> GetLastScan(
|
||||||
|
TvContext dbContext,
|
||||||
|
ISynchronizeEmbyLibraryById request)
|
||||||
|
{
|
||||||
|
return await dbContext.EmbyLibraries
|
||||||
|
.SelectOneAsync(l => l.Id, l => l.Id == request.EmbyLibraryId)
|
||||||
|
.Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ScanIsRequired(
|
||||||
|
DateTimeOffset lastScan,
|
||||||
|
int libraryRefreshInterval,
|
||||||
|
ISynchronizeEmbyLibraryById request)
|
||||||
|
{
|
||||||
|
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
|
||||||
|
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Application.Libraries;
|
using ErsatzTV.Application.Libraries;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.FFmpeg.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Jellyfin;
|
namespace ErsatzTV.Application.Jellyfin;
|
||||||
|
|
||||||
public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler,
|
public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISynchronizeJellyfinLibraryById>,
|
||||||
IRequestHandler<ForceSynchronizeJellyfinLibraryById, Either<BaseError, string>>,
|
IRequestHandler<ForceSynchronizeJellyfinLibraryById, Either<BaseError, string>>,
|
||||||
IRequestHandler<SynchronizeJellyfinLibraryByIdIfNeeded, Either<BaseError, string>>
|
IRequestHandler<SynchronizeJellyfinLibraryByIdIfNeeded, Either<BaseError, string>>
|
||||||
{
|
{
|
||||||
public CallJellyfinLibraryScannerHandler(
|
public CallJellyfinLibraryScannerHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IRuntimeInfo runtimeInfo)
|
IRuntimeInfo runtimeInfo)
|
||||||
: base(channel, mediator, runtimeInfo)
|
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,10 +36,18 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler,
|
|||||||
ISynchronizeJellyfinLibraryById request,
|
ISynchronizeJellyfinLibraryById request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Validation<BaseError, string> validation = Validate();
|
Validation<BaseError, string> validation = await Validate(request);
|
||||||
return await validation.Match(
|
return await validation.Match(
|
||||||
scanner => PerformScan(scanner, request, cancellationToken),
|
scanner => PerformScan(scanner, request, cancellationToken),
|
||||||
error => Task.FromResult<Either<BaseError, string>>(error.Join()));
|
error =>
|
||||||
|
{
|
||||||
|
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
|
||||||
|
{
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(scanIsNotRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(error.Join());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Either<BaseError, string>> PerformScan(
|
private async Task<Either<BaseError, string>> PerformScan(
|
||||||
@@ -52,4 +67,22 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler,
|
|||||||
|
|
||||||
return await base.PerformScan(scanner, arguments, cancellationToken);
|
return await base.PerformScan(scanner, arguments, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task<DateTimeOffset> GetLastScan(
|
||||||
|
TvContext dbContext,
|
||||||
|
ISynchronizeJellyfinLibraryById request)
|
||||||
|
{
|
||||||
|
return await dbContext.JellyfinLibraries
|
||||||
|
.SelectOneAsync(l => l.Id, l => l.Id == request.JellyfinLibraryId)
|
||||||
|
.Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ScanIsRequired(
|
||||||
|
DateTimeOffset lastScan,
|
||||||
|
int libraryRefreshInterval,
|
||||||
|
ISynchronizeJellyfinLibraryById request)
|
||||||
|
{
|
||||||
|
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
|
||||||
|
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ using System.Threading.Channels;
|
|||||||
using CliWrap;
|
using CliWrap;
|
||||||
using ErsatzTV.Application.Search;
|
using ErsatzTV.Application.Search;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.MediaSources;
|
using ErsatzTV.Core.MediaSources;
|
||||||
using ErsatzTV.Core.Metadata;
|
using ErsatzTV.Core.Metadata;
|
||||||
using ErsatzTV.FFmpeg.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
@@ -13,18 +18,24 @@ using Serilog.Formatting.Compact.Reader;
|
|||||||
|
|
||||||
namespace ErsatzTV.Application.Libraries;
|
namespace ErsatzTV.Application.Libraries;
|
||||||
|
|
||||||
public abstract class CallLibraryScannerHandler
|
public abstract class CallLibraryScannerHandler<TRequest>
|
||||||
{
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _channel;
|
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _channel;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly IRuntimeInfo _runtimeInfo;
|
private readonly IRuntimeInfo _runtimeInfo;
|
||||||
private string _libraryName;
|
private string _libraryName;
|
||||||
|
|
||||||
protected CallLibraryScannerHandler(
|
protected CallLibraryScannerHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IRuntimeInfo runtimeInfo)
|
IRuntimeInfo runtimeInfo)
|
||||||
{
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_runtimeInfo = runtimeInfo;
|
_runtimeInfo = runtimeInfo;
|
||||||
@@ -131,8 +142,25 @@ public abstract class CallLibraryScannerHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Validation<BaseError, string> Validate()
|
protected abstract Task<DateTimeOffset> GetLastScan(TvContext dbContext, TRequest request);
|
||||||
|
protected abstract bool ScanIsRequired(DateTimeOffset lastScan, int libraryRefreshInterval, TRequest request);
|
||||||
|
|
||||||
|
protected async Task<Validation<BaseError, string>> Validate(TRequest request)
|
||||||
{
|
{
|
||||||
|
int libraryRefreshInterval = await _configElementRepository
|
||||||
|
.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
|
||||||
|
.IfNoneAsync(0);
|
||||||
|
|
||||||
|
libraryRefreshInterval = Math.Clamp(libraryRefreshInterval, 0, 999_999);
|
||||||
|
|
||||||
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
DateTimeOffset lastScan = await GetLastScan(dbContext, request);
|
||||||
|
if (!ScanIsRequired(lastScan, libraryRefreshInterval, request))
|
||||||
|
{
|
||||||
|
return new ScanIsNotRequired();
|
||||||
|
}
|
||||||
|
|
||||||
string executable = _runtimeInfo.IsOSPlatform(OSPlatform.Windows)
|
string executable = _runtimeInfo.IsOSPlatform(OSPlatform.Windows)
|
||||||
? "ErsatzTV.Scanner.exe"
|
? "ErsatzTV.Scanner.exe"
|
||||||
: "ErsatzTV.Scanner";
|
: "ErsatzTV.Scanner";
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Application.Libraries;
|
using ErsatzTV.Application.Libraries;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.FFmpeg.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaSources;
|
namespace ErsatzTV.Application.MediaSources;
|
||||||
|
|
||||||
public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler,
|
public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLocalLibrary>,
|
||||||
IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>,
|
IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>,
|
||||||
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>
|
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>
|
||||||
{
|
{
|
||||||
public CallLocalLibraryScannerHandler(
|
public CallLocalLibraryScannerHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IRuntimeInfo runtimeInfo)
|
IRuntimeInfo runtimeInfo)
|
||||||
: base(channel, mediator, runtimeInfo)
|
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,10 +33,18 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler,
|
|||||||
|
|
||||||
private async Task<Either<BaseError, string>> Handle(IScanLocalLibrary request, CancellationToken cancellationToken)
|
private async Task<Either<BaseError, string>> Handle(IScanLocalLibrary request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Validation<BaseError, string> validation = Validate();
|
Validation<BaseError, string> validation = await Validate(request);
|
||||||
return await validation.Match(
|
return await validation.Match(
|
||||||
scanner => PerformScan(scanner, request, cancellationToken),
|
scanner => PerformScan(scanner, request, cancellationToken),
|
||||||
error => Task.FromResult<Either<BaseError, string>>(error.Join()));
|
error =>
|
||||||
|
{
|
||||||
|
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
|
||||||
|
{
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(scanIsNotRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(error.Join());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Either<BaseError, string>> PerformScan(
|
private async Task<Either<BaseError, string>> PerformScan(
|
||||||
@@ -50,4 +64,21 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler,
|
|||||||
|
|
||||||
return await base.PerformScan(scanner, arguments, cancellationToken);
|
return await base.PerformScan(scanner, arguments, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task<DateTimeOffset> GetLastScan(TvContext dbContext, IScanLocalLibrary request)
|
||||||
|
{
|
||||||
|
return await dbContext.LibraryPaths
|
||||||
|
.Filter(lp => lp.LibraryId == request.LibraryId)
|
||||||
|
.ToListAsync()
|
||||||
|
.Map(list => list.Min(lp => lp.LastScan ?? SystemTime.MinValueUtc));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ScanIsRequired(
|
||||||
|
DateTimeOffset lastScan,
|
||||||
|
int libraryRefreshInterval,
|
||||||
|
IScanLocalLibrary request)
|
||||||
|
{
|
||||||
|
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
|
||||||
|
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Application.Libraries;
|
using ErsatzTV.Application.Libraries;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.FFmpeg.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Plex;
|
namespace ErsatzTV.Application.Plex;
|
||||||
|
|
||||||
public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler,
|
public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchronizePlexLibraryById>,
|
||||||
IRequestHandler<ForceSynchronizePlexLibraryById, Either<BaseError, string>>,
|
IRequestHandler<ForceSynchronizePlexLibraryById, Either<BaseError, string>>,
|
||||||
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>
|
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>
|
||||||
{
|
{
|
||||||
public CallPlexLibraryScannerHandler(
|
public CallPlexLibraryScannerHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IRuntimeInfo runtimeInfo)
|
IRuntimeInfo runtimeInfo)
|
||||||
: base(channel, mediator, runtimeInfo)
|
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,10 +36,18 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler,
|
|||||||
ISynchronizePlexLibraryById request,
|
ISynchronizePlexLibraryById request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Validation<BaseError, string> validation = Validate();
|
Validation<BaseError, string> validation = await Validate(request);
|
||||||
return await validation.Match(
|
return await validation.Match(
|
||||||
scanner => PerformScan(scanner, request, cancellationToken),
|
scanner => PerformScan(scanner, request, cancellationToken),
|
||||||
error => Task.FromResult<Either<BaseError, string>>(error.Join()));
|
error =>
|
||||||
|
{
|
||||||
|
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
|
||||||
|
{
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(scanIsNotRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(error.Join());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Either<BaseError, string>> PerformScan(
|
private async Task<Either<BaseError, string>> PerformScan(
|
||||||
@@ -57,4 +72,22 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler,
|
|||||||
|
|
||||||
return await base.PerformScan(scanner, arguments, cancellationToken);
|
return await base.PerformScan(scanner, arguments, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task<DateTimeOffset> GetLastScan(
|
||||||
|
TvContext dbContext,
|
||||||
|
ISynchronizePlexLibraryById request)
|
||||||
|
{
|
||||||
|
return await dbContext.PlexLibraries
|
||||||
|
.SelectOneAsync(l => l.Id, l => l.Id == request.PlexLibraryId)
|
||||||
|
.Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ScanIsRequired(
|
||||||
|
DateTimeOffset lastScan,
|
||||||
|
int libraryRefreshInterval,
|
||||||
|
ISynchronizePlexLibraryById request)
|
||||||
|
{
|
||||||
|
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
|
||||||
|
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ErsatzTV.Core.Errors;
|
||||||
|
|
||||||
|
public class ScanIsNotRequired : BaseError
|
||||||
|
{
|
||||||
|
public ScanIsNotRequired() : base("Scan is not required")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<PackageReference Include="Blurhash.ImageSharp" Version="3.0.0" />
|
<PackageReference Include="Blurhash.ImageSharp" Version="3.0.0" />
|
||||||
<PackageReference Include="CliWrap" Version="3.6.0" />
|
<PackageReference Include="CliWrap" Version="3.6.0" />
|
||||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||||
<PackageReference Include="Jint" Version="3.0.0-beta-2045" />
|
<PackageReference Include="Jint" Version="3.0.0-beta-2046" />
|
||||||
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" />
|
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" />
|
||||||
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" />
|
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" />
|
||||||
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" />
|
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using System.Diagnostics;
|
||||||
using ErsatzTV.Scanner.Application.Emby;
|
using ErsatzTV.Scanner.Application.Emby;
|
||||||
using ErsatzTV.Scanner.Application.Jellyfin;
|
using ErsatzTV.Scanner.Application.Jellyfin;
|
||||||
using ErsatzTV.Scanner.Application.MediaSources;
|
using ErsatzTV.Scanner.Application.MediaSources;
|
||||||
@@ -82,6 +83,8 @@ public class Worker : BackgroundService
|
|||||||
if (IsScanningEnabled())
|
if (IsScanningEnabled())
|
||||||
{
|
{
|
||||||
bool force = context.ParseResult.GetValueForOption(forceOption);
|
bool force = context.ParseResult.GetValueForOption(forceOption);
|
||||||
|
SetProcessPriority(force);
|
||||||
|
|
||||||
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
|
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
|
||||||
|
|
||||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||||
@@ -98,6 +101,8 @@ public class Worker : BackgroundService
|
|||||||
if (IsScanningEnabled())
|
if (IsScanningEnabled())
|
||||||
{
|
{
|
||||||
bool force = context.ParseResult.GetValueForOption(forceOption);
|
bool force = context.ParseResult.GetValueForOption(forceOption);
|
||||||
|
SetProcessPriority(force);
|
||||||
|
|
||||||
bool deep = context.ParseResult.GetValueForOption(deepOption);
|
bool deep = context.ParseResult.GetValueForOption(deepOption);
|
||||||
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
|
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
|
||||||
|
|
||||||
@@ -115,6 +120,8 @@ public class Worker : BackgroundService
|
|||||||
if (IsScanningEnabled())
|
if (IsScanningEnabled())
|
||||||
{
|
{
|
||||||
bool force = context.ParseResult.GetValueForOption(forceOption);
|
bool force = context.ParseResult.GetValueForOption(forceOption);
|
||||||
|
SetProcessPriority(force);
|
||||||
|
|
||||||
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
|
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
|
||||||
|
|
||||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||||
@@ -131,6 +138,8 @@ public class Worker : BackgroundService
|
|||||||
if (IsScanningEnabled())
|
if (IsScanningEnabled())
|
||||||
{
|
{
|
||||||
bool force = context.ParseResult.GetValueForOption(forceOption);
|
bool force = context.ParseResult.GetValueForOption(forceOption);
|
||||||
|
SetProcessPriority(force);
|
||||||
|
|
||||||
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
|
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
|
||||||
|
|
||||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||||
@@ -150,18 +159,33 @@ public class Worker : BackgroundService
|
|||||||
return rootCommand;
|
return rootCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !DEBUG_NO_SYNC
|
|
||||||
private bool IsScanningEnabled()
|
private bool IsScanningEnabled()
|
||||||
{
|
{
|
||||||
|
#if !DEBUG_NO_SYNC
|
||||||
// don't want to flag the logger as unused (only used when sync is disabled)
|
// don't want to flag the logger as unused (only used when sync is disabled)
|
||||||
ILogger<Worker> _ = _logger;
|
ILogger<Worker> _ = _logger;
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
private bool IsScanningEnabled()
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Scanning is disabled via DEBUG_NO_SYNC");
|
_logger.LogInformation("Scanning is disabled via DEBUG_NO_SYNC");
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetProcessPriority(bool force)
|
||||||
|
{
|
||||||
|
if (force)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = Process.GetCurrentProcess();
|
||||||
|
process.PriorityClass = ProcessPriorityClass.Idle;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to set scanner priority");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -33,8 +33,8 @@ Global
|
|||||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.ActiveCfg = Debug No Sync|Any CPU
|
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.ActiveCfg = Debug No Sync|Any CPU
|
||||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.Build.0 = Debug No Sync|Any CPU
|
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.Build.0 = Debug No Sync|Any CPU
|
||||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.ActiveCfg = Debug No Sync|Any CPU
|
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug No Sync|Any CPU
|
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@@ -81,8 +81,8 @@ Global
|
|||||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.ActiveCfg = Debug|Any CPU
|
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU
|
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.ActiveCfg = Debug No Sync|Any CPU
|
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.Build.0 = Debug No Sync|Any CPU
|
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" />
|
<PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" />
|
||||||
<PackageReference Include="FluentValidation" Version="11.4.0" />
|
<PackageReference Include="FluentValidation" Version="11.4.0" />
|
||||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2" />
|
<PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="8.0.601" />
|
<PackageReference Include="HtmlSanitizer" Version="8.0.645" />
|
||||||
<PackageReference Include="LanguageExt.Core" Version="4.4.0" />
|
<PackageReference Include="LanguageExt.Core" Version="4.4.0" />
|
||||||
<PackageReference Include="Markdig" Version="0.30.4" />
|
<PackageReference Include="Markdig" Version="0.30.4" />
|
||||||
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
|
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using ErsatzTV.Application;
|
|||||||
using ErsatzTV.Application.Emby;
|
using ErsatzTV.Application.Emby;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -135,10 +136,22 @@ public class EmbyService : BackgroundService
|
|||||||
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
||||||
result.BiIter(
|
result.BiIter(
|
||||||
name => _logger.LogDebug("Done synchronizing emby library {Name}", name),
|
name => _logger.LogDebug("Done synchronizing emby library {Name}", name),
|
||||||
error => _logger.LogWarning(
|
error =>
|
||||||
"Unable to synchronize emby library {LibraryId}: {Error}",
|
{
|
||||||
request.EmbyLibraryId,
|
if (error is ScanIsNotRequired)
|
||||||
error.Value));
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Scan is not required for emby library {LibraryId} at this time",
|
||||||
|
request.EmbyLibraryId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Unable to synchronize emby library {LibraryId}: {Error}",
|
||||||
|
request.EmbyLibraryId,
|
||||||
|
error.Value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (entityLocker.IsLibraryLocked(request.EmbyLibraryId))
|
if (entityLocker.IsLibraryLocked(request.EmbyLibraryId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using ErsatzTV.Application;
|
|||||||
using ErsatzTV.Application.Jellyfin;
|
using ErsatzTV.Application.Jellyfin;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -160,11 +161,23 @@ public class JellyfinService : BackgroundService
|
|||||||
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
||||||
result.BiIter(
|
result.BiIter(
|
||||||
name => _logger.LogDebug("Done synchronizing jellyfin library {Name}", name),
|
name => _logger.LogDebug("Done synchronizing jellyfin library {Name}", name),
|
||||||
error => _logger.LogWarning(
|
error =>
|
||||||
"Unable to synchronize jellyfin library {LibraryId}: {Error}",
|
{
|
||||||
request.JellyfinLibraryId,
|
if (error is ScanIsNotRequired)
|
||||||
error.Value));
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Scan is not required for jellyfin library {LibraryId} at this time",
|
||||||
|
request.JellyfinLibraryId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Unable to synchronize jellyfin library {LibraryId}: {Error}",
|
||||||
|
request.JellyfinLibraryId,
|
||||||
|
error.Value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (entityLocker.IsLibraryLocked(request.JellyfinLibraryId))
|
if (entityLocker.IsLibraryLocked(request.JellyfinLibraryId))
|
||||||
{
|
{
|
||||||
entityLocker.UnlockLibrary(request.JellyfinLibraryId);
|
entityLocker.UnlockLibrary(request.JellyfinLibraryId);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using ErsatzTV.Application;
|
|||||||
using ErsatzTV.Application.Plex;
|
using ErsatzTV.Application.Plex;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -159,10 +160,22 @@ public class PlexService : BackgroundService
|
|||||||
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
|
||||||
result.BiIter(
|
result.BiIter(
|
||||||
name => _logger.LogDebug("Done synchronizing plex library {Name}", name),
|
name => _logger.LogDebug("Done synchronizing plex library {Name}", name),
|
||||||
error => _logger.LogWarning(
|
error =>
|
||||||
"Unable to synchronize plex library {LibraryId}: {Error}",
|
{
|
||||||
request.PlexLibraryId,
|
if (error is ScanIsNotRequired)
|
||||||
error.Value));
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Scan is not required for plex library {LibraryId} at this time",
|
||||||
|
request.PlexLibraryId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Unable to synchronize plex library {LibraryId}: {Error}",
|
||||||
|
request.PlexLibraryId,
|
||||||
|
error.Value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (entityLocker.IsLibraryLocked(request.PlexLibraryId))
|
if (entityLocker.IsLibraryLocked(request.PlexLibraryId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using ErsatzTV.Application.Playouts;
|
|||||||
using ErsatzTV.Application.Search;
|
using ErsatzTV.Application.Search;
|
||||||
using ErsatzTV.Application.Subtitles;
|
using ErsatzTV.Application.Subtitles;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -71,10 +72,22 @@ public class WorkerService : BackgroundService
|
|||||||
name => _logger.LogDebug(
|
name => _logger.LogDebug(
|
||||||
"Done scanning local library {Library}",
|
"Done scanning local library {Library}",
|
||||||
name),
|
name),
|
||||||
error => _logger.LogWarning(
|
error =>
|
||||||
"Unable to scan local library {LibraryId}: {Error}",
|
{
|
||||||
scanLocalLibrary.LibraryId,
|
if (error is ScanIsNotRequired)
|
||||||
error.Value));
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Scan is not required for local library {LibraryId} at this time",
|
||||||
|
scanLocalLibrary.LibraryId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Unable to scan local library {LibraryId}: {Error}",
|
||||||
|
scanLocalLibrary.LibraryId,
|
||||||
|
error.Value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (entityLocker.IsLibraryLocked(scanLocalLibrary.LibraryId))
|
if (entityLocker.IsLibraryLocked(scanLocalLibrary.LibraryId))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user