Browse Source

add library scan progress detail (#133)

* add library scan progress detail

* scan plex libraries on plex thread
pull/134/head
Jason Dove 5 years ago committed by GitHub
parent
commit
6b44873474
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
  2. 9
      ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs
  3. 80
      ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs
  4. 2
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs
  5. 5
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs
  6. 51
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  7. 4
      ErsatzTV.Core/Domain/Library/LibraryPath.cs
  8. 1
      ErsatzTV.Core/ErsatzTV.Core.csproj
  9. 1
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  10. 7
      ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs
  11. 7
      ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs
  12. 7
      ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs
  13. 1
      ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs
  14. 6
      ErsatzTV.Core/Metadata/LibraryScanProgress.cs
  15. 18
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  16. 27
      ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs
  17. 15
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  18. 9
      ErsatzTV.Core/Plex/PlexLibraryScanner.cs
  19. 22
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  20. 12
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  21. 4
      ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs
  22. 2
      ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs
  23. 1964
      ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.Designer.cs
  24. 20
      ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.cs
  25. 1964
      ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.Designer.cs
  26. 18
      ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.cs
  27. 458
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  28. 1
      ErsatzTV/ErsatzTV.csproj
  29. 15
      ErsatzTV/Pages/CollectionItems.razor
  30. 4
      ErsatzTV/Pages/FFmpegEditor.razor
  31. 55
      ErsatzTV/Pages/Libraries.razor
  32. 2
      ErsatzTV/Pages/LocalLibraryPathEditor.razor
  33. 5
      ErsatzTV/Pages/MusicVideoList.razor
  34. 2
      ErsatzTV/Pages/PlexLibrariesEditor.razor
  35. 11
      ErsatzTV/Pages/Search.razor
  36. 56
      ErsatzTV/Services/PlexService.cs
  37. 17
      ErsatzTV/Services/SchedulerService.cs
  38. 14
      ErsatzTV/Services/WorkerService.cs
  39. 2
      ErsatzTV/Startup.cs

2
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs

@ -50,7 +50,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
AudioCodec = request.AudioCodec, AudioCodec = request.AudioCodec,
AudioBitrate = request.AudioBitrate, AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize, AudioBufferSize = request.AudioBufferSize,
NormalizeLoudness= request.NormalizeLoudness, NormalizeLoudness = request.NormalizeLoudness,
AudioChannels = request.AudioChannels, AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate, AudioSampleRate = request.AudioSampleRate,
NormalizeAudio = request.NormalizeAudio, NormalizeAudio = request.NormalizeAudio,

9
ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs

@ -8,24 +8,15 @@ namespace ErsatzTV.Application.MediaSources.Commands
{ {
int LibraryId { get; } int LibraryId { get; }
bool ForceScan { get; } bool ForceScan { get; }
bool Rescan { get; }
} }
public record ScanLocalLibraryIfNeeded(int LibraryId) : IScanLocalLibrary public record ScanLocalLibraryIfNeeded(int LibraryId) : IScanLocalLibrary
{ {
public bool ForceScan => false; public bool ForceScan => false;
public bool Rescan => false;
} }
public record ForceScanLocalLibrary(int LibraryId) : IScanLocalLibrary public record ForceScanLocalLibrary(int LibraryId) : IScanLocalLibrary
{ {
public bool ForceScan => true; public bool ForceScan => true;
public bool Rescan => false;
}
public record ForceRescanLocalLibrary(int LibraryId) : IScanLocalLibrary
{
public bool ForceScan => true;
public bool Rescan => true;
} }
} }

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

@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -16,13 +17,13 @@ using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaSources.Commands namespace ErsatzTV.Application.MediaSources.Commands
{ {
public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>, public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>,
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>, IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>
IRequestHandler<ForceRescanLocalLibrary, Either<BaseError, string>>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository; private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<ScanLocalLibraryHandler> _logger; private readonly ILogger<ScanLocalLibraryHandler> _logger;
private readonly IMediator _mediator;
private readonly IMovieFolderScanner _movieFolderScanner; private readonly IMovieFolderScanner _movieFolderScanner;
private readonly IMusicVideoFolderScanner _musicVideoFolderScanner; private readonly IMusicVideoFolderScanner _musicVideoFolderScanner;
private readonly ITelevisionFolderScanner _televisionFolderScanner; private readonly ITelevisionFolderScanner _televisionFolderScanner;
@ -34,6 +35,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
ITelevisionFolderScanner televisionFolderScanner, ITelevisionFolderScanner televisionFolderScanner,
IMusicVideoFolderScanner musicVideoFolderScanner, IMusicVideoFolderScanner musicVideoFolderScanner,
IEntityLocker entityLocker, IEntityLocker entityLocker,
IMediator mediator,
ILogger<ScanLocalLibraryHandler> logger) ILogger<ScanLocalLibraryHandler> logger)
{ {
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
@ -42,13 +44,10 @@ namespace ErsatzTV.Application.MediaSources.Commands
_televisionFolderScanner = televisionFolderScanner; _televisionFolderScanner = televisionFolderScanner;
_musicVideoFolderScanner = musicVideoFolderScanner; _musicVideoFolderScanner = musicVideoFolderScanner;
_entityLocker = entityLocker; _entityLocker = entityLocker;
_mediator = mediator;
_logger = logger; _logger = logger;
} }
public Task<Either<BaseError, string>> Handle(
ForceRescanLocalLibrary request,
CancellationToken cancellationToken) => Handle(request);
public Task<Either<BaseError, string>> Handle( public Task<Either<BaseError, string>> Handle(
ForceScanLocalLibrary request, ForceScanLocalLibrary request,
CancellationToken cancellationToken) => Handle(request); CancellationToken cancellationToken) => Handle(request);
@ -65,48 +64,64 @@ namespace ErsatzTV.Application.MediaSources.Commands
private async Task<Unit> PerformScan(RequestParameters parameters) private async Task<Unit> PerformScan(RequestParameters parameters)
{ {
(LocalLibrary localLibrary, string ffprobePath, bool forceScan, bool rescan) = parameters; (LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters;
var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero); var sw = new Stopwatch();
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) sw.Start();
for (var i = 0; i < localLibrary.Paths.Count; i++)
{ {
var sw = new Stopwatch(); LibraryPath libraryPath = localLibrary.Paths[i];
sw.Start();
DateTimeOffset effectiveLastScan = rescan ? DateTimeOffset.MinValue : lastScan; decimal progressMin = (decimal) i / localLibrary.Paths.Count;
decimal progressMax = (decimal) (i + 1) / localLibrary.Paths.Count;
foreach (LibraryPath libraryPath in localLibrary.Paths) var lastScan = new DateTimeOffset(libraryPath.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
{ {
switch (localLibrary.MediaKind) switch (localLibrary.MediaKind)
{ {
case LibraryMediaKind.Movies: case LibraryMediaKind.Movies:
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); await _movieFolderScanner.ScanFolder(
libraryPath,
ffprobePath,
lastScan,
progressMin,
progressMax);
break; break;
case LibraryMediaKind.Shows: case LibraryMediaKind.Shows:
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); await _televisionFolderScanner.ScanFolder(
libraryPath,
ffprobePath,
lastScan,
progressMin,
progressMax);
break; break;
case LibraryMediaKind.MusicVideos: case LibraryMediaKind.MusicVideos:
await _musicVideoFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); await _musicVideoFolderScanner.ScanFolder(
libraryPath,
ffprobePath,
lastScan,
progressMin,
progressMax);
break; break;
} }
}
localLibrary.LastScan = DateTime.UtcNow; libraryPath.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(localLibrary); await _libraryRepository.UpdateLastScan(libraryPath);
}
sw.Stop(); await _mediator.Publish(new LibraryScanProgress(libraryPath.LibraryId, progressMax));
_logger.LogDebug(
"Scan of library {Name} completed in {Duration}",
localLibrary.Name,
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
}
else
{
_logger.LogDebug(
"Skipping unforced scan of library {Name}",
localLibrary.Name);
} }
sw.Stop();
_logger.LogDebug(
"Scan of library {Name} completed in {Duration}",
localLibrary.Name,
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
await _mediator.Publish(new LibraryScanProgress(localLibrary.Id, 0));
_entityLocker.UnlockLibrary(localLibrary.Id); _entityLocker.UnlockLibrary(localLibrary.Id);
return Unit.Default; return Unit.Default;
} }
@ -117,8 +132,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
(library, ffprobePath) => new RequestParameters( (library, ffprobePath) => new RequestParameters(
library, library,
ffprobePath, ffprobePath,
request.ForceScan, request.ForceScan));
request.Rescan));
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist( private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
IScanLocalLibrary request) => IScanLocalLibrary request) =>
@ -133,6 +147,6 @@ namespace ErsatzTV.Application.MediaSources.Commands
ffprobePath => ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system")); ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan, bool Rescan); private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan);
} }
} }

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

@ -4,7 +4,7 @@ using MediatR;
namespace ErsatzTV.Application.Plex.Commands namespace ErsatzTV.Application.Plex.Commands
{ {
public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IPlexBackgroundServiceRequest
{ {
int PlexLibraryId { get; } int PlexLibraryId { get; }
bool ForceScan { get; } bool ForceScan { get; }

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

@ -20,6 +20,7 @@ namespace ErsatzTV.Application.Plex.Commands
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>> IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>
{ {
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizePlexLibraryByIdHandler> _logger; private readonly ILogger<SynchronizePlexLibraryByIdHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner; private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner;
@ -31,6 +32,7 @@ namespace ErsatzTV.Application.Plex.Commands
IPlexSecretStore plexSecretStore, IPlexSecretStore plexSecretStore,
IPlexMovieLibraryScanner plexMovieLibraryScanner, IPlexMovieLibraryScanner plexMovieLibraryScanner,
IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner, IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner,
ILibraryRepository libraryRepository,
IEntityLocker entityLocker, IEntityLocker entityLocker,
ILogger<SynchronizePlexLibraryByIdHandler> logger) ILogger<SynchronizePlexLibraryByIdHandler> logger)
{ {
@ -38,6 +40,7 @@ namespace ErsatzTV.Application.Plex.Commands
_plexSecretStore = plexSecretStore; _plexSecretStore = plexSecretStore;
_plexMovieLibraryScanner = plexMovieLibraryScanner; _plexMovieLibraryScanner = plexMovieLibraryScanner;
_plexTelevisionLibraryScanner = plexTelevisionLibraryScanner; _plexTelevisionLibraryScanner = plexTelevisionLibraryScanner;
_libraryRepository = libraryRepository;
_entityLocker = entityLocker; _entityLocker = entityLocker;
_logger = logger; _logger = logger;
} }
@ -78,7 +81,7 @@ namespace ErsatzTV.Application.Plex.Commands
} }
parameters.Library.LastScan = DateTime.UtcNow; parameters.Library.LastScan = DateTime.UtcNow;
await _mediaSourceRepository.Update(parameters.Library); await _libraryRepository.UpdateLastScan(parameters.Library);
} }
else else
{ {

51
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -14,10 +14,12 @@ using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Tests.Fakes; using ErsatzTV.Core.Tests.Fakes;
using FluentAssertions; using FluentAssertions;
using LanguageExt; using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Core.Tests.Metadata namespace ErsatzTV.Core.Tests.Metadata
{ {
@ -84,7 +86,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsLeft.Should().BeTrue(); result.IsLeft.Should().BeTrue();
result.IfLeft(error => error.Should().BeOfType<MediaSourceInaccessible>()); result.IfLeft(error => error.Should().BeOfType<MediaSourceInaccessible>());
@ -107,7 +111,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -146,7 +152,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -186,7 +194,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -230,7 +240,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -277,7 +289,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -324,7 +338,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -370,7 +386,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -412,7 +430,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -448,7 +468,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -486,7 +508,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -513,7 +537,9 @@ namespace ErsatzTV.Core.Tests.Metadata
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFprobePath, FFprobePath,
DateTimeOffset.MinValue); DateTimeOffset.MinValue,
0,
1);
result.IsRight.Should().BeTrue(); result.IsRight.Should().BeTrue();
@ -531,6 +557,7 @@ namespace ErsatzTV.Core.Tests.Metadata
new Mock<IMetadataRepository>().Object, new Mock<IMetadataRepository>().Object,
_imageCache.Object, _imageCache.Object,
new Mock<ISearchIndex>().Object, new Mock<ISearchIndex>().Object,
new Mock<IMediator>().Object,
new Mock<ILogger<MovieFolderScanner>>().Object new Mock<ILogger<MovieFolderScanner>>().Object
); );
} }

4
ErsatzTV.Core/Domain/Library/LibraryPath.cs

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain namespace ErsatzTV.Core.Domain
{ {
@ -6,6 +7,7 @@ namespace ErsatzTV.Core.Domain
{ {
public int Id { get; set; } public int Id { get; set; }
public string Path { get; set; } public string Path { get; set; }
public DateTime? LastScan { get; set; }
public int LibraryId { get; set; } public int LibraryId { get; set; }
public Library Library { get; set; } public Library Library { get; set; }

1
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -11,6 +11,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="LanguageExt.Core" Version="3.4.15" /> <PackageReference Include="LanguageExt.Core" Version="3.4.15" />
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">

1
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -199,6 +199,7 @@ namespace ErsatzTV.Core.FFmpeg
"-c", "copy", "-c", "copy",
"-muxdelay", "0", "-muxdelay", "0",
"-muxpreload", "0" "-muxpreload", "0"
// "-avoid_negative_ts", "make_zero"
}; };
_arguments.AddRange(arguments); _arguments.AddRange(arguments);
return this; return this;

7
ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs

@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Metadata
{ {
public interface IMovieFolderScanner public interface IMovieFolderScanner
{ {
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan); Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath,
string ffprobePath,
DateTimeOffset lastScan,
decimal progressMin,
decimal progressMax);
} }
} }

7
ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs

@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Metadata
{ {
public interface IMusicVideoFolderScanner public interface IMusicVideoFolderScanner
{ {
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan); Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath,
string ffprobePath,
DateTimeOffset lastScan,
decimal progressMin,
decimal progressMax);
} }
} }

7
ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs

@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Metadata
{ {
public interface ITelevisionFolderScanner public interface ITelevisionFolderScanner
{ {
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan); Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath,
string ffprobePath,
DateTimeOffset lastScan,
decimal progressMin,
decimal progressMax);
} }
} }

1
ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs

@ -12,6 +12,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Option<LocalLibrary>> GetLocal(int libraryId); Task<Option<LocalLibrary>> GetLocal(int libraryId);
Task<List<Library>> GetAll(); Task<List<Library>> GetAll();
Task<Unit> UpdateLastScan(Library library); Task<Unit> UpdateLastScan(Library library);
Task<Unit> UpdateLastScan(LibraryPath libraryPath);
Task<List<LibraryPath>> GetLocalPaths(int libraryId); Task<List<LibraryPath>> GetLocalPaths(int libraryId);
Task<Option<LibraryPath>> GetPath(int libraryPathId); Task<Option<LibraryPath>> GetPath(int libraryPathId);
Task<int> CountMediaItemsByPath(int libraryPathId); Task<int> CountMediaItemsByPath(int libraryPathId);

6
ErsatzTV.Core/Metadata/LibraryScanProgress.cs

@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Core.Metadata
{
public record LibraryScanProgress(int LibraryId, decimal Progress) : INotification;
}

18
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -10,9 +10,11 @@ using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using LanguageExt; using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
using Seq = LanguageExt.Seq; using Seq = LanguageExt.Seq;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Core.Metadata namespace ErsatzTV.Core.Metadata
{ {
@ -21,6 +23,7 @@ namespace ErsatzTV.Core.Metadata
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILogger<MovieFolderScanner> _logger; private readonly ILogger<MovieFolderScanner> _logger;
private readonly IMediator _mediator;
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
@ -32,6 +35,7 @@ namespace ErsatzTV.Core.Metadata
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
IImageCache imageCache, IImageCache imageCache,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IMediator mediator,
ILogger<MovieFolderScanner> logger) ILogger<MovieFolderScanner> logger)
: base(localFileSystem, localStatisticsProvider, metadataRepository, imageCache, logger) : base(localFileSystem, localStatisticsProvider, metadataRepository, imageCache, logger)
{ {
@ -39,19 +43,26 @@ namespace ErsatzTV.Core.Metadata
_movieRepository = movieRepository; _movieRepository = movieRepository;
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_mediator = mediator;
_logger = logger; _logger = logger;
} }
public async Task<Either<BaseError, Unit>> ScanFolder( public async Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffprobePath, string ffprobePath,
DateTimeOffset lastScan) DateTimeOffset lastScan,
decimal progressMin,
decimal progressMax)
{ {
decimal progressSpread = progressMax - progressMin;
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) if (!_localFileSystem.IsLibraryPathAccessible(libraryPath))
{ {
return new MediaSourceInaccessible(); return new MediaSourceInaccessible();
} }
var foldersCompleted = 0;
var folderQueue = new Queue<string>(); var folderQueue = new Queue<string>();
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity))
{ {
@ -60,7 +71,12 @@ namespace ErsatzTV.Core.Metadata
while (folderQueue.Count > 0) while (folderQueue.Count > 0)
{ {
decimal percentCompletion = (decimal) foldersCompleted / (foldersCompleted + folderQueue.Count);
await _mediator.Publish(
new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread));
string movieFolder = folderQueue.Dequeue(); string movieFolder = folderQueue.Dequeue();
foldersCompleted++;
var allFiles = _localFileSystem.ListFiles(movieFolder) var allFiles = _localFileSystem.ListFiles(movieFolder)
.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))

27
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

@ -10,8 +10,10 @@ using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using LanguageExt; using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Core.Metadata namespace ErsatzTV.Core.Metadata
{ {
@ -20,6 +22,7 @@ namespace ErsatzTV.Core.Metadata
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILogger<MusicVideoFolderScanner> _logger; private readonly ILogger<MusicVideoFolderScanner> _logger;
private readonly IMediator _mediator;
private readonly IMusicVideoRepository _musicVideoRepository; private readonly IMusicVideoRepository _musicVideoRepository;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
@ -31,6 +34,7 @@ namespace ErsatzTV.Core.Metadata
IImageCache imageCache, IImageCache imageCache,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IMusicVideoRepository musicVideoRepository, IMusicVideoRepository musicVideoRepository,
IMediator mediator,
ILogger<MusicVideoFolderScanner> logger) : base( ILogger<MusicVideoFolderScanner> logger) : base(
localFileSystem, localFileSystem,
localStatisticsProvider, localStatisticsProvider,
@ -42,19 +46,26 @@ namespace ErsatzTV.Core.Metadata
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_musicVideoRepository = musicVideoRepository; _musicVideoRepository = musicVideoRepository;
_mediator = mediator;
_logger = logger; _logger = logger;
} }
public async Task<Either<BaseError, Unit>> ScanFolder( public async Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffprobePath, string ffprobePath,
DateTimeOffset lastScan) DateTimeOffset lastScan,
decimal progressMin,
decimal progressMax)
{ {
decimal progressSpread = progressMax - progressMin;
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) if (!_localFileSystem.IsLibraryPathAccessible(libraryPath))
{ {
return new MediaSourceInaccessible(); return new MediaSourceInaccessible();
} }
var foldersCompleted = 0;
var folderQueue = new Queue<string>(); var folderQueue = new Queue<string>();
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity))
{ {
@ -63,9 +74,14 @@ namespace ErsatzTV.Core.Metadata
while (folderQueue.Count > 0) while (folderQueue.Count > 0)
{ {
string movieFolder = folderQueue.Dequeue(); decimal percentCompletion = (decimal) foldersCompleted / (foldersCompleted + folderQueue.Count);
await _mediator.Publish(
new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread));
string musicVideoFolder = folderQueue.Dequeue();
foldersCompleted++;
var allFiles = _localFileSystem.ListFiles(movieFolder) var allFiles = _localFileSystem.ListFiles(musicVideoFolder)
.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))
.Filter( .Filter(
f => !ExtraFiles.Any( f => !ExtraFiles.Any(
@ -74,7 +90,8 @@ namespace ErsatzTV.Core.Metadata
if (allFiles.Count == 0) if (allFiles.Count == 0)
{ {
foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity)) foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder)
.OrderBy(identity))
{ {
folderQueue.Enqueue(subdirectory); folderQueue.Enqueue(subdirectory);
} }
@ -82,7 +99,7 @@ namespace ErsatzTV.Core.Metadata
continue; continue;
} }
if (_localFileSystem.GetLastWriteTime(movieFolder) < lastScan) if (_localFileSystem.GetLastWriteTime(musicVideoFolder) < lastScan)
{ {
continue; continue;
} }

15
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -10,8 +10,10 @@ using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using LanguageExt; using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Core.Metadata namespace ErsatzTV.Core.Metadata
{ {
@ -20,6 +22,7 @@ namespace ErsatzTV.Core.Metadata
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILogger<TelevisionFolderScanner> _logger; private readonly ILogger<TelevisionFolderScanner> _logger;
private readonly IMediator _mediator;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
@ -31,6 +34,7 @@ namespace ErsatzTV.Core.Metadata
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
IImageCache imageCache, IImageCache imageCache,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IMediator mediator,
ILogger<TelevisionFolderScanner> logger) : base( ILogger<TelevisionFolderScanner> logger) : base(
localFileSystem, localFileSystem,
localStatisticsProvider, localStatisticsProvider,
@ -42,14 +46,19 @@ namespace ErsatzTV.Core.Metadata
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_mediator = mediator;
_logger = logger; _logger = logger;
} }
public async Task<Either<BaseError, Unit>> ScanFolder( public async Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffprobePath, string ffprobePath,
DateTimeOffset lastScan) DateTimeOffset lastScan,
decimal progressMin,
decimal progressMax)
{ {
decimal progressSpread = progressMax - progressMin;
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) if (!_localFileSystem.IsLibraryPathAccessible(libraryPath))
{ {
return new MediaSourceInaccessible(); return new MediaSourceInaccessible();
@ -62,6 +71,10 @@ namespace ErsatzTV.Core.Metadata
foreach (string showFolder in allShowFolders) foreach (string showFolder in allShowFolders)
{ {
decimal percentCompletion = (decimal) allShowFolders.IndexOf(showFolder) / allShowFolders.Count;
await _mediator.Publish(
new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread));
Either<BaseError, MediaItemScanResult<Show>> maybeShow = Either<BaseError, MediaItemScanResult<Show>> maybeShow =
await FindOrCreateShow(libraryPath.Id, showFolder) await FindOrCreateShow(libraryPath.Id, showFolder)
.BindT(show => UpdateMetadataForShow(show, showFolder)) .BindT(show => UpdateMetadataForShow(show, showFolder))

9
ErsatzTV.Core/Plex/PlexLibraryScanner.cs

@ -3,16 +3,21 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Plex namespace ErsatzTV.Core.Plex
{ {
public abstract class PlexLibraryScanner public abstract class PlexLibraryScanner
{ {
private readonly ILogger<PlexLibraryScanner> _logger;
private readonly IMetadataRepository _metadataRepository; private readonly IMetadataRepository _metadataRepository;
protected PlexLibraryScanner(IMetadataRepository metadataRepository) => protected PlexLibraryScanner(IMetadataRepository metadataRepository, ILogger<PlexLibraryScanner> logger)
{
_metadataRepository = metadataRepository; _metadataRepository = metadataRepository;
_logger = logger;
}
protected async Task<Unit> UpdateArtworkIfNeeded( protected async Task<Unit> UpdateArtworkIfNeeded(
Domain.Metadata existingMetadata, Domain.Metadata existingMetadata,
@ -27,6 +32,8 @@ namespace ErsatzTV.Core.Plex
await maybeIncomingArtwork.Match( await maybeIncomingArtwork.Match(
async incomingArtwork => async incomingArtwork =>
{ {
_logger.LogDebug("Refreshing Plex {Attribute} from {Path}", artworkKind, incomingArtwork.Path);
Option<Artwork> maybeExistingArtwork = Optional(existingMetadata.Artwork).Flatten() Option<Artwork> maybeExistingArtwork = Optional(existingMetadata.Artwork).Flatten()
.Find(a => a.ArtworkKind == artworkKind); .Find(a => a.ArtworkKind == artworkKind);

22
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -7,13 +7,16 @@ using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata;
using LanguageExt; using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Core.Plex namespace ErsatzTV.Core.Plex
{ {
public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScanner public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScanner
{ {
private readonly ILogger<PlexMovieLibraryScanner> _logger; private readonly ILogger<PlexMovieLibraryScanner> _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository; private readonly IMetadataRepository _metadataRepository;
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
private readonly IPlexServerApiClient _plexServerApiClient; private readonly IPlexServerApiClient _plexServerApiClient;
@ -24,13 +27,15 @@ namespace ErsatzTV.Core.Plex
IMovieRepository movieRepository, IMovieRepository movieRepository,
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IMediator mediator,
ILogger<PlexMovieLibraryScanner> logger) ILogger<PlexMovieLibraryScanner> logger)
: base(metadataRepository) : base(metadataRepository, logger)
{ {
_plexServerApiClient = plexServerApiClient; _plexServerApiClient = plexServerApiClient;
_movieRepository = movieRepository; _movieRepository = movieRepository;
_metadataRepository = metadataRepository; _metadataRepository = metadataRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_mediator = mediator;
_logger = logger; _logger = logger;
} }
@ -49,6 +54,9 @@ namespace ErsatzTV.Core.Plex
{ {
foreach (PlexMovie incoming in movieEntries) foreach (PlexMovie incoming in movieEntries)
{ {
decimal percentCompletion = (decimal) movieEntries.IndexOf(incoming) / movieEntries.Count;
await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, percentCompletion));
// TODO: figure out how to rebuild playlists // TODO: figure out how to rebuild playlists
Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository
.GetOrAdd(plexMediaSourceLibrary, incoming) .GetOrAdd(plexMediaSourceLibrary, incoming)
@ -81,6 +89,8 @@ namespace ErsatzTV.Core.Plex
var movieKeys = movieEntries.Map(s => s.Key).ToList(); var movieKeys = movieEntries.Map(s => s.Key).ToList();
List<int> ids = await _movieRepository.RemoveMissingPlexMovies(plexMediaSourceLibrary, movieKeys); List<int> ids = await _movieRepository.RemoveMissingPlexMovies(plexMediaSourceLibrary, movieKeys);
await _searchIndex.RemoveItems(ids); await _searchIndex.RemoveItems(ids);
await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, 0));
}, },
error => error =>
{ {
@ -113,6 +123,11 @@ namespace ErsatzTV.Core.Plex
await maybeStatistics.Match( await maybeStatistics.Match(
async mediaVersion => async mediaVersion =>
{ {
_logger.LogDebug(
"Refreshing {Attribute} from {Path}",
"Plex Statistics",
existingVersion.MediaFiles.Head().Path);
existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio; existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio;
existingVersion.VideoScanKind = mediaVersion.VideoScanKind; existingVersion.VideoScanKind = mediaVersion.VideoScanKind;
existingVersion.DateUpdated = mediaVersion.DateUpdated; existingVersion.DateUpdated = mediaVersion.DateUpdated;
@ -135,6 +150,11 @@ namespace ErsatzTV.Core.Plex
if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated)
{ {
_logger.LogDebug(
"Refreshing {Attribute} from {Path}",
"Plex Metadata",
existing.MediaVersions.Head().MediaFiles.Head().Path);
foreach (Genre genre in existingMetadata.Genres foreach (Genre genre in existingMetadata.Genres
.Filter(g => incomingMetadata.Genres.All(g2 => g2.Name != g.Name)) .Filter(g => incomingMetadata.Genres.All(g2 => g2.Name != g.Name))
.ToList()) .ToList())

12
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -7,14 +7,17 @@ using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata;
using LanguageExt; using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Core.Plex namespace ErsatzTV.Core.Plex
{ {
public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionLibraryScanner public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionLibraryScanner
{ {
private readonly ILogger<PlexTelevisionLibraryScanner> _logger; private readonly ILogger<PlexTelevisionLibraryScanner> _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository; private readonly IMetadataRepository _metadataRepository;
private readonly IPlexServerApiClient _plexServerApiClient; private readonly IPlexServerApiClient _plexServerApiClient;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
@ -25,13 +28,15 @@ namespace ErsatzTV.Core.Plex
ITelevisionRepository televisionRepository, ITelevisionRepository televisionRepository,
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IMediator mediator,
ILogger<PlexTelevisionLibraryScanner> logger) ILogger<PlexTelevisionLibraryScanner> logger)
: base(metadataRepository) : base(metadataRepository, logger)
{ {
_plexServerApiClient = plexServerApiClient; _plexServerApiClient = plexServerApiClient;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
_metadataRepository = metadataRepository; _metadataRepository = metadataRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_mediator = mediator;
_logger = logger; _logger = logger;
} }
@ -50,6 +55,9 @@ namespace ErsatzTV.Core.Plex
{ {
foreach (PlexShow incoming in showEntries) foreach (PlexShow incoming in showEntries)
{ {
decimal percentCompletion = (decimal) showEntries.IndexOf(incoming) / showEntries.Count;
await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, percentCompletion));
// TODO: figure out how to rebuild playlists // TODO: figure out how to rebuild playlists
Either<BaseError, MediaItemScanResult<PlexShow>> maybeShow = await _televisionRepository Either<BaseError, MediaItemScanResult<PlexShow>> maybeShow = await _televisionRepository
.GetOrAddPlexShow(plexMediaSourceLibrary, incoming) .GetOrAddPlexShow(plexMediaSourceLibrary, incoming)
@ -85,6 +93,8 @@ namespace ErsatzTV.Core.Plex
await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys); await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys);
await _searchIndex.RemoveItems(ids); await _searchIndex.RemoveItems(ids);
await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, 0));
return Unit.Default; return Unit.Default;
}, },
error => error =>

4
ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs

@ -62,6 +62,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
"UPDATE Library SET LastScan = @LastScan WHERE Id = @Id", "UPDATE Library SET LastScan = @LastScan WHERE Id = @Id",
new { library.LastScan, library.Id }).ToUnit(); new { library.LastScan, library.Id }).ToUnit();
public Task<Unit> UpdateLastScan(LibraryPath libraryPath) => _dbConnection.ExecuteAsync(
"UPDATE LibraryPath SET LastScan = @LastScan WHERE Id = @Id",
new { libraryPath.LastScan, libraryPath.Id }).ToUnit();
public Task<List<LibraryPath>> GetLocalPaths(int libraryId) public Task<List<LibraryPath>> GetLocalPaths(int libraryId)
{ {
using TvContext context = _dbContextFactory.CreateDbContext(); using TvContext context = _dbContextFactory.CreateDbContext();

2
ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs

@ -52,6 +52,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(lp => lp.Library) .ThenInclude(lp => lp.Library)
.Include(mv => mv.MediaVersions) .Include(mv => mv.MediaVersions)
.ThenInclude(mv => mv.MediaFiles) .ThenInclude(mv => mv.MediaFiles)
.Include(mv => mv.MediaVersions)
.ThenInclude(mv => mv.Streams)
.OrderBy(mv => mv.Id) .OrderBy(mv => mv.Id)
.SingleOrDefaultAsync(mv => mv.Id == id) .SingleOrDefaultAsync(mv => mv.Id == id)
.Map(Optional); .Map(Optional);

1964
ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.Designer.cs generated

File diff suppressed because it is too large Load Diff

20
ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.cs

@ -0,0 +1,20 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_LibraryPath_LastScan : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.AddColumn<DateTime>(
"LastScan",
"LibraryPath",
"TEXT",
nullable: true);
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.DropColumn(
"LastScan",
"LibraryPath");
}
}

1964
ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.Designer.cs generated

File diff suppressed because it is too large Load Diff

18
ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.cs

@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Update_LibraryPathLastScan_LibraryLastScan : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.Sql(
@"UPDATE LibraryPath SET LastScan =
(SELECT LastScan FROM Library L
INNER JOIN LocalLibrary LL on L.Id = LL.Id
WHERE LibraryPath.LibraryId = L.Id)");
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

458
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

File diff suppressed because it is too large Load Diff

1
ErsatzTV/ErsatzTV.csproj

@ -19,6 +19,7 @@
<PackageReference Include="FluentValidation" Version="9.5.3" /> <PackageReference Include="FluentValidation" Version="9.5.3" />
<PackageReference Include="FluentValidation.AspNetCore" Version="9.5.3" /> <PackageReference Include="FluentValidation.AspNetCore" Version="9.5.3" />
<PackageReference Include="LanguageExt.Core" Version="3.4.15" /> <PackageReference Include="LanguageExt.Core" Version="3.4.15" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="3.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4">

15
ErsatzTV/Pages/CollectionItems.razor

@ -243,12 +243,15 @@
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
{ {
List<MediaCardViewModel> GetSortedItems() => _data.MovieCards.OrderBy(m => m.SortTitle) List<MediaCardViewModel> GetSortedItems()
.Append<MediaCardViewModel>(_data.ShowCards.OrderBy(s => s.SortTitle)) {
.Append(_data.SeasonCards.OrderBy(s => s.SortTitle)) return _data.MovieCards.OrderBy(m => m.SortTitle)
.Append(_data.EpisodeCards.OrderBy(ep => ep.Aired)) .Append<MediaCardViewModel>(_data.ShowCards.OrderBy(s => s.SortTitle))
.Append(_data.MusicVideoCards.OrderBy(mv => mv.SortTitle)) .Append(_data.SeasonCards.OrderBy(s => s.SortTitle))
.ToList(); .Append(_data.EpisodeCards.OrderBy(ep => ep.Aired))
.Append(_data.MusicVideoCards.OrderBy(mv => mv.SortTitle))
.ToList();
}
SelectClicked(GetSortedItems, card, e); SelectClicked(GetSortedItems, card, e);
} }

4
ErsatzTV/Pages/FFmpegEditor.razor

@ -62,12 +62,12 @@
<MudTextField Disabled="@(!_model.Transcode)" Label="Frame Rate" @bind-Value="_model.FrameRate" For="@(() => _model.FrameRate)" Adornment="Adornment.End" AdornmentText="fps"/> <MudTextField Disabled="@(!_model.Transcode)" Label="Frame Rate" @bind-Value="_model.FrameRate" For="@(() => _model.FrameRate)" Adornment="Adornment.End" AdornmentText="fps"/>
</MudElement> </MudElement>
<MudElement HtmlTag="div" Class="mt-3"> <MudElement HtmlTag="div" Class="mt-3">
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video" @bind-Checked="@_model.NormalizeVideo" For="@(() => _model.NormalizeVideo)"/> <MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video" @bind-Checked="@_model.NormalizeVideo" For="@(() => _model.NormalizeVideo)"/>
</MudElement> </MudElement>
</MudItem> </MudItem>
<MudItem> <MudItem>
<MudText Typo="Typo.h6">Audio</MudText> <MudText Typo="Typo.h6">Audio</MudText>
<MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.AudioCodec" For="@(() => _model.AudioCodec)"/> <MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.AudioCodec" For="@(() => _model.AudioCodec)"/>
<MudElement HtmlTag="div" Class="mt-3"> <MudElement HtmlTag="div" Class="mt-3">
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/> <MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudElement> </MudElement>

55
ErsatzTV/Pages/Libraries.razor

@ -1,12 +1,17 @@
@page "/media/libraries" @page "/media/libraries"
@using MediatR.Courier
@using ErsatzTV.Application.Libraries @using ErsatzTV.Application.Libraries
@using ErsatzTV.Application.Libraries.Queries @using ErsatzTV.Application.Libraries.Queries
@using ErsatzTV.Application.MediaSources.Commands @using ErsatzTV.Application.MediaSources.Commands
@using ErsatzTV.Application.Plex.Commands @using ErsatzTV.Application.Plex.Commands
@using ErsatzTV.Core.Metadata
@using System.Threading
@implements IDisposable @implements IDisposable
@inject IMediator Mediator @inject IMediator Mediator
@inject IEntityLocker Locker @inject IEntityLocker Locker
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> WorkerChannel
@inject ChannelWriter<IPlexBackgroundServiceRequest> PlexWorkerChannel
@inject ICourier Courier
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Items="_libraries" Dense="true"> <MudTable Hover="true" Items="_libraries" Dense="true">
@ -17,7 +22,7 @@
<col/> <col/>
<col/> <col/>
<col/> <col/>
<col style="width: 120px;"/> <col style="width: 180px;"/>
</ColGroup> </ColGroup>
<HeaderContent> <HeaderContent>
<MudTh>Library Kind</MudTh> <MudTh>Library Kind</MudTh>
@ -33,12 +38,21 @@
<div style="align-items: center; display: flex;"> <div style="align-items: center; display: flex;">
@if (Locker.IsLibraryLocked(context.Id)) @if (Locker.IsLibraryLocked(context.Id))
{ {
<div style="width: 48px">
@if (_progressByLibrary[context.Id] > 0)
{
<MudText Color="Color.Primary">
@($"{_progressByLibrary[context.Id]} %")
</MudText>
}
</div>
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;"> <div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/> <MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
</div> </div>
} }
else else
{ {
<div style="width: 48px"></div>
<MudTooltip Text="Scan Library"> <MudTooltip Text="Scan Library">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" <MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@Locker.IsLibraryLocked(context.Id)" Disabled="@Locker.IsLibraryLocked(context.Id)"
@ -63,14 +77,21 @@
@code { @code {
private IList<LibraryViewModel> _libraries; private IList<LibraryViewModel> _libraries;
private Dictionary<int, int> _progressByLibrary;
protected override void OnInitialized() => protected override void OnInitialized()
{
Locker.OnLibraryChanged += LockChanged; Locker.OnLibraryChanged += LockChanged;
Courier.Subscribe<LibraryScanProgress>(HandleScanProgress);
}
protected override async Task OnParametersSetAsync() => await LoadLibraries(); protected override async Task OnParametersSetAsync() => await LoadLibraries();
private async Task LoadLibraries() => private async Task LoadLibraries()
{
_libraries = await Mediator.Send(new GetAllLibraries()); _libraries = await Mediator.Send(new GetAllLibraries());
_progressByLibrary = _libraries.ToDictionary(vm => vm.Id, _ => 0);
}
private async Task ScanLibrary(LibraryViewModel library) private async Task ScanLibrary(LibraryViewModel library)
{ {
@ -79,10 +100,10 @@
switch (library) switch (library)
{ {
case LocalLibraryViewModel: case LocalLibraryViewModel:
await Channel.WriteAsync(new ForceScanLocalLibrary(library.Id)); await WorkerChannel.WriteAsync(new ForceScanLocalLibrary(library.Id));
break; break;
case PlexLibraryViewModel: case PlexLibraryViewModel:
await Channel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id)); await PlexWorkerChannel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id));
break; break;
} }
@ -93,6 +114,26 @@
private void LockChanged(object sender, EventArgs e) => private void LockChanged(object sender, EventArgs e) =>
InvokeAsync(StateHasChanged); InvokeAsync(StateHasChanged);
void IDisposable.Dispose() => Locker.OnLibraryChanged -= LockChanged; private async Task HandleScanProgress(LibraryScanProgress libraryScanProgress, CancellationToken cancellationToken)
{
try
{
if (_progressByLibrary != null && _progressByLibrary.ContainsKey(libraryScanProgress.LibraryId))
{
_progressByLibrary[libraryScanProgress.LibraryId] = (int) (libraryScanProgress.Progress * 100);
await InvokeAsync(StateHasChanged);
}
}
catch (Exception)
{
// ignore
}
}
void IDisposable.Dispose()
{
Locker.OnLibraryChanged -= LockChanged;
Courier.UnSubscribe<LibraryScanProgress>(HandleScanProgress);
}
} }

2
ErsatzTV/Pages/LocalLibraryPathEditor.razor

@ -74,7 +74,7 @@
{ {
if (Locker.LockLibrary(_library.Id)) if (Locker.LockLibrary(_library.Id))
{ {
await Channel.WriteAsync(new ForceRescanLocalLibrary(_library.Id)); await Channel.WriteAsync(new ScanLocalLibraryIfNeeded(_library.Id));
NavigationManager.NavigateTo("/media/libraries"); NavigationManager.NavigateTo("/media/libraries");
} }
}); });

5
ErsatzTV/Pages/MusicVideoList.razor

@ -132,7 +132,10 @@
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
{ {
List<MediaCardViewModel> GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>(); List<MediaCardViewModel> GetSortedItems()
{
return _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
}
SelectClicked(GetSortedItems, card, e); SelectClicked(GetSortedItems, card, e);
} }

2
ErsatzTV/Pages/PlexLibrariesEditor.razor

@ -6,7 +6,7 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ILogger<PlexLibrariesEditor> Logger @inject ILogger<PlexLibrariesEditor> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IPlexBackgroundServiceRequest> Channel
@inject IEntityLocker Locker @inject IEntityLocker Locker
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">

11
ErsatzTV/Pages/Search.razor

@ -169,10 +169,13 @@
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
{ {
List<MediaCardViewModel> GetSortedItems() => _movies.Cards.OrderBy(m => m.SortTitle) List<MediaCardViewModel> GetSortedItems()
.Append<MediaCardViewModel>(_shows.Cards.OrderBy(s => s.SortTitle)) {
.Append(_musicVideos.Cards.OrderBy(s => s.SortTitle)) return _movies.Cards.OrderBy(m => m.SortTitle)
.ToList(); .Append<MediaCardViewModel>(_shows.Cards.OrderBy(s => s.SortTitle))
.Append(_musicVideos.Cards.OrderBy(s => s.SortTitle))
.ToList();
}
SelectClicked(GetSortedItems, card, e); SelectClicked(GetSortedItems, card, e);
} }

56
ErsatzTV/Services/PlexService.cs

@ -52,17 +52,24 @@ namespace ErsatzTV.Services
{ {
try try
{ {
Task requestTask = request switch Task requestTask;
switch (request)
{ {
TryCompletePlexPinFlow pinRequest => CompletePinFlow(pinRequest, cancellationToken), case TryCompletePlexPinFlow pinRequest:
SynchronizePlexMediaSources sourcesRequest => SynchronizeSources( requestTask = CompletePinFlow(pinRequest, cancellationToken);
sourcesRequest, break;
cancellationToken), case SynchronizePlexMediaSources sourcesRequest:
SynchronizePlexLibraries synchronizePlexLibrariesRequest => SynchronizeLibraries( requestTask = SynchronizeSources(sourcesRequest, cancellationToken);
synchronizePlexLibrariesRequest, break;
cancellationToken), case SynchronizePlexLibraries synchronizePlexLibrariesRequest:
_ => throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}") requestTask = SynchronizeLibraries(synchronizePlexLibrariesRequest, cancellationToken);
}; break;
case ISynchronizePlexLibraryById synchronizePlexLibraryById:
requestTask = SynchronizePlexLibrary(synchronizePlexLibraryById, cancellationToken);
break;
default:
throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}");
}
await requestTask; await requestTask;
} }
@ -109,17 +116,8 @@ namespace ErsatzTV.Services
Either<BaseError, bool> result = await mediator.Send(request, cancellationToken); Either<BaseError, bool> result = await mediator.Send(request, cancellationToken);
result.BiIter( result.BiIter(
success => success => _logger.LogInformation(
{ success ? "Successfully authenticated with plex" : "Plex authentication timeout"),
if (success)
{
_logger.LogInformation("Successfully authenticated with plex");
}
else
{
_logger.LogInformation("Plex authentication timeout");
}
},
error => _logger.LogWarning("Unable to poll plex token: {Error}", error.Value)); error => _logger.LogWarning("Unable to poll plex token: {Error}", error.Value));
} }
@ -138,5 +136,21 @@ namespace ErsatzTV.Services
request.PlexMediaSourceId, request.PlexMediaSourceId,
error.Value)); error.Value));
} }
private async Task SynchronizePlexLibrary(
ISynchronizePlexLibraryById request,
CancellationToken cancellationToken)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
Either<BaseError, string> result = await mediator.Send(request, cancellationToken);
result.BiIter(
name => _logger.LogDebug("Done synchronizing plex library {Name}", name),
error => _logger.LogWarning(
"Unable to synchronize plex library {LibraryId}: {Error}",
request.PlexLibraryId,
error.Value));
}
} }
} }

17
ErsatzTV/Services/SchedulerService.cs

@ -21,19 +21,22 @@ namespace ErsatzTV.Services
{ {
public class SchedulerService : BackgroundService public class SchedulerService : BackgroundService
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly ILogger<SchedulerService> _logger; private readonly ILogger<SchedulerService> _logger;
private readonly ChannelWriter<IPlexBackgroundServiceRequest> _plexWorkerChannel;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public SchedulerService( public SchedulerService(
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
ChannelWriter<IBackgroundServiceRequest> channel, ChannelWriter<IBackgroundServiceRequest> workerChannel,
ChannelWriter<IPlexBackgroundServiceRequest> plexWorkerChannel,
IEntityLocker entityLocker, IEntityLocker entityLocker,
ILogger<SchedulerService> logger) ILogger<SchedulerService> logger)
{ {
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_channel = channel; _workerChannel = workerChannel;
_plexWorkerChannel = plexWorkerChannel;
_entityLocker = entityLocker; _entityLocker = entityLocker;
_logger = logger; _logger = logger;
} }
@ -74,7 +77,7 @@ namespace ErsatzTV.Services
List<int> playoutIds = await dbContext.Playouts.Map(p => p.Id).ToListAsync(cancellationToken); List<int> playoutIds = await dbContext.Playouts.Map(p => p.Id).ToListAsync(cancellationToken);
foreach (int playoutId in playoutIds) foreach (int playoutId in playoutIds)
{ {
await _channel.WriteAsync(new BuildPlayout(playoutId), cancellationToken); await _workerChannel.WriteAsync(new BuildPlayout(playoutId), cancellationToken);
} }
} }
@ -92,7 +95,7 @@ namespace ErsatzTV.Services
{ {
if (_entityLocker.LockLibrary(libraryId)) if (_entityLocker.LockLibrary(libraryId))
{ {
await _channel.WriteAsync( await _workerChannel.WriteAsync(
new ScanLocalLibraryIfNeeded(libraryId), new ScanLocalLibraryIfNeeded(libraryId),
cancellationToken); cancellationToken);
} }
@ -112,7 +115,7 @@ namespace ErsatzTV.Services
{ {
if (_entityLocker.LockLibrary(library.Id)) if (_entityLocker.LockLibrary(library.Id))
{ {
await _channel.WriteAsync( await _plexWorkerChannel.WriteAsync(
new SynchronizePlexLibraryByIdIfNeeded(library.Id), new SynchronizePlexLibraryByIdIfNeeded(library.Id),
cancellationToken); cancellationToken);
} }
@ -120,6 +123,6 @@ namespace ErsatzTV.Services
} }
private ValueTask RebuildSearchIndex(CancellationToken cancellationToken) => private ValueTask RebuildSearchIndex(CancellationToken cancellationToken) =>
_channel.WriteAsync(new RebuildSearchIndex(), cancellationToken); _workerChannel.WriteAsync(new RebuildSearchIndex(), cancellationToken);
} }
} }

14
ErsatzTV/Services/WorkerService.cs

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using ErsatzTV.Application; using ErsatzTV.Application;
using ErsatzTV.Application.MediaSources.Commands; using ErsatzTV.Application.MediaSources.Commands;
using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Application.Plex.Commands;
using ErsatzTV.Application.Search.Commands; using ErsatzTV.Application.Search.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
@ -70,19 +69,6 @@ namespace ErsatzTV.Services
scanLocalLibrary.LibraryId, scanLocalLibrary.LibraryId,
error.Value)); error.Value));
break; break;
case ISynchronizePlexLibraryById synchronizePlexLibraryById:
Either<BaseError, string> result = await mediator.Send(
synchronizePlexLibraryById,
cancellationToken);
result.BiIter(
name => _logger.LogDebug(
"Done synchronizing plex library {Name}",
name),
error => _logger.LogWarning(
"Unable to synchronize plex library {LibraryId}: {Error}",
synchronizePlexLibraryById.PlexLibraryId,
error.Value));
break;
case RebuildSearchIndex rebuildSearchIndex: case RebuildSearchIndex rebuildSearchIndex:
await mediator.Send(rebuildSearchIndex, cancellationToken); await mediator.Send(rebuildSearchIndex, cancellationToken);
break; break;

2
ErsatzTV/Startup.cs

@ -33,6 +33,7 @@ using ErsatzTV.Services;
using ErsatzTV.Services.RunOnce; using ErsatzTV.Services.RunOnce;
using FluentValidation.AspNetCore; using FluentValidation.AspNetCore;
using MediatR; using MediatR;
using MediatR.Courier.DependencyInjection;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
@ -88,6 +89,7 @@ namespace ErsatzTV
services.AddServerSideBlazor(); services.AddServerSideBlazor();
services.AddMudServices(); services.AddMudServices();
services.AddCourier(Assembly.GetAssembly(typeof(LibraryScanProgress)));
Log.Logger.Information( Log.Logger.Information(
"ErsatzTV version {Version}", "ErsatzTV version {Version}",

Loading…
Cancel
Save