Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e841c9c53b | ||
|
|
4c78f41c5a | ||
|
|
95cceb95b9 | ||
|
|
58d6f81d2e | ||
|
|
fe5cedfcdc | ||
|
|
0bbed69e85 | ||
|
|
68123a2f9c | ||
|
|
6504ca10a8 | ||
|
|
84770ed250 | ||
|
|
466d33f808 | ||
|
|
8e81d5f197 | ||
|
|
da43e6f7cf | ||
|
|
c9905d0542 | ||
|
|
c9e20e28df | ||
|
|
f9427cac99 | ||
|
|
141a34933d | ||
|
|
0962a1429a | ||
|
|
f8b45ed9db | ||
|
|
266bfbad23 | ||
|
|
60a9640009 | ||
|
|
9291a6b6ed | ||
|
|
9afec19888 | ||
|
|
50529ee6ad | ||
|
|
0b105bf6e1 | ||
|
|
5356f7f293 | ||
|
|
1d35efa429 | ||
|
|
04da4b2964 | ||
|
|
0799fe25d1 | ||
|
|
c0b5ecd388 | ||
|
|
5fd0cc5469 | ||
|
|
34ebe9b006 | ||
|
|
d7c080cafd | ||
|
|
23bab01f2d | ||
|
|
c7fdacf30f | ||
|
|
6e6d53d847 | ||
|
|
47e9a319ce | ||
|
|
9112cb3c1f |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2020.3.2",
|
||||
"version": "2021.1.3",
|
||||
"commands": [
|
||||
"jb"
|
||||
]
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
#release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}"
|
||||
|
||||
# Build everything
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:PublishSingleFile=true --self-contained true
|
||||
#dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
|
||||
|
||||
# Pack files
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public DisconnectEmbyHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IEmbySecretStore embySecretStore,
|
||||
IEntityLocker entityLocker,
|
||||
ISearchIndex searchIndex)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_embySecretStore = embySecretStore;
|
||||
_entityLocker = entityLocker;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DisconnectEmby request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<int> ids = await _mediaSourceRepository.DeleteAllEmby();
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
await _embySecretStore.DeleteAll();
|
||||
_entityLocker.UnlockRemoteMediaSource<EmbyMediaSource>();
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
|
||||
private readonly IEmbyApiClient _embyApiClient;
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public SaveEmbySecretsHandler(
|
||||
IEmbySecretStore embySecretStore,
|
||||
IEmbyApiClient embyApiClient,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
|
||||
{
|
||||
_embySecretStore = embySecretStore;
|
||||
_embyApiClient = embyApiClient;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(SaveEmbySecrets request, CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(PerformSave)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Validation<BaseError, Parameters>> Validate(SaveEmbySecrets request)
|
||||
{
|
||||
Either<BaseError, EmbyServerInformation> maybeServerInformation = await _embyApiClient
|
||||
.GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey);
|
||||
|
||||
return maybeServerInformation.Match(
|
||||
info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)),
|
||||
error => error);
|
||||
}
|
||||
|
||||
private async Task<Unit> PerformSave(Parameters parameters)
|
||||
{
|
||||
await _embySecretStore.SaveSecrets(parameters.Secrets);
|
||||
await _mediaSourceRepository.UpsertEmby(
|
||||
parameters.Secrets.Address,
|
||||
parameters.ServerInformation.ServerName,
|
||||
parameters.ServerInformation.OperatingSystem);
|
||||
await _channel.WriteAsync(new SynchronizeEmbyMediaSources());
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
|
||||
IEmbyBackgroundServiceRequest;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public class
|
||||
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IEmbyApiClient _embyApiClient;
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
private readonly ILogger<SynchronizeEmbyLibrariesHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public SynchronizeEmbyLibrariesHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IEmbySecretStore embySecretStore,
|
||||
IEmbyApiClient embyApiClient,
|
||||
ILogger<SynchronizeEmbyLibrariesHandler> logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_embySecretStore = embySecretStore;
|
||||
_embyApiClient = embyApiClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
SynchronizeEmbyLibraries request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(SynchronizeLibraries)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyLibraries request) =>
|
||||
MediaSourceMustExist(request)
|
||||
.BindT(MediaSourceMustHaveActiveConnection)
|
||||
.BindT(MediaSourceMustHaveApiKey);
|
||||
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
|
||||
SynchronizeEmbyLibraries request) =>
|
||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
|
||||
.Map(o => o.ToValidation<BaseError>("Emby media source does not exist."));
|
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
||||
EmbyMediaSource embyMediaSource)
|
||||
{
|
||||
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
|
||||
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, 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)
|
||||
.Filter(match => match)
|
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||
}
|
||||
|
||||
private async Task<Unit> SynchronizeLibraries(ConnectionParameters connectionParameters)
|
||||
{
|
||||
Either<BaseError, List<EmbyLibrary>> maybeLibraries = await _embyApiClient.GetLibraries(
|
||||
connectionParameters.ActiveConnection.Address,
|
||||
connectionParameters.ApiKey);
|
||||
|
||||
await maybeLibraries.Match(
|
||||
libraries =>
|
||||
{
|
||||
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
|
||||
.ToList();
|
||||
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
return _mediaSourceRepository.UpdateLibraries(
|
||||
connectionParameters.EmbyMediaSource.Id,
|
||||
toAdd,
|
||||
toRemove);
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
|
||||
connectionParameters.EmbyMediaSource.ServerName,
|
||||
error.Value);
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private record ConnectionParameters(
|
||||
EmbyMediaSource EmbyMediaSource,
|
||||
EmbyConnection ActiveConnection)
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>,
|
||||
IEmbyBackgroundServiceRequest
|
||||
{
|
||||
int EmbyLibraryId { get; }
|
||||
bool ForceScan { get; }
|
||||
}
|
||||
|
||||
public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
|
||||
{
|
||||
public bool ForceScan => false;
|
||||
}
|
||||
|
||||
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
|
||||
{
|
||||
public bool ForceScan => true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public class SynchronizeEmbyLibraryByIdHandler :
|
||||
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
|
||||
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IEmbyMovieLibraryScanner _embyMovieLibraryScanner;
|
||||
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _logger;
|
||||
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public SynchronizeEmbyLibraryByIdHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IEmbySecretStore embySecretStore,
|
||||
IEmbyMovieLibraryScanner embyMovieLibraryScanner,
|
||||
IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner,
|
||||
ILibraryRepository libraryRepository,
|
||||
IEntityLocker entityLocker,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_embySecretStore = embySecretStore;
|
||||
_embyMovieLibraryScanner = embyMovieLibraryScanner;
|
||||
_embyTelevisionLibraryScanner = embyTelevisionLibraryScanner;
|
||||
_libraryRepository = libraryRepository;
|
||||
_entityLocker = entityLocker;
|
||||
_configElementRepository = configElementRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, string>> Handle(
|
||||
ForceSynchronizeEmbyLibraryById request,
|
||||
CancellationToken cancellationToken) => Handle(request);
|
||||
|
||||
public Task<Either<BaseError, string>> Handle(
|
||||
SynchronizeEmbyLibraryByIdIfNeeded request,
|
||||
CancellationToken cancellationToken) => Handle(request);
|
||||
|
||||
private Task<Either<BaseError, string>>
|
||||
Handle(ISynchronizeEmbyLibraryById request) =>
|
||||
Validate(request)
|
||||
.MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> Synchronize(RequestParameters parameters)
|
||||
{
|
||||
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
||||
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
|
||||
{
|
||||
switch (parameters.Library.MediaKind)
|
||||
{
|
||||
case LibraryMediaKind.Movies:
|
||||
await _embyMovieLibraryScanner.ScanLibrary(
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey,
|
||||
parameters.Library,
|
||||
parameters.FFprobePath);
|
||||
break;
|
||||
case LibraryMediaKind.Shows:
|
||||
await _embyTelevisionLibraryScanner.ScanLibrary(
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey,
|
||||
parameters.Library,
|
||||
parameters.FFprobePath);
|
||||
break;
|
||||
}
|
||||
|
||||
parameters.Library.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(parameters.Library);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Skipping unforced scan of emby media library {Name}",
|
||||
parameters.Library.Name);
|
||||
}
|
||||
|
||||
_entityLocker.UnlockLibrary(parameters.Library.Id);
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(
|
||||
ISynchronizeEmbyLibraryById request) =>
|
||||
(await ValidateConnection(request), await EmbyLibraryMustExist(request), await ValidateFFprobePath())
|
||||
.Apply(
|
||||
(connectionParameters, embyLibrary, ffprobePath) => new RequestParameters(
|
||||
connectionParameters,
|
||||
embyLibrary,
|
||||
request.ForceScan,
|
||||
ffprobePath
|
||||
));
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
|
||||
ISynchronizeEmbyLibraryById request) =>
|
||||
EmbyMediaSourceMustExist(request)
|
||||
.BindT(MediaSourceMustHaveActiveConnection)
|
||||
.BindT(MediaSourceMustHaveApiKey);
|
||||
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
|
||||
ISynchronizeEmbyLibraryById 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(embyMediaSource, 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)
|
||||
.Filter(match => match)
|
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, EmbyLibrary>> EmbyLibraryMustExist(
|
||||
ISynchronizeEmbyLibraryById request) =>
|
||||
_mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId)
|
||||
.Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist."));
|
||||
|
||||
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
|
||||
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
|
||||
.FilterT(File.Exists)
|
||||
.Map(
|
||||
ffprobePath =>
|
||||
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
|
||||
|
||||
private record RequestParameters(
|
||||
ConnectionParameters ConnectionParameters,
|
||||
EmbyLibrary Library,
|
||||
bool ForceScan,
|
||||
string FFprobePath);
|
||||
|
||||
private record ConnectionParameters(
|
||||
EmbyMediaSource EmbyMediaSource,
|
||||
EmbyConnection ActiveConnection)
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
|
||||
IEmbyBackgroundServiceRequest;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources,
|
||||
Either<BaseError, List<EmbyMediaSource>>>
|
||||
{
|
||||
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public SynchronizeEmbyMediaSourcesHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, List<EmbyMediaSource>>> Handle(
|
||||
SynchronizeEmbyMediaSources request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
|
||||
foreach (EmbyMediaSource mediaSource in mediaSources)
|
||||
{
|
||||
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
|
||||
await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
|
||||
}
|
||||
|
||||
return mediaSources;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public record UpdateEmbyLibraryPreferences
|
||||
(List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public class
|
||||
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public UpdateEmbyLibraryPreferencesHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
ISearchIndex searchIndex)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
UpdateEmbyLibraryPreferences request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
|
||||
List<int> ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable);
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
|
||||
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
|
||||
await _mediaSourceRepository.EnableEmbyLibrarySync(toEnable);
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public record UpdateEmbyPathReplacements(
|
||||
int EmbyMediaSourceId,
|
||||
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public UpdateEmbyPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
UpdateEmbyPathReplacements request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(pms => MergePathReplacements(request, pms))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Unit> MergePathReplacements(
|
||||
UpdateEmbyPathReplacements request,
|
||||
EmbyMediaSource embyMediaSource)
|
||||
{
|
||||
embyMediaSource.PathReplacements ??= new List<EmbyPathReplacement>();
|
||||
|
||||
var incoming = request.PathReplacements.Map(Project).ToList();
|
||||
|
||||
var toAdd = incoming.Filter(r => r.Id < 1).ToList();
|
||||
var toRemove = embyMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
|
||||
var toUpdate = incoming.Except(toAdd).ToList();
|
||||
|
||||
return _mediaSourceRepository.UpdatePathReplacements(embyMediaSource.Id, toAdd, toUpdate, toRemove);
|
||||
}
|
||||
|
||||
private static EmbyPathReplacement Project(EmbyPathReplacementItem vm) =>
|
||||
new() { Id = vm.Id, EmbyPath = vm.EmbyPath, LocalPath = vm.LocalPath };
|
||||
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> Validate(UpdateEmbyPathReplacements request) =>
|
||||
EmbyMediaSourceMustExist(request);
|
||||
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
|
||||
UpdateEmbyPathReplacements request) =>
|
||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Emby
|
||||
{
|
||||
public record EmbyConnectionParametersViewModel(string Address);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Application.Libraries;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Emby
|
||||
{
|
||||
public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems)
|
||||
: LibraryViewModel("Emby", Id, Name, MediaKind);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Application.MediaSources;
|
||||
|
||||
namespace ErsatzTV.Application.Emby
|
||||
{
|
||||
public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel(
|
||||
Id,
|
||||
Name,
|
||||
Address);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Emby
|
||||
{
|
||||
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Emby
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static EmbyMediaSourceViewModel ProjectToViewModel(EmbyMediaSource embyMediaSource) =>
|
||||
new(
|
||||
embyMediaSource.Id,
|
||||
embyMediaSource.ServerName,
|
||||
embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty));
|
||||
|
||||
internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) =>
|
||||
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
|
||||
|
||||
internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) =>
|
||||
new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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.Emby.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public class GetAllEmbyMediaSourcesHandler : IRequestHandler<GetAllEmbyMediaSources, List<EmbyMediaSourceViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public GetAllEmbyMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public Task<List<EmbyMediaSourceViewModel>> Handle(
|
||||
GetAllEmbyMediaSources request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
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 MediatR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters,
|
||||
Either<BaseError, EmbyConnectionParametersViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
public GetEmbyConnectionParametersHandler(
|
||||
IMemoryCache memoryCache,
|
||||
IMediaSourceRepository mediaSourceRepository)
|
||||
{
|
||||
_memoryCache = memoryCache;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, EmbyConnectionParametersViewModel>> Handle(
|
||||
GetEmbyConnectionParameters request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(request, out EmbyConnectionParametersViewModel parameters))
|
||||
{
|
||||
return parameters;
|
||||
}
|
||||
|
||||
Either<BaseError, EmbyConnectionParametersViewModel> maybeParameters =
|
||||
await Validate()
|
||||
.MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address))
|
||||
.Map(v => v.ToEither<EmbyConnectionParametersViewModel>());
|
||||
|
||||
return maybeParameters.Match(
|
||||
p =>
|
||||
{
|
||||
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
|
||||
return maybeParameters;
|
||||
},
|
||||
error => error);
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate() =>
|
||||
EmbyMediaSourceMustExist()
|
||||
.BindT(MediaSourceMustHaveActiveConnection);
|
||||
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
|
||||
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
"Emby media source does not exist."));
|
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
||||
EmbyMediaSource embyMediaSource)
|
||||
{
|
||||
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.FirstOrDefault();
|
||||
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
|
||||
.ToValidation<BaseError>("Emby media source requires an active connection");
|
||||
}
|
||||
|
||||
private record ConnectionParameters(
|
||||
EmbyMediaSource EmbyMediaSource,
|
||||
EmbyConnection ActiveConnection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
|
||||
}
|
||||
@@ -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.Emby.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public class
|
||||
GetEmbyLibrariesBySourceIdHandler : IRequestHandler<GetEmbyLibrariesBySourceId, List<EmbyLibraryViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public GetEmbyLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public Task<List<EmbyLibraryViewModel>> Handle(
|
||||
GetEmbyLibrariesBySourceId request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Emby.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public class
|
||||
GetEmbyMediaSourceByIdHandler : IRequestHandler<GetEmbyMediaSourceById, Option<EmbyMediaSourceViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public GetEmbyMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public Task<Option<EmbyMediaSourceViewModel>> Handle(
|
||||
GetEmbyMediaSourceById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public record GetEmbyPathReplacementsBySourceId
|
||||
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
|
||||
}
|
||||
@@ -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.Emby.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler<GetEmbyPathReplacementsBySourceId,
|
||||
List<EmbyPathReplacementViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public GetEmbyPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public Task<List<EmbyPathReplacementViewModel>> Handle(
|
||||
GetEmbyPathReplacementsBySourceId request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core.Emby;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public record GetEmbySecrets : IRequest<EmbySecrets>;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Emby.Queries
|
||||
{
|
||||
public class GetEmbySecretsHandler : IRequestHandler<GetEmbySecrets, EmbySecrets>
|
||||
{
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
|
||||
public GetEmbySecretsHandler(IEmbySecretStore embySecretStore) =>
|
||||
_embySecretStore = embySecretStore;
|
||||
|
||||
public Task<EmbySecrets> Handle(GetEmbySecrets request, CancellationToken cancellationToken) =>
|
||||
_embySecretStore.ReadSecrets();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<NoWarn>VSTHRD200</NoWarn>
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -16,7 +17,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Winista.MimeDetect" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
@@ -14,13 +16,16 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
|
||||
public UpdateFFmpegSettingsHandler(
|
||||
IConfigElementRepository configElementRepository,
|
||||
ILocalFileSystem localFileSystem)
|
||||
ILocalFileSystem localFileSystem,
|
||||
IRuntimeInfo runtimeInfo)
|
||||
{
|
||||
_configElementRepository = configElementRepository;
|
||||
_localFileSystem = localFileSystem;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -31,8 +36,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
|
||||
(await FFmpegMustExist(request), await FFprobeMustExist(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
(await FFmpegMustExist(request), await FFprobeMustExist(request), ReportsAreNotSupportedOnWindows(request))
|
||||
.Apply((_, _, _) => Unit.Default);
|
||||
|
||||
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
|
||||
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
|
||||
@@ -40,6 +45,16 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
|
||||
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
|
||||
|
||||
private Validation<BaseError, Unit> ReportsAreNotSupportedOnWindows(UpdateFFmpegSettings request)
|
||||
{
|
||||
if (request.Settings.SaveReports && _runtimeInfo.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return BaseError.New("FFmpeg reports are not supported on Windows");
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
|
||||
{
|
||||
if (!_localFileSystem.FileExists(path))
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Application
|
||||
{
|
||||
public interface IEmbyBackgroundServiceRequest
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Images
|
||||
{
|
||||
public record CachedImagePathViewModel(string FileName, string MimeType);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace ErsatzTV.Application.Images
|
||||
{
|
||||
// ReSharper disable once SuggestBaseTypeForParameter
|
||||
public record ImageViewModel(byte[] Contents, string MimeType);
|
||||
}
|
||||
+3
-2
@@ -5,6 +5,7 @@ using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Images.Queries
|
||||
{
|
||||
public record GetImageContents
|
||||
(string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest<Either<BaseError, ImageViewModel>>;
|
||||
public record GetCachedImagePath
|
||||
(string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest<
|
||||
Either<BaseError, CachedImagePathViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Winista.Mime;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Images.Queries
|
||||
{
|
||||
public class
|
||||
GetCachedImagePathHandler : IRequestHandler<GetCachedImagePath, Either<BaseError, CachedImagePathViewModel>>
|
||||
{
|
||||
private static readonly MimeTypes MimeTypes = new();
|
||||
private readonly IImageCache _imageCache;
|
||||
|
||||
public GetCachedImagePathHandler(IImageCache imageCache) => _imageCache = imageCache;
|
||||
|
||||
public async Task<Either<BaseError, CachedImagePathViewModel>> Handle(
|
||||
GetCachedImagePath request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
MimeType mimeType;
|
||||
|
||||
string cachePath = _imageCache.GetPathForImage(
|
||||
request.FileName,
|
||||
request.ArtworkKind,
|
||||
Optional(request.MaxHeight));
|
||||
if (!File.Exists(cachePath))
|
||||
{
|
||||
if (request.MaxHeight.HasValue)
|
||||
{
|
||||
string originalPath = _imageCache.GetPathForImage(request.FileName, request.ArtworkKind, None);
|
||||
byte[] contents = await File.ReadAllBytesAsync(originalPath, cancellationToken);
|
||||
Either<BaseError, byte[]> resizeResult =
|
||||
await _imageCache.ResizeImage(contents, request.MaxHeight.Value);
|
||||
resizeResult.IfRight(result => contents = result);
|
||||
|
||||
string baseFolder = Path.GetDirectoryName(cachePath);
|
||||
if (baseFolder != null && !Directory.Exists(baseFolder))
|
||||
{
|
||||
Directory.CreateDirectory(baseFolder);
|
||||
}
|
||||
|
||||
await File.WriteAllBytesAsync(cachePath, contents, cancellationToken);
|
||||
|
||||
mimeType = new MimeType("image/jpeg");
|
||||
}
|
||||
else
|
||||
{
|
||||
return BaseError.New($"Artwork does not exist on disk at {cachePath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mimeType = MimeTypes.GetMimeTypeFromFile(cachePath);
|
||||
}
|
||||
|
||||
return new CachedImagePathViewModel(cachePath, mimeType.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Winista.Mime;
|
||||
|
||||
namespace ErsatzTV.Application.Images.Queries
|
||||
{
|
||||
public class GetImageContentsHandler : IRequestHandler<GetImageContents, Either<BaseError, ImageViewModel>>
|
||||
{
|
||||
private static readonly MimeTypes MimeTypes = new();
|
||||
private readonly IImageCache _imageCache;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
public GetImageContentsHandler(IImageCache imageCache, IMemoryCache memoryCache)
|
||||
{
|
||||
_imageCache = imageCache;
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, ImageViewModel>> Handle(
|
||||
GetImageContents request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _memoryCache.GetOrCreateAsync(
|
||||
request.FileName,
|
||||
async entry =>
|
||||
{
|
||||
entry.SlidingExpiration = TimeSpan.FromHours(1);
|
||||
|
||||
string subfolder = request.FileName.Substring(0, 2);
|
||||
string baseFolder = request.ArtworkKind switch
|
||||
{
|
||||
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
|
||||
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
|
||||
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
|
||||
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
|
||||
_ => FileSystemLayout.LegacyImageCacheFolder
|
||||
};
|
||||
|
||||
string fileName = Path.Combine(baseFolder, request.FileName);
|
||||
byte[] contents = await File.ReadAllBytesAsync(fileName, cancellationToken);
|
||||
|
||||
if (request.MaxHeight.HasValue)
|
||||
{
|
||||
Either<BaseError, byte[]> resizeResult = await _imageCache
|
||||
.ResizeImage(contents, request.MaxHeight.Value);
|
||||
resizeResult.IfRight(result => contents = result);
|
||||
}
|
||||
|
||||
MimeType mimeType = MimeTypes.GetMimeType(contents);
|
||||
return new ImageViewModel(contents, mimeType.Name);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -35,8 +36,9 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
{
|
||||
List<int> ids = await _mediaSourceRepository.DeleteAllJellyfin();
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
await _jellyfinSecretStore.DeleteAll();
|
||||
_entityLocker.UnlockJellyfin();
|
||||
_entityLocker.UnlockRemoteMediaSource<JellyfinMediaSource>();
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
return await maybeUserId.Match(
|
||||
userId =>
|
||||
{
|
||||
_logger.LogDebug("Jellyfin admin user id is {UserId}", userId);
|
||||
// _logger.LogDebug("Jellyfin admin user id is {UserId}", userId);
|
||||
_memoryCache.Set($"jellyfin_admin_user_id.{parameters.JellyfinMediaSource.Id}", userId);
|
||||
return Task.FromResult<Either<BaseError, Unit>>(Unit.Default);
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
|
||||
List<int> ids = await _mediaSourceRepository.DisableJellyfinLibrarySync(toDisable);
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
|
||||
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
|
||||
await _mediaSourceRepository.EnableJellyfinLibrarySync(toEnable);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Jellyfin
|
||||
{
|
||||
public record JellyfinConnectionParametersViewModel(string Address);
|
||||
}
|
||||
@@ -2,5 +2,8 @@
|
||||
|
||||
namespace ErsatzTV.Application.Jellyfin
|
||||
{
|
||||
public record JellyfinMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(Id, Name);
|
||||
public record JellyfinMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel(
|
||||
Id,
|
||||
Name,
|
||||
Address);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Jellyfin.Queries
|
||||
{
|
||||
public record GetJellyfinConnectionParameters : IRequest<Either<BaseError, JellyfinConnectionParametersViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
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 MediatR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace ErsatzTV.Application.Jellyfin.Queries
|
||||
{
|
||||
public class GetJellyfinConnectionParametersHandler : IRequestHandler<GetJellyfinConnectionParameters,
|
||||
Either<BaseError, JellyfinConnectionParametersViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
public GetJellyfinConnectionParametersHandler(
|
||||
IMemoryCache memoryCache,
|
||||
IMediaSourceRepository mediaSourceRepository)
|
||||
{
|
||||
_memoryCache = memoryCache;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, JellyfinConnectionParametersViewModel>> Handle(
|
||||
GetJellyfinConnectionParameters request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(request, out JellyfinConnectionParametersViewModel parameters))
|
||||
{
|
||||
return parameters;
|
||||
}
|
||||
|
||||
Either<BaseError, JellyfinConnectionParametersViewModel> maybeParameters =
|
||||
await Validate()
|
||||
.MapT(cp => new JellyfinConnectionParametersViewModel(cp.ActiveConnection.Address))
|
||||
.Map(v => v.ToEither<JellyfinConnectionParametersViewModel>());
|
||||
|
||||
return maybeParameters.Match(
|
||||
p =>
|
||||
{
|
||||
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
|
||||
return maybeParameters;
|
||||
},
|
||||
error => error);
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate() =>
|
||||
JellyfinMediaSourceMustExist()
|
||||
.BindT(MediaSourceMustHaveActiveConnection);
|
||||
|
||||
private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist() =>
|
||||
_mediaSourceRepository.GetAllJellyfin().Map(list => list.HeadOrNone())
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
"Jellyfin media source does not exist."));
|
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
||||
JellyfinMediaSource jellyfinMediaSource)
|
||||
{
|
||||
Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.FirstOrDefault();
|
||||
return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection))
|
||||
.ToValidation<BaseError>("Jellyfin media source requires an active connection");
|
||||
}
|
||||
|
||||
private record ConnectionParameters(
|
||||
JellyfinMediaSource JellyfinMediaSource,
|
||||
JellyfinConnection ActiveConnection);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
||||
{
|
||||
List<int> ids = await _libraryRepository.GetMediaIdsByLocalPath(libraryPath.Id);
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
await _libraryRepository.DeleteLocalPath(libraryPath.Id);
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using ErsatzTV.Application.Emby;
|
||||
using ErsatzTV.Application.Jellyfin;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
@@ -12,6 +13,7 @@ namespace ErsatzTV.Application.Libraries
|
||||
LocalLibrary l => ProjectToViewModel(l),
|
||||
PlexLibrary p => new PlexLibraryViewModel(p.Id, p.Name, p.MediaKind),
|
||||
JellyfinLibrary j => new JellyfinLibraryViewModel(j.Id, j.Name, j.MediaKind, j.ShouldSyncItems),
|
||||
EmbyLibrary e => new EmbyLibraryViewModel(e.Id, e.Name, e.MediaKind, e.ShouldSyncItems),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(library))
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace ErsatzTV.Application.Libraries.Queries
|
||||
LocalLibrary => true,
|
||||
PlexLibrary plex => plex.ShouldSyncItems,
|
||||
JellyfinLibrary jellyfin => jellyfin.ShouldSyncItems,
|
||||
EmbyLibrary emby => emby.ShouldSyncItems,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Maintenance.Commands
|
||||
{
|
||||
public record DeleteOrphanedArtwork : MediatR.IRequest<Either<BaseError, Unit>>, IBackgroundServiceRequest;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Maintenance.Commands
|
||||
{
|
||||
public class DeleteOrphanedArtworkHandler : MediatR.IRequestHandler<DeleteOrphanedArtwork, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IArtworkRepository _artworkRepository;
|
||||
|
||||
public DeleteOrphanedArtworkHandler(IArtworkRepository artworkRepository) =>
|
||||
_artworkRepository = artworkRepository;
|
||||
|
||||
public Task<Either<BaseError, Unit>>
|
||||
Handle(DeleteOrphanedArtwork request, CancellationToken cancellationToken) =>
|
||||
_artworkRepository.GetOrphanedArtwork()
|
||||
.Bind(_artworkRepository.Delete)
|
||||
.Map(_ => Right<BaseError, Unit>(Unit.Default));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -10,17 +12,19 @@ namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
internal static TelevisionShowCardViewModel ProjectToViewModel(
|
||||
ShowMetadata showMetadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
new(
|
||||
showMetadata.ShowId,
|
||||
showMetadata.Title,
|
||||
showMetadata.Year?.ToString(),
|
||||
showMetadata.SortTitle,
|
||||
GetPoster(showMetadata, maybeJellyfin));
|
||||
GetPoster(showMetadata, maybeJellyfin, maybeEmby));
|
||||
|
||||
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
||||
Season season,
|
||||
Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
new(
|
||||
season.Show.ShowMetadata.HeadOrNone().Match(m => m.Title ?? string.Empty, () => string.Empty),
|
||||
season.Id,
|
||||
@@ -28,12 +32,14 @@ namespace ErsatzTV.Application.MediaCards
|
||||
GetSeasonName(season.SeasonNumber),
|
||||
string.Empty,
|
||||
GetSeasonName(season.SeasonNumber),
|
||||
season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin)).IfNone(string.Empty),
|
||||
season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby))
|
||||
.IfNone(string.Empty),
|
||||
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString());
|
||||
|
||||
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
|
||||
EpisodeMetadata episodeMetadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
new(
|
||||
episodeMetadata.EpisodeId,
|
||||
episodeMetadata.ReleaseDate ?? DateTime.MinValue,
|
||||
@@ -47,17 +53,18 @@ namespace ErsatzTV.Application.MediaCards
|
||||
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(
|
||||
em => em.Plot ?? string.Empty,
|
||||
() => string.Empty),
|
||||
GetThumbnail(episodeMetadata, maybeJellyfin));
|
||||
GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby));
|
||||
|
||||
internal static MovieCardViewModel ProjectToViewModel(
|
||||
MovieMetadata movieMetadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
new(
|
||||
movieMetadata.MovieId,
|
||||
movieMetadata.Title,
|
||||
movieMetadata.Year?.ToString(),
|
||||
movieMetadata.SortTitle,
|
||||
GetPoster(movieMetadata, maybeJellyfin));
|
||||
GetPoster(movieMetadata, maybeJellyfin, maybeEmby));
|
||||
|
||||
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
||||
new(
|
||||
@@ -66,7 +73,7 @@ namespace ErsatzTV.Application.MediaCards
|
||||
musicVideoMetadata.MusicVideo.Artist.ArtistMetadata.Head().Title,
|
||||
musicVideoMetadata.SortTitle,
|
||||
musicVideoMetadata.Plot,
|
||||
GetThumbnail(musicVideoMetadata, None));
|
||||
GetThumbnail(musicVideoMetadata, None, None));
|
||||
|
||||
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
||||
new(
|
||||
@@ -74,37 +81,48 @@ namespace ErsatzTV.Application.MediaCards
|
||||
artistMetadata.Title,
|
||||
artistMetadata.Disambiguation,
|
||||
artistMetadata.SortTitle,
|
||||
GetThumbnail(artistMetadata, None));
|
||||
GetThumbnail(artistMetadata, None, None));
|
||||
|
||||
internal static CollectionCardResultsViewModel
|
||||
ProjectToViewModel(Collection collection, Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
ProjectToViewModel(
|
||||
Collection collection,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
new(
|
||||
collection.Name,
|
||||
collection.MediaItems.OfType<Movie>().Map(
|
||||
m => ProjectToViewModel(m.MovieMetadata.Head(), maybeJellyfin) with
|
||||
m => ProjectToViewModel(m.MovieMetadata.Head(), maybeJellyfin, maybeEmby) with
|
||||
{
|
||||
CustomIndex = GetCustomIndex(collection, m.Id)
|
||||
}).ToList(),
|
||||
collection.MediaItems.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head(), maybeJellyfin))
|
||||
collection.MediaItems.OfType<Show>()
|
||||
.Map(s => ProjectToViewModel(s.ShowMetadata.Head(), maybeJellyfin, maybeEmby))
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<Season>().Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby))
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<Season>().Map(s => ProjectToViewModel(s, maybeJellyfin)).ToList(),
|
||||
collection.MediaItems.OfType<Episode>()
|
||||
.Map(e => ProjectToViewModel(e.EpisodeMetadata.Head(), maybeJellyfin))
|
||||
.Map(e => ProjectToViewModel(e.EpisodeMetadata.Head(), maybeJellyfin, maybeEmby))
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
||||
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
||||
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||
|
||||
internal static ActorCardViewModel ProjectToViewModel(Actor actor, Option<JellyfinMediaSource> maybeJellyfin)
|
||||
internal static ActorCardViewModel ProjectToViewModel(
|
||||
Actor actor,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby)
|
||||
{
|
||||
string artwork = actor.Artwork?.Path ?? string.Empty;
|
||||
|
||||
if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://"))
|
||||
{
|
||||
string address = maybeJellyfin.Map(ms => ms.Connections.HeadOrNone().Map(c => c.Address))
|
||||
.Flatten()
|
||||
.IfNone("jellyfin://");
|
||||
artwork = artwork.Replace("jellyfin://", address) + "&fillheight=440";
|
||||
artwork = JellyfinUrl.ForArtwork(maybeJellyfin, artwork)
|
||||
.SetQueryParam("fillHeight", 440);
|
||||
}
|
||||
else if (maybeEmby.IsSome && artwork.StartsWith("emby://"))
|
||||
{
|
||||
artwork = EmbyUrl.ForArtwork(maybeEmby, artwork)
|
||||
.SetQueryParam("maxHeight", 440);
|
||||
}
|
||||
|
||||
return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork);
|
||||
@@ -118,34 +136,45 @@ namespace ErsatzTV.Application.MediaCards
|
||||
private static string GetSeasonName(int number) =>
|
||||
number == 0 ? "Specials" : $"Season {number}";
|
||||
|
||||
private static string GetPoster(Metadata metadata, Option<JellyfinMediaSource> maybeJellyfin)
|
||||
private static string GetPoster(
|
||||
Metadata metadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby)
|
||||
{
|
||||
string poster = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
|
||||
if (maybeJellyfin.IsSome && poster.StartsWith("jellyfin://"))
|
||||
{
|
||||
string address = maybeJellyfin.Map(ms => ms.Connections.HeadOrNone().Map(c => c.Address))
|
||||
.Flatten()
|
||||
.IfNone("jellyfin://");
|
||||
poster = poster.Replace("jellyfin://", address) + "&fillHeight=440";
|
||||
poster = JellyfinUrl.ForArtwork(maybeJellyfin, poster)
|
||||
.SetQueryParam("fillHeight", 440);
|
||||
}
|
||||
else if (maybeEmby.IsSome && poster.StartsWith("emby://"))
|
||||
{
|
||||
poster = EmbyUrl.ForArtwork(maybeEmby, poster)
|
||||
.SetQueryParam("maxHeight", 440);
|
||||
}
|
||||
|
||||
return poster;
|
||||
}
|
||||
|
||||
private static string GetThumbnail(Metadata metadata, Option<JellyfinMediaSource> maybeJellyfin)
|
||||
private static string GetThumbnail(
|
||||
Metadata metadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby)
|
||||
{
|
||||
string thumb = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Thumbnail))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
|
||||
if (maybeJellyfin.IsSome && thumb.StartsWith("jellyfin://"))
|
||||
{
|
||||
string address = maybeJellyfin.Map(ms => ms.Connections.HeadOrNone().Map(c => c.Address))
|
||||
.Flatten()
|
||||
.IfNone("jellyfin://");
|
||||
thumb = thumb.Replace("jellyfin://", address) +
|
||||
"&fillHeight=220"; // TODO: this height is optimized for episode
|
||||
thumb = JellyfinUrl.ForArtwork(maybeJellyfin, thumb)
|
||||
.SetQueryParam("fillHeight", 220);
|
||||
}
|
||||
else if (maybeEmby.IsSome && thumb.StartsWith("emby://"))
|
||||
{
|
||||
thumb = EmbyUrl.ForArtwork(maybeEmby, thumb)
|
||||
.SetQueryParam("maxHeight", 220);
|
||||
}
|
||||
|
||||
return thumb;
|
||||
|
||||
@@ -30,10 +30,13 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
return await _collectionRepository
|
||||
.GetCollectionWithItemsUntracked(request.Id)
|
||||
.Map(c => c.ToEither(BaseError.New("Unable to load collection")))
|
||||
.MapT(c => ProjectToViewModel(c, maybeJellyfin));
|
||||
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,12 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
List<TelevisionEpisodeCardViewModel> results = await _televisionRepository
|
||||
.GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize)
|
||||
.Map(list => list.Map(e => ProjectToViewModel(e, maybeJellyfin)).ToList());
|
||||
.Map(list => list.Map(e => ProjectToViewModel(e, maybeJellyfin, maybeEmby)).ToList());
|
||||
|
||||
return new TelevisionEpisodeCardResultsViewModel(count, results);
|
||||
}
|
||||
|
||||
@@ -34,9 +34,12 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
List<TelevisionSeasonCardViewModel> results = await _televisionRepository
|
||||
.GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize)
|
||||
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin)).ToList());
|
||||
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList());
|
||||
|
||||
return new TelevisionSeasonCardResultsViewModel(count, results);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,5 @@ namespace ErsatzTV.Application.MediaCards
|
||||
Title,
|
||||
$"Episode {Episode}",
|
||||
$"Episode {Episode}",
|
||||
Poster)
|
||||
{
|
||||
}
|
||||
Poster);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,5 @@
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster)
|
||||
{
|
||||
}
|
||||
Poster);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,5 @@
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster)
|
||||
{
|
||||
}
|
||||
Poster);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,6 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
await _movieFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
lastScan,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
@@ -98,7 +97,6 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
await _televisionFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
lastScan,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
@@ -106,7 +104,6 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
await _musicVideoFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
lastScan,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.MediaSources
|
||||
{
|
||||
public record RemoteMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(Id, Name);
|
||||
}
|
||||
@@ -3,6 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using Flurl;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -10,7 +13,10 @@ namespace ErsatzTV.Application.Movies
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static MovieViewModel ProjectToViewModel(Movie movie, Option<JellyfinMediaSource> maybeJellyfin)
|
||||
internal static MovieViewModel ProjectToViewModel(
|
||||
Movie movie,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby)
|
||||
{
|
||||
MovieMetadata metadata = Optional(movie.MovieMetadata).Flatten().Head();
|
||||
return new MovieViewModel(
|
||||
@@ -20,13 +26,17 @@ namespace ErsatzTV.Application.Movies
|
||||
metadata.Genres.Map(g => g.Name).ToList(),
|
||||
metadata.Tags.Map(t => t.Name).ToList(),
|
||||
metadata.Studios.Map(s => s.Name).ToList(),
|
||||
(metadata.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)).ToList(),
|
||||
LanguagesForMovie(movie),
|
||||
metadata.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id)
|
||||
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin))
|
||||
.ToList())
|
||||
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))
|
||||
.ToList(),
|
||||
metadata.Directors.Map(d => d.Name).ToList(),
|
||||
metadata.Writers.Map(w => w.Name).ToList())
|
||||
{
|
||||
Poster = Artwork(metadata, ArtworkKind.Poster, maybeJellyfin),
|
||||
FanArt = Artwork(metadata, ArtworkKind.FanArt, maybeJellyfin)
|
||||
Poster = Artwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby),
|
||||
FanArt = Artwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,22 +59,31 @@ namespace ErsatzTV.Application.Movies
|
||||
private static string Artwork(
|
||||
Metadata metadata,
|
||||
ArtworkKind artworkKind,
|
||||
Option<JellyfinMediaSource> maybeJellyfin)
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby)
|
||||
{
|
||||
string artwork = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
|
||||
if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://"))
|
||||
{
|
||||
string address = maybeJellyfin.Map(ms => ms.Connections.HeadOrNone().Map(c => c.Address))
|
||||
.Flatten()
|
||||
.IfNone("jellyfin://");
|
||||
artwork = artwork.Replace("jellyfin://", address);
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(maybeJellyfin, artwork);
|
||||
if (artworkKind is ArtworkKind.Poster or ArtworkKind.Thumbnail)
|
||||
{
|
||||
artwork += "&fillHeight=440";
|
||||
url.SetQueryParam("fillHeight", 440);
|
||||
}
|
||||
|
||||
artwork = url;
|
||||
}
|
||||
else if (maybeEmby.IsSome && artwork.StartsWith("emby://"))
|
||||
{
|
||||
Url url = EmbyUrl.ForArtwork(maybeEmby, artwork);
|
||||
if (artworkKind is ArtworkKind.Poster or ArtworkKind.Thumbnail)
|
||||
{
|
||||
url.SetQueryParam("maxHeight", 440);
|
||||
}
|
||||
|
||||
artwork = url;
|
||||
}
|
||||
|
||||
return artwork;
|
||||
|
||||
@@ -11,8 +11,11 @@ namespace ErsatzTV.Application.Movies
|
||||
List<string> Genres,
|
||||
List<string> Tags,
|
||||
List<string> Studios,
|
||||
List<string> ContentRatings,
|
||||
List<CultureInfo> Languages,
|
||||
List<ActorCardViewModel> Actors)
|
||||
List<ActorCardViewModel> Actors,
|
||||
List<string> Directors,
|
||||
List<string> Writers)
|
||||
{
|
||||
public string Poster { get; set; }
|
||||
public string FanArt { get; set; }
|
||||
|
||||
@@ -26,8 +26,11 @@ namespace ErsatzTV.Application.Movies.Queries
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<Movie> movie = await _movieRepository.GetMovie(request.Id);
|
||||
return movie.Map(m => ProjectToViewModel(m, maybeJellyfin));
|
||||
return movie.Map(m => ProjectToViewModel(m, maybeJellyfin, maybeEmby));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
List<int> ids = await _mediaSourceRepository.DeleteAllPlex();
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
await _plexSecretStore.DeleteAll();
|
||||
_entityLocker.UnlockPlex();
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
|
||||
List<int> ids = await _mediaSourceRepository.DisablePlexLibrarySync(toDisable);
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
|
||||
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
|
||||
await _mediaSourceRepository.EnablePlexLibrarySync(toEnable);
|
||||
|
||||
@@ -5,6 +5,8 @@ using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
public record CreateProgramSchedule(string Name, PlaybackOrder MediaCollectionPlaybackOrder) :
|
||||
IRequest<Either<BaseError, ProgramScheduleViewModel>>;
|
||||
public record CreateProgramSchedule(
|
||||
string Name,
|
||||
PlaybackOrder MediaCollectionPlaybackOrder,
|
||||
bool KeepMultiPartEpisodesTogether) : IRequest<Either<BaseError, ProgramScheduleViewModel>>;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
.MapT(
|
||||
name => new ProgramSchedule
|
||||
{
|
||||
Name = name, MediaCollectionPlaybackOrder = request.MediaCollectionPlaybackOrder
|
||||
Name = name,
|
||||
MediaCollectionPlaybackOrder = request.MediaCollectionPlaybackOrder,
|
||||
KeepMultiPartEpisodesTogether =
|
||||
request.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle &&
|
||||
request.KeepMultiPartEpisodesTogether
|
||||
});
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateName(CreateProgramSchedule createProgramSchedule)
|
||||
|
||||
@@ -132,11 +132,14 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId,
|
||||
PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(),
|
||||
PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()),
|
||||
OfflineTail = item.OfflineTail.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
|
||||
};
|
||||
|
||||
private static TimeSpan FixDuration(TimeSpan duration) =>
|
||||
duration > TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
(
|
||||
int ProgramScheduleId,
|
||||
string Name,
|
||||
PlaybackOrder MediaCollectionPlaybackOrder) : IRequest<Either<BaseError, ProgramScheduleViewModel>>;
|
||||
PlaybackOrder MediaCollectionPlaybackOrder,
|
||||
bool KeepMultiPartEpisodesTogether) : IRequest<Either<BaseError, ProgramScheduleViewModel>>;
|
||||
}
|
||||
|
||||
@@ -37,12 +37,16 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
ProgramSchedule programSchedule,
|
||||
UpdateProgramSchedule update)
|
||||
{
|
||||
// we only need to rebuild playouts if the playback order has been modified
|
||||
// we need to rebuild playouts if the playback order or keep multi-episodes has been modified
|
||||
bool needToRebuildPlayout =
|
||||
programSchedule.MediaCollectionPlaybackOrder != update.MediaCollectionPlaybackOrder;
|
||||
programSchedule.MediaCollectionPlaybackOrder != update.MediaCollectionPlaybackOrder ||
|
||||
programSchedule.KeepMultiPartEpisodesTogether != update.KeepMultiPartEpisodesTogether;
|
||||
|
||||
programSchedule.Name = update.Name;
|
||||
programSchedule.MediaCollectionPlaybackOrder = update.MediaCollectionPlaybackOrder;
|
||||
programSchedule.KeepMultiPartEpisodesTogether =
|
||||
update.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle &&
|
||||
update.KeepMultiPartEpisodesTogether;
|
||||
await _programScheduleRepository.Update(programSchedule);
|
||||
|
||||
if (needToRebuildPlayout)
|
||||
|
||||
@@ -6,7 +6,11 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static ProgramScheduleViewModel ProjectToViewModel(ProgramSchedule programSchedule) =>
|
||||
new(programSchedule.Id, programSchedule.Name, programSchedule.MediaCollectionPlaybackOrder);
|
||||
new(
|
||||
programSchedule.Id,
|
||||
programSchedule.Name,
|
||||
programSchedule.MediaCollectionPlaybackOrder,
|
||||
programSchedule.KeepMultiPartEpisodesTogether);
|
||||
|
||||
internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) =>
|
||||
programScheduleItem switch
|
||||
|
||||
@@ -2,5 +2,9 @@
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules
|
||||
{
|
||||
public record ProgramScheduleViewModel(int Id, string Name, PlaybackOrder MediaCollectionPlaybackOrder);
|
||||
public record ProgramScheduleViewModel(
|
||||
int Id,
|
||||
string Name,
|
||||
PlaybackOrder MediaCollectionPlaybackOrder,
|
||||
bool KeepMultiPartEpisodesTogether);
|
||||
}
|
||||
|
||||
@@ -41,9 +41,12 @@ namespace ErsatzTV.Application.Search.Queries
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
List<MovieCardViewModel> items = await _movieRepository
|
||||
.GetMoviesForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||
.Map(list => list.Map(m => ProjectToViewModel(m, maybeJellyfin)).ToList());
|
||||
.Map(list => list.Map(m => ProjectToViewModel(m, maybeJellyfin, maybeEmby)).ToList());
|
||||
|
||||
return new MovieCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||
}
|
||||
|
||||
@@ -42,9 +42,12 @@ namespace ErsatzTV.Application.Search.Queries
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
List<TelevisionShowCardViewModel> items = await _televisionRepository
|
||||
.GetShowsForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin)).ToList());
|
||||
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList());
|
||||
|
||||
return new TelevisionShowCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming.Queries
|
||||
@@ -12,15 +14,18 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly FFmpegProcessService _ffmpegProcessService;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
|
||||
public GetConcatProcessByChannelNumberHandler(
|
||||
IChannelRepository channelRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
FFmpegProcessService ffmpegProcessService)
|
||||
FFmpegProcessService ffmpegProcessService,
|
||||
IRuntimeInfo runtimeInfo)
|
||||
: base(channelRepository, configElementRepository)
|
||||
{
|
||||
_configElementRepository = configElementRepository;
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
}
|
||||
|
||||
protected override async Task<Either<BaseError, Process>> GetProcess(
|
||||
@@ -28,7 +33,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
Channel channel,
|
||||
string ffmpegPath)
|
||||
{
|
||||
bool saveReports = await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await _configElementRepository
|
||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
return _ffmpegProcessService.ConcatChannel(
|
||||
|
||||
+18
-3
@@ -1,14 +1,17 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -18,11 +21,13 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IEmbyPathReplacementService _embyPathReplacementService;
|
||||
private readonly FFmpegProcessService _ffmpegProcessService;
|
||||
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
private readonly IPlexPathReplacementService _plexPathReplacementService;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
|
||||
public GetPlayoutItemProcessByChannelNumberHandler(
|
||||
IChannelRepository channelRepository,
|
||||
@@ -31,7 +36,9 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
FFmpegProcessService ffmpegProcessService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IPlexPathReplacementService plexPathReplacementService,
|
||||
IJellyfinPathReplacementService jellyfinPathReplacementService)
|
||||
IJellyfinPathReplacementService jellyfinPathReplacementService,
|
||||
IEmbyPathReplacementService embyPathReplacementService,
|
||||
IRuntimeInfo runtimeInfo)
|
||||
: base(channelRepository, configElementRepository)
|
||||
{
|
||||
_configElementRepository = configElementRepository;
|
||||
@@ -40,6 +47,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
_localFileSystem = localFileSystem;
|
||||
_plexPathReplacementService = plexPathReplacementService;
|
||||
_jellyfinPathReplacementService = jellyfinPathReplacementService;
|
||||
_embyPathReplacementService = embyPathReplacementService;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
}
|
||||
|
||||
protected override async Task<Either<BaseError, Process>> GetProcess(
|
||||
@@ -64,7 +73,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath))
|
||||
};
|
||||
|
||||
bool saveReports = await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await _configElementRepository
|
||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
return Right<BaseError, Process>(
|
||||
@@ -143,7 +153,6 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
string path = await GetPlayoutItemPath(playoutItem);
|
||||
|
||||
// TODO: this won't work with url streaming from plex
|
||||
if (_localFileSystem.FileExists(path))
|
||||
{
|
||||
return new PlayoutItemWithPath(playoutItem, path);
|
||||
@@ -178,6 +187,12 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
JellyfinEpisode jellyfinEpisode => await _jellyfinPathReplacementService.GetReplacementJellyfinPath(
|
||||
jellyfinEpisode.LibraryPathId,
|
||||
path),
|
||||
EmbyMovie embyMovie => await _embyPathReplacementService.GetReplacementEmbyPath(
|
||||
embyMovie.LibraryPathId,
|
||||
path),
|
||||
EmbyEpisode embyEpisode => await _embyPathReplacementService.GetReplacementEmbyPath(
|
||||
embyEpisode.LibraryPathId,
|
||||
path),
|
||||
_ => path
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using Flurl;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -14,37 +17,45 @@ namespace ErsatzTV.Application.Television
|
||||
internal static TelevisionShowViewModel ProjectToViewModel(
|
||||
Show show,
|
||||
List<string> languages,
|
||||
Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
new(
|
||||
show.Id,
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? string.Empty).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin)).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin)).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList())
|
||||
.IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone()
|
||||
.Map(
|
||||
m => (m.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()).IfNone(new List<string>()),
|
||||
LanguagesForShow(languages),
|
||||
show.ShowMetadata.HeadOrNone()
|
||||
.Map(
|
||||
m => m.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id)
|
||||
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin))
|
||||
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))
|
||||
.ToList())
|
||||
.IfNone(new List<ActorCardViewModel>()));
|
||||
|
||||
internal static TelevisionSeasonViewModel ProjectToViewModel(
|
||||
Season season,
|
||||
Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
new(
|
||||
season.Id,
|
||||
season.ShowId,
|
||||
season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty),
|
||||
season.Show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? string.Empty).IfNone(string.Empty),
|
||||
season.SeasonNumber == 0 ? "Specials" : $"Season {season.SeasonNumber}",
|
||||
season.SeasonMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin)).IfNone(string.Empty),
|
||||
season.Show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin)).IfNone(string.Empty));
|
||||
season.SeasonMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby))
|
||||
.IfNone(string.Empty),
|
||||
season.Show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby))
|
||||
.IfNone(string.Empty));
|
||||
|
||||
internal static TelevisionEpisodeViewModel ProjectToViewModel(Episode episode) =>
|
||||
new(
|
||||
@@ -53,36 +64,54 @@ namespace ErsatzTV.Application.Television
|
||||
episode.EpisodeNumber,
|
||||
episode.EpisodeMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty),
|
||||
episode.EpisodeMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
|
||||
episode.EpisodeMetadata.HeadOrNone().Map(m => GetThumbnail(m, None)).IfNone(string.Empty));
|
||||
episode.EpisodeMetadata.HeadOrNone().Map(m => GetThumbnail(m, None, None)).IfNone(string.Empty));
|
||||
|
||||
private static string GetPoster(Metadata metadata, Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
GetArtwork(metadata, ArtworkKind.Poster, maybeJellyfin);
|
||||
private static string GetPoster(
|
||||
Metadata metadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
GetArtwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby);
|
||||
|
||||
private static string GetFanArt(Metadata metadata, Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
GetArtwork(metadata, ArtworkKind.FanArt, maybeJellyfin);
|
||||
private static string GetFanArt(
|
||||
Metadata metadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
GetArtwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby);
|
||||
|
||||
private static string GetThumbnail(Metadata metadata, Option<JellyfinMediaSource> maybeJellyfin) =>
|
||||
GetArtwork(metadata, ArtworkKind.Thumbnail, maybeJellyfin);
|
||||
private static string GetThumbnail(
|
||||
Metadata metadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
GetArtwork(metadata, ArtworkKind.Thumbnail, maybeJellyfin, maybeEmby);
|
||||
|
||||
private static string GetArtwork(
|
||||
Metadata metadata,
|
||||
ArtworkKind artworkKind,
|
||||
Option<JellyfinMediaSource> maybeJellyfin)
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby)
|
||||
{
|
||||
string artwork = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
|
||||
if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://"))
|
||||
{
|
||||
string address = maybeJellyfin.Map(ms => ms.Connections.HeadOrNone().Map(c => c.Address))
|
||||
.Flatten()
|
||||
.IfNone("jellyfin://");
|
||||
artwork = artwork.Replace("jellyfin://", address);
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(maybeJellyfin, artwork);
|
||||
if (artworkKind == ArtworkKind.Poster)
|
||||
{
|
||||
artwork += "&fillHeight=440";
|
||||
url.SetQueryParam("fillHeight", 440);
|
||||
}
|
||||
|
||||
artwork = url;
|
||||
}
|
||||
else if (maybeEmby.IsSome && artwork.StartsWith("emby://"))
|
||||
{
|
||||
Url url = EmbyUrl.ForArtwork(maybeEmby, artwork);
|
||||
if (artworkKind == ArtworkKind.Poster)
|
||||
{
|
||||
url.SetQueryParam("maxHeight", 440);
|
||||
}
|
||||
|
||||
artwork = url;
|
||||
}
|
||||
|
||||
return artwork;
|
||||
|
||||
@@ -29,8 +29,11 @@ namespace ErsatzTV.Application.Television.Queries
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
return await _televisionRepository.GetSeason(request.SeasonId)
|
||||
.MapT(s => ProjectToViewModel(s, maybeJellyfin));
|
||||
.MapT(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,11 @@ namespace ErsatzTV.Application.Television.Queries
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
List<string> languages = await _searchRepository.GetLanguagesForShow(show);
|
||||
return ProjectToViewModel(show, languages, maybeJellyfin);
|
||||
return ProjectToViewModel(show, languages, maybeJellyfin, maybeEmby);
|
||||
},
|
||||
() => Task.FromResult(Option<TelevisionShowViewModel>.None));
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace ErsatzTV.Application.Television
|
||||
List<string> Genres,
|
||||
List<string> Tags,
|
||||
List<string> Studios,
|
||||
List<string> ContentRatings,
|
||||
List<CultureInfo> Languages,
|
||||
List<ActorCardViewModel> Actors);
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
@@ -106,7 +107,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShouldNot_SetRealtime_ForHttpLiveStreaming()
|
||||
public void Should_SetRealtime_ForHttpLiveStreaming()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile();
|
||||
|
||||
@@ -119,7 +120,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
actual.RealtimeOutput.Should().BeFalse();
|
||||
actual.RealtimeOutput.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -277,6 +278,32 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
actual.PadToDesiredResolution.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_ScaleToEvenDimensions_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1280, Height = 720 }
|
||||
};
|
||||
|
||||
var version = new MediaVersion { Width = 706, Height = 362, SampleAspectRatio = "32:27" };
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
|
||||
scaledSize.Width.Should().Be(1280);
|
||||
scaledSize.Height.Should().Be(554);
|
||||
actual.PadToDesiredResolution.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_NotPadToDesiredResolution_When_UnscaledContentIsUnderSized_ForHttpLiveStreaming()
|
||||
{
|
||||
@@ -440,6 +467,36 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
actual.VideoCodec.Should().Be("copy");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void
|
||||
Should_SetCorrectVideoCodec_When_ContentIsCorrectSize_And_CorrectCodec_And_Framerate_ForTransportStream()
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
VideoCodec = "libx264",
|
||||
FrameRate = "24"
|
||||
};
|
||||
|
||||
// not anamorphic
|
||||
var version = new MediaVersion
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "libx264" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.VideoCodec.Should().Be("libx264");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void
|
||||
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NotNormalizingVideo_ForTransportStream()
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using FluentAssertions;
|
||||
using Flurl;
|
||||
using NUnit.Framework;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Jellyfin
|
||||
{
|
||||
public class JellyfinUrlTests
|
||||
{
|
||||
[Test]
|
||||
public void Should_Work_Without_Trailing_Slash()
|
||||
{
|
||||
var artwork = "jellyfin://Items/2/Images/3?tag=4";
|
||||
var address = "https://some.jellyfin.server";
|
||||
var mediaSource = new JellyfinMediaSource
|
||||
{
|
||||
Connections = new List<JellyfinConnection>
|
||||
{
|
||||
new() { Address = address }
|
||||
}
|
||||
};
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Work_With_Trailing_Slash()
|
||||
{
|
||||
var artwork = "jellyfin://Items/2/Images/3?tag=4";
|
||||
var address = "https://some.jellyfin.server/";
|
||||
var mediaSource = new JellyfinMediaSource
|
||||
{
|
||||
Connections = new List<JellyfinConnection>
|
||||
{
|
||||
new() { Address = address }
|
||||
}
|
||||
};
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Work_With_Port_Without_Trailing_Slash()
|
||||
{
|
||||
var artwork = "jellyfin://Items/2/Images/3?tag=4";
|
||||
var address = "https://some.jellyfin.server:1000";
|
||||
var mediaSource = new JellyfinMediaSource
|
||||
{
|
||||
Connections = new List<JellyfinConnection>
|
||||
{
|
||||
new() { Address = address }
|
||||
}
|
||||
};
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Work_With_Port_With_Trailing_Slash()
|
||||
{
|
||||
var artwork = "jellyfin://Items/2/Images/3?tag=4";
|
||||
var address = "https://some.jellyfin.server:1000/";
|
||||
var mediaSource = new JellyfinMediaSource
|
||||
{
|
||||
Connections = new List<JellyfinConnection>
|
||||
{
|
||||
new() { Address = address }
|
||||
}
|
||||
};
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Work_With_Path_Prefix_Without_Trailing_Slash()
|
||||
{
|
||||
var artwork = "jellyfin://Items/2/Images/3?tag=4";
|
||||
var address = "https://some.jellyfin.server/jellyfin";
|
||||
var mediaSource = new JellyfinMediaSource
|
||||
{
|
||||
Connections = new List<JellyfinConnection>
|
||||
{
|
||||
new() { Address = address }
|
||||
}
|
||||
};
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Work_With_Path_Prefix_With_Trailing_Slash()
|
||||
{
|
||||
var artwork = "jellyfin://Items/2/Images/3?tag=4";
|
||||
var address = "https://some.jellyfin.server/jellyfin/";
|
||||
var mediaSource = new JellyfinMediaSource
|
||||
{
|
||||
Connections = new List<JellyfinConnection>
|
||||
{
|
||||
new() { Address = address }
|
||||
}
|
||||
};
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,12 +81,11 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(Path.Combine(FakeRoot, Path.Combine("Movie (2020)", "Movie (2020).mkv")))
|
||||
);
|
||||
var libraryPath = new LibraryPath { Path = BadFakeRoot };
|
||||
var libraryPath = new LibraryPath { Path = BadFakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -106,12 +105,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -147,12 +146,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(metadataPath)
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -189,12 +188,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(metadataPath)
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -235,12 +234,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -284,12 +283,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -333,12 +332,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -381,12 +380,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"Movie (2020)-{extraFile}{videoExtension}"))
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -425,12 +424,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
Path.Combine(extraFolder, $"Movie (2020){videoExtension}")))
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -463,12 +462,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -503,12 +502,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -532,12 +531,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
@@ -558,6 +557,7 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_imageCache.Object,
|
||||
new Mock<ISearchIndex>().Object,
|
||||
new Mock<ISearchRepository>().Object,
|
||||
new Mock<ILibraryRepository>().Object,
|
||||
new Mock<IMediator>().Object,
|
||||
new Mock<ILogger<MovieFolderScanner>>().Object
|
||||
);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
public class MultiPartEpisodeGrouperTests
|
||||
{
|
||||
[Test]
|
||||
public void NotGrouped_Grouped_NotGrouped()
|
||||
{
|
||||
var mediaItems = new List<MediaItem>
|
||||
{
|
||||
NamedEpisode("Episode 1"),
|
||||
NamedEpisode("Episode 2 (1)"),
|
||||
NamedEpisode("Episode 3 (2)"),
|
||||
NamedEpisode("Episode 4")
|
||||
};
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||
|
||||
result.Count.Should().Be(3);
|
||||
result[0].First.Should().Be(mediaItems[0]);
|
||||
result[1].First.Should().Be(mediaItems[1]);
|
||||
result[1].Additional[0].Should().Be(mediaItems[2]);
|
||||
result[2].First.Should().Be(mediaItems[3]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Grouped_NotGrouped()
|
||||
{
|
||||
var mediaItems = new List<MediaItem>
|
||||
{
|
||||
NamedEpisode("Episode 1 (1)"),
|
||||
NamedEpisode("Episode 2 (2)"),
|
||||
NamedEpisode("Episode 3")
|
||||
};
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||
|
||||
result.Count.Should().Be(2);
|
||||
result[0].First.Should().Be(mediaItems[0]);
|
||||
result[0].Additional[0].Should().Be(mediaItems[1]);
|
||||
result[1].First.Should().Be(mediaItems[2]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Grouped_NotGrouped_Grouped()
|
||||
{
|
||||
var mediaItems = new List<MediaItem>
|
||||
{
|
||||
NamedEpisode("Episode 1 (1)"),
|
||||
NamedEpisode("Episode 2 (2)"),
|
||||
NamedEpisode("Episode 3"),
|
||||
NamedEpisode("Episode 4 (1)"),
|
||||
NamedEpisode("Episode 5 (2)")
|
||||
};
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||
|
||||
result.Count.Should().Be(3);
|
||||
result[0].First.Should().Be(mediaItems[0]);
|
||||
result[0].Additional[0].Should().Be(mediaItems[1]);
|
||||
result[1].First.Should().Be(mediaItems[2]);
|
||||
result[2].First.Should().Be(mediaItems[3]);
|
||||
result[2].Additional[0].Should().Be(mediaItems[4]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Grouped_Grouped()
|
||||
{
|
||||
var mediaItems = new List<MediaItem>
|
||||
{
|
||||
NamedEpisode("Episode 1 (1)"),
|
||||
NamedEpisode("Episode 2 (2)"),
|
||||
NamedEpisode("Episode 3 (1)"),
|
||||
NamedEpisode("Episode 4 (2)")
|
||||
};
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||
|
||||
result.Count.Should().Be(2);
|
||||
result[0].First.Should().Be(mediaItems[0]);
|
||||
result[0].Additional[0].Should().Be(mediaItems[1]);
|
||||
result[1].First.Should().Be(mediaItems[2]);
|
||||
result[1].Additional[0].Should().Be(mediaItems[3]);
|
||||
}
|
||||
|
||||
private static Episode NamedEpisode(string title) =>
|
||||
new()
|
||||
{
|
||||
EpisodeMetadata = new List<EpisodeMetadata>
|
||||
{
|
||||
new() { Title = title }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
// normally returns 10 5 7 4 3 6 2 8 9 1 1 (note duplicate 1 at end)
|
||||
var state = new CollectionEnumeratorState { Seed = 8 };
|
||||
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state);
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
|
||||
|
||||
var list = new List<int>();
|
||||
for (var i = 1; i <= 1000; i++)
|
||||
@@ -50,7 +50,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
|
||||
var state = new CollectionEnumeratorState();
|
||||
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state);
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
|
||||
|
||||
var list = new List<int>();
|
||||
for (var i = 1; i <= 10; i++)
|
||||
@@ -70,7 +70,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
|
||||
var state = new CollectionEnumeratorState();
|
||||
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state);
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
|
||||
|
||||
var list = new List<int>();
|
||||
for (var i = 1; i <= 10; i++)
|
||||
@@ -90,7 +90,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
List<MediaItem> contents = Episodes(10);
|
||||
var state = new CollectionEnumeratorState();
|
||||
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state);
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
@@ -105,7 +105,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
List<MediaItem> contents = Episodes(10);
|
||||
var state = new CollectionEnumeratorState { Index = 5, Seed = MagicSeed };
|
||||
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state);
|
||||
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
|
||||
|
||||
for (var i = 6; i <= 10; i++)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class EmbyLibrary : Library
|
||||
{
|
||||
public string ItemId { get; set; }
|
||||
public bool ShouldSyncItems { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class LibraryFolder
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Path { get; set; }
|
||||
public int LibraryPathId { get; set; }
|
||||
public LibraryPath LibraryPath { get; set; }
|
||||
public string Etag { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,6 @@ namespace ErsatzTV.Core.Domain
|
||||
public Library Library { get; set; }
|
||||
|
||||
public List<MediaItem> MediaItems { get; set; }
|
||||
public List<LibraryFolder> LibraryFolders { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
[DebuggerDisplay("{EpisodeMetadata[0].Title}")]
|
||||
public class EmbyEpisode : Episode
|
||||
{
|
||||
public string ItemId { get; set; }
|
||||
public string Etag { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class EmbyMovie : Movie
|
||||
{
|
||||
public string ItemId { get; set; }
|
||||
public string Etag { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class EmbySeason : Season
|
||||
{
|
||||
public string ItemId { get; set; }
|
||||
public string Etag { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class EmbyShow : Show
|
||||
{
|
||||
public string ItemId { get; set; }
|
||||
public string Etag { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
[DebuggerDisplay("{EpisodeMetadata[0].Title}")]
|
||||
public class Episode : MediaItem
|
||||
{
|
||||
public int EpisodeNumber { get; set; }
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
[DebuggerDisplay("{EpisodeMetadata[0].Title}")]
|
||||
public class JellyfinEpisode : Episode
|
||||
{
|
||||
public string ItemId { get; set; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class EmbyConnection
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Address { get; set; }
|
||||
public int EmbyMediaSourceId { get; set; }
|
||||
public EmbyMediaSource EmbyMediaSource { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class EmbyMediaSource : MediaSource
|
||||
{
|
||||
public string ServerName { get; set; }
|
||||
public string OperatingSystem { get; set; }
|
||||
public List<EmbyConnection> Connections { get; set; }
|
||||
public List<EmbyPathReplacement> PathReplacements { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class EmbyPathReplacement
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string EmbyPath { get; set; }
|
||||
public string LocalPath { get; set; }
|
||||
public int EmbyMediaSourceId { get; set; }
|
||||
public EmbyMediaSource EmbyMediaSource { get; set; }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user