Interface improvements (#34)
* fix plex library synchronization * add basic plex movie synchronization * proxy plex movie thumbnails (posters) * add plex path replacements * use transcoded plex artwork * remove unsynchronized plex movies on save; queue plex library scan on save * log plex path replacements * prefer buttons instead of menus * lock plex libraries before sync * add movie to collection from paged view * fix plex import memory use; quick add seasons/shows * quick add episode to collection * add favicon * add search page * disable plex for now
This commit is contained in:
@@ -78,8 +78,8 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
var existing = connectionParameters.PlexMediaSource.Libraries.OfType<PlexLibrary>().ToList();
|
||||
var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList();
|
||||
var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList();
|
||||
existing.AddRange(toAdd);
|
||||
toRemove.ForEach(c => existing.Remove(c));
|
||||
connectionParameters.PlexMediaSource.Libraries.AddRange(toAdd);
|
||||
toRemove.ForEach(c => connectionParameters.PlexMediaSource.Libraries.Remove(c));
|
||||
|
||||
return _mediaSourceRepository.Update(connectionParameters.PlexMediaSource);
|
||||
},
|
||||
|
||||
@@ -6,19 +6,18 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
|
||||
{
|
||||
int PlexMediaSourceId { get; }
|
||||
int PlexLibraryId { get; }
|
||||
bool ForceScan { get; }
|
||||
}
|
||||
|
||||
public record SynchronizePlexLibraryByIdIfNeeded
|
||||
(int PlexMediaSourceId, int PlexLibraryId) : ISynchronizePlexLibraryById
|
||||
(int PlexLibraryId) : ISynchronizePlexLibraryById
|
||||
{
|
||||
public bool ForceScan => false;
|
||||
}
|
||||
|
||||
public record ForceSynchronizePlexLibraryById
|
||||
(int PlexMediaSourceId, int PlexLibraryId) : ISynchronizePlexLibraryById
|
||||
(int PlexLibraryId) : ISynchronizePlexLibraryById
|
||||
{
|
||||
public bool ForceScan => true;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
parameters.Library.Name);
|
||||
}
|
||||
|
||||
// _entityLocker.UnlockMediaSource(parameters.MediaSource.Id);
|
||||
_entityLocker.UnlockLibrary(parameters.Library.Id);
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
@@ -103,8 +103,10 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
|
||||
private Task<Validation<BaseError, PlexMediaSource>> PlexMediaSourceMustExist(
|
||||
ISynchronizePlexLibraryById request) =>
|
||||
_mediaSourceRepository.GetPlex(request.PlexMediaSourceId)
|
||||
.Map(v => v.ToValidation<BaseError>($"Plex media source {request.PlexMediaSourceId} does not exist."));
|
||||
_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)
|
||||
@@ -134,9 +136,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
PlexLibrary Library,
|
||||
bool ForceScan);
|
||||
|
||||
private record ConnectionParameters(
|
||||
PlexMediaSource PlexMediaSource,
|
||||
PlexConnection ActiveConnection)
|
||||
private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection)
|
||||
{
|
||||
public PlexServerAuthToken PlexServerAuthToken { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
@@ -20,7 +21,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
UpdatePlexLibraryPreferences request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<int> toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id);
|
||||
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
|
||||
await _mediaSourceRepository.DisablePlexLibrarySync(toDisable);
|
||||
|
||||
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
public record UpdatePlexPathReplacements
|
||||
(
|
||||
int PlexMediaSourceId,
|
||||
List<PlexPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
public record PlexPathReplacementItem(int Id, string PlexPath, string LocalPath);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
public class
|
||||
UpdatePlexPathReplacementsHandler : MediatR.IRequestHandler<UpdatePlexPathReplacements, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public UpdatePlexPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
UpdatePlexPathReplacements request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(pms => MergePathReplacements(request, pms))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Unit> MergePathReplacements(UpdatePlexPathReplacements request, PlexMediaSource plexMediaSource)
|
||||
{
|
||||
plexMediaSource.PathReplacements ??= new List<PlexPathReplacement>();
|
||||
|
||||
var incoming = request.PathReplacements.Map(Project).ToList();
|
||||
|
||||
var toAdd = incoming.Filter(r => r.Id < 1).ToList();
|
||||
var toRemove = plexMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
|
||||
var toUpdate = incoming.Except(toAdd).ToList();
|
||||
|
||||
plexMediaSource.PathReplacements.AddRange(toAdd);
|
||||
toRemove.ForEach(pr => plexMediaSource.PathReplacements.Remove(pr));
|
||||
foreach (PlexPathReplacement pathReplacement in toUpdate)
|
||||
{
|
||||
Optional(plexMediaSource.PathReplacements.SingleOrDefault(pr => pr.Id == pathReplacement.Id))
|
||||
.IfSome(
|
||||
pr =>
|
||||
{
|
||||
pr.PlexPath = pathReplacement.PlexPath;
|
||||
pr.LocalPath = pathReplacement.LocalPath;
|
||||
});
|
||||
}
|
||||
|
||||
return _mediaSourceRepository.Update(plexMediaSource).ToUnit();
|
||||
}
|
||||
|
||||
private static PlexPathReplacement Project(PlexPathReplacementItem vm) =>
|
||||
new() { Id = vm.Id, PlexPath = vm.PlexPath, LocalPath = vm.LocalPath };
|
||||
|
||||
private Task<Validation<BaseError, PlexMediaSource>> Validate(UpdatePlexPathReplacements request) =>
|
||||
PlexMediaSourceMustExist(request);
|
||||
|
||||
private Task<Validation<BaseError, PlexMediaSource>> PlexMediaSourceMustExist(
|
||||
UpdatePlexPathReplacements request) =>
|
||||
_mediaSourceRepository.GetPlex(request.PlexMediaSourceId)
|
||||
.Map(v => v.ToValidation<BaseError>($"Plex media source {request.PlexMediaSourceId} does not exist."));
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,8 @@ namespace ErsatzTV.Application.Plex
|
||||
|
||||
internal static PlexLibraryViewModel ProjectToViewModel(PlexLibrary library) =>
|
||||
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
|
||||
|
||||
internal static PlexPathReplacementViewModel ProjectToViewModel(PlexPathReplacement pathReplacement) =>
|
||||
new(pathReplacement.Id, pathReplacement.PlexPath, pathReplacement.LocalPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace ErsatzTV.Application.Plex
|
||||
{
|
||||
public record PlexConnectionParametersViewModel(Uri Uri, string AuthToken);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Plex
|
||||
{
|
||||
public record PlexPathReplacementViewModel(int Id, string PlexPath, string LocalPath);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Queries
|
||||
{
|
||||
public record GetPlexConnectionParameters
|
||||
(int PlexMediaSourceId) : IRequest<Either<BaseError, PlexConnectionParametersViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Queries
|
||||
{
|
||||
public class GetPlexConnectionParametersHandler : IRequestHandler<GetPlexConnectionParameters,
|
||||
Either<BaseError, PlexConnectionParametersViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IPlexSecretStore _plexSecretStore;
|
||||
|
||||
public GetPlexConnectionParametersHandler(
|
||||
IMemoryCache memoryCache,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexSecretStore plexSecretStore)
|
||||
{
|
||||
_memoryCache = memoryCache;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexSecretStore = plexSecretStore;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, PlexConnectionParametersViewModel>> Handle(
|
||||
GetPlexConnectionParameters request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(request, out PlexConnectionParametersViewModel parameters))
|
||||
{
|
||||
return parameters;
|
||||
}
|
||||
|
||||
Either<BaseError, PlexConnectionParametersViewModel> maybeParameters =
|
||||
await Validate(request)
|
||||
.MapT(
|
||||
cp => new PlexConnectionParametersViewModel(
|
||||
new Uri(cp.ActiveConnection.Uri),
|
||||
cp.PlexServerAuthToken.AuthToken))
|
||||
.Map(v => v.ToEither<PlexConnectionParametersViewModel>());
|
||||
|
||||
return maybeParameters.Match(
|
||||
p =>
|
||||
{
|
||||
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
|
||||
return maybeParameters;
|
||||
},
|
||||
error => error);
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate(GetPlexConnectionParameters request) =>
|
||||
PlexMediaSourceMustExist(request)
|
||||
.BindT(MediaSourceMustHaveActiveConnection)
|
||||
.BindT(MediaSourceMustHaveToken);
|
||||
|
||||
private Task<Validation<BaseError, PlexMediaSource>> PlexMediaSourceMustExist(
|
||||
GetPlexConnectionParameters request) =>
|
||||
_mediaSourceRepository.GetPlex(request.PlexMediaSourceId)
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
$"Plex media source {request.PlexMediaSourceId} 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 record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection)
|
||||
{
|
||||
public PlexServerAuthToken PlexServerAuthToken { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Queries
|
||||
{
|
||||
public record GetPlexPathReplacementsBySourceId
|
||||
(int PlexMediaSourceId) : IRequest<List<PlexPathReplacementViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Plex.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Queries
|
||||
{
|
||||
public class GetPlexPathReplacementsBySourceIdHandler : IRequestHandler<GetPlexPathReplacementsBySourceId,
|
||||
List<PlexPathReplacementViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public GetPlexPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public Task<List<PlexPathReplacementViewModel>> Handle(
|
||||
GetPlexPathReplacementsBySourceId request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetPlexPathReplacements(request.PlexMediaSourceId)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user