Browse Source

change how scanner and main process communicate (#2555)

* report scanner progress using api

* process scanner search index updates through api

* update changelog

* update dependencies
pull/2556/head
Jason Dove 2 months ago committed by GitHub
parent
commit
626048f9c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 17
      ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs
  3. 69
      ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs
  4. 52
      ErsatzTV.Application/Emby/Commands/CallEmbyShowScannerHandler.cs
  5. 17
      ErsatzTV.Application/Jellyfin/Commands/CallJellyfinCollectionScannerHandler.cs
  6. 69
      ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs
  7. 52
      ErsatzTV.Application/Jellyfin/Commands/CallJellyfinShowScannerHandler.cs
  8. 118
      ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs
  9. 54
      ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs
  10. 17
      ErsatzTV.Application/Plex/Commands/CallPlexCollectionScannerHandler.cs
  11. 69
      ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs
  12. 15
      ErsatzTV.Application/Plex/Commands/CallPlexNetworkScannerHandler.cs
  13. 52
      ErsatzTV.Application/Plex/Commands/CallPlexShowScannerHandler.cs
  14. 10
      ErsatzTV.Core/Interfaces/Metadata/IScannerProxyService.cs
  15. 10
      ErsatzTV.Core/MediaSources/ScannerProgressUpdate.cs
  16. 6
      ErsatzTV.Core/Metadata/ScannerProgress.cs
  17. 52
      ErsatzTV.Core/Metadata/ScannerProxyService.cs
  18. 2
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  19. 37
      ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs
  20. 2
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryById.cs
  21. 27
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs
  22. 2
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowById.cs
  23. 12
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs
  24. 2
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs
  25. 26
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs
  26. 2
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowById.cs
  27. 12
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs
  28. 2
      ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibrary.cs
  29. 31
      ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs
  30. 15
      ErsatzTV.Scanner/Application/MediaSources/Commands/ScannerProgressUpdateHandler.cs
  31. 2
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryById.cs
  32. 26
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs
  33. 2
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowById.cs
  34. 12
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowByIdHandler.cs
  35. 15
      ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs
  36. 5
      ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs
  37. 5
      ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs
  38. 12
      ErsatzTV.Scanner/Core/Interfaces/IScannerProxy.cs
  39. 15
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs
  40. 5
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  41. 5
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  42. 56
      ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs
  43. 62
      ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs
  44. 62
      ErsatzTV.Scanner/Core/Metadata/MediaServerOtherVideoLibraryScanner.cs
  45. 101
      ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  46. 47
      ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs
  47. 92
      ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs
  48. 57
      ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs
  49. 57
      ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs
  50. 57
      ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs
  51. 86
      ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs
  52. 15
      ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs
  53. 5
      ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs
  54. 10
      ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs
  55. 5
      ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs
  56. 5
      ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs
  57. 90
      ErsatzTV.Scanner/Core/ScannerProxy.cs
  58. 5
      ErsatzTV.Scanner/Program.cs
  59. 60
      ErsatzTV.Scanner/Worker.cs
  60. 53
      ErsatzTV/Controllers/Api/ScannerController.cs
  61. 2
      ErsatzTV/ErsatzTV.csproj
  62. 25
      ErsatzTV/Pages/Libraries.razor
  63. 140
      ErsatzTV/Services/SearchIndexService.cs
  64. 4
      ErsatzTV/Startup.cs

1
CHANGELOG.md

@ -51,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

17
ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs

@ -1,5 +1,4 @@ @@ -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<Synchr @@ -17,18 +16,16 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
public CallEmbyCollectionScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, runtimeInfo)
{
}
public async Task<Either<BaseError, Unit>>
Handle(SynchronizeEmbyCollections request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -40,7 +37,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr @@ -40,7 +37,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
});
}
protected override async Task<DateTimeOffset> GetLastScan(
protected override async Task<Tuple<string, DateTimeOffset>> GetLastScan(
TvContext dbContext,
SynchronizeEmbyCollections request,
CancellationToken cancellationToken)
@ -49,7 +46,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr @@ -49,7 +46,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
.SelectOneAsync(l => 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, DateTimeOffset>(string.Empty, new DateTimeOffset(minDateTime, TimeSpan.Zero));
}
protected override bool ScanIsRequired(
@ -67,7 +64,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr @@ -67,7 +64,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
}
private async Task<Either<BaseError, Unit>> PerformScan(
string scanner,
ScanParameters parameters,
SynchronizeEmbyCollections request,
CancellationToken cancellationToken)
{
@ -81,6 +78,6 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr @@ -81,6 +78,6 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
arguments.Add("--force");
}
return await base.PerformScan(scanner, arguments, cancellationToken).MapT(_ => Unit.Default);
return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default);
}
}

69
ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs

@ -1,8 +1,9 @@ @@ -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<ISynchron @@ -15,14 +16,16 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
{
private readonly IScannerProxyService _scannerProxyService;
public CallEmbyLibraryScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IScannerProxyService scannerProxyService,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
: base(dbContextFactory, configElementRepository, runtimeInfo)
{
_scannerProxyService = scannerProxyService;
}
Task<Either<BaseError, string>> IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>.Handle(
@ -38,9 +41,9 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron @@ -38,9 +41,9 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
ISynchronizeEmbyLibraryById request,
CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -53,38 +56,58 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron @@ -53,38 +56,58 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
}
private async Task<Either<BaseError, string>> PerformScan(
string scanner,
ScanParameters parameters,
ISynchronizeEmbyLibraryById request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
Option<Guid> maybeScanId = _scannerProxyService.StartScan(request.EmbyLibraryId);
foreach (var scanId in maybeScanId)
{
"scan-emby", request.EmbyLibraryId.ToString(CultureInfo.InvariantCulture)
};
try
{
var arguments = new List<string>
{
"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<DateTimeOffset> GetLastScan(
protected override async Task<Tuple<string, DateTimeOffset>> 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<EmbyLibrary> 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<string, DateTimeOffset>(libraryName, new DateTimeOffset(minDateTime, TimeSpan.Zero));
}
protected override bool ScanIsRequired(

52
ErsatzTV.Application/Emby/Commands/CallEmbyShowScannerHandler.cs

@ -1,8 +1,8 @@ @@ -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; @@ -13,14 +13,16 @@ namespace ErsatzTV.Application.Emby;
public class CallEmbyShowScannerHandler : CallLibraryScannerHandler<SynchronizeEmbyShowById>,
IRequestHandler<SynchronizeEmbyShowById, Either<BaseError, string>>
{
private readonly IScannerProxyService _scannerProxyService;
public CallEmbyShowScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IScannerProxyService scannerProxyService,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
: base(dbContextFactory, configElementRepository, runtimeInfo)
{
_scannerProxyService = scannerProxyService;
}
Task<Either<BaseError, string>> IRequestHandler<SynchronizeEmbyShowById, Either<BaseError, string>>.Handle(
@ -31,9 +33,9 @@ public class CallEmbyShowScannerHandler : CallLibraryScannerHandler<SynchronizeE @@ -31,9 +33,9 @@ public class CallEmbyShowScannerHandler : CallLibraryScannerHandler<SynchronizeE
SynchronizeEmbyShowById request,
CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -46,30 +48,44 @@ public class CallEmbyShowScannerHandler : CallLibraryScannerHandler<SynchronizeE @@ -46,30 +48,44 @@ public class CallEmbyShowScannerHandler : CallLibraryScannerHandler<SynchronizeE
}
private async Task<Either<BaseError, string>> PerformScan(
string scanner,
ScanParameters parameters,
SynchronizeEmbyShowById request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
Option<Guid> 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<string>
{
"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<DateTimeOffset> GetLastScan(
protected override Task<Tuple<string, DateTimeOffset>> GetLastScan(
TvContext dbContext,
SynchronizeEmbyShowById request,
CancellationToken cancellationToken) =>
Task.FromResult(DateTimeOffset.MinValue);
Task.FromResult(new Tuple<string, DateTimeOffset>(string.Empty, DateTimeOffset.MinValue));
protected override bool ScanIsRequired(
DateTimeOffset lastScan,

17
ErsatzTV.Application/Jellyfin/Commands/CallJellyfinCollectionScannerHandler.cs

@ -1,5 +1,4 @@ @@ -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<Sy @@ -17,18 +16,16 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy
public CallJellyfinCollectionScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, runtimeInfo)
{
}
public async Task<Either<BaseError, Unit>>
Handle(SynchronizeJellyfinCollections request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -40,7 +37,7 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy @@ -40,7 +37,7 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy
});
}
protected override async Task<DateTimeOffset> GetLastScan(
protected override async Task<Tuple<string, DateTimeOffset>> GetLastScan(
TvContext dbContext,
SynchronizeJellyfinCollections request,
CancellationToken cancellationToken)
@ -49,7 +46,7 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy @@ -49,7 +46,7 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy
.SelectOneAsync(l => 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, DateTimeOffset>(string.Empty, new DateTimeOffset(minDateTime, TimeSpan.Zero));
}
protected override bool ScanIsRequired(
@ -67,7 +64,7 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy @@ -67,7 +64,7 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy
}
private async Task<Either<BaseError, Unit>> PerformScan(
string scanner,
ScanParameters parameters,
SynchronizeJellyfinCollections request,
CancellationToken cancellationToken)
{
@ -81,6 +78,6 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy @@ -81,6 +78,6 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy
arguments.Add("--force");
}
return await base.PerformScan(scanner, arguments, cancellationToken).MapT(_ => Unit.Default);
return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default);
}
}

69
ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs

@ -1,8 +1,9 @@ @@ -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<ISync @@ -15,14 +16,16 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
IRequestHandler<ForceSynchronizeJellyfinLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizeJellyfinLibraryByIdIfNeeded, Either<BaseError, string>>
{
private readonly IScannerProxyService _scannerProxyService;
public CallJellyfinLibraryScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IScannerProxyService scannerProxyService,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
: base(dbContextFactory, configElementRepository, runtimeInfo)
{
_scannerProxyService = scannerProxyService;
}
Task<Either<BaseError, string>> IRequestHandler<ForceSynchronizeJellyfinLibraryById, Either<BaseError, string>>.
@ -39,9 +42,9 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync @@ -39,9 +42,9 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
ISynchronizeJellyfinLibraryById request,
CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -54,38 +57,58 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync @@ -54,38 +57,58 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
}
private async Task<Either<BaseError, string>> PerformScan(
string scanner,
ScanParameters parameters,
ISynchronizeJellyfinLibraryById request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
Option<Guid> maybeScanId = _scannerProxyService.StartScan(request.JellyfinLibraryId);
foreach (var scanId in maybeScanId)
{
"scan-jellyfin", request.JellyfinLibraryId.ToString(CultureInfo.InvariantCulture)
};
try
{
var arguments = new List<string>
{
"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<DateTimeOffset> GetLastScan(
protected override async Task<Tuple<string, DateTimeOffset>> 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<JellyfinLibrary> 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<string, DateTimeOffset>(libraryName, new DateTimeOffset(minDateTime, TimeSpan.Zero));
}
protected override bool ScanIsRequired(

52
ErsatzTV.Application/Jellyfin/Commands/CallJellyfinShowScannerHandler.cs

@ -1,8 +1,8 @@ @@ -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; @@ -13,14 +13,16 @@ namespace ErsatzTV.Application.Jellyfin;
public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler<SynchronizeJellyfinShowById>,
IRequestHandler<SynchronizeJellyfinShowById, Either<BaseError, string>>
{
private readonly IScannerProxyService _scannerProxyService;
public CallJellyfinShowScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IScannerProxyService scannerProxyService,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
: base(dbContextFactory, configElementRepository, runtimeInfo)
{
_scannerProxyService = scannerProxyService;
}
Task<Either<BaseError, string>> IRequestHandler<SynchronizeJellyfinShowById, Either<BaseError, string>>.Handle(
@ -31,9 +33,9 @@ public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler<Synchron @@ -31,9 +33,9 @@ public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler<Synchron
SynchronizeJellyfinShowById request,
CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -46,30 +48,44 @@ public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler<Synchron @@ -46,30 +48,44 @@ public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler<Synchron
}
private async Task<Either<BaseError, string>> PerformScan(
string scanner,
ScanParameters parameters,
SynchronizeJellyfinShowById request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
Option<Guid> 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<string>
{
"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<DateTimeOffset> GetLastScan(
protected override Task<Tuple<string, DateTimeOffset>> GetLastScan(
TvContext dbContext,
SynchronizeJellyfinShowById request,
CancellationToken cancellationToken) =>
Task.FromResult(DateTimeOffset.MinValue);
Task.FromResult(new Tuple<string, DateTimeOffset>(string.Empty, DateTimeOffset.MinValue));
protected override bool ScanIsRequired(
DateTimeOffset lastScan,

118
ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs

@ -1,17 +1,12 @@ @@ -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; @@ -19,34 +14,15 @@ using Serilog.Formatting.Compact.Reader;
namespace ErsatzTV.Application.Libraries;
public abstract class CallLibraryScannerHandler<TRequest>
public abstract class CallLibraryScannerHandler<TRequest>(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
IRuntimeInfo runtimeInfo)
{
private const int BatchSize = 100;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _channel;
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediator _mediator;
private readonly IRuntimeInfo _runtimeInfo;
private readonly List<int> _toReindex = [];
private readonly List<int> _toRemove = [];
private string _libraryName;
protected CallLibraryScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> 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<Either<BaseError, string>> PerformScan(
string scanner,
ScanParameters parameters,
List<string> arguments,
CancellationToken cancellationToken)
{
@ -57,38 +33,24 @@ public abstract class CallLibraryScannerHandler<TRequest> @@ -57,38 +33,24 @@ public abstract class CallLibraryScannerHandler<TRequest>
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<TRequest> @@ -125,76 +87,32 @@ public abstract class CallLibraryScannerHandler<TRequest>
}
}
private async Task ProcessProgressOutput(string s)
{
if (!string.IsNullOrWhiteSpace(s))
{
try
{
ScannerProgressUpdate progressUpdate = JsonConvert.DeserializeObject<ScannerProgressUpdate>(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<DateTimeOffset> GetLastScan(
protected abstract Task<Tuple<string, DateTimeOffset>> GetLastScan(
TvContext dbContext,
TRequest request,
CancellationToken cancellationToken);
protected abstract bool ScanIsRequired(DateTimeOffset lastScan, int libraryRefreshInterval, TRequest request);
protected async Task<Validation<BaseError, string>> Validate(TRequest request, CancellationToken cancellationToken)
protected async Task<Validation<BaseError, ScanParameters>> Validate(TRequest request, CancellationToken cancellationToken)
{
try
{
int libraryRefreshInterval = await _configElementRepository
int libraryRefreshInterval = await configElementRepository
.GetValue<int>(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<TRequest> @@ -211,7 +129,7 @@ public abstract class CallLibraryScannerHandler<TRequest>
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<TRequest> @@ -222,4 +140,6 @@ public abstract class CallLibraryScannerHandler<TRequest>
return BaseError.New("Scan was canceled");
}
}
protected sealed record ScanParameters(string LibraryName, string Scanner);
}

54
ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs

@ -1,12 +1,13 @@ @@ -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<IScanLoc @@ -15,14 +16,16 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>,
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>
{
private readonly IScannerProxyService _scannerProxyService;
public CallLocalLibraryScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IScannerProxyService scannerProxyService,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
: base(dbContextFactory, configElementRepository, runtimeInfo)
{
_scannerProxyService = scannerProxyService;
}
Task<Either<BaseError, string>> IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>.Handle(
@ -35,9 +38,9 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc @@ -35,9 +38,9 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
private async Task<Either<BaseError, string>> Handle(IScanLocalLibrary request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -50,24 +53,39 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc @@ -50,24 +53,39 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
}
private async Task<Either<BaseError, string>> PerformScan(
string scanner,
ScanParameters parameters,
IScanLocalLibrary request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
Option<Guid> maybeScanId = _scannerProxyService.StartScan(request.LibraryId);
foreach (var scanId in maybeScanId)
{
"scan-local", request.LibraryId.ToString(CultureInfo.InvariantCulture)
};
try
{
var arguments = new List<string>
{
"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<DateTimeOffset> GetLastScan(
protected override async Task<Tuple<string, DateTimeOffset>> GetLastScan(
TvContext dbContext,
IScanLocalLibrary request,
CancellationToken cancellationToken)
@ -80,7 +98,11 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc @@ -80,7 +98,11 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
? libraryPaths.Min(lp => 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<string, DateTimeOffset>(libraryName, new DateTimeOffset(minDateTime, TimeSpan.Zero));
}
protected override bool ScanIsRequired(

17
ErsatzTV.Application/Plex/Commands/CallPlexCollectionScannerHandler.cs

@ -1,5 +1,4 @@ @@ -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<Synchr @@ -17,18 +16,16 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr
public CallPlexCollectionScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, runtimeInfo)
{
}
public async Task<Either<BaseError, Unit>>
Handle(SynchronizePlexCollections request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -40,7 +37,7 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr @@ -40,7 +37,7 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr
});
}
protected override async Task<DateTimeOffset> GetLastScan(
protected override async Task<Tuple<string, DateTimeOffset>> GetLastScan(
TvContext dbContext,
SynchronizePlexCollections request,
CancellationToken cancellationToken)
@ -49,7 +46,7 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr @@ -49,7 +46,7 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr
.SelectOneAsync(l => 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, DateTimeOffset>(string.Empty, new DateTimeOffset(minDateTime, TimeSpan.Zero));
}
protected override bool ScanIsRequired(
@ -67,7 +64,7 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr @@ -67,7 +64,7 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr
}
private async Task<Either<BaseError, Unit>> PerformScan(
string scanner,
ScanParameters parameters,
SynchronizePlexCollections request,
CancellationToken cancellationToken)
{
@ -81,6 +78,6 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr @@ -81,6 +78,6 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr
arguments.Add("--force");
}
return await base.PerformScan(scanner, arguments, cancellationToken).MapT(_ => Unit.Default);
return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default);
}
}

69
ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs

@ -1,8 +1,9 @@ @@ -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<ISynchron @@ -15,14 +16,16 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
IRequestHandler<ForceSynchronizePlexLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>
{
private readonly IScannerProxyService _scannerProxyService;
public CallPlexLibraryScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IScannerProxyService scannerProxyService,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
: base(dbContextFactory, configElementRepository, runtimeInfo)
{
_scannerProxyService = scannerProxyService;
}
Task<Either<BaseError, string>> IRequestHandler<ForceSynchronizePlexLibraryById, Either<BaseError, string>>.Handle(
@ -38,9 +41,9 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron @@ -38,9 +41,9 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
ISynchronizePlexLibraryById request,
CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -53,38 +56,58 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron @@ -53,38 +56,58 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
}
private async Task<Either<BaseError, string>> PerformScan(
string scanner,
ScanParameters parameters,
ISynchronizePlexLibraryById request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
Option<Guid> maybeScanId = _scannerProxyService.StartScan(request.PlexLibraryId);
foreach (var scanId in maybeScanId)
{
"scan-plex", request.PlexLibraryId.ToString(CultureInfo.InvariantCulture)
};
try
{
var arguments = new List<string>
{
"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<DateTimeOffset> GetLastScan(
protected override async Task<Tuple<string, DateTimeOffset>> 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<PlexLibrary> 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<string, DateTimeOffset>(libraryName, new DateTimeOffset(minDateTime, TimeSpan.Zero));
}
protected override bool ScanIsRequired(

15
ErsatzTV.Application/Plex/Commands/CallPlexNetworkScannerHandler.cs

@ -1,5 +1,4 @@ @@ -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<Synchroni @@ -18,16 +17,14 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni
public CallPlexNetworkScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, runtimeInfo)
{
}
public async Task<Either<BaseError, Unit>>
Handle(SynchronizePlexNetworks request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> validation = await Validate(request, cancellationToken);
return await validation.Match(
scanner => PerformScan(scanner, request, cancellationToken),
error =>
@ -41,7 +38,7 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni @@ -41,7 +38,7 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni
});
}
protected override async Task<DateTimeOffset> GetLastScan(
protected override async Task<Tuple<string, DateTimeOffset>> GetLastScan(
TvContext dbContext,
SynchronizePlexNetworks request,
CancellationToken cancellationToken)
@ -51,7 +48,7 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni @@ -51,7 +48,7 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni
.SelectOneAsync(l => 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, DateTimeOffset>(string.Empty, new DateTimeOffset(minDateTime, TimeSpan.Zero));
}
protected override bool ScanIsRequired(
@ -69,7 +66,7 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni @@ -69,7 +66,7 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni
}
private async Task<Either<BaseError, Unit>> PerformScan(
string scanner,
ScanParameters parameters,
SynchronizePlexNetworks request,
CancellationToken cancellationToken)
{
@ -83,6 +80,6 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni @@ -83,6 +80,6 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni
arguments.Add("--force");
}
return await base.PerformScan(scanner, arguments, cancellationToken).MapT(_ => Unit.Default);
return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default);
}
}

52
ErsatzTV.Application/Plex/Commands/CallPlexShowScannerHandler.cs

@ -1,8 +1,8 @@ @@ -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; @@ -13,14 +13,16 @@ namespace ErsatzTV.Application.Plex;
public class CallPlexShowScannerHandler : CallLibraryScannerHandler<SynchronizePlexShowById>,
IRequestHandler<SynchronizePlexShowById, Either<BaseError, string>>
{
private readonly IScannerProxyService _scannerProxyService;
public CallPlexShowScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IScannerProxyService scannerProxyService,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
: base(dbContextFactory, configElementRepository, runtimeInfo)
{
_scannerProxyService = scannerProxyService;
}
Task<Either<BaseError, string>> IRequestHandler<SynchronizePlexShowById, Either<BaseError, string>>.Handle(
@ -31,9 +33,9 @@ public class CallPlexShowScannerHandler : CallLibraryScannerHandler<SynchronizeP @@ -31,9 +33,9 @@ public class CallPlexShowScannerHandler : CallLibraryScannerHandler<SynchronizeP
SynchronizePlexShowById request,
CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request, cancellationToken);
Validation<BaseError, ScanParameters> 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<ScanIsNotRequired>())
@ -46,30 +48,44 @@ public class CallPlexShowScannerHandler : CallLibraryScannerHandler<SynchronizeP @@ -46,30 +48,44 @@ public class CallPlexShowScannerHandler : CallLibraryScannerHandler<SynchronizeP
}
private async Task<Either<BaseError, string>> PerformScan(
string scanner,
ScanParameters parameters,
SynchronizePlexShowById request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
Option<Guid> 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<string>
{
"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<DateTimeOffset> GetLastScan(
protected override Task<Tuple<string, DateTimeOffset>> GetLastScan(
TvContext dbContext,
SynchronizePlexShowById request,
CancellationToken cancellationToken) =>
Task.FromResult(DateTimeOffset.MinValue);
Task.FromResult(new Tuple<string, DateTimeOffset>(string.Empty, DateTimeOffset.MinValue));
protected override bool ScanIsRequired(
DateTimeOffset lastScan,

10
ErsatzTV.Core/Interfaces/Metadata/IScannerProxyService.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Interfaces.Metadata;
public interface IScannerProxyService
{
Option<Guid> StartScan(int libraryId);
void EndScan(Guid scanId);
Task Progress(Guid scanId, decimal percentComplete);
bool IsActive(Guid scanId);
Option<decimal> GetProgress(int libraryId);
}

10
ErsatzTV.Core/MediaSources/ScannerProgressUpdate.cs

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
using MediatR;
namespace ErsatzTV.Core.MediaSources;
public record ScannerProgressUpdate(
int LibraryId,
string LibraryName,
decimal? PercentComplete,
int[] ItemsToReindex,
int[] ItemsToRemove) : INotification;

6
ErsatzTV.Core/Metadata/ScannerProgress.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Metadata;
public class ScannerProgress
{
public decimal Progress { get; set; }
}

52
ErsatzTV.Core/Metadata/ScannerProxyService.cs

@ -0,0 +1,52 @@ @@ -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<int, decimal> _activeLibraries = [];
private readonly ConcurrentDictionary<Guid, int> _scans = new();
public Option<Guid> StartScan(int libraryId)
{
if (!_activeLibraries.TryAdd(libraryId, 0))
{
return Option<Guid>.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<decimal> GetProgress(int libraryId) => _activeLibraries.TryGetValue(libraryId, out decimal progress)
? progress
: Option<decimal>.None;
}

2
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -38,7 +38,7 @@ @@ -38,7 +38,7 @@
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="SkiaSharp" Version="3.119.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="TimeZoneConverter" Version="7.1.0" />
<PackageReference Include="TimeZoneConverter" Version="7.2.0" />
</ItemGroup>
<ItemGroup>

37
ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs

@ -7,14 +7,15 @@ using ErsatzTV.Core.Interfaces.Images; @@ -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 @@ -38,6 +39,20 @@ public class MovieFolderScannerTests
? @"C:\bin\ffprobe.exe"
: "/bin/ffprobe";
private static readonly ILogger<MovieFolderScanner> Logger;
static MovieFolderScannerTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
Logger = loggerFactory.CreateLogger<MovieFolderScanner>();
}
[TestFixture]
public class ScanFolder
{
@ -75,6 +90,11 @@ public class MovieFolderScannerTests @@ -75,6 +90,11 @@ public class MovieFolderScannerTests
_libraryRepository = Substitute.For<ILibraryRepository>();
_libraryRepository.GetOrAddFolder(Arg.Any<LibraryPath>(), Arg.Any<Option<int>>(), Arg.Any<string>())
.Returns(new LibraryFolder());
_scannerProxy = Substitute.For<IScannerProxy>();
_scannerProxy.UpdateProgress(Arg.Any<decimal>(), Arg.Any<CancellationToken>()).Returns(true);
_scannerProxy.ReindexMediaItems(Arg.Any<int[]>(), Arg.Any<CancellationToken>()).Returns(true);
_scannerProxy.RemoveMediaItems(Arg.Any<int[]>(), Arg.Any<CancellationToken>()).Returns(true);
}
private IMovieRepository _movieRepository;
@ -83,6 +103,7 @@ public class MovieFolderScannerTests @@ -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 @@ -699,7 +720,8 @@ public class MovieFolderScannerTests
private MovieFolderScanner GetService(params FakeFileEntry[] files) =>
new(
new FakeLocalFileSystem(new List<FakeFileEntry>(files)),
_scannerProxy,
new FakeLocalFileSystem([..files]),
_movieRepository,
_localStatisticsProvider,
Substitute.For<ILocalSubtitlesProvider>(),
@ -709,16 +731,15 @@ public class MovieFolderScannerTests @@ -709,16 +731,15 @@ public class MovieFolderScannerTests
_imageCache,
_libraryRepository,
_mediaItemRepository,
Substitute.For<IMediator>(),
Substitute.For<IFFmpegPngService>(),
Substitute.For<ITempFilePool>(),
Substitute.For<IClient>(),
Substitute.For<ILogger<MovieFolderScanner>>()
);
Logger);
private MovieFolderScanner GetService(params FakeFolderEntry[] folders) =>
new(
new FakeLocalFileSystem(new List<FakeFileEntry>(), new List<FakeFolderEntry>(folders)),
_scannerProxy,
new FakeLocalFileSystem([], [..folders]),
_movieRepository,
_localStatisticsProvider,
Substitute.For<ILocalSubtitlesProvider>(),
@ -728,11 +749,9 @@ public class MovieFolderScannerTests @@ -728,11 +749,9 @@ public class MovieFolderScannerTests
_imageCache,
_libraryRepository,
_mediaItemRepository,
Substitute.For<IMediator>(),
Substitute.For<IFFmpegPngService>(),
Substitute.For<ITempFilePool>(),
Substitute.For<IClient>(),
Substitute.For<ILogger<MovieFolderScanner>>()
);
Logger);
}
}

2
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryById.cs

@ -2,5 +2,5 @@ @@ -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<Either<BaseError, string>>;

27
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs

@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain; @@ -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<SynchronizeEmby @@ -17,12 +17,11 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _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<SynchronizeEmby @@ -31,7 +30,7 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
IConfigElementRepository configElementRepository,
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
{
_mediator = mediator;
_scannerProxy = scannerProxy;
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_embyMovieLibraryScanner = embyMovieLibraryScanner;
@ -54,6 +53,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby @@ -54,6 +53,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
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)
@ -93,16 +94,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby @@ -93,16 +94,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
_logger.LogDebug("Skipping unforced scan of emby 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<int>(),
Array.Empty<int>()),
cancellationToken);
return parameters.Library.Name;
}
@ -117,7 +108,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby @@ -117,7 +108,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
embyLibrary,
request.ForceScan,
libraryRefreshInterval,
request.DeepScan
request.DeepScan,
request.BaseUrl
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -166,7 +158,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby @@ -166,7 +158,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
EmbyLibrary Library,
bool ForceScan,
int LibraryRefreshInterval,
bool DeepScan);
bool DeepScan,
string BaseUrl);
private record ConnectionParameters(EmbyConnection ActiveConnection)
{

2
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowById.cs

@ -2,5 +2,5 @@ using ErsatzTV.Core; @@ -2,5 +2,5 @@ using ErsatzTV.Core;
namespace ErsatzTV.Scanner.Application.Emby;
public record SynchronizeEmbyShowById(int EmbyLibraryId, int ShowId, bool DeepScan)
public record SynchronizeEmbyShowById(string BaseUrl, int EmbyLibraryId, int ShowId, bool DeepScan)
: IRequest<Either<BaseError, string>>;

12
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; @@ -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<SynchronizeEmbySho @@ -13,15 +14,18 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner;
private readonly IEmbyTelevisionRepository _embyTelevisionRepository;
private readonly ILogger<SynchronizeEmbyShowByIdHandler> _logger;
private readonly IScannerProxy _scannerProxy;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeEmbyShowByIdHandler(
IScannerProxy scannerProxy,
IMediaSourceRepository mediaSourceRepository,
IEmbyTelevisionRepository embyTelevisionRepository,
IEmbySecretStore embySecretStore,
IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner,
ILogger<SynchronizeEmbyShowByIdHandler> logger)
{
_scannerProxy = scannerProxy;
_mediaSourceRepository = mediaSourceRepository;
_embyTelevisionRepository = embyTelevisionRepository;
_embySecretStore = embySecretStore;
@ -48,6 +52,8 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho @@ -48,6 +52,8 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho
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 Emby library {LibraryName}",
parameters.ShowTitle,
@ -81,7 +87,8 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho @@ -81,7 +87,8 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho
embyLibrary,
showTitleItemId.ItemId,
showTitleItemId.Title,
request.DeepScan
request.DeepScan,
request.BaseUrl
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -132,7 +139,8 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho @@ -132,7 +139,8 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho
EmbyLibrary Library,
string ItemId,
string ShowTitle,
bool DeepScan);
bool DeepScan,
string BaseUrl);
private record ConnectionParameters(EmbyConnection ActiveConnection)
{

2
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs

@ -2,5 +2,5 @@ @@ -2,5 +2,5 @@
namespace ErsatzTV.Scanner.Application.Jellyfin;
public record SynchronizeJellyfinLibraryById(int JellyfinLibraryId, bool ForceScan, bool DeepScan)
public record SynchronizeJellyfinLibraryById(string BaseUrl, int JellyfinLibraryId, bool ForceScan, bool DeepScan)
: IRequest<Either<BaseError, string>>;

26
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs

@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain; @@ -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 @@ -20,10 +20,10 @@ public class
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizeJellyfinLibraryByIdHandler> _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 @@ -32,7 +32,7 @@ public class
IConfigElementRepository configElementRepository,
ILogger<SynchronizeJellyfinLibraryByIdHandler> logger)
{
_mediator = mediator;
_scannerProxy = scannerProxy;
_mediaSourceRepository = mediaSourceRepository;
_jellyfinSecretStore = jellyfinSecretStore;
_jellyfinMovieLibraryScanner = jellyfinMovieLibraryScanner;
@ -55,6 +55,8 @@ public class @@ -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 @@ -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<int>(),
Array.Empty<int>()),
cancellationToken);
return parameters.Library.Name;
}
@ -118,7 +110,8 @@ public class @@ -118,7 +110,8 @@ public class
jellyfinLibrary,
request.ForceScan,
libraryRefreshInterval,
request.DeepScan
request.DeepScan,
request.BaseUrl
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -166,7 +159,8 @@ public class @@ -166,7 +159,8 @@ public class
JellyfinLibrary Library,
bool ForceScan,
int LibraryRefreshInterval,
bool DeepScan);
bool DeepScan,
string BaseUrl);
private record ConnectionParameters(JellyfinConnection ActiveConnection)
{

2
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowById.cs

@ -2,5 +2,5 @@ using ErsatzTV.Core; @@ -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<Either<BaseError, string>>;

12
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; @@ -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 @@ -14,15 +15,18 @@ public class
private readonly IJellyfinTelevisionLibraryScanner _jellyfinTelevisionLibraryScanner;
private readonly IJellyfinTelevisionRepository _jellyfinTelevisionRepository;
private readonly ILogger<SynchronizeJellyfinShowByIdHandler> _logger;
private readonly IScannerProxy _scannerProxy;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeJellyfinShowByIdHandler(
IScannerProxy scannerProxy,
IMediaSourceRepository mediaSourceRepository,
IJellyfinTelevisionRepository jellyfinTelevisionRepository,
IJellyfinSecretStore jellyfinSecretStore,
IJellyfinTelevisionLibraryScanner jellyfinTelevisionLibraryScanner,
ILogger<SynchronizeJellyfinShowByIdHandler> logger)
{
_scannerProxy = scannerProxy;
_mediaSourceRepository = mediaSourceRepository;
_jellyfinTelevisionRepository = jellyfinTelevisionRepository;
_jellyfinSecretStore = jellyfinSecretStore;
@ -49,6 +53,8 @@ public class @@ -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 @@ -82,7 +88,8 @@ public class
jellyfinLibrary,
showTitleItemId.ItemId,
showTitleItemId.Title,
request.DeepScan
request.DeepScan,
request.BaseUrl
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -132,7 +139,8 @@ public class @@ -132,7 +139,8 @@ public class
JellyfinLibrary Library,
string ItemId,
string ShowTitle,
bool DeepScan);
bool DeepScan,
string BaseUrl);
private record ConnectionParameters(JellyfinConnection ActiveConnection)
{

2
ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibrary.cs

@ -2,4 +2,4 @@ @@ -2,4 +2,4 @@
namespace ErsatzTV.Scanner.Application.MediaSources;
public record ScanLocalLibrary(int LibraryId, bool ForceScan) : IRequest<Either<BaseError, string>>;
public record ScanLocalLibrary(string BaseUrl, int LibraryId, bool ForceScan) : IRequest<Either<BaseError, string>>;

31
ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs

@ -2,7 +2,7 @@ @@ -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<ScanLocalLibrary, Either< @@ -13,9 +13,9 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IImageFolderScanner _imageFolderScanner;
private readonly IScannerProxy _scannerProxy;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<ScanLocalLibraryHandler> _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<ScanLocalLibrary, Either< @@ -24,6 +24,7 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
private readonly ITelevisionFolderScanner _televisionFolderScanner;
public ScanLocalLibraryHandler(
IScannerProxy scannerProxy,
ILibraryRepository libraryRepository,
IConfigElementRepository configElementRepository,
IMovieFolderScanner movieFolderScanner,
@ -33,9 +34,9 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either< @@ -33,9 +34,9 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
ISongFolderScanner songFolderScanner,
IImageFolderScanner imageFolderScanner,
IRemoteStreamFolderScanner remoteStreamFolderScanner,
IMediator mediator,
ILogger<ScanLocalLibraryHandler> logger)
{
_scannerProxy = scannerProxy;
_libraryRepository = libraryRepository;
_configElementRepository = configElementRepository;
_movieFolderScanner = movieFolderScanner;
@ -45,7 +46,6 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either< @@ -45,7 +46,6 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
_songFolderScanner = songFolderScanner;
_imageFolderScanner = imageFolderScanner;
_remoteStreamFolderScanner = remoteStreamFolderScanner;
_mediator = mediator;
_logger = logger;
}
@ -57,11 +57,13 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either< @@ -57,11 +57,13 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
private async Task<Unit> 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<ScanLocalLibrary, Either< @@ -145,14 +147,7 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
}
}
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
localLibrary.Name,
progressMax,
Array.Empty<int>(),
Array.Empty<int>()),
cancellationToken);
await _scannerProxy.UpdateProgress(progressMax, cancellationToken);
}
sw.Stop();
@ -171,10 +166,6 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either< @@ -171,10 +166,6 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
localLibrary.Name);
}
await _mediator.Publish(
new ScannerProgressUpdate(localLibrary.Id, localLibrary.Name, 0, [], []),
cancellationToken);
return Unit.Default;
}
@ -193,7 +184,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either< @@ -193,7 +184,8 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
ffprobePath,
ffmpegPath,
request.ForceScan,
libraryRefreshInterval));
libraryRefreshInterval,
request.BaseUrl));
}
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(ScanLocalLibrary request) =>
@ -223,5 +215,6 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either< @@ -223,5 +215,6 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
string FFprobePath,
string FFmpegPath,
bool ForceScan,
int LibraryRefreshInterval);
int LibraryRefreshInterval,
string BaseUrl);
}

15
ErsatzTV.Scanner/Application/MediaSources/Commands/ScannerProgressUpdateHandler.cs

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
using ErsatzTV.Core.MediaSources;
using Newtonsoft.Json;
namespace ErsatzTV.Scanner.Application.MediaSources;
public class ScannerProgressUpdateHandler : INotificationHandler<ScannerProgressUpdate>
{
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;
}
}

2
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryById.cs

@ -2,5 +2,5 @@ @@ -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<Either<BaseError, string>>;

26
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs

@ -2,8 +2,8 @@ using ErsatzTV.Core; @@ -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<SynchronizePlex @@ -14,14 +14,14 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizePlexLibraryByIdHandler> _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<SynchronizePlex @@ -31,7 +31,7 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex
ILibraryRepository libraryRepository,
ILogger<SynchronizePlexLibraryByIdHandler> logger)
{
_mediator = mediator;
_scannerProxy = scannerProxy;
_mediaSourceRepository = mediaSourceRepository;
_configElementRepository = configElementRepository;
_plexSecretStore = plexSecretStore;
@ -56,6 +56,8 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex @@ -56,6 +56,8 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex
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)
@ -104,16 +106,6 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex @@ -104,16 +106,6 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex
"Skipping unforced scan of plex 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<int>(),
Array.Empty<int>()),
cancellationToken);
return parameters.Library.Name;
}
@ -128,7 +120,8 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex @@ -128,7 +120,8 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex
plexLibrary,
request.ForceScan,
libraryRefreshInterval,
request.DeepScan
request.DeepScan,
request.BaseUrl
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -176,7 +169,8 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex @@ -176,7 +169,8 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex
PlexLibrary Library,
bool ForceScan,
int LibraryRefreshInterval,
bool DeepScan);
bool DeepScan,
string BaseUrl);
private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection)
{

2
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowById.cs

@ -2,5 +2,5 @@ using ErsatzTV.Core; @@ -2,5 +2,5 @@ using ErsatzTV.Core;
namespace ErsatzTV.Scanner.Application.Plex;
public record SynchronizePlexShowById(int PlexLibraryId, int ShowId, bool DeepScan)
public record SynchronizePlexShowById(string BaseUrl, int PlexLibraryId, int ShowId, bool DeepScan)
: IRequest<Either<BaseError, string>>;

12
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowByIdHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; @@ -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<SynchronizePlexSho @@ -13,15 +14,18 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlexSecretStore _plexSecretStore;
private readonly IPlexTelevisionLibraryScanner _plexTelevisionLibraryScanner;
private readonly IScannerProxy _scannerProxy;
private readonly IPlexTelevisionRepository _plexTelevisionRepository;
public SynchronizePlexShowByIdHandler(
IScannerProxy scannerProxy,
IPlexTelevisionRepository plexTelevisionRepository,
IMediaSourceRepository mediaSourceRepository,
IPlexSecretStore plexSecretStore,
IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner,
ILogger<SynchronizePlexShowByIdHandler> logger)
{
_scannerProxy = scannerProxy;
_plexTelevisionRepository = plexTelevisionRepository;
_mediaSourceRepository = mediaSourceRepository;
_plexSecretStore = plexSecretStore;
@ -48,6 +52,8 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho @@ -48,6 +52,8 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho
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 Plex library {LibraryName}",
parameters.ShowTitle,
@ -81,7 +87,8 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho @@ -81,7 +87,8 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho
plexLibrary,
titleKey.Key,
titleKey.Title,
request.DeepScan
request.DeepScan,
request.BaseUrl
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -131,7 +138,8 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho @@ -131,7 +138,8 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho
PlexLibrary Library,
string ShowKey,
string ShowTitle,
bool DeepScan);
bool DeepScan,
string BaseUrl);
private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection)
{

15
ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs

@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; @@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain;
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.Core.Emby;
@ -10,17 +11,17 @@ namespace ErsatzTV.Scanner.Core.Emby; @@ -10,17 +11,17 @@ namespace ErsatzTV.Scanner.Core.Emby;
public class EmbyCollectionScanner : IEmbyCollectionScanner
{
private readonly IEmbyApiClient _embyApiClient;
private readonly IScannerProxy _scannerProxy;
private readonly IEmbyCollectionRepository _embyCollectionRepository;
private readonly ILogger<EmbyCollectionScanner> _logger;
private readonly IMediator _mediator;
public EmbyCollectionScanner(
IMediator mediator,
IScannerProxy scannerProxy,
IEmbyCollectionRepository embyCollectionRepository,
IEmbyApiClient embyApiClient,
ILogger<EmbyCollectionScanner> logger)
{
_mediator = mediator;
_scannerProxy = scannerProxy;
_embyCollectionRepository = embyCollectionRepository;
_embyApiClient = embyApiClient;
_logger = logger;
@ -107,10 +108,10 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner @@ -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<int>()),
CancellationToken.None);
if (!await _scannerProxy.ReindexMediaItems(changedIds, CancellationToken.None))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
catch (Exception ex)
{

5
ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs

@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.Emby; @@ -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 : @@ -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 : @@ -33,10 +34,10 @@ public class EmbyMovieLibraryScanner :
IMetadataRepository metadataRepository,
ILogger<EmbyMovieLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
localChaptersProvider,
metadataRepository,
mediator,
logger)
{
_embyApiClient = embyApiClient;

5
ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Emby; @@ -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< @@ -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< @@ -31,13 +33,12 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
ILocalFileSystem localFileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
IMediator mediator,
ILogger<EmbyTelevisionLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
localChaptersProvider,
metadataRepository,
mediator,
logger)
{
_embyApiClient = embyApiClient;

12
ErsatzTV.Scanner/Core/Interfaces/IScannerProxy.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
namespace ErsatzTV.Scanner.Core.Interfaces;
public interface IScannerProxy
{
void SetBaseUrl(string baseUrl);
Task<bool> UpdateProgress(decimal progress, CancellationToken cancellationToken);
Task<bool> ReindexMediaItems(int[] mediaItemIds, CancellationToken cancellationToken);
Task<bool> RemoveMediaItems(int[] mediaItemIds, CancellationToken cancellationToken);
}

15
ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs

@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; @@ -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; @@ -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<JellyfinCollectionScanner> _logger;
private readonly IMediator _mediator;
public JellyfinCollectionScanner(
IMediator mediator,
IScannerProxy scannerProxy,
IJellyfinCollectionRepository jellyfinCollectionRepository,
IJellyfinApiClient jellyfinApiClient,
ILogger<JellyfinCollectionScanner> logger)
{
_mediator = mediator;
_scannerProxy = scannerProxy;
_jellyfinCollectionRepository = jellyfinCollectionRepository;
_jellyfinApiClient = jellyfinApiClient;
_logger = logger;
@ -111,10 +112,10 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner @@ -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<int>()),
CancellationToken.None);
if (!await _scannerProxy.ReindexMediaItems(changedIds, CancellationToken.None))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
catch (Exception ex)
{

5
ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -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 : @@ -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 : @@ -33,10 +34,10 @@ public class JellyfinMovieLibraryScanner :
IMetadataRepository metadataRepository,
ILogger<JellyfinMovieLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
localChaptersProvider,
metadataRepository,
mediator,
logger)
{
_jellyfinApiClient = jellyfinApiClient;

5
ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -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 @@ -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 @@ -32,13 +34,12 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
ILocalFileSystem localFileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
IMediator mediator,
ILogger<JellyfinTelevisionLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
localChaptersProvider,
metadataRepository,
mediator,
logger)
{
_jellyfinApiClient = jellyfinApiClient;

56
ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs

@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -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 @@ -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<ImageFolderScanner> _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 @@ -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 @@ -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<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(
@ -211,14 +209,10 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -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 @@ -236,27 +230,19 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
{
_logger.LogInformation("Flagging missing image at {Path}", path);
List<int> imageIds = await FlagFileNotFound(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
imageIds.ToArray(),
Array.Empty<int>()),
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<int> imageIds = await _imageRepository.DeleteByPath(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
Array.Empty<int>(),
imageIds.ToArray()),
cancellationToken);
if (!await _scannerProxy.RemoveMediaItems(imageIds.ToArray(), cancellationToken))
{
_logger.LogWarning("Failed to remove media items from scanner process");
}
}
}

62
ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs

@ -6,8 +6,8 @@ using ErsatzTV.Core.Errors; @@ -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<TConnectionParameters, TLib @@ -20,22 +20,22 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
where TEtag : MediaServerItemEtag
{
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository;
protected MediaServerMovieLibraryScanner(
IScannerProxy scannerProxy,
ILocalFileSystem localFileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
IMediator mediator,
ILogger logger)
{
_scannerProxy = scannerProxy;
_localFileSystem = localFileSystem;
_localChaptersProvider = localChaptersProvider;
_metadataRepository = metadataRepository;
_mediator = mediator;
_logger = logger;
}
@ -90,14 +90,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -90,14 +90,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
incomingItemIds.Add(MediaServerItemId(incoming));
decimal percentCompletion = Math.Clamp((decimal)incomingItemIds.Count / totalMovieCount, 0, 1);
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
library.Name,
percentCompletion,
Array.Empty<int>(),
Array.Empty<int>()),
cancellationToken);
if (!await _scannerProxy.UpdateProgress(percentCompletion, cancellationToken))
{
return new ScanCanceled();
}
string localPath = getLocalPath(incoming);
@ -198,14 +194,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -198,14 +194,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
if (result.IsAdded || result.IsUpdated)
{
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
null,
null,
new[] { result.Item.Id },
Array.Empty<int>()),
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<TConnectionParameters, TLib @@ -213,18 +205,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
// trash movies that are no longer present on the media server
var fileNotFoundItemIds = existingMovies.Keys.Except(incomingItemIds).ToList();
List<int> ids = await movieRepository.FlagFileNotFound(library, fileNotFoundItemIds);
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty<int>()),
cancellationToken);
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
library.Name,
0,
Array.Empty<int>(),
Array.Empty<int>()),
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<TConnectionParameters, TLib @@ -303,9 +287,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
{
foreach (int id in await movieRepository.FlagRemoteOnly(library, incoming))
{
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
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<TConnectionParameters, TLib @@ -315,9 +300,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
{
foreach (int id in await movieRepository.FlagUnavailable(library, incoming))
{
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
CancellationToken.None);
if (!await _scannerProxy.ReindexMediaItems([id], CancellationToken.None))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
}
}

62
ErsatzTV.Scanner/Core/Metadata/MediaServerOtherVideoLibraryScanner.cs

@ -6,8 +6,8 @@ using ErsatzTV.Core.Errors; @@ -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<TConnectionParameters, @@ -20,22 +20,22 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
where TEtag : MediaServerItemEtag
{
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository;
protected MediaServerOtherVideoLibraryScanner(
IScannerProxy scannerProxy,
ILocalFileSystem localFileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
IMediator mediator,
ILogger logger)
{
_scannerProxy = scannerProxy;
_localFileSystem = localFileSystem;
_localChaptersProvider = localChaptersProvider;
_metadataRepository = metadataRepository;
_mediator = mediator;
_logger = logger;
}
@ -91,14 +91,10 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters, @@ -91,14 +91,10 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
incomingItemIds.Add(MediaServerItemId(incoming));
decimal percentCompletion = Math.Clamp((decimal)incomingItemIds.Count / totalOtherVideoCount, 0, 1);
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
library.Name,
percentCompletion,
[],
[]),
cancellationToken);
if (!await _scannerProxy.UpdateProgress(percentCompletion, cancellationToken))
{
return new ScanCanceled();
}
string localPath = getLocalPath(incoming);
@ -205,14 +201,10 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters, @@ -205,14 +201,10 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
if (result.IsAdded || result.IsUpdated)
{
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
null,
null,
new[] { result.Item.Id },
Array.Empty<int>()),
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<TConnectionParameters, @@ -220,18 +212,10 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
// trash OtherVideo that are no longer present on the media server
var fileNotFoundItemIds = existingOtherVideos.Keys.Except(incomingItemIds).ToList();
List<int> ids = await otherVideoRepository.FlagFileNotFound(library, fileNotFoundItemIds);
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty<int>()),
cancellationToken);
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
library.Name,
0,
Array.Empty<int>(),
Array.Empty<int>()),
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<TConnectionParameters, @@ -310,9 +294,10 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
{
foreach (int id in await otherVideoRepository.FlagRemoteOnly(library, incoming))
{
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
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<TConnectionParameters, @@ -322,9 +307,10 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
{
foreach (int id in await otherVideoRepository.FlagUnavailable(library, incoming))
{
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
CancellationToken.None);
if (!await _scannerProxy.ReindexMediaItems([id], CancellationToken.None))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
}
}

101
ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -5,8 +5,8 @@ using ErsatzTV.Core.Errors; @@ -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<TConnectionParameters, @@ -22,22 +22,22 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
where TEtag : MediaServerItemEtag
{
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository;
protected MediaServerTelevisionLibraryScanner(
IScannerProxy scannerProxy,
ILocalFileSystem localFileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
IMediator mediator,
ILogger logger)
{
_scannerProxy = scannerProxy;
_localFileSystem = localFileSystem;
_localChaptersProvider = localChaptersProvider;
_metadataRepository = metadataRepository;
_mediator = mediator;
_logger = logger;
}
@ -103,14 +103,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -103,14 +103,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
incomingItemIds.Add(MediaServerItemId(incoming));
decimal percentCompletion = Math.Clamp((decimal)incomingItemIds.Count / totalShowCount, 0, 1);
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
library.Name,
percentCompletion,
Array.Empty<int>(),
Array.Empty<int>()),
cancellationToken);
if (!await _scannerProxy.UpdateProgress(percentCompletion, cancellationToken))
{
return new ScanCanceled();
}
Either<BaseError, MediaItemScanResult<TShow>> maybeShow = await televisionRepository
.GetOrAdd(library, incoming, cancellationToken)
@ -157,14 +153,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -157,14 +153,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
if (result.IsAdded || result.IsUpdated)
{
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
null,
null,
new[] { result.Item.Id },
Array.Empty<int>()),
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<TConnectionParameters, @@ -174,20 +166,12 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
// trash shows that are no longer present on the media server
var fileNotFoundItemIds = existingShows.Map(s => s.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await televisionRepository.FlagFileNotFoundShows(library, fileNotFoundItemIds, cancellationToken);
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty<int>()),
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<int>(),
Array.Empty<int>()),
cancellationToken);
return Unit.Default;
}
@ -358,14 +342,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -358,14 +342,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
if (result.IsAdded || result.IsUpdated || showIsUpdated)
{
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
null,
null,
new[] { result.Item.Id },
Array.Empty<int>()),
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<TConnectionParameters, @@ -373,9 +353,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
// trash seasons that are no longer present on the media server
var fileNotFoundItemIds = existingSeasons.Map(s => s.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await televisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundItemIds, cancellationToken);
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty<int>()),
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<TConnectionParameters, @@ -515,14 +496,11 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
if (result.IsAdded || result.IsUpdated || showIsUpdated)
{
await _mediator.Publish(
new ScannerProgressUpdate(
library.Id,
null,
null,
new[] { result.Item.Id },
Array.Empty<int>()),
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<TConnectionParameters, @@ -530,9 +508,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
// trash episodes that are no longer present on the media server
var fileNotFoundItemIds = existingEpisodes.Map(m => m.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await televisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundItemIds, cancellationToken);
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty<int>()),
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<TConnectionParameters, @@ -579,9 +558,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{
foreach (int id in await televisionRepository.FlagRemoteOnly(library, incoming, cancellationToken))
{
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, [id], []),
CancellationToken.None);
if (!await _scannerProxy.ReindexMediaItems([id], cancellationToken))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
}
}
@ -591,9 +571,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -591,9 +571,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{
foreach (int id in await televisionRepository.FlagUnavailable(library, incoming, cancellationToken))
{
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, [id], []),
CancellationToken.None);
if (!await _scannerProxy.ReindexMediaItems([id], cancellationToken))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
}
}

47
ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs

@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -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 MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -22,15 +22,16 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
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<MovieFolderScanner> _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 @@ -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 @@ -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 @@ -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 @@ -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<int>(),
Array.Empty<int>()),
cancellationToken);
cancellationToken))
{
return new ScanCanceled();
}
string movieFolder = folderQueue.Dequeue();
Option<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(
@ -198,14 +196,10 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -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<int>()),
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 @@ -219,17 +213,20 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
{
_logger.LogInformation("Flagging missing movie at {Path}", path);
List<int> ids = await FlagFileNotFound(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(libraryPath.LibraryId, null, null, ids.ToArray(), Array.Empty<int>()),
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<int> ids = await _movieRepository.DeleteByPath(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(libraryPath.LibraryId, null, null, Array.Empty<int>(), ids.ToArray()),
cancellationToken);
if (!await _scannerProxy.RemoveMediaItems(ids.ToArray(), cancellationToken))
{
_logger.LogWarning("Failed to remove media items from scanner process");
}
}
}

92
ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs

@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -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 @@ -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<MusicVideoFolderScanner> _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 @@ -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 @@ -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 @@ -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 @@ -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<int>(),
Array.Empty<int>()),
cancellationToken);
cancellationToken))
{
return new ScanCanceled();
}
Either<BaseError, MediaItemScanResult<Artist>> maybeArtist =
await FindOrCreateArtist(libraryPath.Id, artistFolder)
@ -141,14 +139,10 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -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<int>()),
cancellationToken);
if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
Either<BaseError, Unit> scanResult = await ScanMusicVideos(
@ -171,14 +165,10 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -171,14 +165,10 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{
_logger.LogInformation("Removing improperly named music video at {Path}", path);
List<int> musicVideoIds = await _musicVideoRepository.DeleteByPath(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
Array.Empty<int>(),
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 @@ -187,41 +177,29 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{
_logger.LogInformation("Flagging missing music video at {Path}", path);
List<int> musicVideoIds = await FlagFileNotFound(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
musicVideoIds.ToArray(),
Array.Empty<int>()),
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<int> musicVideoIds = await _musicVideoRepository.DeleteByPath(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
Array.Empty<int>(),
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<int> artistIds = await _artistRepository.DeleteEmptyArtists(libraryPath);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
Array.Empty<int>(),
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 @@ -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<int>()),
cancellationToken);
if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
}
}

57
ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs

@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -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 @@ -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<OtherVideoFolderScanner> _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 @@ -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 @@ -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 @@ -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<int>(),
Array.Empty<int>()),
cancellationToken);
cancellationToken))
{
return new ScanCanceled();
}
string otherVideoFolder = folderQueue.Dequeue();
Option<int> maybeParentFolder =
@ -205,14 +203,10 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -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<int>()),
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 @@ -230,27 +224,20 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
{
_logger.LogInformation("Flagging missing other video at {Path}", path);
List<int> otherVideoIds = await FlagFileNotFound(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
otherVideoIds.ToArray(),
Array.Empty<int>()),
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<int> otherVideoIds = await _otherVideoRepository.DeleteByPath(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
Array.Empty<int>(),
otherVideoIds.ToArray()),
cancellationToken);
if (!await _scannerProxy.RemoveMediaItems(otherVideoIds.ToArray(), cancellationToken))
{
_logger.LogWarning("Failed to remove media items from scanner process");
}
}
}

57
ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs

@ -8,9 +8,9 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -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 @@ -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<RemoteStreamFolderScanner> _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 @@ -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 @@ -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<int> maybeParentFolder =
@ -197,14 +195,11 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -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 @@ -222,27 +217,19 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
{
_logger.LogInformation("Flagging missing remote stream at {Path}", path);
List<int> 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<int> 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");
}
}
}

57
ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs

@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -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 @@ -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<SongFolderScanner> _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 @@ -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 @@ -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<int>(),
Array.Empty<int>()),
cancellationToken);
if (!await _scannerProxy.UpdateProgress(progressMin + percentCompletion * progressSpread, cancellationToken))
{
return new ScanCanceled();
}
string songFolder = folderQueue.Dequeue();
Option<int> maybeParentFolder =
@ -187,14 +183,11 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -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<int>()),
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 @@ -212,27 +205,19 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
{
_logger.LogInformation("Flagging missing song at {Path}", path);
List<int> songIds = await FlagFileNotFound(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
songIds.ToArray(),
Array.Empty<int>()),
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<int> songIds = await _songRepository.DeleteByPath(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
Array.Empty<int>(),
songIds.ToArray()),
cancellationToken);
if (!await _scannerProxy.RemoveMediaItems(songIds.ToArray(), cancellationToken))
{
_logger.LogWarning("Failed to remove media items from scanner process");
}
}
}

86
ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs

@ -8,8 +8,8 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -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 @@ -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<TelevisionFolderScanner> _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 @@ -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 @@ -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 @@ -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 @@ -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<int>(),
Array.Empty<int>()),
cancellationToken);
if (!await _scannerProxy.UpdateProgress(progressMin + percentCompletion * progressSpread, cancellationToken))
{
return new ScanCanceled();
}
Option<int> maybeParentFolder =
await _libraryRepository.GetParentFolderId(libraryPath, showFolder, cancellationToken);
@ -149,14 +145,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -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<int>()),
cancellationToken);
if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
Either<BaseError, Unit> scanResult = await ScanSeasons(
@ -182,14 +174,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -182,14 +174,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
_logger.LogInformation("Flagging missing episode at {Path}", path);
List<int> episodeIds = await FlagFileNotFound(libraryPath, path);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
episodeIds.ToArray(),
Array.Empty<int>()),
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 @@ -202,14 +190,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
await _televisionRepository.DeleteEmptySeasons(libraryPath);
List<int> ids = await _televisionRepository.DeleteEmptyShows(libraryPath);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
null,
null,
Array.Empty<int>(),
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 @@ -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<int>()),
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 @@ -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<int>()),
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 @@ -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))
};

15
ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs

@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Plex; @@ -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; @@ -11,17 +12,17 @@ namespace ErsatzTV.Scanner.Core.Plex;
public class PlexCollectionScanner : IPlexCollectionScanner
{
private readonly ILogger<PlexCollectionScanner> _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<PlexCollectionScanner> logger)
{
_mediator = mediator;
_scannerProxy = scannerProxy;
_plexCollectionRepository = plexCollectionRepository;
_plexServerApiClient = plexServerApiClient;
_logger = logger;
@ -112,10 +113,10 @@ public class PlexCollectionScanner : IPlexCollectionScanner @@ -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<int>()),
CancellationToken.None);
if (!await _scannerProxy.ReindexMediaItems(changedIds, CancellationToken.None))
{
_logger.LogWarning("Failed to reindex media items from scanner process");
}
}
catch (Exception ex)
{

5
ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs

@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.Plex; @@ -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 : @@ -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 : @@ -36,10 +37,10 @@ public class PlexMovieLibraryScanner :
ILocalChaptersProvider localChaptersProvider,
ILogger<PlexMovieLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
localChaptersProvider,
metadataRepository,
mediator,
logger)
{
_plexServerApiClient = plexServerApiClient;

10
ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs

@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Plex; @@ -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( @@ -12,7 +13,7 @@ public class PlexNetworkScanner(
IPlexServerApiClient plexServerApiClient,
IPlexTelevisionRepository plexTelevisionRepository,
ITelevisionRepository televisionRepository,
IMediator mediator,
IScannerProxy scannerProxy,
ILogger<PlexNetworkScanner> logger) : IPlexNetworkScanner
{
public async Task<Either<BaseError, Unit>> ScanNetworks(
@ -92,9 +93,10 @@ public class PlexNetworkScanner( @@ -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)
{

5
ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs

@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.Plex; @@ -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 : @@ -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 : @@ -36,10 +37,10 @@ public class PlexOtherVideoLibraryScanner :
ILocalChaptersProvider localChaptersProvider,
ILogger<PlexOtherVideoLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
localChaptersProvider,
metadataRepository,
mediator,
logger)
{
_plexServerApiClient = plexServerApiClient;

5
ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs

@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Plex; @@ -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 : @@ -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 : @@ -40,10 +41,10 @@ public partial class PlexTelevisionLibraryScanner :
ILocalChaptersProvider localChaptersProvider,
ILogger<PlexTelevisionLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
localChaptersProvider,
metadataRepository,
mediator,
logger)
{
_plexServerApiClient = plexServerApiClient;

90
ErsatzTV.Scanner/Core/ScannerProxy.cs

@ -0,0 +1,90 @@ @@ -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<bool> 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<bool> 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<bool> 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;
}
}

5
ErsatzTV.Scanner/Program.cs

@ -29,8 +29,10 @@ using ErsatzTV.Infrastructure.Plex; @@ -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 @@ -166,6 +168,8 @@ public class Program
TvContext.CaseInsensitiveCollation = "utf8mb4_general_ci";
}
services.AddHttpClient();
services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
services.AddScoped<IMetadataRepository, MetadataRepository>();
services.AddScoped<IMediaSourceRepository, MediaSourceRepository>();
@ -245,6 +249,7 @@ public class Program @@ -245,6 +249,7 @@ public class Program
services.AddSingleton<RecyclableMemoryStreamManager>();
// TODO: real bugsnag?
services.AddSingleton<IClient>(_ => new BugsnagNoopClient());
services.AddSingleton<IScannerProxy, ScannerProxy>();
services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining<Worker>());
services.AddMemoryCache();

60
ErsatzTV.Scanner/Worker.cs

@ -65,13 +65,19 @@ public class Worker : BackgroundService @@ -65,13 +65,19 @@ public class Worker : BackgroundService
{
Description = "The media source id to scan"
};
var baseUrlArgument = new Argument<string>("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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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<IMediator>();
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 @@ -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<IMediator>();
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 @@ -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<IMediator>();
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 @@ -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<IMediator>();
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 @@ -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<IMediator>();
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 @@ -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<IMediator>();
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 @@ -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<IMediator>();
var scan = new SynchronizeJellyfinShowById(libraryId, showId, deep);
var scan = new SynchronizeJellyfinShowById(baseUrl, libraryId, showId, deep);
await mediator.Send(scan, token);
}
});

53
ErsatzTV/Controllers/Api/ScannerController.cs

@ -0,0 +1,53 @@ @@ -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<ISearchIndexBackgroundServiceRequest> channelWriter)
{
[HttpPost("progress")]
[EndpointSummary("Scanner progress update")]
public async Task<IActionResult> 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<IActionResult> UpdateItems(
Guid scanId,
[FromBody] List<int> 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<IActionResult> RemoveItems(
Guid scanId,
[FromBody] List<int> itemsToRemove,
CancellationToken cancellationToken)
{
if (scannerProxyService.IsActive(scanId))
{
await channelWriter.WriteAsync(new RemoveMediaItems(itemsToRemove), cancellationToken);
}
return new OkResult();
}
}

2
ErsatzTV/ErsatzTV.csproj

@ -35,7 +35,7 @@ @@ -35,7 +35,7 @@
<PackageReference Include="Heron.MudCalendar" Version="3.2.0" />
<PackageReference Include="HtmlSanitizer" Version="9.0.886" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Markdig" Version="0.42.0" />
<PackageReference Include="Markdig" Version="0.43.0" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.10" />

25
ErsatzTV/Pages/Libraries.razor

@ -4,14 +4,16 @@ @@ -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<IScannerBackgroundServiceRequest> ScannerWorkerChannel;
@inject ChannelWriter<IScannerBackgroundServiceRequest> ScannerWorkerChannel
@inject ICourier Courier
@inject IScannerProxyService ScannerProxyService
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
@ -67,7 +69,7 @@ @@ -67,7 +69,7 @@
</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
@if (Locker.IsLibraryLocked(context.Id))
@if (IsLibraryLocked(context.Id))
{
<div style="width: 48px">
@if (_progressByLibrary[context.Id] > 0)
@ -190,7 +192,12 @@ @@ -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 @@ @@ -302,4 +309,16 @@
_cts?.Cancel();
_cts?.Dispose();
}
private bool IsLibraryLocked(int libraryId)
{
if (Locker.IsLibraryLocked(libraryId))
{
return true;
}
_progressByLibrary[libraryId] = 0;
return false;
}
}

140
ErsatzTV/Services/SearchIndexService.cs

@ -14,6 +14,11 @@ public class SearchIndexService : BackgroundService @@ -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<ISearchIndexBackgroundServiceRequest> channel,
IServiceScopeFactory serviceScopeFactory,
@ -35,39 +40,51 @@ public class SearchIndexService : BackgroundService @@ -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<IMediator>();
var batch = new Dictionary<int, SearchOperation>();
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<IClient>();
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 @@ -75,4 +92,81 @@ public class SearchIndexService : BackgroundService
_logger.LogInformation("Search index worker service shutting down");
}
}
private static void AddRequestToBatch(
ISearchIndexBackgroundServiceRequest request,
IDictionary<int, SearchOperation> 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<int, SearchOperation> batch, CancellationToken stoppingToken)
{
var idsToReindex = new List<int>();
var idsToRemove = new List<int>();
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<IMediator>();
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<IClient>();
client.Notify(ex);
}
catch (Exception)
{
// do nothing
}
}
}
}

4
ErsatzTV/Startup.cs

@ -557,6 +557,9 @@ public class Startup @@ -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 @@ -716,6 +719,7 @@ public class Startup
services.AddSingleton<ISearchIndex, LuceneSearchIndex>();
}
services.AddSingleton<IScannerProxyService, ScannerProxyService>();
services.AddSingleton<IScriptedPlayoutBuilderService, ScriptedPlayoutBuilderService>();
services.AddSingleton<IFFmpegSegmenterService, FFmpegSegmenterService>();
services.AddSingleton<ITempFilePool, TempFilePool>();

Loading…
Cancel
Save