mirror of https://github.com/ErsatzTV/ErsatzTV.git
193 changed files with 17038 additions and 1149 deletions
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Application.MediaItems |
||||
{ |
||||
public record MediaItemViewModel(int Id, int MediaSourceId, string Path); |
||||
public record MediaItemViewModel(int Id, int LibraryPathId, string Path); |
||||
} |
||||
|
||||
@ -0,0 +1,114 @@
@@ -0,0 +1,114 @@
|
||||
using System; |
||||
using System.IO; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Locking; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.Extensions.Logging; |
||||
using Unit = LanguageExt.Unit; |
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Commands |
||||
{ |
||||
public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>, |
||||
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>> |
||||
{ |
||||
private readonly IConfigElementRepository _configElementRepository; |
||||
private readonly IEntityLocker _entityLocker; |
||||
private readonly ILibraryRepository _libraryRepository; |
||||
private readonly ILogger<ScanLocalLibraryHandler> _logger; |
||||
private readonly IMovieFolderScanner _movieFolderScanner; |
||||
private readonly ITelevisionFolderScanner _televisionFolderScanner; |
||||
|
||||
public ScanLocalLibraryHandler( |
||||
ILibraryRepository libraryRepository, |
||||
IConfigElementRepository configElementRepository, |
||||
IMovieFolderScanner movieFolderScanner, |
||||
ITelevisionFolderScanner televisionFolderScanner, |
||||
IEntityLocker entityLocker, |
||||
ILogger<ScanLocalLibraryHandler> logger) |
||||
{ |
||||
_libraryRepository = libraryRepository; |
||||
_configElementRepository = configElementRepository; |
||||
_movieFolderScanner = movieFolderScanner; |
||||
_televisionFolderScanner = televisionFolderScanner; |
||||
_entityLocker = entityLocker; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public Task<Either<BaseError, string>> Handle( |
||||
ForceScanLocalLibrary request, |
||||
CancellationToken cancellationToken) => Handle(request); |
||||
|
||||
public Task<Either<BaseError, string>> Handle( |
||||
ScanLocalLibraryIfNeeded request, |
||||
CancellationToken cancellationToken) => Handle(request); |
||||
|
||||
private Task<Either<BaseError, string>> |
||||
Handle(IScanLocalLibrary request) => |
||||
Validate(request) |
||||
.MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalLibrary.Name)) |
||||
.Bind(v => v.ToEitherAsync()); |
||||
|
||||
private async Task<Unit> PerformScan(RequestParameters parameters) |
||||
{ |
||||
(LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters; |
||||
|
||||
DateTimeOffset lastScan = localLibrary.LastScan ?? DateTime.MinValue; |
||||
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) |
||||
{ |
||||
foreach (LibraryPath libraryPath in localLibrary.Paths) |
||||
{ |
||||
switch (localLibrary.MediaKind) |
||||
{ |
||||
case LibraryMediaKind.Movies: |
||||
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath); |
||||
break; |
||||
case LibraryMediaKind.Shows: |
||||
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
localLibrary.LastScan = DateTimeOffset.Now; |
||||
await _libraryRepository.UpdateLastScan(localLibrary); |
||||
} |
||||
else |
||||
{ |
||||
_logger.LogDebug( |
||||
"Skipping unforced scan of library {Name}", |
||||
localLibrary.Name); |
||||
} |
||||
|
||||
_entityLocker.UnlockLibrary(localLibrary.Id); |
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalLibrary request) => |
||||
(await LocalLibraryMustExist(request), await ValidateFFprobePath()) |
||||
.Apply( |
||||
(library, ffprobePath) => new RequestParameters( |
||||
library, |
||||
ffprobePath, |
||||
request.ForceScan)); |
||||
|
||||
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist( |
||||
IScanLocalLibrary request) => |
||||
_libraryRepository.Get(request.LibraryId) |
||||
.Map(maybeLibrary => maybeLibrary.Map(ms => ms as LocalLibrary)) |
||||
.Map(v => v.ToValidation<BaseError>($"Local library {request.LibraryId} 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(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan); |
||||
} |
||||
} |
||||
@ -1,111 +0,0 @@
@@ -1,111 +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.Locking; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.Extensions.Logging; |
||||
using Unit = LanguageExt.Unit; |
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Commands |
||||
{ |
||||
public class ScanLocalMediaSourceHandler : IRequestHandler<ForceScanLocalMediaSource, Either<BaseError, string>>, |
||||
IRequestHandler<ScanLocalMediaSourceIfNeeded, Either<BaseError, string>> |
||||
{ |
||||
private readonly IConfigElementRepository _configElementRepository; |
||||
private readonly IEntityLocker _entityLocker; |
||||
private readonly ILogger<ScanLocalMediaSourceHandler> _logger; |
||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||
private readonly IMovieFolderScanner _movieFolderScanner; |
||||
private readonly ITelevisionFolderScanner _televisionFolderScanner; |
||||
|
||||
public ScanLocalMediaSourceHandler( |
||||
IMediaSourceRepository mediaSourceRepository, |
||||
IConfigElementRepository configElementRepository, |
||||
IMovieFolderScanner movieFolderScanner, |
||||
ITelevisionFolderScanner televisionFolderScanner, |
||||
IEntityLocker entityLocker, |
||||
ILogger<ScanLocalMediaSourceHandler> logger) |
||||
{ |
||||
_mediaSourceRepository = mediaSourceRepository; |
||||
_configElementRepository = configElementRepository; |
||||
_movieFolderScanner = movieFolderScanner; |
||||
_televisionFolderScanner = televisionFolderScanner; |
||||
_entityLocker = entityLocker; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public Task<Either<BaseError, string>> Handle( |
||||
ForceScanLocalMediaSource request, |
||||
CancellationToken cancellationToken) => |
||||
Handle((IScanLocalMediaSource) request, cancellationToken); |
||||
|
||||
public Task<Either<BaseError, string>> Handle( |
||||
ScanLocalMediaSourceIfNeeded request, |
||||
CancellationToken cancellationToken) => |
||||
Handle((IScanLocalMediaSource) request, cancellationToken); |
||||
|
||||
private Task<Either<BaseError, string>> |
||||
Handle(IScanLocalMediaSource request, CancellationToken cancellationToken) => |
||||
Validate(request) |
||||
.MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalMediaSource.Folder)) |
||||
.Bind(v => v.ToEitherAsync()); |
||||
|
||||
private async Task<Unit> PerformScan(RequestParameters parameters) |
||||
{ |
||||
DateTimeOffset lastScan = parameters.LocalMediaSource.LastScan ?? DateTime.MinValue; |
||||
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) |
||||
{ |
||||
switch (parameters.LocalMediaSource.MediaType) |
||||
{ |
||||
case MediaType.Movie: |
||||
await _movieFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath); |
||||
break; |
||||
case MediaType.TvShow: |
||||
await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath); |
||||
break; |
||||
} |
||||
|
||||
parameters.LocalMediaSource.LastScan = DateTimeOffset.Now; |
||||
await _mediaSourceRepository.Update(parameters.LocalMediaSource); |
||||
} |
||||
else |
||||
{ |
||||
_logger.LogDebug( |
||||
"Skipping unforced scan of media source {Folder}", |
||||
parameters.LocalMediaSource.Folder); |
||||
} |
||||
|
||||
_entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id); |
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalMediaSource request) => |
||||
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath()) |
||||
.Apply( |
||||
(localMediaSource, ffprobePath) => new RequestParameters( |
||||
localMediaSource, |
||||
ffprobePath, |
||||
request.ForceScan)); |
||||
|
||||
private Task<Validation<BaseError, LocalMediaSource>> LocalMediaSourceMustExist( |
||||
IScanLocalMediaSource request) => |
||||
_mediaSourceRepository.Get(request.MediaSourceId) |
||||
.Map(maybeMediaSource => maybeMediaSource.Map(ms => ms as LocalMediaSource)) |
||||
.Map(v => v.ToValidation<BaseError>($"Local media source {request.MediaSourceId} 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(LocalMediaSource LocalMediaSource, string FFprobePath, bool ForceScan); |
||||
} |
||||
} |
||||
@ -1,7 +1,5 @@
@@ -1,7 +1,5 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.MediaSources |
||||
namespace ErsatzTV.Application.MediaSources |
||||
{ |
||||
public record LocalMediaSourceViewModel(int Id, string Name, string Folder) |
||||
: MediaSourceViewModel(Id, Name, MediaSourceType.Local); |
||||
: MediaSourceViewModel(Id, Name); |
||||
} |
||||
|
||||
@ -1,6 +1,4 @@
@@ -1,6 +1,4 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.MediaSources |
||||
namespace ErsatzTV.Application.MediaSources |
||||
{ |
||||
public record MediaSourceViewModel(int Id, string Name, MediaSourceType SourceType); |
||||
public record MediaSourceViewModel(int Id, string Name); |
||||
} |
||||
|
||||
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.MediaSources |
||||
{ |
||||
public record PlexMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel( |
||||
Id, |
||||
Name, |
||||
MediaSourceType.Plex); |
||||
} |
||||
@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
|
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Commands |
||||
namespace ErsatzTV.Application.Plex.Commands |
||||
{ |
||||
public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands |
||||
{ |
||||
public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest |
||||
{ |
||||
int PlexMediaSourceId { get; } |
||||
int PlexLibraryId { get; } |
||||
bool ForceScan { get; } |
||||
} |
||||
|
||||
public record SynchronizePlexLibraryByIdIfNeeded |
||||
(int PlexMediaSourceId, int PlexLibraryId) : ISynchronizePlexLibraryById |
||||
{ |
||||
public bool ForceScan => false; |
||||
} |
||||
|
||||
public record ForceSynchronizePlexLibraryById |
||||
(int PlexMediaSourceId, int PlexLibraryId) : ISynchronizePlexLibraryById |
||||
{ |
||||
public bool ForceScan => true; |
||||
} |
||||
} |
||||
@ -0,0 +1,144 @@
@@ -0,0 +1,144 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Locking; |
||||
using ErsatzTV.Core.Interfaces.Plex; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Plex; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.Extensions.Logging; |
||||
using Unit = LanguageExt.Unit; |
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands |
||||
{ |
||||
public class |
||||
SynchronizePlexLibraryByIdHandler : IRequestHandler<ForceSynchronizePlexLibraryById, Either<BaseError, string>>, |
||||
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>> |
||||
{ |
||||
private readonly IEntityLocker _entityLocker; |
||||
private readonly ILogger<SynchronizePlexLibraryByIdHandler> _logger; |
||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||
private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner; |
||||
private readonly IPlexSecretStore _plexSecretStore; |
||||
|
||||
public SynchronizePlexLibraryByIdHandler( |
||||
IMediaSourceRepository mediaSourceRepository, |
||||
IPlexSecretStore plexSecretStore, |
||||
IPlexMovieLibraryScanner plexMovieLibraryScanner, |
||||
IEntityLocker entityLocker, |
||||
ILogger<SynchronizePlexLibraryByIdHandler> logger) |
||||
{ |
||||
_mediaSourceRepository = mediaSourceRepository; |
||||
_plexSecretStore = plexSecretStore; |
||||
_plexMovieLibraryScanner = plexMovieLibraryScanner; |
||||
_entityLocker = entityLocker; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public Task<Either<BaseError, string>> Handle( |
||||
ForceSynchronizePlexLibraryById request, |
||||
CancellationToken cancellationToken) => Handle(request); |
||||
|
||||
public Task<Either<BaseError, string>> Handle( |
||||
SynchronizePlexLibraryByIdIfNeeded request, |
||||
CancellationToken cancellationToken) => Handle(request); |
||||
|
||||
private Task<Either<BaseError, string>> |
||||
Handle(ISynchronizePlexLibraryById request) => |
||||
Validate(request) |
||||
.MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name)) |
||||
.Bind(v => v.ToEitherAsync()); |
||||
|
||||
private async Task<Unit> Synchronize(RequestParameters parameters) |
||||
{ |
||||
DateTimeOffset lastScan = parameters.Library.LastScan ?? DateTime.MinValue; |
||||
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) |
||||
{ |
||||
switch (parameters.Library.MediaKind) |
||||
{ |
||||
case LibraryMediaKind.Movies: |
||||
await _plexMovieLibraryScanner.ScanLibrary( |
||||
parameters.ConnectionParameters.ActiveConnection, |
||||
parameters.ConnectionParameters.PlexServerAuthToken, |
||||
parameters.Library); |
||||
break; |
||||
case LibraryMediaKind.Shows: |
||||
// TODO: plex tv scanner
|
||||
// await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
|
||||
break; |
||||
} |
||||
|
||||
parameters.Library.LastScan = DateTimeOffset.Now; |
||||
await _mediaSourceRepository.Update(parameters.Library); |
||||
} |
||||
else |
||||
{ |
||||
_logger.LogDebug( |
||||
"Skipping unforced scan of plex media library {Name}", |
||||
parameters.Library.Name); |
||||
} |
||||
|
||||
// _entityLocker.UnlockMediaSource(parameters.MediaSource.Id);
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(ISynchronizePlexLibraryById request) => |
||||
(await ValidateConnection(request), await PlexLibraryMustExist(request)) |
||||
.Apply( |
||||
(connectionParameters, plexLibrary) => new RequestParameters( |
||||
connectionParameters, |
||||
plexLibrary, |
||||
request.ForceScan |
||||
)); |
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( |
||||
ISynchronizePlexLibraryById request) => |
||||
PlexMediaSourceMustExist(request) |
||||
.BindT(MediaSourceMustHaveActiveConnection) |
||||
.BindT(MediaSourceMustHaveToken); |
||||
|
||||
private Task<Validation<BaseError, PlexMediaSource>> PlexMediaSourceMustExist( |
||||
ISynchronizePlexLibraryById request) => |
||||
_mediaSourceRepository.GetPlex(request.PlexMediaSourceId) |
||||
.Map(v => v.ToValidation<BaseError>($"Plex media source {request.PlexMediaSourceId} does not exist.")); |
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( |
||||
PlexMediaSource plexMediaSource) |
||||
{ |
||||
Option<PlexConnection> maybeConnection = |
||||
plexMediaSource.Connections.SingleOrDefault(c => c.IsActive); |
||||
return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection)) |
||||
.ToValidation<BaseError>("Plex media source requires an active connection"); |
||||
} |
||||
|
||||
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveToken( |
||||
ConnectionParameters connectionParameters) |
||||
{ |
||||
Option<PlexServerAuthToken> maybeToken = await |
||||
_plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier); |
||||
return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token }) |
||||
.ToValidation<BaseError>("Plex media source requires a token"); |
||||
} |
||||
|
||||
private Task<Validation<BaseError, PlexLibrary>> PlexLibraryMustExist( |
||||
ISynchronizePlexLibraryById request) => |
||||
_mediaSourceRepository.GetPlexLibrary(request.PlexLibraryId) |
||||
.Map(v => v.ToValidation<BaseError>($"Plex library {request.PlexLibraryId} does not exist.")); |
||||
|
||||
private record RequestParameters( |
||||
ConnectionParameters ConnectionParameters, |
||||
PlexLibrary Library, |
||||
bool ForceScan); |
||||
|
||||
private record ConnectionParameters( |
||||
PlexMediaSource PlexMediaSource, |
||||
PlexConnection ActiveConnection) |
||||
{ |
||||
public PlexServerAuthToken PlexServerAuthToken { get; set; } |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic; |
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands |
||||
{ |
||||
public record UpdatePlexLibraryPreferences |
||||
(List<PlexLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||
|
||||
public record PlexLibraryPreference(int Id, bool ShouldSyncItems); |
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands |
||||
{ |
||||
public class |
||||
UpdatePlexLibraryPreferencesHandler : MediatR.IRequestHandler<UpdatePlexLibraryPreferences, |
||||
Either<BaseError, Unit>> |
||||
{ |
||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||
|
||||
public UpdatePlexLibraryPreferencesHandler(IMediaSourceRepository mediaSourceRepository) => |
||||
_mediaSourceRepository = mediaSourceRepository; |
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle( |
||||
UpdatePlexLibraryPreferences request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
IEnumerable<int> toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id); |
||||
await _mediaSourceRepository.DisablePlexLibrarySync(toDisable); |
||||
|
||||
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id); |
||||
await _mediaSourceRepository.EnablePlexLibrarySync(toEnable); |
||||
|
||||
return Unit.Default; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using System.Linq; |
||||
using ErsatzTV.Core.Domain; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.Application.Plex |
||||
{ |
||||
internal static class Mapper |
||||
{ |
||||
internal static PlexMediaSourceViewModel ProjectToViewModel(PlexMediaSource plexMediaSource) => |
||||
new( |
||||
plexMediaSource.Id, |
||||
plexMediaSource.ServerName, |
||||
Optional(plexMediaSource.Connections.SingleOrDefault(c => c.IsActive)).Match(c => c.Uri, string.Empty)); |
||||
|
||||
internal static PlexLibraryViewModel ProjectToViewModel(PlexLibrary library) => |
||||
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems); |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.Plex |
||||
{ |
||||
public record PlexLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems); |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Application.MediaSources; |
||||
|
||||
namespace ErsatzTV.Application.Plex |
||||
{ |
||||
public record PlexMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(Id, Name); |
||||
} |
||||
@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Queries |
||||
namespace ErsatzTV.Application.Plex.Queries |
||||
{ |
||||
public record GetAllPlexMediaSources : IRequest<List<PlexMediaSourceViewModel>>; |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Plex.Queries |
||||
{ |
||||
public record GetPlexLibrariesBySourceId(int PlexMediaSourceId) : IRequest<List<PlexLibraryViewModel>>; |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using static ErsatzTV.Application.Plex.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Plex.Queries |
||||
{ |
||||
public class |
||||
GetPlexLibrariesBySourceIdHandler : IRequestHandler<GetPlexLibrariesBySourceId, |
||||
List<PlexLibraryViewModel>> |
||||
{ |
||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||
|
||||
public GetPlexLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => |
||||
_mediaSourceRepository = mediaSourceRepository; |
||||
|
||||
public Task<List<PlexLibraryViewModel>> Handle( |
||||
GetPlexLibrariesBySourceId request, |
||||
CancellationToken cancellationToken) => |
||||
_mediaSourceRepository.GetPlexLibraries(request.PlexMediaSourceId) |
||||
.Map(list => list.Map(ProjectToViewModel).ToList()); |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
using LanguageExt; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Plex.Queries |
||||
{ |
||||
public record GetPlexMediaSourceById(int PlexMediaSourceId) : IRequest<Option<PlexMediaSourceViewModel>>; |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using static ErsatzTV.Application.Plex.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Plex.Queries |
||||
{ |
||||
public class |
||||
GetPlexMediaSourceByIdHandler : IRequestHandler<GetPlexMediaSourceById, Option<PlexMediaSourceViewModel>> |
||||
{ |
||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||
|
||||
public GetPlexMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) => |
||||
_mediaSourceRepository = mediaSourceRepository; |
||||
|
||||
public Task<Option<PlexMediaSourceViewModel>> Handle( |
||||
GetPlexMediaSourceById request, |
||||
CancellationToken cancellationToken) => |
||||
_mediaSourceRepository.GetPlex(request.PlexMediaSourceId).MapT(ProjectToViewModel); |
||||
} |
||||
} |
||||
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class Collection |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Name { get; set; } |
||||
public List<MediaItem> MediaItems { get; set; } |
||||
public List<CollectionItem> CollectionItems { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class CollectionItem |
||||
{ |
||||
public int CollectionId { get; set; } |
||||
public Collection Collection { get; set; } |
||||
public int MediaItemId { get; set; } |
||||
public MediaItem MediaItem { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public abstract class Library |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Name { get; set; } |
||||
public LibraryMediaKind MediaKind { get; set; } |
||||
public DateTimeOffset? LastScan { get; set; } |
||||
|
||||
public int MediaSourceId { get; set; } |
||||
public MediaSource MediaSource { get; set; } |
||||
|
||||
public List<LibraryPath> Paths { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public enum LibraryMediaKind |
||||
{ |
||||
Movies = 1, |
||||
Shows = 2 |
||||
} |
||||
} |
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class LibraryPath |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Path { get; set; } |
||||
|
||||
public int LibraryId { get; set; } |
||||
public Library Library { get; set; } |
||||
|
||||
public List<MediaItem> MediaItems { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class LocalLibrary : Library |
||||
{ |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class PlexLibrary : Library |
||||
{ |
||||
public string Key { get; set; } |
||||
public bool ShouldSyncItems { get; set; } |
||||
} |
||||
} |
||||
@ -1,10 +0,0 @@
@@ -1,10 +0,0 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class LocalMediaSource : MediaSource |
||||
{ |
||||
public LocalMediaSource() => SourceType = MediaSourceType.Local; |
||||
|
||||
public MediaType MediaType { get; set; } |
||||
public string Folder { get; set; } |
||||
} |
||||
} |
||||
@ -1,17 +0,0 @@
@@ -1,17 +0,0 @@
|
||||
using System; |
||||
using ErsatzTV.Core.Interfaces.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class MediaItem : IHasAPoster |
||||
{ |
||||
public int Id { get; set; } |
||||
public int MediaSourceId { get; set; } |
||||
public MediaSource Source { get; set; } |
||||
public MediaItemStatistics Statistics { get; set; } |
||||
public DateTime? LastWriteTime { get; set; } |
||||
public string Path { get; set; } |
||||
public string Poster { get; set; } |
||||
public DateTime? PosterLastWriteTime { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class Episode : MediaItem |
||||
{ |
||||
public int EpisodeNumber { get; set; } |
||||
public int SeasonId { get; set; } |
||||
public Season Season { get; set; } |
||||
public List<EpisodeMetadata> EpisodeMetadata { get; set; } |
||||
public List<MediaVersion> MediaVersions { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class MediaFile |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Path { get; set; } |
||||
|
||||
public int MediaVersionId { get; set; } |
||||
public MediaVersion MediaVersion { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using ErsatzTV.Core.Interfaces.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class MediaItem : IHasAPoster |
||||
{ |
||||
public int Id { get; set; } |
||||
public MediaItemStatistics Statistics { get; set; } |
||||
public DateTime? LastWriteTime { get; set; } |
||||
|
||||
public int LibraryPathId { get; set; } |
||||
public LibraryPath LibraryPath { get; set; } |
||||
public string Path { get; set; } |
||||
public string Poster { get; set; } |
||||
public DateTime? PosterLastWriteTime { get; set; } |
||||
|
||||
// temporary fields to help migrations...
|
||||
public int TelevisionShowId { get; set; } |
||||
public int TelevisionSeasonId { get; set; } |
||||
public int TelevisionEpisodeId { get; set; } |
||||
|
||||
public List<Collection> Collections { get; set; } |
||||
public List<CollectionItem> CollectionItems { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class MediaVersion |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Name { get; set; } |
||||
|
||||
public List<MediaFile> MediaFiles { get; set; } |
||||
|
||||
public TimeSpan Duration { get; set; } |
||||
public string SampleAspectRatio { get; set; } |
||||
public string DisplayAspectRatio { get; set; } |
||||
public string VideoCodec { get; set; } |
||||
public string AudioCodec { get; set; } |
||||
public bool IsInterlaced { get; set; } |
||||
public int Width { get; set; } |
||||
public int Height { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class Movie : MediaItem |
||||
{ |
||||
// TODO: remove MetadataId
|
||||
public int MetadataId { get; set; } |
||||
// TODO: remove Metadata
|
||||
public MovieMetadata Metadata { get; set; } |
||||
public List<NewMovieMetadata> MovieMetadata { get; set; } |
||||
|
||||
public List<MediaVersion> MediaVersions { get; set; } |
||||
|
||||
// TODO: remove SimpleMediaCollections
|
||||
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class PlexMediaItemPart |
||||
{ |
||||
public int Id { get; set; } |
||||
public int PlexId { get; set; } |
||||
public string Key { get; set; } |
||||
public int Duration { get; set; } |
||||
public string File { get; set; } |
||||
public int Size { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class PlexMovie : Movie |
||||
{ |
||||
public string Key { get; set; } |
||||
public PlexMediaItemPart Part { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class Season : MediaItem |
||||
{ |
||||
public int SeasonNumber { get; set; } |
||||
public int ShowId { get; set; } |
||||
public Show Show { get; set; } |
||||
|
||||
public List<Episode> Episodes { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class Show : MediaItem |
||||
{ |
||||
public List<Season> Seasons { get; set; } |
||||
public List<ShowMetadata> ShowMetadata { get; set; } |
||||
} |
||||
} |
||||
@ -1,12 +0,0 @@
@@ -1,12 +0,0 @@
|
||||
using System; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public abstract class MediaSource |
||||
{ |
||||
public int Id { get; set; } |
||||
public MediaSourceType SourceType { get; set; } |
||||
public string Name { get; set; } |
||||
public DateTimeOffset? LastScan { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class LocalMediaSource : MediaSource |
||||
{ |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public abstract class MediaSource |
||||
{ |
||||
public int Id { get; set; } |
||||
|
||||
public List<Library> Libraries { get; set; } |
||||
} |
||||
} |
||||
@ -1,9 +1,11 @@
@@ -1,9 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class PlexMediaSourceConnection |
||||
public class PlexConnection |
||||
{ |
||||
public int Id { get; set; } |
||||
public bool IsActive { get; set; } |
||||
public string Uri { get; set; } |
||||
public int PlexMediaSourceId { get; set; } |
||||
public PlexMediaSource PlexMediaSource { get; set; } |
||||
} |
||||
} |
||||
@ -1,10 +0,0 @@
@@ -1,10 +0,0 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public enum MediaSourceType |
||||
{ |
||||
None = 0, |
||||
Local = 1, |
||||
Plex = 2, |
||||
Jellyfin = 3 |
||||
} |
||||
} |
||||
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public enum MediaType |
||||
{ |
||||
Other = 0, |
||||
TvShow = 1, |
||||
Movie = 2 |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class EpisodeMetadata : Metadata |
||||
{ |
||||
public string Outline { get; set; } |
||||
public string Plot { get; set; } |
||||
public string Tagline { get; set; } |
||||
public int EpisodeId { get; set; } |
||||
public Episode Episode { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
using System; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class Metadata |
||||
{ |
||||
public int Id { get; set; } |
||||
public MetadataKind MetadataKind { get; set; } |
||||
public string Title { get; set; } |
||||
public string OriginalTitle { get; set; } |
||||
public string SortTitle { get; set; } |
||||
public DateTimeOffset? ReleaseDate { get; set; } |
||||
public DateTimeOffset DateAdded { get; set; } |
||||
public DateTimeOffset DateUpdated { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public enum MetadataKind |
||||
{ |
||||
Fallback, |
||||
Sidecar |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class NewMovieMetadata : Metadata |
||||
{ |
||||
public string Outline { get; set; } |
||||
public string Plot { get; set; } |
||||
public string Tagline { get; set; } |
||||
public int MovieId { get; set; } |
||||
public Movie Movie { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class ShowMetadata : Metadata |
||||
{ |
||||
public string Outline { get; set; } |
||||
public string Plot { get; set; } |
||||
public string Tagline { get; set; } |
||||
public int ShowId { get; set; } |
||||
public Show Show { get; set; } |
||||
} |
||||
} |
||||
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class MovieMediaItem : MediaItem |
||||
{ |
||||
public int MetadataId { get; set; } |
||||
public MovieMetadata Metadata { get; set; } |
||||
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; } |
||||
} |
||||
} |
||||
@ -1,10 +0,0 @@
@@ -1,10 +0,0 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class PlexMediaSourceLibrary |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Key { get; init; } |
||||
public string Name { get; init; } |
||||
public MediaType MediaType { get; init; } |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> |
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Ccollection/@EntryIndexedValue">True</s:Boolean> |
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Clibrary/@EntryIndexedValue">True</s:Boolean> |
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Cmediaitem/@EntryIndexedValue">True</s:Boolean> |
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Cmediasource/@EntryIndexedValue">True</s:Boolean> |
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Cmediasources/@EntryIndexedValue">True</s:Boolean> |
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Cmetadata/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> |
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Plex; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Plex |
||||
{ |
||||
public interface IPlexMovieLibraryScanner |
||||
{ |
||||
Task<Either<BaseError, Unit>> ScanLibrary( |
||||
PlexConnection connection, |
||||
PlexServerAuthToken token, |
||||
PlexLibrary plexMediaSourceLibrary); |
||||
} |
||||
} |
||||
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Domain; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories |
||||
{ |
||||
public interface ILibraryRepository |
||||
{ |
||||
Task<Option<Library>> Get(int libraryId); |
||||
Task<Unit> UpdateLastScan(Library library); |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Plex |
||||
{ |
||||
public class PlexMediaEntry |
||||
{ |
||||
public int Id { get; set; } |
||||
public int Duration { get; set; } |
||||
public int Bitrate { get; set; } |
||||
public int Width { get; set; } |
||||
public int Height { get; set; } |
||||
public double AspectRatio { get; set; } |
||||
public int AudioChannels { get; set; } |
||||
public string AudioCodec { get; set; } |
||||
public string VideoCodec { get; set; } |
||||
public string Container { get; set; } |
||||
public string VideoFrameRate { get; set; } |
||||
public List<PlexPartEntry> Part { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Plex |
||||
{ |
||||
public class PlexMetadataEntry |
||||
{ |
||||
public string Key { get; set; } |
||||
public string Title { get; set; } |
||||
public string Summary { get; set; } |
||||
public int Year { get; set; } |
||||
public string Tagline { get; set; } |
||||
public string Thumb { get; set; } |
||||
public string Art { get; set; } |
||||
public string OriginallyAvailableAt { get; set; } |
||||
public int AddedAt { get; set; } |
||||
public int UpdatedAt { get; set; } |
||||
public List<PlexMediaEntry> Media { get; set; } |
||||
} |
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue