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 @@ -50,7 +50,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
AudioCodec = request.AudioCodec,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudness= request.NormalizeLoudness,
NormalizeLoudness = request.NormalizeLoudness,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeAudio = request.NormalizeAudio,

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

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

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

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

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

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

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

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

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

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

1
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -11,6 +11,7 @@ @@ -11,6 +11,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">

1
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

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

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

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

6
ErsatzTV.Core/Metadata/LibraryScanProgress.cs

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

27
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

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

15
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

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

9
ErsatzTV.Core/Plex/PlexLibraryScanner.cs

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

22
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

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

12
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

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

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

@ -62,6 +62,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -62,6 +62,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
"UPDATE Library SET LastScan = @LastScan WHERE Id = @Id",
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)
{
using TvContext context = _dbContextFactory.CreateDbContext();

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

@ -52,6 +52,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -52,6 +52,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(lp => lp.Library)
.Include(mv => mv.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mv => mv.MediaVersions)
.ThenInclude(mv => mv.Streams)
.OrderBy(mv => mv.Id)
.SingleOrDefaultAsync(mv => mv.Id == id)
.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 @@ @@ -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 @@ @@ -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 @@ @@ -19,6 +19,7 @@
<PackageReference Include="FluentValidation" Version="9.5.3" />
<PackageReference Include="FluentValidation.AspNetCore" Version="9.5.3" />
<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="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4">

15
ErsatzTV/Pages/CollectionItems.razor

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

4
ErsatzTV/Pages/FFmpegEditor.razor

@ -62,12 +62,12 @@ @@ -62,12 +62,12 @@
<MudTextField Disabled="@(!_model.Transcode)" Label="Frame Rate" @bind-Value="_model.FrameRate" For="@(() => _model.FrameRate)" Adornment="Adornment.End" AdornmentText="fps"/>
</MudElement>
<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>
</MudItem>
<MudItem>
<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">
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudElement>

55
ErsatzTV/Pages/Libraries.razor

@ -1,12 +1,17 @@ @@ -1,12 +1,17 @@
@page "/media/libraries"
@using MediatR.Courier
@using ErsatzTV.Application.Libraries
@using ErsatzTV.Application.Libraries.Queries
@using ErsatzTV.Application.MediaSources.Commands
@using ErsatzTV.Application.Plex.Commands
@using ErsatzTV.Core.Metadata
@using System.Threading
@implements IDisposable
@inject IMediator Mediator
@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">
<MudTable Hover="true" Items="_libraries" Dense="true">
@ -17,7 +22,7 @@ @@ -17,7 +22,7 @@
<col/>
<col/>
<col/>
<col style="width: 120px;"/>
<col style="width: 180px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Library Kind</MudTh>
@ -33,12 +38,21 @@ @@ -33,12 +38,21 @@
<div style="align-items: center; display: flex;">
@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;">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
</div>
}
else
{
<div style="width: 48px"></div>
<MudTooltip Text="Scan Library">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@Locker.IsLibraryLocked(context.Id)"
@ -63,14 +77,21 @@ @@ -63,14 +77,21 @@
@code {
private IList<LibraryViewModel> _libraries;
private Dictionary<int, int> _progressByLibrary;
protected override void OnInitialized() =>
protected override void OnInitialized()
{
Locker.OnLibraryChanged += LockChanged;
Courier.Subscribe<LibraryScanProgress>(HandleScanProgress);
}
protected override async Task OnParametersSetAsync() => await LoadLibraries();
private async Task LoadLibraries() =>
private async Task LoadLibraries()
{
_libraries = await Mediator.Send(new GetAllLibraries());
_progressByLibrary = _libraries.ToDictionary(vm => vm.Id, _ => 0);
}
private async Task ScanLibrary(LibraryViewModel library)
{
@ -79,10 +100,10 @@ @@ -79,10 +100,10 @@
switch (library)
{
case LocalLibraryViewModel:
await Channel.WriteAsync(new ForceScanLocalLibrary(library.Id));
await WorkerChannel.WriteAsync(new ForceScanLocalLibrary(library.Id));
break;
case PlexLibraryViewModel:
await Channel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id));
await PlexWorkerChannel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id));
break;
}
@ -93,6 +114,26 @@ @@ -93,6 +114,26 @@
private void LockChanged(object sender, EventArgs e) =>
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 @@ @@ -74,7 +74,7 @@
{
if (Locker.LockLibrary(_library.Id))
{
await Channel.WriteAsync(new ForceRescanLocalLibrary(_library.Id));
await Channel.WriteAsync(new ScanLocalLibraryIfNeeded(_library.Id));
NavigationManager.NavigateTo("/media/libraries");
}
});

5
ErsatzTV/Pages/MusicVideoList.razor

@ -132,7 +132,10 @@ @@ -132,7 +132,10 @@
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);
}

2
ErsatzTV/Pages/PlexLibrariesEditor.razor

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

11
ErsatzTV/Pages/Search.razor

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

56
ErsatzTV/Services/PlexService.cs

@ -52,17 +52,24 @@ namespace ErsatzTV.Services @@ -52,17 +52,24 @@ namespace ErsatzTV.Services
{
try
{
Task requestTask = request switch
Task requestTask;
switch (request)
{
TryCompletePlexPinFlow pinRequest => CompletePinFlow(pinRequest, cancellationToken),
SynchronizePlexMediaSources sourcesRequest => SynchronizeSources(
sourcesRequest,
cancellationToken),
SynchronizePlexLibraries synchronizePlexLibrariesRequest => SynchronizeLibraries(
synchronizePlexLibrariesRequest,
cancellationToken),
_ => throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}")
};
case TryCompletePlexPinFlow pinRequest:
requestTask = CompletePinFlow(pinRequest, cancellationToken);
break;
case SynchronizePlexMediaSources sourcesRequest:
requestTask = SynchronizeSources(sourcesRequest, cancellationToken);
break;
case SynchronizePlexLibraries synchronizePlexLibrariesRequest:
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;
}
@ -109,17 +116,8 @@ namespace ErsatzTV.Services @@ -109,17 +116,8 @@ namespace ErsatzTV.Services
Either<BaseError, bool> result = await mediator.Send(request, cancellationToken);
result.BiIter(
success =>
{
if (success)
{
_logger.LogInformation("Successfully authenticated with plex");
}
else
{
_logger.LogInformation("Plex authentication timeout");
}
},
success => _logger.LogInformation(
success ? "Successfully authenticated with plex" : "Plex authentication timeout"),
error => _logger.LogWarning("Unable to poll plex token: {Error}", error.Value));
}
@ -138,5 +136,21 @@ namespace ErsatzTV.Services @@ -138,5 +136,21 @@ namespace ErsatzTV.Services
request.PlexMediaSourceId,
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 @@ -21,19 +21,22 @@ namespace ErsatzTV.Services
{
public class SchedulerService : BackgroundService
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IEntityLocker _entityLocker;
private readonly ILogger<SchedulerService> _logger;
private readonly ChannelWriter<IPlexBackgroundServiceRequest> _plexWorkerChannel;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public SchedulerService(
IServiceScopeFactory serviceScopeFactory,
ChannelWriter<IBackgroundServiceRequest> channel,
ChannelWriter<IBackgroundServiceRequest> workerChannel,
ChannelWriter<IPlexBackgroundServiceRequest> plexWorkerChannel,
IEntityLocker entityLocker,
ILogger<SchedulerService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_channel = channel;
_workerChannel = workerChannel;
_plexWorkerChannel = plexWorkerChannel;
_entityLocker = entityLocker;
_logger = logger;
}
@ -74,7 +77,7 @@ namespace ErsatzTV.Services @@ -74,7 +77,7 @@ namespace ErsatzTV.Services
List<int> playoutIds = await dbContext.Playouts.Map(p => p.Id).ToListAsync(cancellationToken);
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 @@ -92,7 +95,7 @@ namespace ErsatzTV.Services
{
if (_entityLocker.LockLibrary(libraryId))
{
await _channel.WriteAsync(
await _workerChannel.WriteAsync(
new ScanLocalLibraryIfNeeded(libraryId),
cancellationToken);
}
@ -112,7 +115,7 @@ namespace ErsatzTV.Services @@ -112,7 +115,7 @@ namespace ErsatzTV.Services
{
if (_entityLocker.LockLibrary(library.Id))
{
await _channel.WriteAsync(
await _plexWorkerChannel.WriteAsync(
new SynchronizePlexLibraryByIdIfNeeded(library.Id),
cancellationToken);
}
@ -120,6 +123,6 @@ namespace ErsatzTV.Services @@ -120,6 +123,6 @@ namespace ErsatzTV.Services
}
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; @@ -5,7 +5,6 @@ using System.Threading.Tasks;
using ErsatzTV.Application;
using ErsatzTV.Application.MediaSources.Commands;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Application.Plex.Commands;
using ErsatzTV.Application.Search.Commands;
using ErsatzTV.Core;
using LanguageExt;
@ -70,19 +69,6 @@ namespace ErsatzTV.Services @@ -70,19 +69,6 @@ namespace ErsatzTV.Services
scanLocalLibrary.LibraryId,
error.Value));
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:
await mediator.Send(rebuildSearchIndex, cancellationToken);
break;

2
ErsatzTV/Startup.cs

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

Loading…
Cancel
Save