Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efae005447 | ||
|
|
cead787c55 | ||
|
|
77a69af1a8 | ||
|
|
8fea24a3a5 | ||
|
|
6b44873474 |
@@ -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,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record GetAllLanguageCodes : IRequest<List<CultureInfo>>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<CultureInfo>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task<List<CultureInfo>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<CultureInfo>();
|
||||
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
|
||||
List<string> allLanguageCodes = await _mediaItemRepository.GetAllLanguageCodes();
|
||||
foreach (string code in allLanguageCodes)
|
||||
{
|
||||
Option<CultureInfo> maybeCulture = allCultures.Find(
|
||||
ci => string.Equals(code, ci.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase));
|
||||
await maybeCulture.IfSomeAsync(cultureInfo => result.Add(cultureInfo));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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
|
||||
ITelevisionFolderScanner televisionFolderScanner,
|
||||
IMusicVideoFolderScanner musicVideoFolderScanner,
|
||||
IEntityLocker entityLocker,
|
||||
IMediator mediator,
|
||||
ILogger<ScanLocalLibraryHandler> logger)
|
||||
{
|
||||
_libraryRepository = libraryRepository;
|
||||
@@ -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,47 +64,63 @@ 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;
|
||||
}
|
||||
|
||||
libraryPath.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(libraryPath);
|
||||
}
|
||||
|
||||
localLibrary.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(localLibrary);
|
||||
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));
|
||||
}
|
||||
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);
|
||||
return Unit.Default;
|
||||
@@ -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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
IPlexSecretStore plexSecretStore,
|
||||
IPlexMovieLibraryScanner plexMovieLibraryScanner,
|
||||
IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner,
|
||||
ILibraryRepository libraryRepository,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SynchronizePlexLibraryByIdHandler> logger)
|
||||
{
|
||||
@@ -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
|
||||
}
|
||||
|
||||
parameters.Library.LastScan = DateTime.UtcNow;
|
||||
await _mediaSourceRepository.Update(parameters.Library);
|
||||
await _libraryRepository.UpdateLastScan(parameters.Library);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
new Mock<IMetadataRepository>().Object,
|
||||
_imageCache.Object,
|
||||
new Mock<ISearchIndex>().Object,
|
||||
new Mock<IMediator>().Object,
|
||||
new Mock<ILogger<MovieFolderScanner>>().Object
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
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; }
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -199,6 +199,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
"-c", "copy",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0"
|
||||
// "-avoid_negative_ts", "make_zero"
|
||||
};
|
||||
_arguments.AddRange(arguments);
|
||||
return this;
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
ShowMetadata GetFallbackMetadataForShow(string showFolder);
|
||||
Tuple<EpisodeMetadata, int> GetFallbackMetadata(Episode episode);
|
||||
MovieMetadata GetFallbackMetadata(Movie movie);
|
||||
MusicVideoMetadata GetFallbackMetadata(MusicVideo musicVideo);
|
||||
string GetSortTitle(string title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalMetadataProvider
|
||||
{
|
||||
Task<ShowMetadata> GetMetadataForShow(string showFolder);
|
||||
Task<Option<MusicVideoMetadata>> GetMetadataForMusicVideo(string filePath);
|
||||
Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(Show televisionShow, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName);
|
||||
Task<bool> RefreshFallbackMetadata(Movie movie);
|
||||
Task<bool> RefreshFallbackMetadata(Episode episode);
|
||||
Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo);
|
||||
Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -10,5 +10,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<Option<MediaItem>> Get(int id);
|
||||
Task<List<MediaItem>> GetAll();
|
||||
Task<bool> Update(MediaItem mediaItem);
|
||||
Task<List<string>> GetAllLanguageCodes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
public interface IMusicVideoRepository
|
||||
{
|
||||
Task<Option<MusicVideo>> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata);
|
||||
|
||||
Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> Add(
|
||||
LibraryPath libraryPath,
|
||||
string filePath,
|
||||
MusicVideoMetadata metadata);
|
||||
|
||||
Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> GetOrAdd(LibraryPath libraryPath, string path);
|
||||
Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath);
|
||||
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
|
||||
Task<bool> AddGenre(MusicVideoMetadata metadata, Genre genre);
|
||||
|
||||
@@ -36,6 +36,19 @@ namespace ErsatzTV.Core.Metadata
|
||||
return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata;
|
||||
}
|
||||
|
||||
public MusicVideoMetadata GetFallbackMetadata(MusicVideo musicVideo)
|
||||
{
|
||||
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
string fileName = Path.GetFileName(path);
|
||||
var metadata = new MusicVideoMetadata
|
||||
{
|
||||
MetadataKind = MetadataKind.Fallback,
|
||||
Title = fileName ?? path
|
||||
};
|
||||
|
||||
return fileName != null ? GetMusicVideoMetadata(fileName, metadata) : metadata;
|
||||
}
|
||||
|
||||
public string GetSortTitle(string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
@@ -112,6 +125,30 @@ namespace ErsatzTV.Core.Metadata
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private MusicVideoMetadata GetMusicVideoMetadata(string fileName, MusicVideoMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
const string PATTERN = @"^(.*?) - (.*?).\w+$";
|
||||
Match match = Regex.Match(fileName, PATTERN);
|
||||
if (match.Success)
|
||||
{
|
||||
metadata.Artist = match.Groups[1].Value;
|
||||
metadata.Title = match.Groups[2].Value;
|
||||
metadata.Genres = new List<Genre>();
|
||||
metadata.Tags = new List<Tag>();
|
||||
metadata.Studios = new List<Studio>();
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private ShowMetadata GetTelevisionShowMetadata(string fileName, ShowMetadata metadata)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public record LibraryScanProgress(int LibraryId, decimal Progress) : INotification;
|
||||
}
|
||||
@@ -70,23 +70,6 @@ namespace ErsatzTV.Core.Metadata
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<Option<MusicVideoMetadata>> GetMetadataForMusicVideo(string filePath)
|
||||
{
|
||||
string nfoFileName = Path.ChangeExtension(filePath, "nfo");
|
||||
Option<MusicVideoMetadata> maybeMetadata = None;
|
||||
if (_localFileSystem.FileExists(nfoFileName))
|
||||
{
|
||||
maybeMetadata = await LoadMusicVideoMetadata(nfoFileName);
|
||||
}
|
||||
|
||||
return maybeMetadata.Map(
|
||||
metadata =>
|
||||
{
|
||||
metadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(metadata.Title);
|
||||
return metadata;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName) =>
|
||||
LoadMovieMetadata(movie, nfoFileName).Bind(
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
@@ -109,7 +92,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
LoadMusicVideoMetadata(nfoFileName).Bind(
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
metadata => ApplyMetadataUpdate(musicVideo, metadata),
|
||||
() => Task.FromResult(false)));
|
||||
() => RefreshFallbackMetadata(musicVideo)));
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(Movie movie) =>
|
||||
ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie));
|
||||
@@ -117,6 +100,9 @@ namespace ErsatzTV.Core.Metadata
|
||||
public Task<bool> RefreshFallbackMetadata(Episode episode) =>
|
||||
ApplyMetadataUpdate(episode, _fallbackMetadataProvider.GetFallbackMetadata(episode));
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo) =>
|
||||
ApplyMetadataUpdate(musicVideo, _fallbackMetadataProvider.GetFallbackMetadata(musicVideo));
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder) =>
|
||||
ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder));
|
||||
|
||||
|
||||
@@ -164,7 +164,9 @@ namespace ErsatzTV.Core.Metadata
|
||||
FFprobeStream videoStream = json.streams.FirstOrDefault(s => s.codec_type == "video");
|
||||
if (videoStream != null)
|
||||
{
|
||||
version.SampleAspectRatio = videoStream.sample_aspect_ratio;
|
||||
version.SampleAspectRatio = string.IsNullOrWhiteSpace(videoStream.sample_aspect_ratio)
|
||||
? "1:1"
|
||||
: videoStream.sample_aspect_ratio;
|
||||
version.DisplayAspectRatio = videoStream.display_aspect_ratio;
|
||||
version.Width = videoStream.width;
|
||||
version.Height = videoStream.height;
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
_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
|
||||
|
||||
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)))
|
||||
|
||||
@@ -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
|
||||
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
|
||||
IImageCache imageCache,
|
||||
ISearchIndex searchIndex,
|
||||
IMusicVideoRepository musicVideoRepository,
|
||||
IMediator mediator,
|
||||
ILogger<MusicVideoFolderScanner> logger) : base(
|
||||
localFileSystem,
|
||||
localStatisticsProvider,
|
||||
@@ -42,47 +46,50 @@ 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))
|
||||
{
|
||||
folderQueue.Enqueue(folder);
|
||||
}
|
||||
folderQueue.Enqueue(libraryPath.Path);
|
||||
|
||||
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));
|
||||
|
||||
var allFiles = _localFileSystem.ListFiles(movieFolder)
|
||||
string musicVideoFolder = folderQueue.Dequeue();
|
||||
foldersCompleted++;
|
||||
|
||||
var allFiles = _localFileSystem.ListFiles(musicVideoFolder)
|
||||
.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))
|
||||
.Filter(
|
||||
f => !ExtraFiles.Any(
|
||||
e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase)))
|
||||
.Filter(f => f.Contains(" - "))
|
||||
.ToList();
|
||||
|
||||
if (allFiles.Count == 0)
|
||||
foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder)
|
||||
.OrderBy(identity))
|
||||
{
|
||||
foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity))
|
||||
{
|
||||
folderQueue.Enqueue(subdirectory);
|
||||
}
|
||||
|
||||
continue;
|
||||
folderQueue.Enqueue(subdirectory);
|
||||
}
|
||||
|
||||
if (_localFileSystem.GetLastWriteTime(movieFolder) < lastScan)
|
||||
if (_localFileSystem.GetLastWriteTime(musicVideoFolder) < lastScan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -90,11 +97,11 @@ namespace ErsatzTV.Core.Metadata
|
||||
foreach (string file in allFiles.OrderBy(identity))
|
||||
{
|
||||
// TODO: figure out how to rebuild playouts
|
||||
Either<BaseError, MediaItemScanResult<MusicVideo>> maybeMusicVideo =
|
||||
await FindOrCreateMusicVideo(libraryPath, file)
|
||||
.BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath))
|
||||
.BindT(UpdateMetadata)
|
||||
.BindT(UpdateThumbnail);
|
||||
Either<BaseError, MediaItemScanResult<MusicVideo>> maybeMusicVideo = await _musicVideoRepository
|
||||
.GetOrAdd(libraryPath, file)
|
||||
.BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath))
|
||||
.BindT(UpdateMetadata)
|
||||
.BindT(UpdateThumbnail);
|
||||
|
||||
await maybeMusicVideo.Match(
|
||||
async result =>
|
||||
@@ -129,34 +136,13 @@ namespace ErsatzTV.Core.Metadata
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> FindOrCreateMusicVideo(
|
||||
LibraryPath libraryPath,
|
||||
string filePath)
|
||||
{
|
||||
Option<MusicVideoMetadata> maybeMetadata = await _localMetadataProvider.GetMetadataForMusicVideo(filePath);
|
||||
return await maybeMetadata.Match(
|
||||
async metadata =>
|
||||
{
|
||||
Option<MusicVideo> maybeMusicVideo =
|
||||
await _musicVideoRepository.GetByMetadata(libraryPath, metadata);
|
||||
return await maybeMusicVideo.Match(
|
||||
musicVideo =>
|
||||
Right<BaseError, MediaItemScanResult<MusicVideo>>(
|
||||
new MediaItemScanResult<MusicVideo>(musicVideo))
|
||||
.AsTask(),
|
||||
async () => await _musicVideoRepository.Add(libraryPath, filePath, metadata));
|
||||
},
|
||||
() => Left<BaseError, MediaItemScanResult<MusicVideo>>(
|
||||
BaseError.New("Unable to locate metadata for music video")).AsTask());
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateMetadata(
|
||||
MediaItemScanResult<MusicVideo> result)
|
||||
{
|
||||
try
|
||||
{
|
||||
MusicVideo musicVideo = result.Item;
|
||||
return await LocateNfoFile(musicVideo).Match<Task<Either<BaseError, MediaItemScanResult<MusicVideo>>>>(
|
||||
await LocateNfoFile(musicVideo).Match(
|
||||
async nfoFile =>
|
||||
{
|
||||
bool shouldUpdate = Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match(
|
||||
@@ -172,11 +158,21 @@ namespace ErsatzTV.Core.Metadata
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
() => Left<BaseError, MediaItemScanResult<MusicVideo>>(
|
||||
BaseError.New("Unable to locate metadata for music video")).AsTask());
|
||||
async () =>
|
||||
{
|
||||
if (!Optional(musicVideo.MusicVideoMetadata).Flatten().Any())
|
||||
{
|
||||
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
|
||||
if (await _localMetadataProvider.RefreshFallbackMetadata(musicVideo))
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
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
|
||||
IMetadataRepository metadataRepository,
|
||||
IImageCache imageCache,
|
||||
ISearchIndex searchIndex,
|
||||
IMediator mediator,
|
||||
ILogger<TelevisionFolderScanner> logger) : base(
|
||||
localFileSystem,
|
||||
localStatisticsProvider,
|
||||
@@ -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
|
||||
|
||||
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))
|
||||
|
||||
@@ -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
|
||||
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);
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
{
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
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())
|
||||
|
||||
@@ -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
|
||||
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
|
||||
{
|
||||
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
|
||||
await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys);
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
|
||||
await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, 0));
|
||||
|
||||
return Unit.Default;
|
||||
},
|
||||
error =>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
@@ -11,10 +13,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
{
|
||||
public class MediaItemRepository : IMediaItemRepository
|
||||
{
|
||||
private readonly IDbConnection _dbConnection;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public MediaItemRepository(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public MediaItemRepository(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_dbConnection = dbConnection;
|
||||
}
|
||||
|
||||
public async Task<Option<MediaItem>> Get(int id)
|
||||
{
|
||||
@@ -38,5 +44,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
context.MediaItems.Update(mediaItem);
|
||||
return await context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
public Task<List<string>> GetAllLanguageCodes() =>
|
||||
_dbConnection.QueryAsync<string>(
|
||||
@"SELECT LanguageCode FROM
|
||||
(SELECT Language AS LanguageCode
|
||||
FROM MediaStream WHERE Language IS NOT NULL
|
||||
UNION ALL SELECT PreferredLanguageCode AS LanguageCode
|
||||
FROM Channel WHERE PreferredLanguageCode IS NOT NULL)
|
||||
GROUP BY LanguageCode
|
||||
ORDER BY COUNT(LanguageCode) DESC")
|
||||
.Map(result => result.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,81 +25,35 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
_dbConnection = dbConnection;
|
||||
}
|
||||
|
||||
public async Task<Option<MusicVideo>> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Option<int> maybeId = await dbContext.MusicVideoMetadata
|
||||
.Where(s => s.Artist == metadata.Artist && s.Title == metadata.Title && s.Year == metadata.Year)
|
||||
.Where(s => s.MusicVideo.LibraryPathId == libraryPath.Id)
|
||||
.SingleOrDefaultAsync()
|
||||
.Map(Optional)
|
||||
.MapT(sm => sm.MusicVideoId);
|
||||
|
||||
return await maybeId.Match(
|
||||
id =>
|
||||
{
|
||||
return dbContext.MusicVideos
|
||||
.AsNoTracking()
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Artwork)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Genres)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Tags)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Studios)
|
||||
.Include(mv => mv.LibraryPath)
|
||||
.ThenInclude(lp => lp.Library)
|
||||
.Include(mv => mv.MediaVersions)
|
||||
.ThenInclude(mv => mv.MediaFiles)
|
||||
.OrderBy(mv => mv.Id)
|
||||
.SingleOrDefaultAsync(mv => mv.Id == id)
|
||||
.Map(Optional);
|
||||
},
|
||||
() => Option<MusicVideo>.None.AsTask());
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> Add(
|
||||
public async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> GetOrAdd(
|
||||
LibraryPath libraryPath,
|
||||
string filePath,
|
||||
MusicVideoMetadata metadata)
|
||||
string path)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Option<MusicVideo> maybeExisting = await dbContext.MusicVideos
|
||||
.AsNoTracking()
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Artwork)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Genres)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Tags)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Studios)
|
||||
.Include(mv => mv.LibraryPath)
|
||||
.ThenInclude(lp => lp.Library)
|
||||
.Include(mv => mv.MediaVersions)
|
||||
.ThenInclude(mv => mv.MediaFiles)
|
||||
.Include(mv => mv.MediaVersions)
|
||||
.ThenInclude(mv => mv.Streams)
|
||||
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
|
||||
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
|
||||
|
||||
try
|
||||
{
|
||||
metadata.DateAdded = DateTime.UtcNow;
|
||||
metadata.Genres ??= new List<Genre>();
|
||||
metadata.Tags ??= new List<Tag>();
|
||||
metadata.Studios ??= new List<Studio>();
|
||||
var musicVideo = new MusicVideo
|
||||
{
|
||||
LibraryPathId = libraryPath.Id,
|
||||
MusicVideoMetadata = new List<MusicVideoMetadata> { metadata },
|
||||
MediaVersions = new List<MediaVersion>
|
||||
{
|
||||
new()
|
||||
{
|
||||
MediaFiles = new List<MediaFile>
|
||||
{
|
||||
new() { Path = filePath }
|
||||
},
|
||||
Streams = new List<MediaStream>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await dbContext.MusicVideos.AddAsync(musicVideo);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.Entry(musicVideo).Reference(s => s.LibraryPath).LoadAsync();
|
||||
await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||
|
||||
return new MediaItemScanResult<MusicVideo>(musicVideo) { IsAdded = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
return await maybeExisting.Match(
|
||||
mediaItem =>
|
||||
Right<BaseError, MediaItemScanResult<MusicVideo>>(
|
||||
new MediaItemScanResult<MusicVideo>(mediaItem) { IsAdded = false }).AsTask(),
|
||||
async () => await AddMusicVideo(dbContext, libraryPath.Id, path));
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath) =>
|
||||
@@ -177,5 +131,40 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.SingleOrDefaultAsync(m => m.Id == musicVideoId)
|
||||
.Map(Optional);
|
||||
}
|
||||
|
||||
private static async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> AddMusicVideo(
|
||||
TvContext dbContext,
|
||||
int libraryPathId,
|
||||
string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var musicVideo = new MusicVideo
|
||||
{
|
||||
LibraryPathId = libraryPathId,
|
||||
MediaVersions = new List<MediaVersion>
|
||||
{
|
||||
new()
|
||||
{
|
||||
MediaFiles = new List<MediaFile>
|
||||
{
|
||||
new() { Path = path }
|
||||
},
|
||||
Streams = new List<MediaStream>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await dbContext.MusicVideos.AddAsync(musicVideo);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.Entry(musicVideo).Reference(m => m.LibraryPath).LoadAsync();
|
||||
await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||
return new MediaItemScanResult<MusicVideo>(musicVideo) { IsAdded = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1964
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
+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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+1964
File diff suppressed because it is too large
Load Diff
+14
@@ -0,0 +1,14 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Update_MediaVersion_SampleAspectRatio11 : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder) => migrationBuilder.Sql(
|
||||
@"UPDATE MediaVersion SET SampleAspectRatio = '1:1' where SampleAspectRatio is null");
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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">
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
@using ErsatzTV.Application.FFmpegProfiles
|
||||
@using ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
@using ErsatzTV.Application.Images.Commands
|
||||
@using ErsatzTV.Application.MediaItems.Queries
|
||||
@using System.Globalization
|
||||
@using ErsatzTV.Application.Channels
|
||||
@using ErsatzTV.Application.Channels.Queries
|
||||
@inject NavigationManager NavigationManager
|
||||
@@ -34,7 +36,13 @@
|
||||
<MudSelectItem Value="@profile.Id">@profile.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField Class="mt-3" Label="Preferred Language Code" @bind-Value="_model.PreferredLanguageCode" For="@(() => _model.PreferredLanguageCode)"/>
|
||||
<MudSelect Class="mt-3" Label="Preferred Language" @bind-Value="_model.PreferredLanguageCode" For="@(() => _model.PreferredLanguageCode)">
|
||||
<MudSelectItem Value="@((string) null)">(none)</MudSelectItem>
|
||||
@foreach (CultureInfo culture in _availableCultures)
|
||||
{
|
||||
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
|
||||
<MudItem xs="6">
|
||||
<InputFile id="fileInput" OnChange="UploadLogo" hidden/>
|
||||
@@ -74,10 +82,12 @@
|
||||
private ValidationMessageStore _messageStore;
|
||||
|
||||
private List<FFmpegProfileViewModel> _ffmpegProfiles;
|
||||
private List<CultureInfo> _availableCultures;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await LoadFFmpegProfilesAsync();
|
||||
_availableCultures = await Mediator.Send(new GetAllLanguageCodes());
|
||||
|
||||
if (Id.HasValue)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@using ErsatzTV.Application.Channels.Queries
|
||||
@using ErsatzTV.Application.FFmpegProfiles
|
||||
@using ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
@using System.Globalization
|
||||
@inject IDialogService Dialog
|
||||
@inject IMediator Mediator
|
||||
|
||||
@@ -99,7 +100,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadChannelsAsync() => _channels = await Mediator.Send(new GetAllChannels())
|
||||
.Map(list => list.OrderBy(c => decimal.Parse(c.Number)).ToList());
|
||||
private async Task LoadChannelsAsync()
|
||||
{
|
||||
List<ChannelViewModel> channels = await Mediator.Send(new GetAllChannels());
|
||||
IOrderedEnumerable<ChannelViewModel> sorted = channels.OrderBy(c => decimal.Parse(c.Number));
|
||||
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
_channels = new List<ChannelViewModel>();
|
||||
foreach (ChannelViewModel channel in sorted)
|
||||
{
|
||||
Option<CultureInfo> maybeCultureInfo = allCultures.Find(
|
||||
ci => string.Equals(
|
||||
ci.ThreeLetterISOLanguageName,
|
||||
channel.PreferredLanguageCode,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
maybeCultureInfo.Match(
|
||||
cultureInfo => _channels.Add(channel with { PreferredLanguageCode = cultureInfo.EnglishName }),
|
||||
() => _channels.Add(channel));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+10
-17
@@ -2,8 +2,9 @@
|
||||
@using ErsatzTV.Application.FFmpegProfiles
|
||||
@using ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
@using ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
@using Unit = LanguageExt.Unit
|
||||
@using ErsatzTV.Application.MediaItems.Queries
|
||||
@using System.Globalization
|
||||
@using Unit = LanguageExt.Unit
|
||||
@inject IDialogService Dialog
|
||||
@inject IMediator Mediator
|
||||
@inject ILogger<FFmpeg> Logger
|
||||
@@ -30,9 +31,12 @@
|
||||
}
|
||||
</MudSelect>
|
||||
</MudElement>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudTextField T="string" Label="Preferred Language Code" @bind-Value="_ffmpegSettings.PreferredLanguageCode" Validation="@(new Func<string, string>(ValidateLanguageCode))" Required="true" RequiredError="Preferred Language Code is required!"/>
|
||||
</MudElement>
|
||||
<MudSelect Class="mt-3" Label="Preferred Language" @bind-Value="_ffmpegSettings.PreferredLanguageCode" For="@(() => _ffmpegSettings.PreferredLanguageCode)" Required="true" RequiredError="Preferred Language Code is required!">
|
||||
@foreach (CultureInfo culture in _availableCultures)
|
||||
{
|
||||
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudSwitch T="bool"
|
||||
Label="Save troubleshooting reports to disk"
|
||||
@@ -115,12 +119,14 @@
|
||||
private FFmpegSettingsViewModel _ffmpegSettings;
|
||||
|
||||
private List<FFmpegProfileViewModel> _ffmpegProfiles;
|
||||
private List<CultureInfo> _availableCultures;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
_ffmpegSettings = await Mediator.Send(new GetFFmpegSettings());
|
||||
_success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath);
|
||||
await LoadFFmpegProfilesAsync();
|
||||
_availableCultures = await Mediator.Send(new GetAllLanguageCodes());
|
||||
}
|
||||
|
||||
private async Task SaveSettings()
|
||||
@@ -137,19 +143,6 @@
|
||||
|
||||
private static string ValidatePathExists(string path) => !File.Exists(path) ? "Path does not exist" : null;
|
||||
|
||||
private static string ValidateLanguageCode(string languageCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(languageCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Option<CultureInfo> culture = CultureInfo.GetCultures(CultureTypes.NeutralCultures)
|
||||
.FirstOrDefault(ci => string.Equals(ci.ThreeLetterISOLanguageName, languageCode, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return culture.IsNone ? "Preferred language code is invalid" : null;
|
||||
}
|
||||
|
||||
private async Task LoadFFmpegProfilesAsync() =>
|
||||
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles());
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 @@
|
||||
<col/>
|
||||
<col/>
|
||||
<col/>
|
||||
<col style="width: 120px;"/>
|
||||
<col style="width: 180px;"/>
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh>Library Kind</MudTh>
|
||||
@@ -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 @@
|
||||
|
||||
@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 @@
|
||||
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 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -40,8 +40,10 @@
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
_channels = await Mediator.Send(new GetAllChannels());
|
||||
_programSchedules = await Mediator.Send(new GetAllProgramSchedules());
|
||||
_channels = await Mediator.Send(new GetAllChannels())
|
||||
.Map(list => list.OrderBy(vm => decimal.Parse(vm.Number)).ToList());
|
||||
_programSchedules = await Mediator.Send(new GetAllProgramSchedules())
|
||||
.Map(list => list.OrderBy(vm => vm.Name).ToList());
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
{
|
||||
if (_entityLocker.LockLibrary(libraryId))
|
||||
{
|
||||
await _channel.WriteAsync(
|
||||
await _workerChannel.WriteAsync(
|
||||
new ScanLocalLibraryIfNeeded(libraryId),
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
private ValueTask RebuildSearchIndex(CancellationToken cancellationToken) =>
|
||||
_channel.WriteAsync(new RebuildSearchIndex(), cancellationToken);
|
||||
_workerChannel.WriteAsync(new RebuildSearchIndex(), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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;
|
||||
|
||||
@@ -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
|
||||
services.AddServerSideBlazor();
|
||||
|
||||
services.AddMudServices();
|
||||
services.AddCourier(Assembly.GetAssembly(typeof(LibraryScanProgress)));
|
||||
|
||||
Log.Logger.Information(
|
||||
"ErsatzTV version {Version}",
|
||||
|
||||
Reference in New Issue
Block a user