mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* add ability to deep scan just a single tv show for Plex, Emby, and Jellyfin
Including "/api/libraries/{id:int}/scan-show" REST API endpoint to
trigger.
* restrict plex search results to the intended library
* restrict scanning to media server libraries that are marked to sync with etv
* fix previous commit
* also guard library scan api
* add scan buttons to show ui
* scan single plex show by id
* scan jellyfin and emby single shows by id
* update changelog
---------
Co-authored-by: Jeff Slutter <MrMustard@gmail.com>
Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
pull/2321/head
52 changed files with 1795 additions and 55 deletions
@ -0,0 +1,82 @@ |
|||||||
|
using ErsatzTV.Application.Libraries; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Errors; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.FFmpeg.Runtime; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using System.Globalization; |
||||||
|
using System.Threading.Channels; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Emby; |
||||||
|
|
||||||
|
public class CallEmbyShowScannerHandler : CallLibraryScannerHandler<SynchronizeEmbyShowById>, |
||||||
|
IRequestHandler<SynchronizeEmbyShowById, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
public CallEmbyShowScannerHandler( |
||||||
|
IDbContextFactory<TvContext> dbContextFactory, |
||||||
|
IConfigElementRepository configElementRepository, |
||||||
|
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel, |
||||||
|
IMediator mediator, |
||||||
|
IRuntimeInfo runtimeInfo) |
||||||
|
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
Task<Either<BaseError, string>> IRequestHandler<SynchronizeEmbyShowById, Either<BaseError, string>>.Handle( |
||||||
|
SynchronizeEmbyShowById request, |
||||||
|
CancellationToken cancellationToken) => Handle(request, cancellationToken); |
||||||
|
|
||||||
|
private async Task<Either<BaseError, string>> Handle( |
||||||
|
SynchronizeEmbyShowById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
Validation<BaseError, string> validation = await Validate(request); |
||||||
|
return await validation.Match( |
||||||
|
scanner => PerformScan(scanner, request, cancellationToken), |
||||||
|
error => |
||||||
|
{ |
||||||
|
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>()) |
||||||
|
{ |
||||||
|
return Task.FromResult<Either<BaseError, string>>(scanIsNotRequired); |
||||||
|
} |
||||||
|
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(error.Join()); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Either<BaseError, string>> PerformScan( |
||||||
|
string scanner, |
||||||
|
SynchronizeEmbyShowById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
var arguments = new List<string> |
||||||
|
{ |
||||||
|
"scan-emby-show", |
||||||
|
request.EmbyLibraryId.ToString(CultureInfo.InvariantCulture), |
||||||
|
request.ShowId.ToString(CultureInfo.InvariantCulture) |
||||||
|
}; |
||||||
|
|
||||||
|
if (request.DeepScan) |
||||||
|
{ |
||||||
|
arguments.Add("--deep"); |
||||||
|
} |
||||||
|
|
||||||
|
return await base.PerformScan(scanner, arguments, cancellationToken); |
||||||
|
} |
||||||
|
|
||||||
|
protected override Task<DateTimeOffset> GetLastScan( |
||||||
|
TvContext dbContext, |
||||||
|
SynchronizeEmbyShowById request) |
||||||
|
{ |
||||||
|
return Task.FromResult(DateTimeOffset.MinValue); |
||||||
|
} |
||||||
|
|
||||||
|
protected override bool ScanIsRequired( |
||||||
|
DateTimeOffset lastScan, |
||||||
|
int libraryRefreshInterval, |
||||||
|
SynchronizeEmbyShowById request) |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Emby; |
||||||
|
|
||||||
|
public record SynchronizeEmbyShowById(int EmbyLibraryId, int ShowId, bool DeepScan) |
||||||
|
: IRequest<Either<BaseError, string>>, IScannerBackgroundServiceRequest; |
||||||
@ -0,0 +1,82 @@ |
|||||||
|
using ErsatzTV.Application.Libraries; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Errors; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.FFmpeg.Runtime; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using System.Globalization; |
||||||
|
using System.Threading.Channels; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Jellyfin; |
||||||
|
|
||||||
|
public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler<SynchronizeJellyfinShowById>, |
||||||
|
IRequestHandler<SynchronizeJellyfinShowById, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
public CallJellyfinShowScannerHandler( |
||||||
|
IDbContextFactory<TvContext> dbContextFactory, |
||||||
|
IConfigElementRepository configElementRepository, |
||||||
|
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel, |
||||||
|
IMediator mediator, |
||||||
|
IRuntimeInfo runtimeInfo) |
||||||
|
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
Task<Either<BaseError, string>> IRequestHandler<SynchronizeJellyfinShowById, Either<BaseError, string>>.Handle( |
||||||
|
SynchronizeJellyfinShowById request, |
||||||
|
CancellationToken cancellationToken) => Handle(request, cancellationToken); |
||||||
|
|
||||||
|
private async Task<Either<BaseError, string>> Handle( |
||||||
|
SynchronizeJellyfinShowById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
Validation<BaseError, string> validation = await Validate(request); |
||||||
|
return await validation.Match( |
||||||
|
scanner => PerformScan(scanner, request, cancellationToken), |
||||||
|
error => |
||||||
|
{ |
||||||
|
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>()) |
||||||
|
{ |
||||||
|
return Task.FromResult<Either<BaseError, string>>(scanIsNotRequired); |
||||||
|
} |
||||||
|
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(error.Join()); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Either<BaseError, string>> PerformScan( |
||||||
|
string scanner, |
||||||
|
SynchronizeJellyfinShowById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
var arguments = new List<string> |
||||||
|
{ |
||||||
|
"scan-jellyfin-show", |
||||||
|
request.JellyfinLibraryId.ToString(CultureInfo.InvariantCulture), |
||||||
|
request.ShowId.ToString(CultureInfo.InvariantCulture) |
||||||
|
}; |
||||||
|
|
||||||
|
if (request.DeepScan) |
||||||
|
{ |
||||||
|
arguments.Add("--deep"); |
||||||
|
} |
||||||
|
|
||||||
|
return await base.PerformScan(scanner, arguments, cancellationToken); |
||||||
|
} |
||||||
|
|
||||||
|
protected override Task<DateTimeOffset> GetLastScan( |
||||||
|
TvContext dbContext, |
||||||
|
SynchronizeJellyfinShowById request) |
||||||
|
{ |
||||||
|
return Task.FromResult(DateTimeOffset.MinValue); |
||||||
|
} |
||||||
|
|
||||||
|
protected override bool ScanIsRequired( |
||||||
|
DateTimeOffset lastScan, |
||||||
|
int libraryRefreshInterval, |
||||||
|
SynchronizeJellyfinShowById request) |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Jellyfin; |
||||||
|
|
||||||
|
public record SynchronizeJellyfinShowById(int JellyfinLibraryId, int ShowId, bool DeepScan) |
||||||
|
: IRequest<Either<BaseError, string>>, IScannerBackgroundServiceRequest; |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Libraries; |
||||||
|
|
||||||
|
public record QueueShowScanByLibraryId(int LibraryId, int ShowId, string ShowTitle, bool DeepScan) : IRequest<bool>; |
||||||
@ -0,0 +1,90 @@ |
|||||||
|
using ErsatzTV.Application.Emby; |
||||||
|
using ErsatzTV.Application.Jellyfin; |
||||||
|
using ErsatzTV.Application.Plex; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Locking; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using ErsatzTV.Infrastructure.Extensions; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries; |
||||||
|
|
||||||
|
public class QueueShowScanByLibraryIdHandler( |
||||||
|
IDbContextFactory<TvContext> dbContextFactory, |
||||||
|
IEntityLocker locker, |
||||||
|
IMediator mediator, |
||||||
|
ILogger<QueueShowScanByLibraryIdHandler> logger) |
||||||
|
: IRequestHandler<QueueShowScanByLibraryId, bool> |
||||||
|
{ |
||||||
|
public async Task<bool> Handle(QueueShowScanByLibraryId request, CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||||
|
|
||||||
|
Option<Library> maybeLibrary = await dbContext.Libraries |
||||||
|
.AsNoTracking() |
||||||
|
.SelectOneAsync(l => l.Id, l => l.Id == request.LibraryId); |
||||||
|
|
||||||
|
foreach (Library library in maybeLibrary) |
||||||
|
{ |
||||||
|
bool shouldSyncItems = library switch |
||||||
|
{ |
||||||
|
PlexLibrary plexLibrary => plexLibrary.ShouldSyncItems, |
||||||
|
JellyfinLibrary jellyfinLibrary => jellyfinLibrary.ShouldSyncItems, |
||||||
|
EmbyLibrary embyLibrary => embyLibrary.ShouldSyncItems, |
||||||
|
_ => true |
||||||
|
}; |
||||||
|
|
||||||
|
if (!shouldSyncItems) |
||||||
|
{ |
||||||
|
logger.LogWarning("Library sync is disabled for library id {Id}", library.Id); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// Check if library is already being scanned - return false if locked
|
||||||
|
if (!locker.LockLibrary(library.Id)) |
||||||
|
{ |
||||||
|
logger.LogWarning("Library {Id} is already being scanned, cannot scan individual show", library.Id); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
logger.LogDebug("Queued show scan for library id {Id}, show: {ShowTitle}, deepScan: {DeepScan}", |
||||||
|
library.Id, request.ShowTitle, request.DeepScan); |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
switch (library) |
||||||
|
{ |
||||||
|
case PlexLibrary: |
||||||
|
var plexResult = await mediator.Send( |
||||||
|
new SynchronizePlexShowById(library.Id, request.ShowId, request.DeepScan), |
||||||
|
cancellationToken); |
||||||
|
return plexResult.IsRight; |
||||||
|
case JellyfinLibrary: |
||||||
|
var jellyfinResult = await mediator.Send( |
||||||
|
new SynchronizeJellyfinShowById(library.Id, request.ShowId, request.DeepScan), |
||||||
|
cancellationToken); |
||||||
|
return jellyfinResult.IsRight; |
||||||
|
case EmbyLibrary: |
||||||
|
var embyResult = await mediator.Send( |
||||||
|
new SynchronizeEmbyShowById(library.Id, request.ShowId, request.DeepScan), |
||||||
|
cancellationToken); |
||||||
|
return embyResult.IsRight; |
||||||
|
case LocalLibrary: |
||||||
|
logger.LogWarning("Single show scanning is not supported for local libraries"); |
||||||
|
return false; |
||||||
|
default: |
||||||
|
logger.LogWarning("Unknown library type for library {Id}", library.Id); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
// Always unlock the library when we're done
|
||||||
|
locker.UnlockLibrary(library.Id); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,82 @@ |
|||||||
|
using ErsatzTV.Application.Libraries; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Errors; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.FFmpeg.Runtime; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using System.Globalization; |
||||||
|
using System.Threading.Channels; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Plex; |
||||||
|
|
||||||
|
public class CallPlexShowScannerHandler : CallLibraryScannerHandler<SynchronizePlexShowById>, |
||||||
|
IRequestHandler<SynchronizePlexShowById, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
public CallPlexShowScannerHandler( |
||||||
|
IDbContextFactory<TvContext> dbContextFactory, |
||||||
|
IConfigElementRepository configElementRepository, |
||||||
|
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel, |
||||||
|
IMediator mediator, |
||||||
|
IRuntimeInfo runtimeInfo) |
||||||
|
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
Task<Either<BaseError, string>> IRequestHandler<SynchronizePlexShowById, Either<BaseError, string>>.Handle( |
||||||
|
SynchronizePlexShowById request, |
||||||
|
CancellationToken cancellationToken) => Handle(request, cancellationToken); |
||||||
|
|
||||||
|
private async Task<Either<BaseError, string>> Handle( |
||||||
|
SynchronizePlexShowById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
Validation<BaseError, string> validation = await Validate(request); |
||||||
|
return await validation.Match( |
||||||
|
scanner => PerformScan(scanner, request, cancellationToken), |
||||||
|
error => |
||||||
|
{ |
||||||
|
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>()) |
||||||
|
{ |
||||||
|
return Task.FromResult<Either<BaseError, string>>(scanIsNotRequired); |
||||||
|
} |
||||||
|
|
||||||
|
return Task.FromResult<Either<BaseError, string>>(error.Join()); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Either<BaseError, string>> PerformScan( |
||||||
|
string scanner, |
||||||
|
SynchronizePlexShowById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
var arguments = new List<string> |
||||||
|
{ |
||||||
|
"scan-plex-show", |
||||||
|
request.PlexLibraryId.ToString(CultureInfo.InvariantCulture), |
||||||
|
request.ShowId.ToString(CultureInfo.InvariantCulture) |
||||||
|
}; |
||||||
|
|
||||||
|
if (request.DeepScan) |
||||||
|
{ |
||||||
|
arguments.Add("--deep"); |
||||||
|
} |
||||||
|
|
||||||
|
return await base.PerformScan(scanner, arguments, cancellationToken); |
||||||
|
} |
||||||
|
|
||||||
|
protected override Task<DateTimeOffset> GetLastScan( |
||||||
|
TvContext dbContext, |
||||||
|
SynchronizePlexShowById request) |
||||||
|
{ |
||||||
|
return Task.FromResult(DateTimeOffset.MinValue); |
||||||
|
} |
||||||
|
|
||||||
|
protected override bool ScanIsRequired( |
||||||
|
DateTimeOffset lastScan, |
||||||
|
int libraryRefreshInterval, |
||||||
|
SynchronizePlexShowById request) |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Plex; |
||||||
|
|
||||||
|
public record SynchronizePlexShowById(int PlexLibraryId, int ShowId, bool DeepScan) : |
||||||
|
IRequest<Either<BaseError, string>>, IScannerBackgroundServiceRequest; |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
namespace ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
public enum MediaSourceKind |
||||||
|
{ |
||||||
|
Local = 1, |
||||||
|
Plex = 2, |
||||||
|
Jellyfin = 3, |
||||||
|
Emby = 4 |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
namespace ErsatzTV.Infrastructure.Emby.Models; |
||||||
|
|
||||||
|
public class EmbySearchHintsResponse |
||||||
|
{ |
||||||
|
public List<EmbySearchHintResponse> SearchHints { get; set; } = []; |
||||||
|
public int TotalRecordCount { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
public class EmbySearchHintResponse |
||||||
|
{ |
||||||
|
public string Id { get; set; } |
||||||
|
public string Name { get; set; } |
||||||
|
public string Type { get; set; } |
||||||
|
public string MatchedTerm { get; set; } |
||||||
|
public int? IndexNumber { get; set; } |
||||||
|
public int? ProductionYear { get; set; } |
||||||
|
public string Overview { get; set; } |
||||||
|
public EmbyImageTagsResponse ImageTags { get; set; } = new(); |
||||||
|
public List<string> BackdropImageTags { get; set; } = []; |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
namespace ErsatzTV.Infrastructure.Jellyfin.Models; |
||||||
|
|
||||||
|
public class JellyfinSearchHintsResponse |
||||||
|
{ |
||||||
|
public List<JellyfinSearchHintResponse> SearchHints { get; set; } = []; |
||||||
|
public int TotalRecordCount { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
public class JellyfinSearchHintResponse |
||||||
|
{ |
||||||
|
public string Id { get; set; } |
||||||
|
public string Name { get; set; } |
||||||
|
public string Type { get; set; } |
||||||
|
public string MatchedTerm { get; set; } |
||||||
|
public int? IndexNumber { get; set; } |
||||||
|
public int? ProductionYear { get; set; } |
||||||
|
public string Overview { get; set; } |
||||||
|
public JellyfinImageTagsResponse ImageTags { get; set; } = new(); |
||||||
|
public List<string> BackdropImageTags { get; set; } = []; |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
namespace ErsatzTV.Infrastructure.Plex.Models; |
||||||
|
|
||||||
|
public class PlexMediaContainerHubContent<T> |
||||||
|
{ |
||||||
|
public List<T> Hub { get; set; } = []; |
||||||
|
} |
||||||
|
|
||||||
|
public class PlexHubResponse |
||||||
|
{ |
||||||
|
public string HubIdentifier { get; set; } |
||||||
|
public string HubKey { get; set; } |
||||||
|
public string Title { get; set; } |
||||||
|
public string Type { get; set; } |
||||||
|
public List<PlexMetadataResponse> Metadata { get; set; } = []; |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Scanner.Application.Emby; |
||||||
|
|
||||||
|
public record SynchronizeEmbyShowById(int EmbyLibraryId, int ShowId, bool DeepScan) |
||||||
|
: IRequest<Either<BaseError, string>>; |
||||||
@ -0,0 +1,135 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Emby; |
||||||
|
using ErsatzTV.Core.Interfaces.Emby; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
|
||||||
|
namespace ErsatzTV.Scanner.Application.Emby; |
||||||
|
|
||||||
|
public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbyShowById, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
private readonly IEmbySecretStore _embySecretStore; |
||||||
|
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner; |
||||||
|
private readonly ILogger<SynchronizeEmbyShowByIdHandler> _logger; |
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||||
|
private readonly IEmbyTelevisionRepository _embyTelevisionRepository; |
||||||
|
|
||||||
|
public SynchronizeEmbyShowByIdHandler( |
||||||
|
IMediaSourceRepository mediaSourceRepository, |
||||||
|
IEmbyTelevisionRepository embyTelevisionRepository, |
||||||
|
IEmbySecretStore embySecretStore, |
||||||
|
IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner, |
||||||
|
ILogger<SynchronizeEmbyShowByIdHandler> logger) |
||||||
|
{ |
||||||
|
_mediaSourceRepository = mediaSourceRepository; |
||||||
|
_embyTelevisionRepository = embyTelevisionRepository; |
||||||
|
_embySecretStore = embySecretStore; |
||||||
|
_embyTelevisionLibraryScanner = embyTelevisionLibraryScanner; |
||||||
|
_logger = logger; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<Either<BaseError, string>> Handle( |
||||||
|
SynchronizeEmbyShowById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
Validation<BaseError, RequestParameters> validation = await Validate(request); |
||||||
|
return await validation.Match( |
||||||
|
parameters => Synchronize(parameters, cancellationToken), |
||||||
|
error => Task.FromResult<Either<BaseError, string>>(error.Join())); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Either<BaseError, string>> Synchronize( |
||||||
|
RequestParameters parameters, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
if (parameters.Library.MediaKind != LibraryMediaKind.Shows) |
||||||
|
{ |
||||||
|
return BaseError.New($"Library {parameters.Library.Name} is not a TV show library"); |
||||||
|
} |
||||||
|
|
||||||
|
_logger.LogInformation( |
||||||
|
"Starting targeted scan for show '{ShowTitle}' in Emby library {LibraryName}", |
||||||
|
parameters.ShowTitle, |
||||||
|
parameters.Library.Name); |
||||||
|
|
||||||
|
Either<BaseError, Unit> result = await _embyTelevisionLibraryScanner.ScanSingleShow( |
||||||
|
parameters.ConnectionParameters.ActiveConnection.Address, |
||||||
|
parameters.ConnectionParameters.ApiKey, |
||||||
|
parameters.Library, |
||||||
|
parameters.ItemId, |
||||||
|
parameters.ShowTitle, |
||||||
|
parameters.DeepScan, |
||||||
|
cancellationToken); |
||||||
|
|
||||||
|
foreach (BaseError error in result.LeftToSeq()) |
||||||
|
{ |
||||||
|
_logger.LogError("Error synchronizing Emby show '{ShowTitle}': {Error}", parameters.ShowTitle, error); |
||||||
|
} |
||||||
|
|
||||||
|
return result.Map(_ => $"Show '{parameters.ShowTitle}' in {parameters.Library.Name}"); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RequestParameters>> Validate(SynchronizeEmbyShowById request) => |
||||||
|
(await ValidateConnection(request), await EmbyLibraryMustExist(request), await EmbyShowMustExist(request)) |
||||||
|
.Apply((connectionParameters, embyLibrary, showTitleItemId) => |
||||||
|
new RequestParameters( |
||||||
|
connectionParameters, |
||||||
|
embyLibrary, |
||||||
|
showTitleItemId.ItemId, |
||||||
|
showTitleItemId.Title, |
||||||
|
request.DeepScan |
||||||
|
)); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( |
||||||
|
SynchronizeEmbyShowById request) => |
||||||
|
EmbyMediaSourceMustExist(request) |
||||||
|
.BindT(MediaSourceMustHaveActiveConnection) |
||||||
|
.BindT(MediaSourceMustHaveApiKey); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist( |
||||||
|
SynchronizeEmbyShowById request) => |
||||||
|
_mediaSourceRepository.GetEmbyByLibraryId(request.EmbyLibraryId) |
||||||
|
.Map(v => v.ToValidation<BaseError>( |
||||||
|
$"Emby media source for library {request.EmbyLibraryId} does not exist.")); |
||||||
|
|
||||||
|
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( |
||||||
|
EmbyMediaSource embyMediaSource) |
||||||
|
{ |
||||||
|
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone(); |
||||||
|
return maybeConnection.Map(connection => new ConnectionParameters(connection)) |
||||||
|
.ToValidation<BaseError>("Emby media source requires an active connection"); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey( |
||||||
|
ConnectionParameters connectionParameters) |
||||||
|
{ |
||||||
|
EmbySecrets secrets = await _embySecretStore.ReadSecrets(); |
||||||
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) |
||||||
|
.Where(match => match) |
||||||
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) |
||||||
|
.ToValidation<BaseError>("Emby media source requires an api key"); |
||||||
|
} |
||||||
|
|
||||||
|
private Task<Validation<BaseError, EmbyLibrary>> EmbyLibraryMustExist( |
||||||
|
SynchronizeEmbyShowById request) => |
||||||
|
_mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId) |
||||||
|
.Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist.")); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, EmbyShowTitleItemIdResult>> EmbyShowMustExist( |
||||||
|
SynchronizeEmbyShowById request) => |
||||||
|
_embyTelevisionRepository.GetShowTitleItemId(request.EmbyLibraryId, request.ShowId) |
||||||
|
.Map(v => v.ToValidation<BaseError>($"Jellyfin show {request.ShowId} does not exist in library {request.EmbyLibraryId}.")); |
||||||
|
|
||||||
|
private record RequestParameters( |
||||||
|
ConnectionParameters ConnectionParameters, |
||||||
|
EmbyLibrary Library, |
||||||
|
string ItemId, |
||||||
|
string ShowTitle, |
||||||
|
bool DeepScan); |
||||||
|
|
||||||
|
private record ConnectionParameters(EmbyConnection ActiveConnection) |
||||||
|
{ |
||||||
|
public string? ApiKey { get; init; } |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Scanner.Application.Jellyfin; |
||||||
|
|
||||||
|
public record SynchronizeJellyfinShowById(int JellyfinLibraryId, int ShowId, bool DeepScan) |
||||||
|
: IRequest<Either<BaseError, string>>; |
||||||
@ -0,0 +1,135 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Jellyfin; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.Core.Jellyfin; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
|
||||||
|
namespace ErsatzTV.Scanner.Application.Jellyfin; |
||||||
|
|
||||||
|
public class SynchronizeJellyfinShowByIdHandler : IRequestHandler<SynchronizeJellyfinShowById, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
private readonly IJellyfinSecretStore _jellyfinSecretStore; |
||||||
|
private readonly IJellyfinTelevisionLibraryScanner _jellyfinTelevisionLibraryScanner; |
||||||
|
private readonly ILogger<SynchronizeJellyfinShowByIdHandler> _logger; |
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||||
|
private readonly IJellyfinTelevisionRepository _jellyfinTelevisionRepository; |
||||||
|
|
||||||
|
public SynchronizeJellyfinShowByIdHandler( |
||||||
|
IMediaSourceRepository mediaSourceRepository, |
||||||
|
IJellyfinTelevisionRepository jellyfinTelevisionRepository, |
||||||
|
IJellyfinSecretStore jellyfinSecretStore, |
||||||
|
IJellyfinTelevisionLibraryScanner jellyfinTelevisionLibraryScanner, |
||||||
|
ILogger<SynchronizeJellyfinShowByIdHandler> logger) |
||||||
|
{ |
||||||
|
_mediaSourceRepository = mediaSourceRepository; |
||||||
|
_jellyfinTelevisionRepository = jellyfinTelevisionRepository; |
||||||
|
_jellyfinSecretStore = jellyfinSecretStore; |
||||||
|
_jellyfinTelevisionLibraryScanner = jellyfinTelevisionLibraryScanner; |
||||||
|
_logger = logger; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<Either<BaseError, string>> Handle( |
||||||
|
SynchronizeJellyfinShowById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
Validation<BaseError, RequestParameters> validation = await Validate(request); |
||||||
|
return await validation.Match( |
||||||
|
parameters => Synchronize(parameters, cancellationToken), |
||||||
|
error => Task.FromResult<Either<BaseError, string>>(error.Join())); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Either<BaseError, string>> Synchronize( |
||||||
|
RequestParameters parameters, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
if (parameters.Library.MediaKind != LibraryMediaKind.Shows) |
||||||
|
{ |
||||||
|
return BaseError.New($"Library {parameters.Library.Name} is not a TV show library"); |
||||||
|
} |
||||||
|
|
||||||
|
_logger.LogInformation( |
||||||
|
"Starting targeted scan for show '{ShowTitle}' in Jellyfin library {LibraryName}", |
||||||
|
parameters.ShowTitle, |
||||||
|
parameters.Library.Name); |
||||||
|
|
||||||
|
Either<BaseError, Unit> result = await _jellyfinTelevisionLibraryScanner.ScanSingleShow( |
||||||
|
parameters.ConnectionParameters.ActiveConnection.Address, |
||||||
|
parameters.ConnectionParameters.ApiKey, |
||||||
|
parameters.Library, |
||||||
|
parameters.ItemId, |
||||||
|
parameters.ShowTitle, |
||||||
|
parameters.DeepScan, |
||||||
|
cancellationToken); |
||||||
|
|
||||||
|
foreach (BaseError error in result.LeftToSeq()) |
||||||
|
{ |
||||||
|
_logger.LogError("Error synchronizing Jellyfin show '{ShowTitle}': {Error}", parameters.ShowTitle, error); |
||||||
|
} |
||||||
|
|
||||||
|
return result.Map(_ => $"Show '{parameters.ShowTitle}' in {parameters.Library.Name}"); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RequestParameters>> Validate(SynchronizeJellyfinShowById request) => |
||||||
|
(await ValidateConnection(request), await JellyfinLibraryMustExist(request), await JellyfinShowMustExist(request)) |
||||||
|
.Apply((connectionParameters, jellyfinLibrary, showTitleItemId) => |
||||||
|
new RequestParameters( |
||||||
|
connectionParameters, |
||||||
|
jellyfinLibrary, |
||||||
|
showTitleItemId.ItemId, |
||||||
|
showTitleItemId.Title, |
||||||
|
request.DeepScan |
||||||
|
)); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( |
||||||
|
SynchronizeJellyfinShowById request) => |
||||||
|
JellyfinMediaSourceMustExist(request) |
||||||
|
.BindT(MediaSourceMustHaveActiveConnection) |
||||||
|
.BindT(MediaSourceMustHaveApiKey); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist( |
||||||
|
SynchronizeJellyfinShowById request) => |
||||||
|
_mediaSourceRepository.GetJellyfinByLibraryId(request.JellyfinLibraryId) |
||||||
|
.Map(v => v.ToValidation<BaseError>( |
||||||
|
$"Jellyfin media source for library {request.JellyfinLibraryId} does not exist.")); |
||||||
|
|
||||||
|
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( |
||||||
|
JellyfinMediaSource jellyfinMediaSource) |
||||||
|
{ |
||||||
|
Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); |
||||||
|
return maybeConnection.Map(connection => new ConnectionParameters(connection)) |
||||||
|
.ToValidation<BaseError>("Jellyfin media source requires an active connection"); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey( |
||||||
|
ConnectionParameters connectionParameters) |
||||||
|
{ |
||||||
|
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); |
||||||
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) |
||||||
|
.Where(match => match) |
||||||
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) |
||||||
|
.ToValidation<BaseError>("Jellyfin media source requires an api key"); |
||||||
|
} |
||||||
|
|
||||||
|
private Task<Validation<BaseError, JellyfinLibrary>> JellyfinLibraryMustExist( |
||||||
|
SynchronizeJellyfinShowById request) => |
||||||
|
_mediaSourceRepository.GetJellyfinLibrary(request.JellyfinLibraryId) |
||||||
|
.Map(v => v.ToValidation<BaseError>($"Jellyfin library {request.JellyfinLibraryId} does not exist.")); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, JellyfinShowTitleItemIdResult>> JellyfinShowMustExist( |
||||||
|
SynchronizeJellyfinShowById request) => |
||||||
|
_jellyfinTelevisionRepository.GetShowTitleItemId(request.JellyfinLibraryId, request.ShowId) |
||||||
|
.Map(v => v.ToValidation<BaseError>($"Jellyfin show {request.ShowId} does not exist in library {request.JellyfinLibraryId}.")); |
||||||
|
|
||||||
|
private record RequestParameters( |
||||||
|
ConnectionParameters ConnectionParameters, |
||||||
|
JellyfinLibrary Library, |
||||||
|
string ItemId, |
||||||
|
string ShowTitle, |
||||||
|
bool DeepScan); |
||||||
|
|
||||||
|
private record ConnectionParameters(JellyfinConnection ActiveConnection) |
||||||
|
{ |
||||||
|
public string? ApiKey { get; init; } |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Scanner.Application.Plex; |
||||||
|
|
||||||
|
public record SynchronizePlexShowById(int PlexLibraryId, int ShowId, bool DeepScan) |
||||||
|
: IRequest<Either<BaseError, string>>; |
||||||
@ -0,0 +1,135 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Plex; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.Core.Plex; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
|
||||||
|
namespace ErsatzTV.Scanner.Application.Plex; |
||||||
|
|
||||||
|
public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexShowById, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
private readonly ILogger<SynchronizePlexShowByIdHandler> _logger; |
||||||
|
private readonly IPlexTelevisionRepository _plexTelevisionRepository; |
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||||
|
private readonly IPlexSecretStore _plexSecretStore; |
||||||
|
private readonly IPlexTelevisionLibraryScanner _plexTelevisionLibraryScanner; |
||||||
|
|
||||||
|
public SynchronizePlexShowByIdHandler( |
||||||
|
IPlexTelevisionRepository plexTelevisionRepository, |
||||||
|
IMediaSourceRepository mediaSourceRepository, |
||||||
|
IPlexSecretStore plexSecretStore, |
||||||
|
IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner, |
||||||
|
ILogger<SynchronizePlexShowByIdHandler> logger) |
||||||
|
{ |
||||||
|
_plexTelevisionRepository = plexTelevisionRepository; |
||||||
|
_mediaSourceRepository = mediaSourceRepository; |
||||||
|
_plexSecretStore = plexSecretStore; |
||||||
|
_plexTelevisionLibraryScanner = plexTelevisionLibraryScanner; |
||||||
|
_logger = logger; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<Either<BaseError, string>> Handle( |
||||||
|
SynchronizePlexShowById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
Validation<BaseError, RequestParameters> validation = await Validate(request); |
||||||
|
return await validation.Match( |
||||||
|
parameters => Synchronize(parameters, cancellationToken), |
||||||
|
error => Task.FromResult<Either<BaseError, string>>(error.Join())); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Either<BaseError, string>> Synchronize( |
||||||
|
RequestParameters parameters, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
if (parameters.Library.MediaKind != LibraryMediaKind.Shows) |
||||||
|
{ |
||||||
|
return BaseError.New($"Library {parameters.Library.Name} is not a TV show library"); |
||||||
|
} |
||||||
|
|
||||||
|
_logger.LogInformation( |
||||||
|
"Starting targeted scan for show '{ShowTitle}' in Plex library {LibraryName}", |
||||||
|
parameters.ShowTitle, |
||||||
|
parameters.Library.Name); |
||||||
|
|
||||||
|
Either<BaseError, Unit> result = await _plexTelevisionLibraryScanner.ScanSingleShow( |
||||||
|
parameters.ConnectionParameters.ActiveConnection, |
||||||
|
parameters.ConnectionParameters.PlexServerAuthToken, |
||||||
|
parameters.Library, |
||||||
|
parameters.ShowKey, |
||||||
|
parameters.ShowTitle, |
||||||
|
parameters.DeepScan, |
||||||
|
cancellationToken); |
||||||
|
|
||||||
|
foreach (BaseError error in result.LeftToSeq()) |
||||||
|
{ |
||||||
|
_logger.LogError("Error synchronizing Plex show '{ShowTitle}': {Error}", parameters.ShowTitle, error); |
||||||
|
} |
||||||
|
|
||||||
|
return result.Map(_ => $"Show '{parameters.ShowTitle}' in {parameters.Library.Name}"); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RequestParameters>> Validate(SynchronizePlexShowById request) => |
||||||
|
(await ValidateConnection(request), await PlexLibraryMustExist(request), await PlexShowMustExist(request)) |
||||||
|
.Apply((connectionParameters, plexLibrary, titleKey) => |
||||||
|
new RequestParameters( |
||||||
|
connectionParameters, |
||||||
|
plexLibrary, |
||||||
|
titleKey.Key, |
||||||
|
titleKey.Title, |
||||||
|
request.DeepScan |
||||||
|
)); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( |
||||||
|
SynchronizePlexShowById request) => |
||||||
|
PlexMediaSourceMustExist(request) |
||||||
|
.BindT(MediaSourceMustHaveActiveConnection) |
||||||
|
.BindT(MediaSourceMustHaveToken); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, PlexMediaSource>> PlexMediaSourceMustExist( |
||||||
|
SynchronizePlexShowById request) => |
||||||
|
_mediaSourceRepository.GetPlexByLibraryId(request.PlexLibraryId) |
||||||
|
.Map(v => v.ToValidation<BaseError>( |
||||||
|
$"Plex media source for library {request.PlexLibraryId} does not exist.")); |
||||||
|
|
||||||
|
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( |
||||||
|
PlexMediaSource plexMediaSource) |
||||||
|
{ |
||||||
|
Option<PlexConnection> maybeConnection = |
||||||
|
plexMediaSource.Connections.SingleOrDefault(c => c.IsActive); |
||||||
|
return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection)) |
||||||
|
.ToValidation<BaseError>("Plex media source requires an active connection"); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveToken( |
||||||
|
ConnectionParameters connectionParameters) |
||||||
|
{ |
||||||
|
Option<PlexServerAuthToken> maybeToken = await |
||||||
|
_plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier); |
||||||
|
return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token }) |
||||||
|
.ToValidation<BaseError>("Plex media source requires a token"); |
||||||
|
} |
||||||
|
|
||||||
|
private Task<Validation<BaseError, PlexLibrary>> PlexLibraryMustExist( |
||||||
|
SynchronizePlexShowById request) => |
||||||
|
_mediaSourceRepository.GetPlexLibrary(request.PlexLibraryId) |
||||||
|
.Map(v => v.ToValidation<BaseError>($"Plex library {request.PlexLibraryId} does not exist.")); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, PlexShowTitleKeyResult>> PlexShowMustExist( |
||||||
|
SynchronizePlexShowById request) => |
||||||
|
_plexTelevisionRepository.GetShowTitleKey(request.PlexLibraryId, request.ShowId) |
||||||
|
.Map(v => v.ToValidation<BaseError>($"Plex show {request.ShowId} does not exist in library {request.PlexLibraryId}.")); |
||||||
|
|
||||||
|
private record RequestParameters( |
||||||
|
ConnectionParameters ConnectionParameters, |
||||||
|
PlexLibrary Library, |
||||||
|
string ShowKey, |
||||||
|
string ShowTitle, |
||||||
|
bool DeepScan); |
||||||
|
|
||||||
|
private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) |
||||||
|
{ |
||||||
|
public PlexServerAuthToken? PlexServerAuthToken { get; set; } |
||||||
|
} |
||||||
|
} |
||||||
@ -1,15 +1,40 @@ |
|||||||
using ErsatzTV.Application.Libraries; |
using ErsatzTV.Application.Libraries; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
using MediatR; |
using MediatR; |
||||||
using Microsoft.AspNetCore.Mvc; |
using Microsoft.AspNetCore.Mvc; |
||||||
|
|
||||||
namespace ErsatzTV.Controllers.Api; |
namespace ErsatzTV.Controllers.Api; |
||||||
|
|
||||||
[ApiController] |
[ApiController] |
||||||
public class LibrariesController(IMediator mediator) |
public class LibrariesController(ITelevisionRepository televisionRepository, IMediator mediator) |
||||||
{ |
{ |
||||||
[HttpPost("/api/libraries/{id:int}/scan")] |
[HttpPost("/api/libraries/{id:int}/scan")] |
||||||
public async Task<IActionResult> ResetPlayout(int id) => |
public async Task<IActionResult> ResetPlayout(int id) => |
||||||
await mediator.Send(new QueueLibraryScanByLibraryId(id)) |
await mediator.Send(new QueueLibraryScanByLibraryId(id)) |
||||||
? new OkResult() |
? new OkResult() |
||||||
: new NotFoundResult(); |
: new NotFoundResult(); |
||||||
|
|
||||||
|
[HttpPost("/api/libraries/{id:int}/scan-show")] |
||||||
|
public async Task<IActionResult> ScanShow(int id, [FromBody] ScanShowRequest request) |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(request.ShowTitle)) |
||||||
|
{ |
||||||
|
return new BadRequestObjectResult(new { error = "ShowTitle is required" }); |
||||||
|
} |
||||||
|
|
||||||
|
var trimmedTitle = request.ShowTitle.Trim(); |
||||||
|
var maybeShowId = await televisionRepository.GetShowIdByTitle(id, trimmedTitle); |
||||||
|
foreach (var showId in maybeShowId) |
||||||
|
{ |
||||||
|
bool result = await mediator.Send(new QueueShowScanByLibraryId(id, showId, trimmedTitle, request.DeepScan)); |
||||||
|
|
||||||
|
return result |
||||||
|
? new OkResult() |
||||||
|
: new BadRequestObjectResult(new { error = "Unable to queue show scan. Library may not exist, may not support single show scanning, or may already be scanning." }); |
||||||
|
} |
||||||
|
|
||||||
|
return new BadRequestObjectResult(new { error = $"Unable to locate show with title {request.ShowTitle} in library {id}" }); |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
|
public record ScanShowRequest(string ShowTitle, bool DeepScan = false); |
||||||
|
|||||||
Loading…
Reference in new issue