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:
Jason Dove
2021-03-02 02:54:23 +00:00
committed by GitHub
parent aef486103e
commit f281d9fca5
89 changed files with 4727 additions and 472 deletions
@@ -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."));
}
}
+3
View File
@@ -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());
}
}