diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0d00cc9..839815764 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Greatly reduce debug log spam during playout builds by logging summaries of certain warnings at the end - Remove *experimental* `HLS Segmenter V2` streaming mode; it is not possible to maintain quality output using this mode - Remove *experimental* `HLS Segmenter (fmp4)` streaming mode; this mode only worked properly in a browser, many clients did not like it +- Change how scanner process and main process communicate, which should improve reliability of search index updates when scanning ## [25.7.1] - 2025-10-09 ### Added diff --git a/ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs b/ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs index 3b8bcb765..a5a544086 100644 --- a/ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; using ErsatzTV.Core.Errors; @@ -17,18 +16,16 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, - IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, runtimeInfo) { } public async Task> Handle(SynchronizeEmbyCollections request, CancellationToken cancellationToken) { - Validation validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -40,7 +37,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler GetLastScan( + protected override async Task> GetLastScan( TvContext dbContext, SynchronizeEmbyCollections request, CancellationToken cancellationToken) @@ -49,7 +46,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler l.Id, l => l.Id == request.EmbyMediaSourceId, cancellationToken) .Match(l => l.LastCollectionsScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc); - return new DateTimeOffset(minDateTime, TimeSpan.Zero); + return new Tuple(string.Empty, new DateTimeOffset(minDateTime, TimeSpan.Zero)); } protected override bool ScanIsRequired( @@ -67,7 +64,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, SynchronizeEmbyCollections request, CancellationToken cancellationToken) { @@ -81,6 +78,6 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler Unit.Default); + return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default); } } diff --git a/ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs b/ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs index 4538f1b62..d2c01dbf6 100644 --- a/ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs @@ -1,8 +1,9 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; @@ -15,14 +16,16 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler>, IRequestHandler> { + private readonly IScannerProxyService _scannerProxyService; + public CallEmbyLibraryScannerHandler( IDbContextFactory dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, + IScannerProxyService scannerProxyService, IRuntimeInfo runtimeInfo) - : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + : base(dbContextFactory, configElementRepository, runtimeInfo) { + _scannerProxyService = scannerProxyService; } Task> IRequestHandler>.Handle( @@ -38,9 +41,9 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -53,38 +56,58 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, ISynchronizeEmbyLibraryById request, CancellationToken cancellationToken) { - var arguments = new List + Option maybeScanId = _scannerProxyService.StartScan(request.EmbyLibraryId); + foreach (var scanId in maybeScanId) { - "scan-emby", request.EmbyLibraryId.ToString(CultureInfo.InvariantCulture) - }; + try + { + var arguments = new List + { + "scan-emby", + request.EmbyLibraryId.ToString(CultureInfo.InvariantCulture), + GetBaseUrl(scanId) + }; - if (request.ForceScan) - { - arguments.Add("--force"); - } + if (request.ForceScan) + { + arguments.Add("--force"); + } - if (request.DeepScan) - { - arguments.Add("--deep"); + if (request.DeepScan) + { + arguments.Add("--deep"); + } + + return await base.PerformScan(parameters, arguments, cancellationToken); + } + finally + { + _scannerProxyService.EndScan(scanId); + } } - return await base.PerformScan(scanner, arguments, cancellationToken); + return BaseError.New($"Library {request.EmbyLibraryId} is already scanning"); } - protected override async Task GetLastScan( + protected override async Task> GetLastScan( TvContext dbContext, ISynchronizeEmbyLibraryById request, CancellationToken cancellationToken) { - DateTime minDateTime = await dbContext.EmbyLibraries - .SelectOneAsync(l => l.Id, l => l.Id == request.EmbyLibraryId, cancellationToken) - .Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc); + Option maybeLibrary = await dbContext.EmbyLibraries + .SelectOneAsync(l => l.Id, l => l.Id == request.EmbyLibraryId, cancellationToken); + + DateTime minDateTime = maybeLibrary.Match( + l => l.LastScan ?? SystemTime.MinValueUtc, + () => SystemTime.MaxValueUtc); + + string libraryName = maybeLibrary.Match(l => l.Name, string.Empty); - return new DateTimeOffset(minDateTime, TimeSpan.Zero); + return new Tuple(libraryName, new DateTimeOffset(minDateTime, TimeSpan.Zero)); } protected override bool ScanIsRequired( diff --git a/ErsatzTV.Application/Emby/Commands/CallEmbyShowScannerHandler.cs b/ErsatzTV.Application/Emby/Commands/CallEmbyShowScannerHandler.cs index c36709993..7c385108d 100644 --- a/ErsatzTV.Application/Emby/Commands/CallEmbyShowScannerHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/CallEmbyShowScannerHandler.cs @@ -1,8 +1,8 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; @@ -13,14 +13,16 @@ namespace ErsatzTV.Application.Emby; public class CallEmbyShowScannerHandler : CallLibraryScannerHandler, IRequestHandler> { + private readonly IScannerProxyService _scannerProxyService; + public CallEmbyShowScannerHandler( IDbContextFactory dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, + IScannerProxyService scannerProxyService, IRuntimeInfo runtimeInfo) - : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + : base(dbContextFactory, configElementRepository, runtimeInfo) { + _scannerProxyService = scannerProxyService; } Task> IRequestHandler>.Handle( @@ -31,9 +33,9 @@ public class CallEmbyShowScannerHandler : CallLibraryScannerHandler validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -46,30 +48,44 @@ public class CallEmbyShowScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, SynchronizeEmbyShowById request, CancellationToken cancellationToken) { - var arguments = new List + Option maybeScanId = _scannerProxyService.StartScan(request.EmbyLibraryId); + foreach (var scanId in maybeScanId) { - "scan-emby-show", - request.EmbyLibraryId.ToString(CultureInfo.InvariantCulture), - request.ShowId.ToString(CultureInfo.InvariantCulture) - }; + try + { + var arguments = new List + { + "scan-emby-show", + request.EmbyLibraryId.ToString(CultureInfo.InvariantCulture), + request.ShowId.ToString(CultureInfo.InvariantCulture), + GetBaseUrl(scanId) + }; - if (request.DeepScan) - { - arguments.Add("--deep"); + if (request.DeepScan) + { + arguments.Add("--deep"); + } + + return await base.PerformScan(parameters, arguments, cancellationToken); + } + finally + { + _scannerProxyService.EndScan(scanId); + } } - return await base.PerformScan(scanner, arguments, cancellationToken); + return BaseError.New($"Library {request.EmbyLibraryId} is already scanning"); } - protected override Task GetLastScan( + protected override Task> GetLastScan( TvContext dbContext, SynchronizeEmbyShowById request, CancellationToken cancellationToken) => - Task.FromResult(DateTimeOffset.MinValue); + Task.FromResult(new Tuple(string.Empty, DateTimeOffset.MinValue)); protected override bool ScanIsRequired( DateTimeOffset lastScan, diff --git a/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinCollectionScannerHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinCollectionScannerHandler.cs index ac8c98cb7..2434ee59d 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinCollectionScannerHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinCollectionScannerHandler.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; using ErsatzTV.Core.Errors; @@ -17,18 +16,16 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, - IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, runtimeInfo) { } public async Task> Handle(SynchronizeJellyfinCollections request, CancellationToken cancellationToken) { - Validation validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -40,7 +37,7 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler GetLastScan( + protected override async Task> GetLastScan( TvContext dbContext, SynchronizeJellyfinCollections request, CancellationToken cancellationToken) @@ -49,7 +46,7 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler l.Id, l => l.Id == request.JellyfinMediaSourceId, cancellationToken) .Match(l => l.LastCollectionsScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc); - return new DateTimeOffset(minDateTime, TimeSpan.Zero); + return new Tuple(string.Empty, new DateTimeOffset(minDateTime, TimeSpan.Zero)); } protected override bool ScanIsRequired( @@ -67,7 +64,7 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, SynchronizeJellyfinCollections request, CancellationToken cancellationToken) { @@ -81,6 +78,6 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler Unit.Default); + return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default); } } diff --git a/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs index 8fb68974a..24d640f5b 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs @@ -1,8 +1,9 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; @@ -15,14 +16,16 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler>, IRequestHandler> { + private readonly IScannerProxyService _scannerProxyService; + public CallJellyfinLibraryScannerHandler( IDbContextFactory dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, + IScannerProxyService scannerProxyService, IRuntimeInfo runtimeInfo) - : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + : base(dbContextFactory, configElementRepository, runtimeInfo) { + _scannerProxyService = scannerProxyService; } Task> IRequestHandler>. @@ -39,9 +42,9 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -54,38 +57,58 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, ISynchronizeJellyfinLibraryById request, CancellationToken cancellationToken) { - var arguments = new List + Option maybeScanId = _scannerProxyService.StartScan(request.JellyfinLibraryId); + foreach (var scanId in maybeScanId) { - "scan-jellyfin", request.JellyfinLibraryId.ToString(CultureInfo.InvariantCulture) - }; + try + { + var arguments = new List + { + "scan-jellyfin", + request.JellyfinLibraryId.ToString(CultureInfo.InvariantCulture), + GetBaseUrl(scanId) + }; - if (request.ForceScan) - { - arguments.Add("--force"); - } + if (request.ForceScan) + { + arguments.Add("--force"); + } - if (request.DeepScan) - { - arguments.Add("--deep"); + if (request.DeepScan) + { + arguments.Add("--deep"); + } + + return await base.PerformScan(parameters, arguments, cancellationToken); + } + finally + { + _scannerProxyService.EndScan(scanId); + } } - return await base.PerformScan(scanner, arguments, cancellationToken); + return BaseError.New($"Library {request.JellyfinLibraryId} is already scanning"); } - protected override async Task GetLastScan( + protected override async Task> GetLastScan( TvContext dbContext, ISynchronizeJellyfinLibraryById request, CancellationToken cancellationToken) { - DateTime minDateTime = await dbContext.JellyfinLibraries - .SelectOneAsync(l => l.Id, l => l.Id == request.JellyfinLibraryId, cancellationToken) - .Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc); + Option maybeLibrary = await dbContext.JellyfinLibraries + .SelectOneAsync(l => l.Id, l => l.Id == request.JellyfinLibraryId, cancellationToken); + + DateTime minDateTime = maybeLibrary.Match( + l => l.LastScan ?? SystemTime.MinValueUtc, + () => SystemTime.MaxValueUtc); + + string libraryName = maybeLibrary.Match(l => l.Name, string.Empty); - return new DateTimeOffset(minDateTime, TimeSpan.Zero); + return new Tuple(libraryName, new DateTimeOffset(minDateTime, TimeSpan.Zero)); } protected override bool ScanIsRequired( diff --git a/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinShowScannerHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinShowScannerHandler.cs index 212a1b196..d009cabc9 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinShowScannerHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinShowScannerHandler.cs @@ -1,8 +1,8 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; @@ -13,14 +13,16 @@ namespace ErsatzTV.Application.Jellyfin; public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler, IRequestHandler> { + private readonly IScannerProxyService _scannerProxyService; + public CallJellyfinShowScannerHandler( IDbContextFactory dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, + IScannerProxyService scannerProxyService, IRuntimeInfo runtimeInfo) - : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + : base(dbContextFactory, configElementRepository, runtimeInfo) { + _scannerProxyService = scannerProxyService; } Task> IRequestHandler>.Handle( @@ -31,9 +33,9 @@ public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -46,30 +48,44 @@ public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, SynchronizeJellyfinShowById request, CancellationToken cancellationToken) { - var arguments = new List + Option maybeScanId = _scannerProxyService.StartScan(request.JellyfinLibraryId); + foreach (var scanId in maybeScanId) { - "scan-jellyfin-show", - request.JellyfinLibraryId.ToString(CultureInfo.InvariantCulture), - request.ShowId.ToString(CultureInfo.InvariantCulture) - }; + try + { + var arguments = new List + { + "scan-jellyfin-show", + request.JellyfinLibraryId.ToString(CultureInfo.InvariantCulture), + request.ShowId.ToString(CultureInfo.InvariantCulture), + GetBaseUrl(scanId) + }; - if (request.DeepScan) - { - arguments.Add("--deep"); + if (request.DeepScan) + { + arguments.Add("--deep"); + } + + return await base.PerformScan(parameters, arguments, cancellationToken); + } + finally + { + _scannerProxyService.EndScan(scanId); + } } - return await base.PerformScan(scanner, arguments, cancellationToken); + return BaseError.New($"Library {request.JellyfinLibraryId} is already scanning"); } - protected override Task GetLastScan( + protected override Task> GetLastScan( TvContext dbContext, SynchronizeJellyfinShowById request, CancellationToken cancellationToken) => - Task.FromResult(DateTimeOffset.MinValue); + Task.FromResult(new Tuple(string.Empty, DateTimeOffset.MinValue)); protected override bool ScanIsRequired( DateTimeOffset lastScan, diff --git a/ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs b/ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs index fdf30bad8..e84a93b0e 100644 --- a/ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs +++ b/ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs @@ -1,17 +1,12 @@ using System.Runtime.InteropServices; -using System.Threading.Channels; using CliWrap; -using ErsatzTV.Application.Search; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; -using ErsatzTV.Core.Metadata; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; using Serilog; using Serilog.Core; using Serilog.Events; @@ -19,34 +14,15 @@ using Serilog.Formatting.Compact.Reader; namespace ErsatzTV.Application.Libraries; -public abstract class CallLibraryScannerHandler +public abstract class CallLibraryScannerHandler( + IDbContextFactory dbContextFactory, + IConfigElementRepository configElementRepository, + IRuntimeInfo runtimeInfo) { - private const int BatchSize = 100; - private readonly ChannelWriter _channel; - private readonly IConfigElementRepository _configElementRepository; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediator _mediator; - private readonly IRuntimeInfo _runtimeInfo; - private readonly List _toReindex = []; - private readonly List _toRemove = []; - private string _libraryName; - - protected CallLibraryScannerHandler( - IDbContextFactory dbContextFactory, - IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, - IRuntimeInfo runtimeInfo) - { - _dbContextFactory = dbContextFactory; - _configElementRepository = configElementRepository; - _channel = channel; - _mediator = mediator; - _runtimeInfo = runtimeInfo; - } + protected static string GetBaseUrl(Guid scanId) => $"http://localhost:{Settings.UiPort}/api/scan/{scanId}"; protected async Task> PerformScan( - string scanner, + ScanParameters parameters, List arguments, CancellationToken cancellationToken) { @@ -57,38 +33,24 @@ public abstract class CallLibraryScannerHandler await using CancellationTokenRegistration link = cancellationToken.Register(() => forcefulCts.CancelAfter(TimeSpan.FromSeconds(10))); - CommandResult process = await Cli.Wrap(scanner) + CommandResult process = await Cli.Wrap(parameters.Scanner) .WithArguments(arguments) .WithValidation(CommandResultValidation.None) .WithStandardErrorPipe(PipeTarget.ToDelegate(ProcessLogOutput)) - .WithStandardOutputPipe(PipeTarget.ToDelegate(ProcessProgressOutput)) + .WithStandardOutputPipe(PipeTarget.Null) .ExecuteAsync(forcefulCts.Token, cancellationToken); if (process.ExitCode != 0) { return BaseError.New($"ErsatzTV.Scanner exited with code {process.ExitCode}"); } - - if (_toReindex.Count > 0) - { - // ReSharper disable once PossiblyMistakenUseOfCancellationToken - await _channel.WriteAsync(new ReindexMediaItems(_toReindex.ToArray()), cancellationToken); - _toReindex.Clear(); - } - - if (_toRemove.Count > 0) - { - // ReSharper disable once PossiblyMistakenUseOfCancellationToken - await _channel.WriteAsync(new RemoveMediaItems(_toReindex.ToArray()), cancellationToken); - _toRemove.Clear(); - } } catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) { // do nothing } - return _libraryName ?? string.Empty; + return parameters.LibraryName; } private static void ProcessLogOutput(string s) @@ -125,76 +87,32 @@ public abstract class CallLibraryScannerHandler } } - private async Task ProcessProgressOutput(string s) - { - if (!string.IsNullOrWhiteSpace(s)) - { - try - { - ScannerProgressUpdate progressUpdate = JsonConvert.DeserializeObject(s); - if (progressUpdate != null) - { - if (!string.IsNullOrWhiteSpace(progressUpdate.LibraryName)) - { - _libraryName = progressUpdate.LibraryName; - } - - _toReindex.AddRange(progressUpdate.ItemsToReindex); - if (_toReindex.Count >= BatchSize) - { - await _channel.WriteAsync(new ReindexMediaItems(_toReindex.ToArray())); - _toReindex.Clear(); - } - - _toRemove.AddRange(progressUpdate.ItemsToRemove); - if (_toRemove.Count >= BatchSize) - { - await _channel.WriteAsync(new RemoveMediaItems(_toReindex.ToArray())); - _toRemove.Clear(); - } - - if (progressUpdate.PercentComplete is not null) - { - var progress = new LibraryScanProgress( - progressUpdate.LibraryId, - progressUpdate.PercentComplete.Value); - - await _mediator.Publish(progress); - } - } - } - catch (Exception ex) - { - Log.Logger.Warning(ex, "Unable to process scanner progress update"); - } - } - } - - protected abstract Task GetLastScan( + protected abstract Task> GetLastScan( TvContext dbContext, TRequest request, CancellationToken cancellationToken); + protected abstract bool ScanIsRequired(DateTimeOffset lastScan, int libraryRefreshInterval, TRequest request); - protected async Task> Validate(TRequest request, CancellationToken cancellationToken) + protected async Task> Validate(TRequest request, CancellationToken cancellationToken) { try { - int libraryRefreshInterval = await _configElementRepository + int libraryRefreshInterval = await configElementRepository .GetValue(ConfigElementKey.LibraryRefreshInterval, cancellationToken) .IfNoneAsync(0); libraryRefreshInterval = Math.Clamp(libraryRefreshInterval, 0, 999_999); - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); - DateTimeOffset lastScan = await GetLastScan(dbContext, request, cancellationToken); + (string libraryName, DateTimeOffset lastScan) = await GetLastScan(dbContext, request, cancellationToken); 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"; @@ -211,7 +129,7 @@ public abstract class CallLibraryScannerHandler string localFileName = Path.Combine(folderName, executable); if (File.Exists(localFileName)) { - return localFileName; + return new ScanParameters(libraryName, localFileName); } } @@ -222,4 +140,6 @@ public abstract class CallLibraryScannerHandler return BaseError.New("Scan was canceled"); } } + + protected sealed record ScanParameters(string LibraryName, string Scanner); } diff --git a/ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs b/ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs index f033318f3..60fffb439 100644 --- a/ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs @@ -1,12 +1,13 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.MediaSources; @@ -15,14 +16,16 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler>, IRequestHandler> { + private readonly IScannerProxyService _scannerProxyService; + public CallLocalLibraryScannerHandler( IDbContextFactory dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, + IScannerProxyService scannerProxyService, IRuntimeInfo runtimeInfo) - : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + : base(dbContextFactory, configElementRepository, runtimeInfo) { + _scannerProxyService = scannerProxyService; } Task> IRequestHandler>.Handle( @@ -35,9 +38,9 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler> Handle(IScanLocalLibrary request, CancellationToken cancellationToken) { - Validation validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -50,24 +53,39 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, IScanLocalLibrary request, CancellationToken cancellationToken) { - var arguments = new List + Option maybeScanId = _scannerProxyService.StartScan(request.LibraryId); + foreach (var scanId in maybeScanId) { - "scan-local", request.LibraryId.ToString(CultureInfo.InvariantCulture) - }; + try + { + var arguments = new List + { + "scan-local", + request.LibraryId.ToString(CultureInfo.InvariantCulture), + GetBaseUrl(scanId) + }; - if (request.ForceScan) - { - arguments.Add("--force"); + if (request.ForceScan) + { + arguments.Add("--force"); + } + + return await base.PerformScan(parameters, arguments, cancellationToken); + } + finally + { + _scannerProxyService.EndScan(scanId); + } } - return await base.PerformScan(scanner, arguments, cancellationToken); + return BaseError.New($"Library {request.LibraryId} is already scanning"); } - protected override async Task GetLastScan( + protected override async Task> GetLastScan( TvContext dbContext, IScanLocalLibrary request, CancellationToken cancellationToken) @@ -80,7 +98,11 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler lp.LastScan ?? SystemTime.MinValueUtc) : SystemTime.MaxValueUtc; - return new DateTimeOffset(minDateTime, TimeSpan.Zero); + string libraryName = await dbContext.Libraries + .SelectOneAsync(l => l.Id, l => l.Id == request.LibraryId, cancellationToken) + .Match(l => l.Name, () => string.Empty); + + return new Tuple(libraryName, new DateTimeOffset(minDateTime, TimeSpan.Zero)); } protected override bool ScanIsRequired( diff --git a/ErsatzTV.Application/Plex/Commands/CallPlexCollectionScannerHandler.cs b/ErsatzTV.Application/Plex/Commands/CallPlexCollectionScannerHandler.cs index d854419fb..3de7c2910 100644 --- a/ErsatzTV.Application/Plex/Commands/CallPlexCollectionScannerHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/CallPlexCollectionScannerHandler.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; using ErsatzTV.Core.Errors; @@ -17,18 +16,16 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, - IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, runtimeInfo) { } public async Task> Handle(SynchronizePlexCollections request, CancellationToken cancellationToken) { - Validation validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -40,7 +37,7 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler GetLastScan( + protected override async Task> GetLastScan( TvContext dbContext, SynchronizePlexCollections request, CancellationToken cancellationToken) @@ -49,7 +46,7 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler l.Id, l => l.Id == request.PlexMediaSourceId, cancellationToken) .Match(l => l.LastCollectionsScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc); - return new DateTimeOffset(minDateTime, TimeSpan.Zero); + return new Tuple(string.Empty, new DateTimeOffset(minDateTime, TimeSpan.Zero)); } protected override bool ScanIsRequired( @@ -67,7 +64,7 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, SynchronizePlexCollections request, CancellationToken cancellationToken) { @@ -81,6 +78,6 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler Unit.Default); + return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default); } } diff --git a/ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs b/ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs index cece8d7f6..894a4674b 100644 --- a/ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs @@ -1,8 +1,9 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; @@ -15,14 +16,16 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler>, IRequestHandler> { + private readonly IScannerProxyService _scannerProxyService; + public CallPlexLibraryScannerHandler( IDbContextFactory dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, + IScannerProxyService scannerProxyService, IRuntimeInfo runtimeInfo) - : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + : base(dbContextFactory, configElementRepository, runtimeInfo) { + _scannerProxyService = scannerProxyService; } Task> IRequestHandler>.Handle( @@ -38,9 +41,9 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -53,38 +56,58 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, ISynchronizePlexLibraryById request, CancellationToken cancellationToken) { - var arguments = new List + Option maybeScanId = _scannerProxyService.StartScan(request.PlexLibraryId); + foreach (var scanId in maybeScanId) { - "scan-plex", request.PlexLibraryId.ToString(CultureInfo.InvariantCulture) - }; + try + { + var arguments = new List + { + "scan-plex", + request.PlexLibraryId.ToString(CultureInfo.InvariantCulture), + GetBaseUrl(scanId) + }; - if (request.ForceScan) - { - arguments.Add("--force"); - } + if (request.ForceScan) + { + arguments.Add("--force"); + } - if (request.DeepScan) - { - arguments.Add("--deep"); + if (request.DeepScan) + { + arguments.Add("--deep"); + } + + return await base.PerformScan(parameters, arguments, cancellationToken); + } + finally + { + _scannerProxyService.EndScan(scanId); + } } - return await base.PerformScan(scanner, arguments, cancellationToken); + return BaseError.New($"Library {request.PlexLibraryId} is already scanning"); } - protected override async Task GetLastScan( + protected override async Task> GetLastScan( TvContext dbContext, ISynchronizePlexLibraryById request, CancellationToken cancellationToken) { - DateTime minDateTime = await dbContext.PlexLibraries - .SelectOneAsync(l => l.Id, l => l.Id == request.PlexLibraryId, cancellationToken) - .Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc); + Option maybeLibrary = await dbContext.PlexLibraries + .SelectOneAsync(l => l.Id, l => l.Id == request.PlexLibraryId, cancellationToken); + + DateTime minDateTime = maybeLibrary.Match( + l => l.LastScan ?? SystemTime.MinValueUtc, + () => SystemTime.MaxValueUtc); + + string libraryName = maybeLibrary.Match(l => l.Name, () => string.Empty); - return new DateTimeOffset(minDateTime, TimeSpan.Zero); + return new Tuple(libraryName, new DateTimeOffset(minDateTime, TimeSpan.Zero)); } protected override bool ScanIsRequired( diff --git a/ErsatzTV.Application/Plex/Commands/CallPlexNetworkScannerHandler.cs b/ErsatzTV.Application/Plex/Commands/CallPlexNetworkScannerHandler.cs index b250e17f4..05cbc7cd5 100644 --- a/ErsatzTV.Application/Plex/Commands/CallPlexNetworkScannerHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/CallPlexNetworkScannerHandler.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; using ErsatzTV.Core.Domain; @@ -18,16 +17,14 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, - IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, runtimeInfo) { } public async Task> Handle(SynchronizePlexNetworks request, CancellationToken cancellationToken) { - Validation validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( scanner => PerformScan(scanner, request, cancellationToken), error => @@ -41,7 +38,7 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler GetLastScan( + protected override async Task> GetLastScan( TvContext dbContext, SynchronizePlexNetworks request, CancellationToken cancellationToken) @@ -51,7 +48,7 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler l.Id, l => l.Id == request.PlexLibraryId, cancellationToken) .Match(l => l.LastNetworksScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc); - return new DateTimeOffset(minDateTime, TimeSpan.Zero); + return new Tuple(string.Empty, new DateTimeOffset(minDateTime, TimeSpan.Zero)); } protected override bool ScanIsRequired( @@ -69,7 +66,7 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, SynchronizePlexNetworks request, CancellationToken cancellationToken) { @@ -83,6 +80,6 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler Unit.Default); + return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default); } } diff --git a/ErsatzTV.Application/Plex/Commands/CallPlexShowScannerHandler.cs b/ErsatzTV.Application/Plex/Commands/CallPlexShowScannerHandler.cs index 0033a97b1..1cac69ef5 100644 --- a/ErsatzTV.Application/Plex/Commands/CallPlexShowScannerHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/CallPlexShowScannerHandler.cs @@ -1,8 +1,8 @@ using System.Globalization; -using System.Threading.Channels; using ErsatzTV.Application.Libraries; using ErsatzTV.Core; using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; @@ -13,14 +13,16 @@ namespace ErsatzTV.Application.Plex; public class CallPlexShowScannerHandler : CallLibraryScannerHandler, IRequestHandler> { + private readonly IScannerProxyService _scannerProxyService; + public CallPlexShowScannerHandler( IDbContextFactory dbContextFactory, IConfigElementRepository configElementRepository, - ChannelWriter channel, - IMediator mediator, + IScannerProxyService scannerProxyService, IRuntimeInfo runtimeInfo) - : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) + : base(dbContextFactory, configElementRepository, runtimeInfo) { + _scannerProxyService = scannerProxyService; } Task> IRequestHandler>.Handle( @@ -31,9 +33,9 @@ public class CallPlexShowScannerHandler : CallLibraryScannerHandler validation = await Validate(request, cancellationToken); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( - scanner => PerformScan(scanner, request, cancellationToken), + parameters => PerformScan(parameters, request, cancellationToken), error => { foreach (ScanIsNotRequired scanIsNotRequired in error.OfType()) @@ -46,30 +48,44 @@ public class CallPlexShowScannerHandler : CallLibraryScannerHandler> PerformScan( - string scanner, + ScanParameters parameters, SynchronizePlexShowById request, CancellationToken cancellationToken) { - var arguments = new List + Option maybeScanId = _scannerProxyService.StartScan(request.PlexLibraryId); + foreach (var scanId in maybeScanId) { - "scan-plex-show", - request.PlexLibraryId.ToString(CultureInfo.InvariantCulture), - request.ShowId.ToString(CultureInfo.InvariantCulture) - }; + try + { + var arguments = new List + { + "scan-plex-show", + request.PlexLibraryId.ToString(CultureInfo.InvariantCulture), + request.ShowId.ToString(CultureInfo.InvariantCulture), + GetBaseUrl(scanId) + }; - if (request.DeepScan) - { - arguments.Add("--deep"); + if (request.DeepScan) + { + arguments.Add("--deep"); + } + + return await base.PerformScan(parameters, arguments, cancellationToken); + } + finally + { + _scannerProxyService.EndScan(scanId); + } } - return await base.PerformScan(scanner, arguments, cancellationToken); + return BaseError.New($"Library {request.PlexLibraryId} is already scanning"); } - protected override Task GetLastScan( + protected override Task> GetLastScan( TvContext dbContext, SynchronizePlexShowById request, CancellationToken cancellationToken) => - Task.FromResult(DateTimeOffset.MinValue); + Task.FromResult(new Tuple(string.Empty, DateTimeOffset.MinValue)); protected override bool ScanIsRequired( DateTimeOffset lastScan, diff --git a/ErsatzTV.Core/Interfaces/Metadata/IScannerProxyService.cs b/ErsatzTV.Core/Interfaces/Metadata/IScannerProxyService.cs new file mode 100644 index 000000000..556f46720 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/IScannerProxyService.cs @@ -0,0 +1,10 @@ +namespace ErsatzTV.Core.Interfaces.Metadata; + +public interface IScannerProxyService +{ + Option StartScan(int libraryId); + void EndScan(Guid scanId); + Task Progress(Guid scanId, decimal percentComplete); + bool IsActive(Guid scanId); + Option GetProgress(int libraryId); +} diff --git a/ErsatzTV.Core/MediaSources/ScannerProgressUpdate.cs b/ErsatzTV.Core/MediaSources/ScannerProgressUpdate.cs deleted file mode 100644 index 0957d723e..000000000 --- a/ErsatzTV.Core/MediaSources/ScannerProgressUpdate.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediatR; - -namespace ErsatzTV.Core.MediaSources; - -public record ScannerProgressUpdate( - int LibraryId, - string LibraryName, - decimal? PercentComplete, - int[] ItemsToReindex, - int[] ItemsToRemove) : INotification; diff --git a/ErsatzTV.Core/Metadata/ScannerProgress.cs b/ErsatzTV.Core/Metadata/ScannerProgress.cs new file mode 100644 index 000000000..34808a1c0 --- /dev/null +++ b/ErsatzTV.Core/Metadata/ScannerProgress.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Core.Metadata; + +public class ScannerProgress +{ + public decimal Progress { get; set; } +} diff --git a/ErsatzTV.Core/Metadata/ScannerProxyService.cs b/ErsatzTV.Core/Metadata/ScannerProxyService.cs new file mode 100644 index 000000000..561116132 --- /dev/null +++ b/ErsatzTV.Core/Metadata/ScannerProxyService.cs @@ -0,0 +1,52 @@ +using System.Collections.Concurrent; +using ErsatzTV.Core.Interfaces.Metadata; +using MediatR; + +namespace ErsatzTV.Core.Metadata; + +public class ScannerProxyService(IMediator mediator) : IScannerProxyService +{ + private readonly ConcurrentDictionary _activeLibraries = []; + private readonly ConcurrentDictionary _scans = new(); + + public Option StartScan(int libraryId) + { + if (!_activeLibraries.TryAdd(libraryId, 0)) + { + return Option.None; + } + + var buildId = Guid.NewGuid(); + _scans[buildId] = libraryId; + return buildId; + } + + public void EndScan(Guid scanId) + { + if (_scans.TryRemove(scanId, out int libraryId)) + { + _activeLibraries.TryRemove(libraryId, out _); + } + } + + public async Task Progress(Guid scanId, decimal percentComplete) + { + //logger.LogInformation("Scanning {ScanId}", scanId); + + if (_scans.TryGetValue(scanId, out int libraryId)) + { + //logger.LogDebug("Scan progress {Progress} for library {LibraryId}", percentComplete, libraryId); + + _activeLibraries[libraryId] = percentComplete; + + var progress = new LibraryScanProgress(libraryId, percentComplete); + await mediator.Publish(progress); + } + } + + public bool IsActive(Guid scanId) => _scans.ContainsKey(scanId); + + public Option GetProgress(int libraryId) => _activeLibraries.TryGetValue(libraryId, out decimal progress) + ? progress + : Option.None; +} diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index 4f1476c30..53d54075d 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -38,7 +38,7 @@ - + diff --git a/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs index c4d8f9acd..639be8c17 100644 --- a/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs @@ -7,14 +7,15 @@ using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using ErsatzTV.Scanner.Tests.Core.Fakes; -using MediatR; using Microsoft.Extensions.Logging; using NSubstitute; using NUnit.Framework; +using Serilog; using Shouldly; namespace ErsatzTV.Scanner.Tests.Core.Metadata; @@ -38,6 +39,20 @@ public class MovieFolderScannerTests ? @"C:\bin\ffprobe.exe" : "/bin/ffprobe"; + private static readonly ILogger Logger; + + static MovieFolderScannerTests() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); + + ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + + Logger = loggerFactory.CreateLogger(); + } + [TestFixture] public class ScanFolder { @@ -75,6 +90,11 @@ public class MovieFolderScannerTests _libraryRepository = Substitute.For(); _libraryRepository.GetOrAddFolder(Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new LibraryFolder()); + + _scannerProxy = Substitute.For(); + _scannerProxy.UpdateProgress(Arg.Any(), Arg.Any()).Returns(true); + _scannerProxy.ReindexMediaItems(Arg.Any(), Arg.Any()).Returns(true); + _scannerProxy.RemoveMediaItems(Arg.Any(), Arg.Any()).Returns(true); } private IMovieRepository _movieRepository; @@ -83,6 +103,7 @@ public class MovieFolderScannerTests private ILocalMetadataProvider _localMetadataProvider; private IImageCache _imageCache; private ILibraryRepository _libraryRepository; + private IScannerProxy _scannerProxy; [Test] public async Task NewMovie_Statistics_And_FallbackMetadata( @@ -699,7 +720,8 @@ public class MovieFolderScannerTests private MovieFolderScanner GetService(params FakeFileEntry[] files) => new( - new FakeLocalFileSystem(new List(files)), + _scannerProxy, + new FakeLocalFileSystem([..files]), _movieRepository, _localStatisticsProvider, Substitute.For(), @@ -709,16 +731,15 @@ public class MovieFolderScannerTests _imageCache, _libraryRepository, _mediaItemRepository, - Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For(), - Substitute.For>() - ); + Logger); private MovieFolderScanner GetService(params FakeFolderEntry[] folders) => new( - new FakeLocalFileSystem(new List(), new List(folders)), + _scannerProxy, + new FakeLocalFileSystem([], [..folders]), _movieRepository, _localStatisticsProvider, Substitute.For(), @@ -728,11 +749,9 @@ public class MovieFolderScannerTests _imageCache, _libraryRepository, _mediaItemRepository, - Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For(), - Substitute.For>() - ); + Logger); } } diff --git a/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryById.cs b/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryById.cs index 23c6d75bd..41830e5be 100644 --- a/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryById.cs +++ b/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryById.cs @@ -2,5 +2,5 @@ namespace ErsatzTV.Scanner.Application.Emby; -public record SynchronizeEmbyLibraryById(int EmbyLibraryId, bool ForceScan, bool DeepScan) +public record SynchronizeEmbyLibraryById(string BaseUrl, int EmbyLibraryId, bool ForceScan, bool DeepScan) : IRequest>; diff --git a/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs b/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs index da195d9ab..5f8b4239a 100644 --- a/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs @@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; +using ErsatzTV.Scanner.Core.Interfaces; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Application.Emby; @@ -17,12 +17,11 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler _logger; + private readonly IScannerProxy _scannerProxy; private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IMediator _mediator; - public SynchronizeEmbyLibraryByIdHandler( - IMediator mediator, + IScannerProxy scannerProxy, IMediaSourceRepository mediaSourceRepository, IEmbySecretStore embySecretStore, IEmbyMovieLibraryScanner embyMovieLibraryScanner, @@ -31,7 +30,7 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler logger) { - _mediator = mediator; + _scannerProxy = scannerProxy; _mediaSourceRepository = mediaSourceRepository; _embySecretStore = embySecretStore; _embyMovieLibraryScanner = embyMovieLibraryScanner; @@ -54,6 +53,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler 0 && nextScan < DateTimeOffset.Now) @@ -93,16 +94,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler(), - Array.Empty()), - cancellationToken); - return parameters.Library.Name; } @@ -117,7 +108,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler> ValidateConnection( @@ -166,7 +158,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler>; diff --git a/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs b/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs index b6b327e4a..0a97ffe1e 100644 --- a/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Scanner.Core.Interfaces; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Application.Emby; @@ -13,15 +14,18 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler _logger; + private readonly IScannerProxy _scannerProxy; private readonly IMediaSourceRepository _mediaSourceRepository; public SynchronizeEmbyShowByIdHandler( + IScannerProxy scannerProxy, IMediaSourceRepository mediaSourceRepository, IEmbyTelevisionRepository embyTelevisionRepository, IEmbySecretStore embySecretStore, IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner, ILogger logger) { + _scannerProxy = scannerProxy; _mediaSourceRepository = mediaSourceRepository; _embyTelevisionRepository = embyTelevisionRepository; _embySecretStore = embySecretStore; @@ -48,6 +52,8 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler> ValidateConnection( @@ -132,7 +139,8 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler>; diff --git a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs index c9d9f299f..a3a79f787 100644 --- a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs @@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; -using ErsatzTV.Core.MediaSources; +using ErsatzTV.Scanner.Core.Interfaces; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Application.Jellyfin; @@ -20,10 +20,10 @@ public class private readonly ILibraryRepository _libraryRepository; private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IMediator _mediator; + private readonly IScannerProxy _scannerProxy; public SynchronizeJellyfinLibraryByIdHandler( - IMediator mediator, + IScannerProxy scannerProxy, IMediaSourceRepository mediaSourceRepository, IJellyfinSecretStore jellyfinSecretStore, IJellyfinMovieLibraryScanner jellyfinMovieLibraryScanner, @@ -32,7 +32,7 @@ public class IConfigElementRepository configElementRepository, ILogger logger) { - _mediator = mediator; + _scannerProxy = scannerProxy; _mediaSourceRepository = mediaSourceRepository; _jellyfinSecretStore = jellyfinSecretStore; _jellyfinMovieLibraryScanner = jellyfinMovieLibraryScanner; @@ -55,6 +55,8 @@ public class RequestParameters parameters, CancellationToken cancellationToken) { + _scannerProxy.SetBaseUrl(parameters.BaseUrl); + var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval); if (parameters.ForceScan || parameters.LibraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now) @@ -94,16 +96,6 @@ public class _logger.LogDebug("Skipping unforced scan of jellyfin media library {Name}", parameters.Library.Name); - // send an empty progress update for the library name - await _mediator.Publish( - new ScannerProgressUpdate( - parameters.Library.Id, - parameters.Library.Name, - 0, - Array.Empty(), - Array.Empty()), - cancellationToken); - return parameters.Library.Name; } @@ -118,7 +110,8 @@ public class jellyfinLibrary, request.ForceScan, libraryRefreshInterval, - request.DeepScan + request.DeepScan, + request.BaseUrl )); private Task> ValidateConnection( @@ -166,7 +159,8 @@ public class JellyfinLibrary Library, bool ForceScan, int LibraryRefreshInterval, - bool DeepScan); + bool DeepScan, + string BaseUrl); private record ConnectionParameters(JellyfinConnection ActiveConnection) { diff --git a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowById.cs b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowById.cs index 97eadabb2..76832b191 100644 --- a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowById.cs +++ b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowById.cs @@ -2,5 +2,5 @@ using ErsatzTV.Core; namespace ErsatzTV.Scanner.Application.Jellyfin; -public record SynchronizeJellyfinShowById(int JellyfinLibraryId, int ShowId, bool DeepScan) +public record SynchronizeJellyfinShowById(string BaseUrl, int JellyfinLibraryId, int ShowId, bool DeepScan) : IRequest>; diff --git a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs index d5a61a36d..dd14bcc71 100644 --- a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; +using ErsatzTV.Scanner.Core.Interfaces; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Application.Jellyfin; @@ -14,15 +15,18 @@ public class private readonly IJellyfinTelevisionLibraryScanner _jellyfinTelevisionLibraryScanner; private readonly IJellyfinTelevisionRepository _jellyfinTelevisionRepository; private readonly ILogger _logger; + private readonly IScannerProxy _scannerProxy; private readonly IMediaSourceRepository _mediaSourceRepository; public SynchronizeJellyfinShowByIdHandler( + IScannerProxy scannerProxy, IMediaSourceRepository mediaSourceRepository, IJellyfinTelevisionRepository jellyfinTelevisionRepository, IJellyfinSecretStore jellyfinSecretStore, IJellyfinTelevisionLibraryScanner jellyfinTelevisionLibraryScanner, ILogger logger) { + _scannerProxy = scannerProxy; _mediaSourceRepository = mediaSourceRepository; _jellyfinTelevisionRepository = jellyfinTelevisionRepository; _jellyfinSecretStore = jellyfinSecretStore; @@ -49,6 +53,8 @@ public class return BaseError.New($"Library {parameters.Library.Name} is not a TV show library"); } + _scannerProxy.SetBaseUrl(parameters.BaseUrl); + _logger.LogInformation( "Starting targeted scan for show '{ShowTitle}' in Jellyfin library {LibraryName}", parameters.ShowTitle, @@ -82,7 +88,8 @@ public class jellyfinLibrary, showTitleItemId.ItemId, showTitleItemId.Title, - request.DeepScan + request.DeepScan, + request.BaseUrl )); private Task> ValidateConnection( @@ -132,7 +139,8 @@ public class JellyfinLibrary Library, string ItemId, string ShowTitle, - bool DeepScan); + bool DeepScan, + string BaseUrl); private record ConnectionParameters(JellyfinConnection ActiveConnection) { diff --git a/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibrary.cs b/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibrary.cs index 2bb9f365f..64d71d6d8 100644 --- a/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibrary.cs +++ b/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibrary.cs @@ -2,4 +2,4 @@ namespace ErsatzTV.Scanner.Application.MediaSources; -public record ScanLocalLibrary(int LibraryId, bool ForceScan) : IRequest>; +public record ScanLocalLibrary(string BaseUrl, int LibraryId, bool ForceScan) : IRequest>; diff --git a/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs b/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs index d076eee1d..e2ec05a77 100644 --- a/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs +++ b/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs @@ -2,7 +2,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Humanizer; using Microsoft.Extensions.Logging; @@ -13,9 +13,9 @@ public class ScanLocalLibraryHandler : IRequestHandler _logger; - private readonly IMediator _mediator; private readonly IMovieFolderScanner _movieFolderScanner; private readonly IMusicVideoFolderScanner _musicVideoFolderScanner; private readonly IOtherVideoFolderScanner _otherVideoFolderScanner; @@ -24,6 +24,7 @@ public class ScanLocalLibraryHandler : IRequestHandler logger) { + _scannerProxy = scannerProxy; _libraryRepository = libraryRepository; _configElementRepository = configElementRepository; _movieFolderScanner = movieFolderScanner; @@ -45,7 +46,6 @@ public class ScanLocalLibraryHandler : IRequestHandler PerformScan(RequestParameters parameters, CancellationToken cancellationToken) { (LocalLibrary localLibrary, string ffprobePath, string ffmpegPath, bool forceScan, - int libraryRefreshInterval) = parameters; + int libraryRefreshInterval, string baseUrl) = parameters; var sw = new Stopwatch(); sw.Start(); + _scannerProxy.SetBaseUrl(baseUrl); + var scanned = false; for (var i = 0; i < localLibrary.Paths.Count; i++) @@ -145,14 +147,7 @@ public class ScanLocalLibraryHandler : IRequestHandler(), - Array.Empty()), - cancellationToken); + await _scannerProxy.UpdateProgress(progressMax, cancellationToken); } sw.Stop(); @@ -171,10 +166,6 @@ public class ScanLocalLibraryHandler : IRequestHandler> LocalLibraryMustExist(ScanLocalLibrary request) => @@ -223,5 +215,6 @@ public class ScanLocalLibraryHandler : IRequestHandler -{ - public Task Handle(ScannerProgressUpdate notification, CancellationToken cancellationToken) - { - // dump progress to stdout for main process to read - string json = JsonConvert.SerializeObject(notification); - Console.WriteLine(json); - return Task.CompletedTask; - } -} diff --git a/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryById.cs b/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryById.cs index d0fcd21a6..2ab0de750 100644 --- a/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryById.cs +++ b/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryById.cs @@ -2,5 +2,5 @@ namespace ErsatzTV.Scanner.Application.Plex; -public record SynchronizePlexLibraryById(int PlexLibraryId, bool ForceScan, bool DeepScan) +public record SynchronizePlexLibraryById(string BaseUrl, int PlexLibraryId, bool ForceScan, bool DeepScan) : IRequest>; diff --git a/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs b/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs index 2d3403d62..a4633737a 100644 --- a/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs @@ -2,8 +2,8 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Plex; +using ErsatzTV.Scanner.Core.Interfaces; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Application.Plex; @@ -14,14 +14,14 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler _logger; private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IMediator _mediator; + private readonly IScannerProxy _scannerProxy; private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner; private readonly IPlexOtherVideoLibraryScanner _plexOtherVideoLibraryScanner; private readonly IPlexSecretStore _plexSecretStore; private readonly IPlexTelevisionLibraryScanner _plexTelevisionLibraryScanner; public SynchronizePlexLibraryByIdHandler( - IMediator mediator, + IScannerProxy scannerProxy, IMediaSourceRepository mediaSourceRepository, IConfigElementRepository configElementRepository, IPlexSecretStore plexSecretStore, @@ -31,7 +31,7 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler logger) { - _mediator = mediator; + _scannerProxy = scannerProxy; _mediaSourceRepository = mediaSourceRepository; _configElementRepository = configElementRepository; _plexSecretStore = plexSecretStore; @@ -56,6 +56,8 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler 0 && nextScan < DateTimeOffset.Now) @@ -104,16 +106,6 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler(), - Array.Empty()), - cancellationToken); - return parameters.Library.Name; } @@ -128,7 +120,8 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler> ValidateConnection( @@ -176,7 +169,8 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler>; diff --git a/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowByIdHandler.cs b/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowByIdHandler.cs index dde06cd10..9b51910df 100644 --- a/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowByIdHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Plex; +using ErsatzTV.Scanner.Core.Interfaces; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Application.Plex; @@ -13,15 +14,18 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler logger) { + _scannerProxy = scannerProxy; _plexTelevisionRepository = plexTelevisionRepository; _mediaSourceRepository = mediaSourceRepository; _plexSecretStore = plexSecretStore; @@ -48,6 +52,8 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler> ValidateConnection( @@ -131,7 +138,8 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler _logger; - private readonly IMediator _mediator; public EmbyCollectionScanner( - IMediator mediator, + IScannerProxy scannerProxy, IEmbyCollectionRepository embyCollectionRepository, IEmbyApiClient embyApiClient, ILogger logger) { - _mediator = mediator; + _scannerProxy = scannerProxy; _embyCollectionRepository = embyCollectionRepository; _embyApiClient = embyApiClient; _logger = logger; @@ -107,10 +108,10 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner _logger.LogDebug("Emby collection {Name} contains {Count} items", collection.Name, addedIds.Count); int[] changedIds = removedIds.Concat(addedIds).Distinct().ToArray(); - - await _mediator.Publish( - new ScannerProgressUpdate(0, null, null, changedIds.ToArray(), Array.Empty()), - CancellationToken.None); + if (!await _scannerProxy.ReindexMediaItems(changedIds, CancellationToken.None)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } catch (Exception ex) { diff --git a/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs index e52d83162..098d9ef8c 100644 --- a/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs @@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; @@ -23,8 +24,8 @@ public class EmbyMovieLibraryScanner : private readonly IEmbyPathReplacementService _pathReplacementService; public EmbyMovieLibraryScanner( + IScannerProxy scannerProxy, IEmbyApiClient embyApiClient, - IMediator mediator, IMediaSourceRepository mediaSourceRepository, IEmbyMovieRepository embyMovieRepository, IEmbyPathReplacementService pathReplacementService, @@ -33,10 +34,10 @@ public class EmbyMovieLibraryScanner : IMetadataRepository metadataRepository, ILogger logger) : base( + scannerProxy, localFileSystem, localChaptersProvider, metadataRepository, - mediator, logger) { _embyApiClient = embyApiClient; diff --git a/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs index 50c7febf1..8a45e72d0 100644 --- a/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs @@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; @@ -24,6 +25,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< private readonly IEmbyTelevisionRepository _televisionRepository; public EmbyTelevisionLibraryScanner( + IScannerProxy scannerProxy, IEmbyApiClient embyApiClient, IMediaSourceRepository mediaSourceRepository, IEmbyTelevisionRepository televisionRepository, @@ -31,13 +33,12 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< ILocalFileSystem localFileSystem, ILocalChaptersProvider localChaptersProvider, IMetadataRepository metadataRepository, - IMediator mediator, ILogger logger) : base( + scannerProxy, localFileSystem, localChaptersProvider, metadataRepository, - mediator, logger) { _embyApiClient = embyApiClient; diff --git a/ErsatzTV.Scanner/Core/Interfaces/IScannerProxy.cs b/ErsatzTV.Scanner/Core/Interfaces/IScannerProxy.cs new file mode 100644 index 000000000..f7ede2bb1 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Interfaces/IScannerProxy.cs @@ -0,0 +1,12 @@ +namespace ErsatzTV.Scanner.Core.Interfaces; + +public interface IScannerProxy +{ + void SetBaseUrl(string baseUrl); + + Task UpdateProgress(decimal progress, CancellationToken cancellationToken); + + Task ReindexMediaItems(int[] mediaItemIds, CancellationToken cancellationToken); + + Task RemoveMediaItems(int[] mediaItemIds, CancellationToken cancellationToken); +} diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs index 20575d3f4..b1e0fefb7 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs @@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; +using ErsatzTV.Scanner.Core.Interfaces; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Jellyfin; @@ -10,17 +11,17 @@ namespace ErsatzTV.Scanner.Core.Jellyfin; public class JellyfinCollectionScanner : IJellyfinCollectionScanner { private readonly IJellyfinApiClient _jellyfinApiClient; + private readonly IScannerProxy _scannerProxy; private readonly IJellyfinCollectionRepository _jellyfinCollectionRepository; private readonly ILogger _logger; - private readonly IMediator _mediator; public JellyfinCollectionScanner( - IMediator mediator, + IScannerProxy scannerProxy, IJellyfinCollectionRepository jellyfinCollectionRepository, IJellyfinApiClient jellyfinApiClient, ILogger logger) { - _mediator = mediator; + _scannerProxy = scannerProxy; _jellyfinCollectionRepository = jellyfinCollectionRepository; _jellyfinApiClient = jellyfinApiClient; _logger = logger; @@ -111,10 +112,10 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner _logger.LogDebug("Jellyfin collection {Name} contains {Count} items", collection.Name, addedIds.Count); int[] changedIds = removedIds.Concat(addedIds).Distinct().ToArray(); - - await _mediator.Publish( - new ScannerProgressUpdate(0, null, null, changedIds.ToArray(), Array.Empty()), - CancellationToken.None); + if (!await _scannerProxy.ReindexMediaItems(changedIds, CancellationToken.None)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } catch (Exception ex) { diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs index baf8ba7ce..215cbfbb9 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs @@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; @@ -23,8 +24,8 @@ public class JellyfinMovieLibraryScanner : private readonly IJellyfinPathReplacementService _pathReplacementService; public JellyfinMovieLibraryScanner( + IScannerProxy scannerProxy, IJellyfinApiClient jellyfinApiClient, - IMediator mediator, IJellyfinMovieRepository jellyfinMovieRepository, IJellyfinPathReplacementService pathReplacementService, IMediaSourceRepository mediaSourceRepository, @@ -33,10 +34,10 @@ public class JellyfinMovieLibraryScanner : IMetadataRepository metadataRepository, ILogger logger) : base( + scannerProxy, localFileSystem, localChaptersProvider, metadataRepository, - mediator, logger) { _jellyfinApiClient = jellyfinApiClient; diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs index b87834feb..b26255f0c 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs @@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; @@ -25,6 +26,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan private readonly IJellyfinTelevisionRepository _televisionRepository; public JellyfinTelevisionLibraryScanner( + IScannerProxy scannerProxy, IJellyfinApiClient jellyfinApiClient, IMediaSourceRepository mediaSourceRepository, IJellyfinTelevisionRepository televisionRepository, @@ -32,13 +34,12 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan ILocalFileSystem localFileSystem, ILocalChaptersProvider localChaptersProvider, IMetadataRepository metadataRepository, - IMediator mediator, ILogger logger) : base( + scannerProxy, localFileSystem, localChaptersProvider, metadataRepository, - mediator, logger) { _jellyfinApiClient = jellyfinApiClient; diff --git a/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs index d69332407..9401e6327 100644 --- a/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs @@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; @@ -21,19 +21,19 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner private readonly IClient _client; private readonly IImageRepository _imageRepository; private readonly ILibraryRepository _libraryRepository; + private readonly IScannerProxy _scannerProxy; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; - private readonly IMediator _mediator; public ImageFolderScanner( + IScannerProxy scannerProxy, ILocalFileSystem localFileSystem, ILocalStatisticsProvider localStatisticsProvider, ILocalMetadataProvider localMetadataProvider, IMetadataRepository metadataRepository, IImageCache imageCache, - IMediator mediator, IImageRepository imageRepository, ILibraryRepository libraryRepository, IMediaItemRepository mediaItemRepository, @@ -51,9 +51,9 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner client, logger) { + _scannerProxy = scannerProxy; _localFileSystem = localFileSystem; _localMetadataProvider = localMetadataProvider; - _mediator = mediator; _imageRepository = imageRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; @@ -109,14 +109,12 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner } decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, + if (!await _scannerProxy.UpdateProgress( progressMin + percentCompletion * progressSpread, - [], - []), - cancellationToken); + cancellationToken)) + { + return new ScanCanceled(); + } string imageFolder = folderQueue.Dequeue(); Option maybeParentFolder = await _libraryRepository.GetParentFolderId( @@ -211,14 +209,10 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner { if (result.IsAdded || result.IsUpdated) { - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - [result.Item.Id], - []), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } @@ -236,27 +230,19 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner { _logger.LogInformation("Flagging missing image at {Path}", path); List imageIds = await FlagFileNotFound(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - imageIds.ToArray(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(imageIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List imageIds = await _imageRepository.DeleteByPath(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - Array.Empty(), - imageIds.ToArray()), - cancellationToken); + if (!await _scannerProxy.RemoveMediaItems(imageIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to remove media items from scanner process"); + } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs index a49cec4bc..01010b402 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs @@ -6,8 +6,8 @@ using ErsatzTV.Core.Errors; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; @@ -20,22 +20,22 @@ public abstract class MediaServerMovieLibraryScanner(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.UpdateProgress(percentCompletion, cancellationToken)) + { + return new ScanCanceled(); + } string localPath = getLocalPath(incoming); @@ -198,14 +194,10 @@ public abstract class MediaServerMovieLibraryScanner()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } @@ -213,18 +205,10 @@ public abstract class MediaServerMovieLibraryScanner ids = await movieRepository.FlagFileNotFound(library, fileNotFoundItemIds); - await _mediator.Publish( - new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty()), - cancellationToken); - - await _mediator.Publish( - new ScannerProgressUpdate( - library.Id, - library.Name, - 0, - Array.Empty(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(ids.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } return Unit.Default; } @@ -303,9 +287,10 @@ public abstract class MediaServerMovieLibraryScanner()), - CancellationToken.None); + if (!await _scannerProxy.ReindexMediaItems([id], CancellationToken.None)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } @@ -315,9 +300,10 @@ public abstract class MediaServerMovieLibraryScanner()), - CancellationToken.None); + if (!await _scannerProxy.ReindexMediaItems([id], CancellationToken.None)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/MediaServerOtherVideoLibraryScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MediaServerOtherVideoLibraryScanner.cs index e052d6682..2544b06c6 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MediaServerOtherVideoLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MediaServerOtherVideoLibraryScanner.cs @@ -6,8 +6,8 @@ using ErsatzTV.Core.Errors; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; @@ -20,22 +20,22 @@ public abstract class MediaServerOtherVideoLibraryScanner()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } @@ -220,18 +212,10 @@ public abstract class MediaServerOtherVideoLibraryScanner ids = await otherVideoRepository.FlagFileNotFound(library, fileNotFoundItemIds); - await _mediator.Publish( - new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty()), - cancellationToken); - - await _mediator.Publish( - new ScannerProgressUpdate( - library.Id, - library.Name, - 0, - Array.Empty(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(ids.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } return Unit.Default; } @@ -310,9 +294,10 @@ public abstract class MediaServerOtherVideoLibraryScanner()), - CancellationToken.None); + if (!await _scannerProxy.ReindexMediaItems([id], CancellationToken.None)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } @@ -322,9 +307,10 @@ public abstract class MediaServerOtherVideoLibraryScanner()), - CancellationToken.None); + if (!await _scannerProxy.ReindexMediaItems([id], CancellationToken.None)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs index f5464675e..395da24a2 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs @@ -5,8 +5,8 @@ using ErsatzTV.Core.Errors; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; @@ -22,22 +22,22 @@ public abstract class MediaServerTelevisionLibraryScanner(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.UpdateProgress(percentCompletion, cancellationToken)) + { + return new ScanCanceled(); + } Either> maybeShow = await televisionRepository .GetOrAdd(library, incoming, cancellationToken) @@ -157,14 +153,10 @@ public abstract class MediaServerTelevisionLibraryScanner()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } @@ -174,20 +166,12 @@ public abstract class MediaServerTelevisionLibraryScanner s.MediaServerItemId).Except(incomingItemIds).ToList(); List ids = await televisionRepository.FlagFileNotFoundShows(library, fileNotFoundItemIds, cancellationToken); - await _mediator.Publish( - new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(ids.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } - await _mediator.Publish( - new ScannerProgressUpdate( - library.Id, - library.Name, - 0, - Array.Empty(), - Array.Empty()), - cancellationToken); - return Unit.Default; } @@ -358,14 +342,10 @@ public abstract class MediaServerTelevisionLibraryScanner()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } @@ -373,9 +353,10 @@ public abstract class MediaServerTelevisionLibraryScanner s.MediaServerItemId).Except(incomingItemIds).ToList(); List ids = await televisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundItemIds, cancellationToken); - await _mediator.Publish( - new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(ids.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } return Unit.Default; } @@ -515,14 +496,11 @@ public abstract class MediaServerTelevisionLibraryScanner()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } + } } } @@ -530,9 +508,10 @@ public abstract class MediaServerTelevisionLibraryScanner m.MediaServerItemId).Except(incomingItemIds).ToList(); List ids = await televisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundItemIds, cancellationToken); - await _mediator.Publish( - new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(ids.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } return Unit.Default; } @@ -579,9 +558,10 @@ public abstract class MediaServerTelevisionLibraryScanner _logger; private readonly IMediaItemRepository _mediaItemRepository; - private readonly IMediator _mediator; private readonly IMovieRepository _movieRepository; public MovieFolderScanner( + IScannerProxy scannerProxy, ILocalFileSystem localFileSystem, IMovieRepository movieRepository, ILocalStatisticsProvider localStatisticsProvider, @@ -41,7 +42,6 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner IImageCache imageCache, ILibraryRepository libraryRepository, IMediaItemRepository mediaItemRepository, - IMediator mediator, IFFmpegPngService ffmpegPngService, ITempFilePool tempFilePool, IClient client, @@ -57,6 +57,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner client, logger) { + _scannerProxy = scannerProxy; _localFileSystem = localFileSystem; _movieRepository = movieRepository; _localSubtitlesProvider = localSubtitlesProvider; @@ -64,7 +65,6 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner _localMetadataProvider = localMetadataProvider; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; - _mediator = mediator; _client = client; _logger = logger; } @@ -110,14 +110,12 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner } decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, + if (!await _scannerProxy.UpdateProgress( progressMin + percentCompletion * progressSpread, - Array.Empty(), - Array.Empty()), - cancellationToken); + cancellationToken)) + { + return new ScanCanceled(); + } string movieFolder = folderQueue.Dequeue(); Option maybeParentFolder = await _libraryRepository.GetParentFolderId( @@ -198,14 +196,10 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner { if (result.IsAdded || result.IsUpdated) { - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - new[] { result.Item.Id }, - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag); @@ -219,17 +213,20 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner { _logger.LogInformation("Flagging missing movie at {Path}", path); List ids = await FlagFileNotFound(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate(libraryPath.LibraryId, null, null, ids.ToArray(), Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(ids.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } + } else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List ids = await _movieRepository.DeleteByPath(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate(libraryPath.LibraryId, null, null, Array.Empty(), ids.ToArray()), - cancellationToken); + if (!await _scannerProxy.RemoveMediaItems(ids.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to remove media items from scanner process"); + } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs index 22dad3711..7aaaf441f 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs @@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; @@ -22,15 +22,16 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan private readonly IClient _client; private readonly ILibraryRepository _libraryRepository; private readonly ILocalChaptersProvider _localChaptersProvider; + private readonly IScannerProxy _scannerProxy; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalSubtitlesProvider _localSubtitlesProvider; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; - private readonly IMediator _mediator; private readonly IMusicVideoRepository _musicVideoRepository; public MusicVideoFolderScanner( + IScannerProxy scannerProxy, ILocalFileSystem localFileSystem, ILocalStatisticsProvider localStatisticsProvider, ILocalMetadataProvider localMetadataProvider, @@ -42,7 +43,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan IMusicVideoRepository musicVideoRepository, ILibraryRepository libraryRepository, IMediaItemRepository mediaItemRepository, - IMediator mediator, IFFmpegPngService ffmpegPngService, ITempFilePool tempFilePool, IClient client, @@ -57,6 +57,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan client, logger) { + _scannerProxy = scannerProxy; _localFileSystem = localFileSystem; _localMetadataProvider = localMetadataProvider; _localSubtitlesProvider = localSubtitlesProvider; @@ -65,7 +66,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan _musicVideoRepository = musicVideoRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; - _mediator = mediator; _client = client; _logger = logger; } @@ -106,14 +106,12 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan } decimal percentCompletion = (decimal)allArtistFolders.IndexOf(artistFolder) / allArtistFolders.Count; - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, + if (!await _scannerProxy.UpdateProgress( progressMin + percentCompletion * progressSpread, - Array.Empty(), - Array.Empty()), - cancellationToken); + cancellationToken)) + { + return new ScanCanceled(); + } Either> maybeArtist = await FindOrCreateArtist(libraryPath.Id, artistFolder) @@ -141,14 +139,10 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan { if (result.IsAdded || result.IsUpdated) { - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - new[] { result.Item.Id }, - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } Either scanResult = await ScanMusicVideos( @@ -171,14 +165,10 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan { _logger.LogInformation("Removing improperly named music video at {Path}", path); List musicVideoIds = await _musicVideoRepository.DeleteByPath(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - Array.Empty(), - musicVideoIds.ToArray()), - cancellationToken); + if (!await _scannerProxy.RemoveMediaItems(musicVideoIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to remove media items from scanner process"); + } } foreach (string path in await _musicVideoRepository.FindMusicVideoPaths(libraryPath)) @@ -187,41 +177,29 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan { _logger.LogInformation("Flagging missing music video at {Path}", path); List musicVideoIds = await FlagFileNotFound(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - musicVideoIds.ToArray(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(musicVideoIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List musicVideoIds = await _musicVideoRepository.DeleteByPath(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - Array.Empty(), - musicVideoIds.ToArray()), - cancellationToken); + if (!await _scannerProxy.RemoveMediaItems(musicVideoIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to remove media items from scanner process"); + } } } await _libraryRepository.CleanEtagsForLibraryPath(libraryPath); List artistIds = await _artistRepository.DeleteEmptyArtists(libraryPath); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - Array.Empty(), - artistIds.ToArray()), - cancellationToken); + if (!await _scannerProxy.RemoveMediaItems(artistIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to remove media items from scanner process"); + } return Unit.Default; } @@ -399,14 +377,10 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan { if (result.IsAdded || result.IsUpdated) { - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - new[] { result.Item.Id }, - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs index 1b8941832..7d42856e4 100644 --- a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs @@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; @@ -21,15 +21,16 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan private readonly IClient _client; private readonly ILibraryRepository _libraryRepository; private readonly ILocalChaptersProvider _localChaptersProvider; + private readonly IScannerProxy _scannerProxy; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalSubtitlesProvider _localSubtitlesProvider; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; - private readonly IMediator _mediator; private readonly IOtherVideoRepository _otherVideoRepository; public OtherVideoFolderScanner( + IScannerProxy scannerProxy, ILocalFileSystem localFileSystem, ILocalStatisticsProvider localStatisticsProvider, ILocalMetadataProvider localMetadataProvider, @@ -37,7 +38,6 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan ILocalChaptersProvider localChaptersProvider, IMetadataRepository metadataRepository, IImageCache imageCache, - IMediator mediator, IOtherVideoRepository otherVideoRepository, ILibraryRepository libraryRepository, IMediaItemRepository mediaItemRepository, @@ -55,11 +55,11 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan client, logger) { + _scannerProxy = scannerProxy; _localFileSystem = localFileSystem; _localMetadataProvider = localMetadataProvider; _localSubtitlesProvider = localSubtitlesProvider; _localChaptersProvider = localChaptersProvider; - _mediator = mediator; _otherVideoRepository = otherVideoRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; @@ -121,14 +121,12 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan } decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, + if (!await _scannerProxy.UpdateProgress( progressMin + percentCompletion * progressSpread, - Array.Empty(), - Array.Empty()), - cancellationToken); + cancellationToken)) + { + return new ScanCanceled(); + } string otherVideoFolder = folderQueue.Dequeue(); Option maybeParentFolder = @@ -205,14 +203,10 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan { if (result.IsAdded || result.IsUpdated) { - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - [result.Item.Id], - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } @@ -230,27 +224,20 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan { _logger.LogInformation("Flagging missing other video at {Path}", path); List otherVideoIds = await FlagFileNotFound(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - otherVideoIds.ToArray(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(otherVideoIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } + } else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List otherVideoIds = await _otherVideoRepository.DeleteByPath(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - Array.Empty(), - otherVideoIds.ToArray()), - cancellationToken); + if (!await _scannerProxy.RemoveMediaItems(otherVideoIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to remove media items from scanner process"); + } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs index 04059f406..e6f0fdba0 100644 --- a/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs @@ -8,9 +8,9 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Streaming; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; @@ -23,20 +23,20 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder { private readonly IClient _client; private readonly ILibraryRepository _libraryRepository; + private readonly IScannerProxy _scannerProxy; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; - private readonly IMediator _mediator; private readonly IRemoteStreamRepository _remoteStreamRepository; public RemoteStreamFolderScanner( + IScannerProxy scannerProxy, ILocalFileSystem localFileSystem, ILocalStatisticsProvider localStatisticsProvider, ILocalMetadataProvider localMetadataProvider, IMetadataRepository metadataRepository, IImageCache imageCache, - IMediator mediator, IRemoteStreamRepository remoteStreamRepository, ILibraryRepository libraryRepository, IMediaItemRepository mediaItemRepository, @@ -54,9 +54,9 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder client, logger) { + _scannerProxy = scannerProxy; _localFileSystem = localFileSystem; _localMetadataProvider = localMetadataProvider; - _mediator = mediator; _remoteStreamRepository = remoteStreamRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; @@ -116,14 +116,12 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder } decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, + if (!await _scannerProxy.UpdateProgress( progressMin + percentCompletion * progressSpread, - [], - []), - cancellationToken); + cancellationToken)) + { + return new ScanCanceled(); + } string remoteStreamFolder = folderQueue.Dequeue(); Option maybeParentFolder = @@ -197,14 +195,11 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder { if (result.IsAdded || result.IsUpdated) { - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - [result.Item.Id], - []), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + hasErrors = true; + } } } } @@ -222,27 +217,19 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder { _logger.LogInformation("Flagging missing remote stream at {Path}", path); List remoteStreamIds = await FlagFileNotFound(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - remoteStreamIds.ToArray(), - []), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(remoteStreamIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List remoteStreamIds = await _remoteStreamRepository.DeleteByPath(libraryPath, path, cancellationToken); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - [], - remoteStreamIds.ToArray()), - cancellationToken); + if (!await _scannerProxy.RemoveMediaItems(remoteStreamIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to remove media items from scanner process"); + } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs index 2df986ef3..994bab319 100644 --- a/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs @@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; @@ -20,20 +20,20 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner { private readonly IClient _client; private readonly ILibraryRepository _libraryRepository; + private readonly IScannerProxy _scannerProxy; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; - private readonly IMediator _mediator; private readonly ISongRepository _songRepository; public SongFolderScanner( + IScannerProxy scannerProxy, ILocalFileSystem localFileSystem, ILocalStatisticsProvider localStatisticsProvider, ILocalMetadataProvider localMetadataProvider, IMetadataRepository metadataRepository, IImageCache imageCache, - IMediator mediator, ISongRepository songRepository, ILibraryRepository libraryRepository, IMediaItemRepository mediaItemRepository, @@ -51,9 +51,9 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner client, logger) { + _scannerProxy = scannerProxy; _localFileSystem = localFileSystem; _localMetadataProvider = localMetadataProvider; - _mediator = mediator; _songRepository = songRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; @@ -107,14 +107,10 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner } decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - progressMin + percentCompletion * progressSpread, - Array.Empty(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.UpdateProgress(progressMin + percentCompletion * progressSpread, cancellationToken)) + { + return new ScanCanceled(); + } string songFolder = folderQueue.Dequeue(); Option maybeParentFolder = @@ -187,14 +183,11 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner { if (result.IsAdded || result.IsUpdated) { - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - new[] { result.Item.Id }, - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + hasErrors = true; + } } } } @@ -212,27 +205,19 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner { _logger.LogInformation("Flagging missing song at {Path}", path); List songIds = await FlagFileNotFound(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - songIds.ToArray(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(songIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List songIds = await _songRepository.DeleteByPath(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - Array.Empty(), - songIds.ToArray()), - cancellationToken); + if (!await _scannerProxy.RemoveMediaItems(songIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to remove media items from scanner process"); + } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs index 7842ed815..9dcd79f3d 100644 --- a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs @@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; @@ -22,16 +22,17 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly ILibraryRepository _libraryRepository; private readonly ILocalChaptersProvider _localChaptersProvider; + private readonly IScannerProxy _scannerProxy; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalSubtitlesProvider _localSubtitlesProvider; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; - private readonly IMediator _mediator; private readonly IMetadataRepository _metadataRepository; private readonly ITelevisionRepository _televisionRepository; public TelevisionFolderScanner( + IScannerProxy scannerProxy, ILocalFileSystem localFileSystem, ITelevisionRepository televisionRepository, ILocalStatisticsProvider localStatisticsProvider, @@ -42,7 +43,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan IImageCache imageCache, ILibraryRepository libraryRepository, IMediaItemRepository mediaItemRepository, - IMediator mediator, IFFmpegPngService ffmpegPngService, ITempFilePool tempFilePool, IClient client, @@ -58,6 +58,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan client, logger) { + _scannerProxy = scannerProxy; _localFileSystem = localFileSystem; _televisionRepository = televisionRepository; _localMetadataProvider = localMetadataProvider; @@ -66,7 +67,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan _metadataRepository = metadataRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; - _mediator = mediator; _client = client; _fallbackMetadataProvider = fallbackMetadataProvider; _logger = logger; @@ -107,14 +107,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan } decimal percentCompletion = (decimal)allShowFolders.IndexOf(showFolder) / allShowFolders.Count; - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - progressMin + percentCompletion * progressSpread, - Array.Empty(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.UpdateProgress(progressMin + percentCompletion * progressSpread, cancellationToken)) + { + return new ScanCanceled(); + } Option maybeParentFolder = await _libraryRepository.GetParentFolderId(libraryPath, showFolder, cancellationToken); @@ -149,14 +145,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan // add show to search index right away if (result.IsAdded || result.IsUpdated) { - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - new[] { result.Item.Id }, - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } Either scanResult = await ScanSeasons( @@ -182,14 +174,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan _logger.LogInformation("Flagging missing episode at {Path}", path); List episodeIds = await FlagFileNotFound(libraryPath, path); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - episodeIds.ToArray(), - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems(episodeIds.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)) { @@ -202,14 +190,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan await _televisionRepository.DeleteEmptySeasons(libraryPath); List ids = await _televisionRepository.DeleteEmptyShows(libraryPath); - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - Array.Empty(), - ids.ToArray()), - cancellationToken); + if (!await _scannerProxy.RemoveMediaItems(ids.ToArray(), cancellationToken)) + { + _logger.LogWarning("Failed to remove media items from scanner process"); + } return Unit.Default; } @@ -310,14 +294,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan season.Show = show; - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - new[] { season.Id }, - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([season.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } } @@ -365,14 +345,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan foreach (Episode episode in maybeEpisode.RightToSeq()) { - await _mediator.Publish( - new ScannerProgressUpdate( - libraryPath.LibraryId, - null, - null, - new[] { episode.Id }, - Array.Empty()), - cancellationToken); + if (!await _scannerProxy.ReindexMediaItems([episode.Id], cancellationToken)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } } @@ -620,9 +596,9 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan { string[] segments = artworkKind switch { - ArtworkKind.Poster => new[] { "poster", "folder" }, - ArtworkKind.FanArt => new[] { "fanart" }, - ArtworkKind.Thumbnail => new[] { "thumb" }, + ArtworkKind.Poster => ["poster", "folder"], + ArtworkKind.FanArt => ["fanart"], + ArtworkKind.Thumbnail => ["thumb"], _ => throw new ArgumentOutOfRangeException(nameof(artworkKind)) }; diff --git a/ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs index 720dbeaa8..788bf7829 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs @@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Plex; +using ErsatzTV.Scanner.Core.Interfaces; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Plex; @@ -11,17 +12,17 @@ namespace ErsatzTV.Scanner.Core.Plex; public class PlexCollectionScanner : IPlexCollectionScanner { private readonly ILogger _logger; - private readonly IMediator _mediator; + private readonly IScannerProxy _scannerProxy; private readonly IPlexCollectionRepository _plexCollectionRepository; private readonly IPlexServerApiClient _plexServerApiClient; public PlexCollectionScanner( - IMediator mediator, + IScannerProxy scannerProxy, IPlexCollectionRepository plexCollectionRepository, IPlexServerApiClient plexServerApiClient, ILogger logger) { - _mediator = mediator; + _scannerProxy = scannerProxy; _plexCollectionRepository = plexCollectionRepository; _plexServerApiClient = plexServerApiClient; _logger = logger; @@ -112,10 +113,10 @@ public class PlexCollectionScanner : IPlexCollectionScanner _logger.LogDebug("Plex collection {Name} contains {Count} items", collection.Name, addedIds.Count); int[] changedIds = removedIds.Concat(addedIds).Distinct().ToArray(); - - await _mediator.Publish( - new ScannerProgressUpdate(0, null, null, changedIds.ToArray(), Array.Empty()), - CancellationToken.None); + if (!await _scannerProxy.ReindexMediaItems(changedIds, CancellationToken.None)) + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } catch (Exception ex) { diff --git a/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs index cefd272e7..d20c8bfb2 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs @@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Plex; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; @@ -25,10 +26,10 @@ public class PlexMovieLibraryScanner : private readonly IPlexServerApiClient _plexServerApiClient; public PlexMovieLibraryScanner( + IScannerProxy scannerProxy, IPlexServerApiClient plexServerApiClient, IMovieRepository movieRepository, IMetadataRepository metadataRepository, - IMediator mediator, IMediaSourceRepository mediaSourceRepository, IPlexMovieRepository plexMovieRepository, IPlexPathReplacementService plexPathReplacementService, @@ -36,10 +37,10 @@ public class PlexMovieLibraryScanner : ILocalChaptersProvider localChaptersProvider, ILogger logger) : base( + scannerProxy, localFileSystem, localChaptersProvider, metadataRepository, - mediator, logger) { _plexServerApiClient = plexServerApiClient; diff --git a/ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs index 62267712d..45a68a0f3 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs @@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Plex; +using ErsatzTV.Scanner.Core.Interfaces; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Plex; @@ -12,7 +13,7 @@ public class PlexNetworkScanner( IPlexServerApiClient plexServerApiClient, IPlexTelevisionRepository plexTelevisionRepository, ITelevisionRepository televisionRepository, - IMediator mediator, + IScannerProxy scannerProxy, ILogger logger) : IPlexNetworkScanner { public async Task> ScanNetworks( @@ -92,9 +93,10 @@ public class PlexNetworkScanner( changedIds.AddRange(await televisionRepository.GetEpisodeIdsForShow(showId)); } - await mediator.Publish( - new ScannerProgressUpdate(0, null, null, changedIds.ToArray(), []), - CancellationToken.None); + if (!await scannerProxy.ReindexMediaItems(changedIds.ToArray(), CancellationToken.None)) + { + logger.LogWarning("Failed to reindex media items from scanner process"); + } } catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) { diff --git a/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs index c5ae7f958..a9052dc74 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs @@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Plex; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; @@ -25,10 +26,10 @@ public class PlexOtherVideoLibraryScanner : private readonly IPlexServerApiClient _plexServerApiClient; public PlexOtherVideoLibraryScanner( + IScannerProxy scannerProxy, IPlexServerApiClient plexServerApiClient, IOtherVideoRepository otherVideoRepository, IMetadataRepository metadataRepository, - IMediator mediator, IMediaSourceRepository mediaSourceRepository, IPlexOtherVideoRepository plexOtherVideoRepository, IPlexPathReplacementService plexPathReplacementService, @@ -36,10 +37,10 @@ public class PlexOtherVideoLibraryScanner : ILocalChaptersProvider localChaptersProvider, ILogger logger) : base( + scannerProxy, localFileSystem, localChaptersProvider, metadataRepository, - mediator, logger) { _plexServerApiClient = plexServerApiClient; diff --git a/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs index 03253adc1..a94180034 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs @@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Plex; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; @@ -29,10 +30,10 @@ public partial class PlexTelevisionLibraryScanner : private readonly ITelevisionRepository _televisionRepository; public PlexTelevisionLibraryScanner( + IScannerProxy scannerProxy, IPlexServerApiClient plexServerApiClient, ITelevisionRepository televisionRepository, IMetadataRepository metadataRepository, - IMediator mediator, IMediaSourceRepository mediaSourceRepository, IPlexPathReplacementService plexPathReplacementService, IPlexTelevisionRepository plexTelevisionRepository, @@ -40,10 +41,10 @@ public partial class PlexTelevisionLibraryScanner : ILocalChaptersProvider localChaptersProvider, ILogger logger) : base( + scannerProxy, localFileSystem, localChaptersProvider, metadataRepository, - mediator, logger) { _plexServerApiClient = plexServerApiClient; diff --git a/ErsatzTV.Scanner/Core/ScannerProxy.cs b/ErsatzTV.Scanner/Core/ScannerProxy.cs new file mode 100644 index 000000000..9a24c55b6 --- /dev/null +++ b/ErsatzTV.Scanner/Core/ScannerProxy.cs @@ -0,0 +1,90 @@ +using System.Net.Http.Json; +using ErsatzTV.Scanner.Core.Interfaces; + +namespace ErsatzTV.Scanner.Core; + +public class ScannerProxy(IHttpClientFactory httpClientFactory) : IScannerProxy +{ + private string? _baseUrl; + + public void SetBaseUrl(string baseUrl) + { + _baseUrl = baseUrl; + } + + public async Task UpdateProgress(decimal progress, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(_baseUrl)) + { + return false; + } + + try + { + using var httpClient = httpClientFactory.CreateClient(); + var url = $"{_baseUrl}/progress"; + await httpClient.PostAsJsonAsync(url, progress, cancellationToken); + return true; + } + catch + { + // do nothing + } + + return false; + } + + public async Task ReindexMediaItems(int[] mediaItemIds, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(_baseUrl)) + { + return false; + } + + if (mediaItemIds.Length == 0) + { + return true; + } + + try + { + using var httpClient = httpClientFactory.CreateClient(); + var url = $"{_baseUrl}/items/reindex"; + await httpClient.PostAsJsonAsync(url, mediaItemIds, cancellationToken); + return true; + } + catch + { + // do nothing + } + + return false; + } + + public async Task RemoveMediaItems(int[] mediaItemIds, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(_baseUrl)) + { + return false; + } + + if (mediaItemIds.Length == 0) + { + return true; + } + + try + { + using var httpClient = httpClientFactory.CreateClient(); + var url = $"{_baseUrl}/items/remove"; + await httpClient.PostAsJsonAsync(url, mediaItemIds, cancellationToken); + return true; + } + catch + { + // do nothing + } + + return false; + } +} diff --git a/ErsatzTV.Scanner/Program.cs b/ErsatzTV.Scanner/Program.cs index f665407b1..7f608e2ad 100644 --- a/ErsatzTV.Scanner/Program.cs +++ b/ErsatzTV.Scanner/Program.cs @@ -29,8 +29,10 @@ using ErsatzTV.Infrastructure.Plex; using ErsatzTV.Infrastructure.Runtime; using ErsatzTV.Infrastructure.Search; using ErsatzTV.Infrastructure.Sqlite.Data; +using ErsatzTV.Scanner.Core; using ErsatzTV.Scanner.Core.Emby; using ErsatzTV.Scanner.Core.FFmpeg; +using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; @@ -166,6 +168,8 @@ public class Program TvContext.CaseInsensitiveCollation = "utf8mb4_general_ci"; } + services.AddHttpClient(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -245,6 +249,7 @@ public class Program services.AddSingleton(); // TODO: real bugsnag? services.AddSingleton(_ => new BugsnagNoopClient()); + services.AddSingleton(); services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining()); services.AddMemoryCache(); diff --git a/ErsatzTV.Scanner/Worker.cs b/ErsatzTV.Scanner/Worker.cs index 8865cae69..b89647190 100644 --- a/ErsatzTV.Scanner/Worker.cs +++ b/ErsatzTV.Scanner/Worker.cs @@ -65,13 +65,19 @@ public class Worker : BackgroundService { Description = "The media source id to scan" }; + var baseUrlArgument = new Argument("base-url") + { + Description = "The base url for communication with the main ErsatzTV process" + }; var scanLocalCommand = new Command("scan-local", "Scan a local library"); scanLocalCommand.Arguments.Add(libraryIdArgument); + scanLocalCommand.Arguments.Add(baseUrlArgument); scanLocalCommand.Options.Add(forceOption); var scanPlexCommand = new Command("scan-plex", "Scan a Plex library"); scanPlexCommand.Arguments.Add(libraryIdArgument); + scanPlexCommand.Arguments.Add(baseUrlArgument); scanPlexCommand.Options.Add(forceOption); scanPlexCommand.Options.Add(deepOption); @@ -85,6 +91,7 @@ public class Worker : BackgroundService var scanEmbyCommand = new Command("scan-emby", "Scan an Emby library"); scanEmbyCommand.Arguments.Add(libraryIdArgument); + scanEmbyCommand.Arguments.Add(baseUrlArgument); scanEmbyCommand.Options.Add(forceOption); scanEmbyCommand.Options.Add(deepOption); @@ -94,6 +101,7 @@ public class Worker : BackgroundService var scanJellyfinCommand = new Command("scan-jellyfin", "Scan a Jellyfin library"); scanJellyfinCommand.Arguments.Add(libraryIdArgument); + scanJellyfinCommand.Arguments.Add(baseUrlArgument); scanJellyfinCommand.Options.Add(forceOption); scanJellyfinCommand.Options.Add(deepOption); @@ -110,11 +118,13 @@ public class Worker : BackgroundService var scanPlexShowCommand = new Command("scan-plex-show", "Scan a specific TV show in a Plex library"); scanPlexShowCommand.Arguments.Add(libraryIdArgument); scanPlexShowCommand.Arguments.Add(showIdArgument); + scanPlexShowCommand.Arguments.Add(baseUrlArgument); scanPlexShowCommand.Options.Add(deepOption); var scanEmbyShowCommand = new Command("scan-emby-show", "Scan a specific TV show in an Emby library"); scanEmbyShowCommand.Arguments.Add(libraryIdArgument); scanEmbyShowCommand.Arguments.Add(showIdArgument); + scanEmbyShowCommand.Arguments.Add(baseUrlArgument); scanEmbyShowCommand.Options.Add(deepOption); var scanJellyfinShowCommand = new Command( @@ -122,6 +132,7 @@ public class Worker : BackgroundService "Scan a specific TV show in a Jellyfin library"); scanJellyfinShowCommand.Arguments.Add(libraryIdArgument); scanJellyfinShowCommand.Arguments.Add(showIdArgument); + scanJellyfinShowCommand.Arguments.Add(baseUrlArgument); scanJellyfinShowCommand.Options.Add(deepOption); scanLocalCommand.SetAction(async (parseResult, token) => @@ -132,11 +143,16 @@ public class Worker : BackgroundService SetProcessPriority(force); int libraryId = parseResult.GetValue(libraryIdArgument); + string? baseUrl = parseResult.GetValue(baseUrlArgument); + if (baseUrl is null) + { + return; + } using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var scan = new ScanLocalLibrary(libraryId, force); + var scan = new ScanLocalLibrary(baseUrl, libraryId, force); await mediator.Send(scan, token); } }); @@ -150,11 +166,16 @@ public class Worker : BackgroundService bool deep = parseResult.GetValue(deepOption); int libraryId = parseResult.GetValue(libraryIdArgument); + string? baseUrl = parseResult.GetValue(baseUrlArgument); + if (baseUrl is null) + { + return; + } using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var scan = new SynchronizePlexLibraryById(libraryId, force, deep); + var scan = new SynchronizePlexLibraryById(baseUrl, libraryId, force, deep); await mediator.Send(scan, token); } }); @@ -202,11 +223,16 @@ public class Worker : BackgroundService bool deep = parseResult.GetValue(deepOption); int libraryId = parseResult.GetValue(libraryIdArgument); + string? baseUrl = parseResult.GetValue(baseUrlArgument); + if (baseUrl is null) + { + return; + } using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var scan = new SynchronizeEmbyLibraryById(libraryId, force, deep); + var scan = new SynchronizeEmbyLibraryById(baseUrl, libraryId, force, deep); await mediator.Send(scan, token); } }); @@ -237,11 +263,16 @@ public class Worker : BackgroundService bool deep = parseResult.GetValue(deepOption); int libraryId = parseResult.GetValue(libraryIdArgument); + string? baseUrl = parseResult.GetValue(baseUrlArgument); + if (baseUrl is null) + { + return; + } using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var scan = new SynchronizeJellyfinLibraryById(libraryId, force, deep); + var scan = new SynchronizeJellyfinLibraryById(baseUrl, libraryId, force, deep); await mediator.Send(scan, token); } }); @@ -270,11 +301,16 @@ public class Worker : BackgroundService bool deep = parseResult.GetValue(deepOption); int libraryId = parseResult.GetValue(libraryIdArgument); int showId = parseResult.GetValue(showIdArgument); + string? baseUrl = parseResult.GetValue(baseUrlArgument); + if (baseUrl is null) + { + return; + } using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var scan = new SynchronizePlexShowById(libraryId, showId, deep); + var scan = new SynchronizePlexShowById(baseUrl, libraryId, showId, deep); await mediator.Send(scan, token); } }); @@ -286,11 +322,16 @@ public class Worker : BackgroundService bool deep = parseResult.GetValue(deepOption); int libraryId = parseResult.GetValue(libraryIdArgument); int showId = parseResult.GetValue(showIdArgument); + string? baseUrl = parseResult.GetValue(baseUrlArgument); + if (baseUrl is null) + { + return; + } using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var scan = new SynchronizeEmbyShowById(libraryId, showId, deep); + var scan = new SynchronizeEmbyShowById(baseUrl, libraryId, showId, deep); await mediator.Send(scan, token); } }); @@ -302,11 +343,16 @@ public class Worker : BackgroundService bool deep = parseResult.GetValue(deepOption); int libraryId = parseResult.GetValue(libraryIdArgument); int showId = parseResult.GetValue(showIdArgument); + string? baseUrl = parseResult.GetValue(baseUrlArgument); + if (baseUrl is null) + { + return; + } using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var scan = new SynchronizeJellyfinShowById(libraryId, showId, deep); + var scan = new SynchronizeJellyfinShowById(baseUrl, libraryId, showId, deep); await mediator.Send(scan, token); } }); diff --git a/ErsatzTV/Controllers/Api/ScannerController.cs b/ErsatzTV/Controllers/Api/ScannerController.cs new file mode 100644 index 000000000..8c8404160 --- /dev/null +++ b/ErsatzTV/Controllers/Api/ScannerController.cs @@ -0,0 +1,53 @@ +using System.Threading.Channels; +using ErsatzTV.Application; +using ErsatzTV.Application.Search; +using ErsatzTV.Core.Interfaces.Metadata; +using Microsoft.AspNetCore.Mvc; + +namespace ErsatzTV.Controllers.Api; + +[ApiController] +[ApiExplorerSettings(IgnoreApi = true)] +[Route("api/scan/{scanId:guid}")] +public class ScannerController( + IScannerProxyService scannerProxyService, + ChannelWriter channelWriter) +{ + [HttpPost("progress")] + [EndpointSummary("Scanner progress update")] + public async Task Progress(Guid scanId, [FromBody] decimal percentComplete) + { + await scannerProxyService.Progress(scanId, percentComplete); + return new OkResult(); + } + + [HttpPost("items/reindex")] + [EndpointSummary("Scanner reindex items in search index")] + public async Task UpdateItems( + Guid scanId, + [FromBody] List itemsToUpdate, + CancellationToken cancellationToken) + { + if (scannerProxyService.IsActive(scanId)) + { + await channelWriter.WriteAsync(new ReindexMediaItems(itemsToUpdate), cancellationToken); + } + + return new OkResult(); + } + + [HttpPost("items/remove")] + [EndpointSummary("Scanner remove items from search index")] + public async Task RemoveItems( + Guid scanId, + [FromBody] List itemsToRemove, + CancellationToken cancellationToken) + { + if (scannerProxyService.IsActive(scanId)) + { + await channelWriter.WriteAsync(new RemoveMediaItems(itemsToRemove), cancellationToken); + } + + return new OkResult(); + } +} diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 05281ef29..98fe14b41 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -35,7 +35,7 @@ - + diff --git a/ErsatzTV/Pages/Libraries.razor b/ErsatzTV/Pages/Libraries.razor index 9fc21552c..cfb5e26f5 100644 --- a/ErsatzTV/Pages/Libraries.razor +++ b/ErsatzTV/Pages/Libraries.razor @@ -4,14 +4,16 @@ @using ErsatzTV.Application.Libraries @using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.Plex +@using ErsatzTV.Core.Interfaces.Metadata @using ErsatzTV.Core.Metadata @using MediatR.Courier @using PlexLibraryViewModel = ErsatzTV.Application.Libraries.PlexLibraryViewModel @implements IDisposable @inject IMediator Mediator @inject IEntityLocker Locker -@inject ChannelWriter ScannerWorkerChannel; +@inject ChannelWriter ScannerWorkerChannel @inject ICourier Courier +@inject IScannerProxyService ScannerProxyService
@@ -67,7 +69,7 @@
- @if (Locker.IsLibraryLocked(context.Id)) + @if (IsLibraryLocked(context.Id)) {
@if (_progressByLibrary[context.Id] > 0) @@ -190,7 +192,12 @@ token.ThrowIfCancellationRequested(); _externalCollections = await Mediator.Send(new GetExternalCollections(), token); - _progressByLibrary = _libraries.ToDictionary(vm => vm.Id, _ => 0); + + foreach (var library in _libraries) + { + decimal progress = ScannerProxyService.GetProgress(library.Id).IfNone(0); + _progressByLibrary[library.Id] = (int)(progress * 100); + } } catch (OperationCanceledException) { @@ -302,4 +309,16 @@ _cts?.Cancel(); _cts?.Dispose(); } + + private bool IsLibraryLocked(int libraryId) + { + if (Locker.IsLibraryLocked(libraryId)) + { + return true; + } + + _progressByLibrary[libraryId] = 0; + return false; + } + } diff --git a/ErsatzTV/Services/SearchIndexService.cs b/ErsatzTV/Services/SearchIndexService.cs index b1cccbfb9..f1e9e26bd 100644 --- a/ErsatzTV/Services/SearchIndexService.cs +++ b/ErsatzTV/Services/SearchIndexService.cs @@ -14,6 +14,11 @@ public class SearchIndexService : BackgroundService private readonly IServiceScopeFactory _serviceScopeFactory; private readonly SystemStartup _systemStartup; + private const int MaxBatchSize = 1000; + private readonly TimeSpan _maxBatchTime = TimeSpan.FromSeconds(10); + + private enum SearchOperation { Reindex, Remove } + public SearchIndexService( ChannelReader channel, IServiceScopeFactory serviceScopeFactory, @@ -35,39 +40,51 @@ public class SearchIndexService : BackgroundService { _logger.LogInformation("Search index worker service started"); - await foreach (ISearchIndexBackgroundServiceRequest request in _channel.ReadAllAsync(stoppingToken)) - { - using IServiceScope scope = _serviceScopeFactory.CreateScope(); - IMediator mediator = scope.ServiceProvider.GetRequiredService(); + var batch = new Dictionary(); + while (!stoppingToken.IsCancellationRequested) + { try { - switch (request) - { - case ReindexMediaItems reindexMediaItems: - _logger.LogDebug("Reindexing media items: {MediaItemIds}", reindexMediaItems.MediaItemIds); - await mediator.Send(reindexMediaItems, stoppingToken); - break; - case RemoveMediaItems removeMediaItems: - _logger.LogDebug("Removing media items: {MediaItemIds}", removeMediaItems.MediaItemIds); - await mediator.Send(removeMediaItems, stoppingToken); - break; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to handle search index worker request"); + var firstRequest = await _channel.ReadAsync(stoppingToken); + AddRequestToBatch(firstRequest, batch); + + using var timeoutCts = new CancellationTokenSource(_maxBatchTime); + using var linkedCts = + CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, timeoutCts.Token); try { - IClient client = scope.ServiceProvider.GetRequiredService(); - client.Notify(ex); + while (batch.Count < MaxBatchSize && await _channel.WaitToReadAsync(linkedCts.Token)) + { + if (_channel.TryRead(out var nextRequest)) + { + AddRequestToBatch(nextRequest, batch); + } + } } - catch (Exception) + catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) { - // do nothing + // batch time expired. } } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading from search index channel."); + + // avoid fast-looping on error + await Task.Delay(1000, stoppingToken); + } + + if (batch.Count > 0) + { + await ProcessBatchAsync(batch, stoppingToken); + batch.Clear(); + } } } catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) @@ -75,4 +92,81 @@ public class SearchIndexService : BackgroundService _logger.LogInformation("Search index worker service shutting down"); } } + + private static void AddRequestToBatch( + ISearchIndexBackgroundServiceRequest request, + IDictionary batch) + { + switch (request) + { + case ReindexMediaItems reindex: + foreach (int id in reindex.MediaItemIds) + { + batch[id] = SearchOperation.Reindex; + } + + break; + case RemoveMediaItems remove: + foreach (int id in remove.MediaItemIds) + { + batch[id] = SearchOperation.Remove; + } + + break; + } + } + + private async Task ProcessBatchAsync(Dictionary batch, CancellationToken stoppingToken) + { + var idsToReindex = new List(); + var idsToRemove = new List(); + + foreach ((int id, SearchOperation op) in batch) + { + switch (op) + { + case SearchOperation.Reindex: + idsToReindex.Add(id); + break; + case SearchOperation.Remove: + idsToRemove.Add(id); + break; + } + } + + _logger.LogDebug( + "Processing search index batch. Reindexing: {ReindexCount}, Removing: {RemoveCount}", + idsToReindex.Count, + idsToRemove.Count); + + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + + try + { + if (idsToRemove.Count > 0) + { + await mediator.Send(new RemoveMediaItems(idsToRemove), stoppingToken); + } + + if (idsToReindex.Count > 0) + { + await mediator.Send(new ReindexMediaItems(idsToReindex), stoppingToken); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to handle search index batch worker request"); + + try + { + IClient client = scope.ServiceProvider.GetRequiredService(); + client.Notify(ex); + } + catch (Exception) + { + // do nothing + } + } + } } diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 4e7bbbdc9..369376f16 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -557,6 +557,9 @@ public class Startup if (httpContext.Request.Path.ToUriComponent().StartsWith( "/api", + StringComparison.OrdinalIgnoreCase) && + !httpContext.Request.Path.ToUriComponent().StartsWith( + "/api/scan", StringComparison.OrdinalIgnoreCase)) { return LogEventLevel.Debug; @@ -716,6 +719,7 @@ public class Startup services.AddSingleton(); } + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton();