mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* starting database redesign * set season and episode numbers * use datetimes in db (utc); update movie metadata * get movie cards from new table * copy show/episode metadata * remove old movie metadata type * rename new movie metadata type * code cleanup * start to remove old television classes * remove old television tables from database * fix playout building * fix collection views * fix show/season views * clean up movie metadata table * fix scanner tests * add libraries ui * code cleanup * fix movie scanning/metadata * add library scan button to ui * delete library path from ui * temp disable movie scanning * remove orphan media items and prevent duplicate paths * attach artwork to metadata * fix split show/season display * fix television artwork * store year distinct from release date * fix collections ui * code cleanup * add library paths from ui * fix adding to collections from ui * fix schedule items loading * schedule editing works again * remove some todos * more cleanup * fix unit tests * fix episode sorting * fix deleting show library paths * remove unused class * fix playout list in ui * fix log viewer * start to use version/file instead of statistics * clean up old columns * fix playout display (time zone) * fix playback * fix channel guide time zone * cascade more deletes * fix compiler warnings * fix adding new seasons * use artwork for channel logo * clean cache folder on startup (move channel logos, delete everything else) * log database migration * update homepage docs for libraries * fix adding new channel with logo * fix episode numbers in epgpull/33/head
392 changed files with 51994 additions and 3965 deletions
@ -1,10 +1,22 @@ |
|||||||
using ErsatzTV.Core.Domain; |
using System.Linq; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels |
namespace ErsatzTV.Application.Channels |
||||||
{ |
{ |
||||||
internal static class Mapper |
internal static class Mapper |
||||||
{ |
{ |
||||||
internal static ChannelViewModel ProjectToViewModel(Channel channel) => |
internal static ChannelViewModel ProjectToViewModel(Channel channel) => |
||||||
new(channel.Id, channel.Number, channel.Name, channel.FFmpegProfileId, channel.Logo, channel.StreamingMode); |
new( |
||||||
|
channel.Id, |
||||||
|
channel.Number, |
||||||
|
channel.Name, |
||||||
|
channel.FFmpegProfileId, |
||||||
|
GetLogo(channel), |
||||||
|
channel.StreamingMode); |
||||||
|
|
||||||
|
private static string GetLogo(Channel channel) => |
||||||
|
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo)) |
||||||
|
.Match(a => a.Path, string.Empty); |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -1,9 +1,10 @@ |
|||||||
using ErsatzTV.Core; |
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
using LanguageExt; |
using LanguageExt; |
||||||
using MediatR; |
using MediatR; |
||||||
|
|
||||||
namespace ErsatzTV.Application.Images.Commands |
namespace ErsatzTV.Application.Images.Commands |
||||||
{ |
{ |
||||||
// ReSharper disable once SuggestBaseTypeForParameter
|
// ReSharper disable once SuggestBaseTypeForParameter
|
||||||
public record SaveImageToDisk(byte[] Buffer) : IRequest<Either<BaseError, string>>; |
public record SaveArtworkToDisk(byte[] Buffer, ArtworkKind ArtworkKind) : IRequest<Either<BaseError, string>>; |
||||||
} |
} |
@ -0,0 +1,19 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Interfaces.Images; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Images.Commands |
||||||
|
{ |
||||||
|
public class SaveArtworkToDiskHandler : IRequestHandler<SaveArtworkToDisk, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
private readonly IImageCache _imageCache; |
||||||
|
|
||||||
|
public SaveArtworkToDiskHandler(IImageCache imageCache) => _imageCache = imageCache; |
||||||
|
|
||||||
|
public Task<Either<BaseError, string>> Handle(SaveArtworkToDisk request, CancellationToken cancellationToken) => |
||||||
|
_imageCache.SaveArtworkToCache(request.Buffer, request.ArtworkKind); |
||||||
|
} |
||||||
|
} |
@ -1,20 +0,0 @@ |
|||||||
using System.Threading; |
|
||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Core; |
|
||||||
using ErsatzTV.Core.Interfaces.Images; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Images.Commands |
|
||||||
{ |
|
||||||
public class SaveImageToDiskHandler : IRequestHandler<SaveImageToDisk, Either<BaseError, string>> |
|
||||||
{ |
|
||||||
private readonly IImageCache _imageCache; |
|
||||||
|
|
||||||
public SaveImageToDiskHandler(IImageCache imageCache) => _imageCache = imageCache; |
|
||||||
|
|
||||||
public Task<Either<BaseError, string>> Handle( |
|
||||||
SaveImageToDisk request, |
|
||||||
CancellationToken cancellationToken) => _imageCache.SaveImage(request.Buffer); |
|
||||||
} |
|
||||||
} |
|
@ -1,8 +1,10 @@ |
|||||||
using ErsatzTV.Core; |
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
using LanguageExt; |
using LanguageExt; |
||||||
using MediatR; |
using MediatR; |
||||||
|
|
||||||
namespace ErsatzTV.Application.Images.Queries |
namespace ErsatzTV.Application.Images.Queries |
||||||
{ |
{ |
||||||
public record GetImageContents(string FileName) : IRequest<Either<BaseError, ImageViewModel>>; |
public record GetImageContents |
||||||
|
(string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest<Either<BaseError, ImageViewModel>>; |
||||||
} |
} |
||||||
|
@ -0,0 +1,9 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Commands |
||||||
|
{ |
||||||
|
public record CreateLocalLibraryPath |
||||||
|
(int LibraryId, string Path) : IRequest<Either<BaseError, LocalLibraryPathViewModel>>; |
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
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 static LanguageExt.Prelude; |
||||||
|
using static ErsatzTV.Application.Libraries.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Commands |
||||||
|
{ |
||||||
|
public class CreateLocalLibraryPathHandler : IRequestHandler<CreateLocalLibraryPath, |
||||||
|
Either<BaseError, LocalLibraryPathViewModel>> |
||||||
|
{ |
||||||
|
private readonly ILibraryRepository _libraryRepository; |
||||||
|
|
||||||
|
public CreateLocalLibraryPathHandler(ILibraryRepository mediaSourceRepository) => |
||||||
|
_libraryRepository = mediaSourceRepository; |
||||||
|
|
||||||
|
public Task<Either<BaseError, LocalLibraryPathViewModel>> Handle( |
||||||
|
CreateLocalLibraryPath request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request).MapT(PersistLocalLibraryPath).Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<LocalLibraryPathViewModel> PersistLocalLibraryPath(LibraryPath p) => |
||||||
|
_libraryRepository.Add(p).Map(ProjectToViewModel); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, LibraryPath>> Validate(CreateLocalLibraryPath request) => |
||||||
|
ValidateFolder(request) |
||||||
|
.MapT( |
||||||
|
folder => |
||||||
|
new LibraryPath |
||||||
|
{ |
||||||
|
LibraryId = request.LibraryId, |
||||||
|
Path = folder |
||||||
|
}); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalLibraryPath request) |
||||||
|
{ |
||||||
|
List<string> allPaths = await _libraryRepository.GetLocalPaths(request.LibraryId) |
||||||
|
.Map(list => list.Map(c => c.Path).ToList()); |
||||||
|
|
||||||
|
|
||||||
|
return Optional(request.Path) |
||||||
|
.Filter(folder => allPaths.ForAll(f => !AreSubPaths(f, folder))) |
||||||
|
.ToValidation<BaseError>("Path must not belong to another library path"); |
||||||
|
} |
||||||
|
|
||||||
|
private static bool AreSubPaths(string path1, string path2) |
||||||
|
{ |
||||||
|
string one = path1 + Path.DirectorySeparatorChar; |
||||||
|
string two = path2 + Path.DirectorySeparatorChar; |
||||||
|
return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) || |
||||||
|
two.StartsWith(one, StringComparison.OrdinalIgnoreCase); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Commands |
||||||
|
{ |
||||||
|
public record DeleteLocalLibraryPath(int LocalLibraryPathId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
DeleteLocalLibraryPathHandler : MediatR.IRequestHandler<DeleteLocalLibraryPath, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly ILibraryRepository _libraryRepository; |
||||||
|
|
||||||
|
public DeleteLocalLibraryPathHandler(ILibraryRepository libraryRepository) => |
||||||
|
_libraryRepository = libraryRepository; |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
DeleteLocalLibraryPath request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
MediaSourceMustExist(request) |
||||||
|
.MapT(DoDeletion) |
||||||
|
.Bind(t => t.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<Unit> DoDeletion(LibraryPath libraryPath) => |
||||||
|
_libraryRepository.DeleteLocalPath(libraryPath.Id).ToUnit(); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, LibraryPath>> MediaSourceMustExist(DeleteLocalLibraryPath request) => |
||||||
|
(await _libraryRepository.GetPath(request.LocalLibraryPathId)) |
||||||
|
.HeadOrNone() |
||||||
|
.ToValidation<BaseError>( |
||||||
|
$"Local library path {request.LocalLibraryPathId} does not exist."); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
namespace ErsatzTV.Application.Libraries |
||||||
|
{ |
||||||
|
public record LocalLibraryPathViewModel(int Id, int LibraryId, string Path); |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries |
||||||
|
{ |
||||||
|
public record LocalLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind); |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries |
||||||
|
{ |
||||||
|
internal static class Mapper |
||||||
|
{ |
||||||
|
public static LocalLibraryViewModel ProjectToViewModel(LocalLibrary library) => |
||||||
|
new(library.Id, library.Name, library.MediaKind); |
||||||
|
|
||||||
|
public static LocalLibraryPathViewModel ProjectToViewModel(LibraryPath libraryPath) => |
||||||
|
new(libraryPath.Id, libraryPath.LibraryId, libraryPath.Path); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Queries |
||||||
|
{ |
||||||
|
public record CountMediaItemsByLibraryPath(int LibraryPathId) : IRequest<int>; |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Queries |
||||||
|
{ |
||||||
|
public class CountMediaItemsByLibraryPathHandler : IRequestHandler<CountMediaItemsByLibraryPath, int> |
||||||
|
{ |
||||||
|
private readonly ILibraryRepository _libraryRepository; |
||||||
|
|
||||||
|
public CountMediaItemsByLibraryPathHandler(ILibraryRepository libraryRepository) => |
||||||
|
_libraryRepository = libraryRepository; |
||||||
|
|
||||||
|
public Task<int> Handle(CountMediaItemsByLibraryPath request, CancellationToken cancellationToken) => |
||||||
|
_libraryRepository.CountMediaItemsByPath(request.LibraryPathId); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Queries |
||||||
|
{ |
||||||
|
public record GetLocalLibraries : IRequest<List<LocalLibraryViewModel>>; |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
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.Libraries.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Queries |
||||||
|
{ |
||||||
|
public class GetLocalLibrariesHandler : IRequestHandler<GetLocalLibraries, List<LocalLibraryViewModel>> |
||||||
|
{ |
||||||
|
private readonly ILibraryRepository _libraryRepository; |
||||||
|
|
||||||
|
public GetLocalLibrariesHandler(ILibraryRepository libraryRepository) => _libraryRepository = libraryRepository; |
||||||
|
|
||||||
|
public Task<List<LocalLibraryViewModel>> |
||||||
|
Handle(GetLocalLibraries request, CancellationToken cancellationToken) => |
||||||
|
_libraryRepository.GetAllLocal().Map(list => list.Map(ProjectToViewModel).ToList()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Queries |
||||||
|
{ |
||||||
|
public record GetLocalLibraryById(int LibraryId) : IRequest<Option<LocalLibraryViewModel>>; |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.Libraries.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Queries |
||||||
|
{ |
||||||
|
public class GetLocalLibraryByIdHandler : IRequestHandler<GetLocalLibraryById, Option<LocalLibraryViewModel>> |
||||||
|
{ |
||||||
|
private readonly ILibraryRepository _libraryRepository; |
||||||
|
|
||||||
|
public GetLocalLibraryByIdHandler(ILibraryRepository libraryRepository) => |
||||||
|
_libraryRepository = libraryRepository; |
||||||
|
|
||||||
|
public Task<Option<LocalLibraryViewModel>> Handle( |
||||||
|
GetLocalLibraryById request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
_libraryRepository.GetLocal(request.LibraryId).MapT(ProjectToViewModel); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Queries |
||||||
|
{ |
||||||
|
public record GetLocalLibraryPaths(int LocalLibraryId) : IRequest<List<LocalLibraryPathViewModel>>; |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
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.Libraries.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Libraries.Queries |
||||||
|
{ |
||||||
|
public class GetLocalLibraryPathsHandler : IRequestHandler<GetLocalLibraryPaths, List<LocalLibraryPathViewModel>> |
||||||
|
{ |
||||||
|
private readonly ILibraryRepository _libraryRepository; |
||||||
|
|
||||||
|
public GetLocalLibraryPathsHandler(ILibraryRepository libraryRepository) => |
||||||
|
_libraryRepository = libraryRepository; |
||||||
|
|
||||||
|
public Task<List<LocalLibraryPathViewModel>> Handle( |
||||||
|
GetLocalLibraryPaths request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
_libraryRepository.GetLocalPaths(request.LocalLibraryId) |
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList()); |
||||||
|
} |
||||||
|
} |
@ -1,60 +1,70 @@ |
|||||||
using System; |
using System; |
||||||
using System.Linq; |
using System.Linq; |
||||||
using ErsatzTV.Core.Domain; |
using ErsatzTV.Core.Domain; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards |
namespace ErsatzTV.Application.MediaCards |
||||||
{ |
{ |
||||||
internal static class Mapper |
internal static class Mapper |
||||||
{ |
{ |
||||||
internal static TelevisionShowCardViewModel ProjectToViewModel(TelevisionShow televisionShow) => |
internal static TelevisionShowCardViewModel ProjectToViewModel(ShowMetadata showMetadata) => |
||||||
new( |
new( |
||||||
televisionShow.Id, |
showMetadata.ShowId, |
||||||
televisionShow.Metadata?.Title, |
showMetadata.Title, |
||||||
televisionShow.Metadata?.Year.ToString(), |
showMetadata.Year?.ToString(), |
||||||
televisionShow.Metadata?.SortTitle, |
showMetadata.SortTitle, |
||||||
televisionShow.Poster); |
GetPoster(showMetadata)); |
||||||
|
|
||||||
internal static TelevisionSeasonCardViewModel ProjectToViewModel(TelevisionSeason televisionSeason) => |
internal static TelevisionSeasonCardViewModel ProjectToViewModel(Season season) => |
||||||
new( |
new( |
||||||
televisionSeason.TelevisionShow.Metadata?.Title, |
season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title).IfNone(string.Empty), |
||||||
televisionSeason.Id, |
season.Id, |
||||||
televisionSeason.Number, |
season.SeasonNumber, |
||||||
GetSeasonName(televisionSeason.Number), |
GetSeasonName(season.SeasonNumber), |
||||||
string.Empty, |
string.Empty, |
||||||
GetSeasonName(televisionSeason.Number), |
GetSeasonName(season.SeasonNumber), |
||||||
televisionSeason.Poster, |
season.SeasonMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty), |
||||||
televisionSeason.Number == 0 ? "S" : televisionSeason.Number.ToString()); |
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString()); |
||||||
|
|
||||||
internal static TelevisionEpisodeCardViewModel ProjectToViewModel( |
internal static TelevisionEpisodeCardViewModel ProjectToViewModel( |
||||||
TelevisionEpisodeMediaItem televisionEpisode) => |
EpisodeMetadata episodeMetadata) => |
||||||
new( |
new( |
||||||
televisionEpisode.Id, |
episodeMetadata.EpisodeId, |
||||||
televisionEpisode.Metadata?.Aired ?? DateTime.MinValue, |
episodeMetadata.ReleaseDate ?? DateTime.MinValue, |
||||||
televisionEpisode.Season.TelevisionShow.Metadata.Title, |
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title).IfNone(string.Empty), |
||||||
televisionEpisode.Metadata?.Title, |
episodeMetadata.Title, |
||||||
$"Episode {televisionEpisode.Metadata?.Episode}", |
$"Episode {episodeMetadata.Episode.EpisodeNumber}", |
||||||
televisionEpisode.Metadata?.Episode.ToString(), |
episodeMetadata.Episode.EpisodeNumber.ToString(), |
||||||
televisionEpisode.Poster, |
GetThumbnail(episodeMetadata), |
||||||
televisionEpisode.Metadata?.Episode.ToString()); |
episodeMetadata.Episode.EpisodeNumber.ToString()); |
||||||
|
|
||||||
internal static MovieCardViewModel ProjectToViewModel(MovieMediaItem movie) => |
internal static MovieCardViewModel ProjectToViewModel(MovieMetadata movieMetadata) => |
||||||
new( |
new( |
||||||
movie.Id, |
movieMetadata.MovieId, |
||||||
movie.Metadata?.Title, |
movieMetadata.Title, |
||||||
movie.Metadata?.Year?.ToString(), |
movieMetadata.Year?.ToString(), |
||||||
movie.Metadata?.SortTitle, |
movieMetadata.SortTitle, |
||||||
movie.Poster); |
GetPoster(movieMetadata)); |
||||||
|
|
||||||
internal static SimpleMediaCollectionCardResultsViewModel |
internal static CollectionCardResultsViewModel |
||||||
ProjectToViewModel(SimpleMediaCollection collection) => |
ProjectToViewModel(Collection collection) => |
||||||
new( |
new( |
||||||
collection.Name, |
collection.Name, |
||||||
collection.Movies.Map(ProjectToViewModel).ToList(), |
collection.MediaItems.OfType<Movie>().Map(m => ProjectToViewModel(m.MovieMetadata.Head())).ToList(), |
||||||
collection.TelevisionShows.Map(ProjectToViewModel).ToList(), |
collection.MediaItems.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList(), |
||||||
collection.TelevisionSeasons.Map(ProjectToViewModel).ToList(), |
collection.MediaItems.OfType<Season>().Map(ProjectToViewModel).ToList(), |
||||||
collection.TelevisionEpisodes.Map(ProjectToViewModel).ToList()); |
collection.MediaItems.OfType<Episode>().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head())) |
||||||
|
.ToList()); |
||||||
|
|
||||||
private static string GetSeasonName(int number) => |
private static string GetSeasonName(int number) => |
||||||
number == 0 ? "Specials" : $"Season {number}"; |
number == 0 ? "Specials" : $"Season {number}"; |
||||||
|
|
||||||
|
private static string GetPoster(Metadata metadata) => |
||||||
|
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster)) |
||||||
|
.Match(a => a.Path, string.Empty); |
||||||
|
|
||||||
|
private static string GetThumbnail(Metadata metadata) => |
||||||
|
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Thumbnail)) |
||||||
|
.Match(a => a.Path, string.Empty); |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,8 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards.Queries |
||||||
|
{ |
||||||
|
public record GetCollectionCards(int Id) : IRequest<Either<BaseError, CollectionCardResultsViewModel>>; |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.MediaCards.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards.Queries |
||||||
|
{ |
||||||
|
public class GetCollectionCardsHandler : IRequestHandler<GetCollectionCards, |
||||||
|
Either<BaseError, CollectionCardResultsViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _collectionRepository; |
||||||
|
|
||||||
|
public GetCollectionCardsHandler(IMediaCollectionRepository collectionRepository) => |
||||||
|
_collectionRepository = collectionRepository; |
||||||
|
|
||||||
|
public async Task<Either<BaseError, CollectionCardResultsViewModel>> Handle( |
||||||
|
GetCollectionCards request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
(await _collectionRepository.GetCollectionWithItemsUntracked(request.Id)) |
||||||
|
.ToEither(BaseError.New("Unable to load collection")) |
||||||
|
.Map(ProjectToViewModel); |
||||||
|
} |
||||||
|
} |
@ -1,9 +0,0 @@ |
|||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards.Queries |
|
||||||
{ |
|
||||||
public record GetSimpleMediaCollectionCards |
|
||||||
(int Id) : IRequest<Either<BaseError, SimpleMediaCollectionCardResultsViewModel>>; |
|
||||||
} |
|
@ -1,26 +0,0 @@ |
|||||||
using System.Threading; |
|
||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Core; |
|
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
using static ErsatzTV.Application.MediaCards.Mapper; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards.Queries |
|
||||||
{ |
|
||||||
public class GetSimpleMediaCollectionCardsHandler : IRequestHandler<GetSimpleMediaCollectionCards, |
|
||||||
Either<BaseError, SimpleMediaCollectionCardResultsViewModel>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
|
|
||||||
public GetSimpleMediaCollectionCardsHandler(IMediaCollectionRepository mediaCollectionRepository) => |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
|
|
||||||
public async Task<Either<BaseError, SimpleMediaCollectionCardResultsViewModel>> Handle( |
|
||||||
GetSimpleMediaCollectionCards request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
(await _mediaCollectionRepository.GetSimpleMediaCollectionWithItemsUntracked(request.Id)) |
|
||||||
.ToEither(BaseError.New("Unable to load collection")) |
|
||||||
.Map(ProjectToViewModel); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,7 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record AddEpisodeToCollection(int CollectionId, int EpisodeId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
AddEpisodeToCollectionHandler : MediatR.IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
private readonly ITelevisionRepository _televisionRepository; |
||||||
|
|
||||||
|
public AddEpisodeToCollectionHandler( |
||||||
|
IMediaCollectionRepository mediaCollectionRepository, |
||||||
|
ITelevisionRepository televisionRepository) |
||||||
|
{ |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
_televisionRepository = televisionRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
AddEpisodeToCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(_ => ApplyAddTelevisionEpisodeRequest(request)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<Unit> ApplyAddTelevisionEpisodeRequest(AddEpisodeToCollection request) => |
||||||
|
_mediaCollectionRepository.AddMediaItem(request.CollectionId, request.EpisodeId); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Unit>> Validate(AddEpisodeToCollection request) => |
||||||
|
(await CollectionMustExist(request), await ValidateEpisode(request)) |
||||||
|
.Apply((_, _) => Unit.Default); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddEpisodeToCollection request) => |
||||||
|
_mediaCollectionRepository.Get(request.CollectionId) |
||||||
|
.MapT(_ => Unit.Default) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> ValidateEpisode(AddEpisodeToCollection request) => |
||||||
|
LoadTelevisionEpisode(request) |
||||||
|
.MapT(_ => Unit.Default) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Episode does not exist")); |
||||||
|
|
||||||
|
private Task<Option<int>> LoadTelevisionEpisode(AddEpisodeToCollection request) => |
||||||
|
_televisionRepository.GetEpisode(request.EpisodeId).MapT(e => e.Id); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record AddMovieToCollection(int CollectionId, int MovieId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class AddMovieToCollectionHandler : MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
private readonly IMovieRepository _movieRepository; |
||||||
|
|
||||||
|
public AddMovieToCollectionHandler( |
||||||
|
IMediaCollectionRepository mediaCollectionRepository, |
||||||
|
IMovieRepository movieRepository) |
||||||
|
{ |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
_movieRepository = movieRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
AddMovieToCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(_ => ApplyAddMoviesRequest(request)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<Unit> ApplyAddMoviesRequest(AddMovieToCollection request) => |
||||||
|
_mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MovieId); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Unit>> Validate(AddMovieToCollection request) => |
||||||
|
(await CollectionMustExist(request), await ValidateMovies(request)) |
||||||
|
.Apply((_, _) => Unit.Default); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddMovieToCollection request) => |
||||||
|
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) |
||||||
|
.MapT(_ => Unit.Default) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> ValidateMovies(AddMovieToCollection request) => |
||||||
|
LoadMovie(request) |
||||||
|
.MapT(_ => Unit.Default) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Movie does not exist")); |
||||||
|
|
||||||
|
private Task<Option<Movie>> LoadMovie(AddMovieToCollection request) => |
||||||
|
_movieRepository.GetMovie(request.MovieId); |
||||||
|
} |
||||||
|
} |
@ -1,8 +0,0 @@ |
|||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public record AddMovieToSimpleMediaCollection |
|
||||||
(int MediaCollectionId, int MovieId) : MediatR.IRequest<Either<BaseError, Unit>>; |
|
||||||
} |
|
@ -1,62 +0,0 @@ |
|||||||
using System.Threading; |
|
||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Core; |
|
||||||
using ErsatzTV.Core.Domain; |
|
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
|
||||||
using LanguageExt; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public class |
|
||||||
AddMovieToSimpleMediaCollectionHandler : MediatR.IRequestHandler<AddMovieToSimpleMediaCollection, |
|
||||||
Either<BaseError, Unit>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
private readonly IMovieRepository _movieRepository; |
|
||||||
|
|
||||||
public AddMovieToSimpleMediaCollectionHandler( |
|
||||||
IMediaCollectionRepository mediaCollectionRepository, |
|
||||||
IMovieRepository movieRepository) |
|
||||||
{ |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
_movieRepository = movieRepository; |
|
||||||
} |
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle( |
|
||||||
AddMovieToSimpleMediaCollection request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
Validate(request) |
|
||||||
.MapT(ApplyAddMoviesRequest) |
|
||||||
.Bind(v => v.ToEitherAsync()); |
|
||||||
|
|
||||||
private async Task<Unit> ApplyAddMoviesRequest(RequestParameters parameters) |
|
||||||
{ |
|
||||||
parameters.Collection.Movies.Add(parameters.MovieToAdd); |
|
||||||
await _mediaCollectionRepository.Update(parameters.Collection); |
|
||||||
|
|
||||||
return Unit.Default; |
|
||||||
} |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, RequestParameters>> |
|
||||||
Validate(AddMovieToSimpleMediaCollection request) => |
|
||||||
(await SimpleMediaCollectionMustExist(request), await ValidateMovies(request)) |
|
||||||
.Apply( |
|
||||||
(simpleMediaCollectionToUpdate, movieToAdd) => |
|
||||||
new RequestParameters(simpleMediaCollectionToUpdate, movieToAdd)); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist( |
|
||||||
AddMovieToSimpleMediaCollection updateSimpleMediaCollection) => |
|
||||||
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) |
|
||||||
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist.")); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, MovieMediaItem>> ValidateMovies( |
|
||||||
AddMovieToSimpleMediaCollection request) => |
|
||||||
LoadMovie(request) |
|
||||||
.Map(v => v.ToValidation<BaseError>("MovieMediaItem does not exist")); |
|
||||||
|
|
||||||
private Task<Option<MovieMediaItem>> LoadMovie(AddMovieToSimpleMediaCollection request) => |
|
||||||
_movieRepository.GetMovie(request.MovieId); |
|
||||||
|
|
||||||
private record RequestParameters(SimpleMediaCollection Collection, MovieMediaItem MovieToAdd); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,7 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record AddSeasonToCollection(int CollectionId, int SeasonId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class AddSeasonToCollectionHandler : MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
private readonly ITelevisionRepository _televisionRepository; |
||||||
|
|
||||||
|
public AddSeasonToCollectionHandler( |
||||||
|
IMediaCollectionRepository mediaCollectionRepository, |
||||||
|
ITelevisionRepository televisionRepository) |
||||||
|
{ |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
_televisionRepository = televisionRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
AddSeasonToCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(_ => ApplyAddTelevisionSeasonRequest(request)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private async Task<Unit> ApplyAddTelevisionSeasonRequest(AddSeasonToCollection request) => |
||||||
|
await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.SeasonId); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Unit>> Validate(AddSeasonToCollection request) => |
||||||
|
(await CollectionMustExist(request), await ValidateSeason(request)) |
||||||
|
.Apply((_, _) => Unit.Default); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddSeasonToCollection request) => |
||||||
|
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) |
||||||
|
.MapT(_ => Unit.Default) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> ValidateSeason(AddSeasonToCollection request) => |
||||||
|
LoadTelevisionSeason(request) |
||||||
|
.MapT(_ => Unit.Default) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Season does not exist")); |
||||||
|
|
||||||
|
private Task<Option<Season>> LoadTelevisionSeason( |
||||||
|
AddSeasonToCollection request) => |
||||||
|
_televisionRepository.GetSeason(request.SeasonId); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record AddShowToCollection(int CollectionId, int ShowId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class AddShowToCollectionHandler : MediatR.IRequestHandler<AddShowToCollection, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
private readonly ITelevisionRepository _televisionRepository; |
||||||
|
|
||||||
|
public AddShowToCollectionHandler( |
||||||
|
IMediaCollectionRepository mediaCollectionRepository, |
||||||
|
ITelevisionRepository televisionRepository) |
||||||
|
{ |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
_televisionRepository = televisionRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
AddShowToCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(_ => ApplyAddTelevisionShowRequest(request)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<Unit> ApplyAddTelevisionShowRequest(AddShowToCollection request) |
||||||
|
=> _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ShowId); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Unit>> Validate(AddShowToCollection request) => |
||||||
|
(await CollectionMustExist(request), await ValidateShow(request)) |
||||||
|
.Apply((_, _) => Unit.Default); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddShowToCollection request) => |
||||||
|
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) |
||||||
|
.MapT(_ => Unit.Default) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> ValidateShow(AddShowToCollection request) => |
||||||
|
LoadTelevisionShow(request) |
||||||
|
.MapT(_ => Unit.Default) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Show does not exist")); |
||||||
|
|
||||||
|
private Task<Option<Show>> LoadTelevisionShow(AddShowToCollection request) => |
||||||
|
_televisionRepository.GetShow(request.ShowId); |
||||||
|
} |
||||||
|
} |
@ -1,8 +0,0 @@ |
|||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public record AddTelevisionEpisodeToSimpleMediaCollection |
|
||||||
(int MediaCollectionId, int TelevisionEpisodeId) : MediatR.IRequest<Either<BaseError, Unit>>; |
|
||||||
} |
|
@ -1,68 +0,0 @@ |
|||||||
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.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public class |
|
||||||
AddTelevisionEpisodeToSimpleMediaCollectionHandler : MediatR.IRequestHandler< |
|
||||||
AddTelevisionEpisodeToSimpleMediaCollection, |
|
||||||
Either<BaseError, Unit>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
private readonly ITelevisionRepository _televisionRepository; |
|
||||||
|
|
||||||
public AddTelevisionEpisodeToSimpleMediaCollectionHandler( |
|
||||||
IMediaCollectionRepository mediaCollectionRepository, |
|
||||||
ITelevisionRepository televisionRepository) |
|
||||||
{ |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
_televisionRepository = televisionRepository; |
|
||||||
} |
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle( |
|
||||||
AddTelevisionEpisodeToSimpleMediaCollection request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
Validate(request) |
|
||||||
.MapT(ApplyAddTelevisionEpisodeRequest) |
|
||||||
.Bind(v => v.ToEitherAsync()); |
|
||||||
|
|
||||||
private async Task<Unit> ApplyAddTelevisionEpisodeRequest(RequestParameters parameters) |
|
||||||
{ |
|
||||||
if (parameters.Collection.TelevisionEpisodes.All(s => s.Id != parameters.EpisodeToAdd.Id)) |
|
||||||
{ |
|
||||||
parameters.Collection.TelevisionEpisodes.Add(parameters.EpisodeToAdd); |
|
||||||
await _mediaCollectionRepository.Update(parameters.Collection); |
|
||||||
} |
|
||||||
|
|
||||||
return Unit.Default; |
|
||||||
} |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, RequestParameters>> |
|
||||||
Validate(AddTelevisionEpisodeToSimpleMediaCollection request) => |
|
||||||
(await SimpleMediaCollectionMustExist(request), await ValidateEpisode(request)) |
|
||||||
.Apply( |
|
||||||
(simpleMediaCollectionToUpdate, episode) => |
|
||||||
new RequestParameters(simpleMediaCollectionToUpdate, episode)); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist( |
|
||||||
AddTelevisionEpisodeToSimpleMediaCollection updateSimpleMediaCollection) => |
|
||||||
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) |
|
||||||
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist.")); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, TelevisionEpisodeMediaItem>> ValidateEpisode( |
|
||||||
AddTelevisionEpisodeToSimpleMediaCollection request) => |
|
||||||
LoadTelevisionEpisode(request) |
|
||||||
.Map(v => v.ToValidation<BaseError>("TelevisionEpisode does not exist")); |
|
||||||
|
|
||||||
private Task<Option<TelevisionEpisodeMediaItem>> LoadTelevisionEpisode( |
|
||||||
AddTelevisionEpisodeToSimpleMediaCollection request) => |
|
||||||
_televisionRepository.GetEpisode(request.TelevisionEpisodeId); |
|
||||||
|
|
||||||
private record RequestParameters(SimpleMediaCollection Collection, TelevisionEpisodeMediaItem EpisodeToAdd); |
|
||||||
} |
|
||||||
} |
|
@ -1,8 +0,0 @@ |
|||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public record AddTelevisionSeasonToSimpleMediaCollection |
|
||||||
(int MediaCollectionId, int TelevisionSeasonId) : MediatR.IRequest<Either<BaseError, Unit>>; |
|
||||||
} |
|
@ -1,68 +0,0 @@ |
|||||||
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.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public class |
|
||||||
AddTelevisionSeasonToSimpleMediaCollectionHandler : MediatR.IRequestHandler< |
|
||||||
AddTelevisionSeasonToSimpleMediaCollection, |
|
||||||
Either<BaseError, Unit>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
private readonly ITelevisionRepository _televisionRepository; |
|
||||||
|
|
||||||
public AddTelevisionSeasonToSimpleMediaCollectionHandler( |
|
||||||
IMediaCollectionRepository mediaCollectionRepository, |
|
||||||
ITelevisionRepository televisionRepository) |
|
||||||
{ |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
_televisionRepository = televisionRepository; |
|
||||||
} |
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle( |
|
||||||
AddTelevisionSeasonToSimpleMediaCollection request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
Validate(request) |
|
||||||
.MapT(ApplyAddTelevisionSeasonRequest) |
|
||||||
.Bind(v => v.ToEitherAsync()); |
|
||||||
|
|
||||||
private async Task<Unit> ApplyAddTelevisionSeasonRequest(RequestParameters parameters) |
|
||||||
{ |
|
||||||
if (parameters.Collection.TelevisionSeasons.All(s => s.Id != parameters.SeasonToAdd.Id)) |
|
||||||
{ |
|
||||||
parameters.Collection.TelevisionSeasons.Add(parameters.SeasonToAdd); |
|
||||||
await _mediaCollectionRepository.Update(parameters.Collection); |
|
||||||
} |
|
||||||
|
|
||||||
return Unit.Default; |
|
||||||
} |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, RequestParameters>> |
|
||||||
Validate(AddTelevisionSeasonToSimpleMediaCollection request) => |
|
||||||
(await SimpleMediaCollectionMustExist(request), await ValidateSeason(request)) |
|
||||||
.Apply( |
|
||||||
(simpleMediaCollectionToUpdate, season) => |
|
||||||
new RequestParameters(simpleMediaCollectionToUpdate, season)); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist( |
|
||||||
AddTelevisionSeasonToSimpleMediaCollection updateSimpleMediaCollection) => |
|
||||||
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) |
|
||||||
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist.")); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, TelevisionSeason>> ValidateSeason( |
|
||||||
AddTelevisionSeasonToSimpleMediaCollection request) => |
|
||||||
LoadTelevisionSeason(request) |
|
||||||
.Map(v => v.ToValidation<BaseError>("TelevisionSeason does not exist")); |
|
||||||
|
|
||||||
private Task<Option<TelevisionSeason>> LoadTelevisionSeason( |
|
||||||
AddTelevisionSeasonToSimpleMediaCollection request) => |
|
||||||
_televisionRepository.GetSeason(request.TelevisionSeasonId); |
|
||||||
|
|
||||||
private record RequestParameters(SimpleMediaCollection Collection, TelevisionSeason SeasonToAdd); |
|
||||||
} |
|
||||||
} |
|
@ -1,8 +0,0 @@ |
|||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public record AddTelevisionShowToSimpleMediaCollection |
|
||||||
(int MediaCollectionId, int TelevisionShowId) : MediatR.IRequest<Either<BaseError, Unit>>; |
|
||||||
} |
|
@ -1,67 +0,0 @@ |
|||||||
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.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public class |
|
||||||
AddTelevisionShowToSimpleMediaCollectionHandler : MediatR.IRequestHandler< |
|
||||||
AddTelevisionShowToSimpleMediaCollection, |
|
||||||
Either<BaseError, Unit>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
private readonly ITelevisionRepository _televisionRepository; |
|
||||||
|
|
||||||
public AddTelevisionShowToSimpleMediaCollectionHandler( |
|
||||||
IMediaCollectionRepository mediaCollectionRepository, |
|
||||||
ITelevisionRepository televisionRepository) |
|
||||||
{ |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
_televisionRepository = televisionRepository; |
|
||||||
} |
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle( |
|
||||||
AddTelevisionShowToSimpleMediaCollection request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
Validate(request) |
|
||||||
.MapT(ApplyAddTelevisionShowRequest) |
|
||||||
.Bind(v => v.ToEitherAsync()); |
|
||||||
|
|
||||||
private async Task<Unit> ApplyAddTelevisionShowRequest(RequestParameters parameters) |
|
||||||
{ |
|
||||||
if (parameters.Collection.TelevisionShows.All(s => s.Id != parameters.ShowToAdd.Id)) |
|
||||||
{ |
|
||||||
parameters.Collection.TelevisionShows.Add(parameters.ShowToAdd); |
|
||||||
await _mediaCollectionRepository.Update(parameters.Collection); |
|
||||||
} |
|
||||||
|
|
||||||
return Unit.Default; |
|
||||||
} |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, RequestParameters>> |
|
||||||
Validate(AddTelevisionShowToSimpleMediaCollection request) => |
|
||||||
(await SimpleMediaCollectionMustExist(request), await ValidateShow(request)) |
|
||||||
.Apply( |
|
||||||
(simpleMediaCollectionToUpdate, show) => |
|
||||||
new RequestParameters(simpleMediaCollectionToUpdate, show)); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist( |
|
||||||
AddTelevisionShowToSimpleMediaCollection updateSimpleMediaCollection) => |
|
||||||
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) |
|
||||||
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist.")); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, TelevisionShow>> ValidateShow( |
|
||||||
AddTelevisionShowToSimpleMediaCollection request) => |
|
||||||
LoadTelevisionShow(request) |
|
||||||
.Map(v => v.ToValidation<BaseError>("TelevisionShow does not exist")); |
|
||||||
|
|
||||||
private Task<Option<TelevisionShow>> LoadTelevisionShow(AddTelevisionShowToSimpleMediaCollection request) => |
|
||||||
_televisionRepository.GetShow(request.TelevisionShowId); |
|
||||||
|
|
||||||
private record RequestParameters(SimpleMediaCollection Collection, TelevisionShow ShowToAdd); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,8 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record CreateCollection(string Name) : IRequest<Either<BaseError, MediaCollectionViewModel>>; |
||||||
|
} |
@ -1,9 +0,0 @@ |
|||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public record CreateSimpleMediaCollection |
|
||||||
(string Name) : IRequest<Either<BaseError, MediaCollectionViewModel>>; |
|
||||||
} |
|
@ -0,0 +1,11 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record RemoveItemsFromCollection(int MediaCollectionId) : MediatR.IRequest<Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
public List<int> MediaItemIds { get; set; } = new(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
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.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
RemoveItemsFromCollectionHandler : MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public RemoveItemsFromCollectionHandler( |
||||||
|
IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
RemoveItemsFromCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(collection => ApplyAddTelevisionEpisodeRequest(request, collection)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<Unit> ApplyAddTelevisionEpisodeRequest( |
||||||
|
RemoveItemsFromCollection request, |
||||||
|
Collection collection) |
||||||
|
{ |
||||||
|
var itemsToRemove = collection.MediaItems |
||||||
|
.Filter(m => request.MediaItemIds.Contains(m.Id)) |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
itemsToRemove.ForEach(m => collection.MediaItems.Remove(m)); |
||||||
|
|
||||||
|
return itemsToRemove.Any() |
||||||
|
? _mediaCollectionRepository.Update(collection).ToUnit() |
||||||
|
: Task.FromResult(Unit.Default); |
||||||
|
} |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Collection>> Validate( |
||||||
|
RemoveItemsFromCollection request) => |
||||||
|
CollectionMustExist(request); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Collection>> CollectionMustExist( |
||||||
|
RemoveItemsFromCollection updateCollection) => |
||||||
|
_mediaCollectionRepository.GetCollectionWithItems(updateCollection.MediaCollectionId) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); |
||||||
|
} |
||||||
|
} |
@ -1,15 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public record RemoveItemsFromSimpleMediaCollection |
|
||||||
(int MediaCollectionId) : MediatR.IRequest<Either<BaseError, Unit>> |
|
||||||
{ |
|
||||||
public List<int> MovieIds { get; set; } = new(); |
|
||||||
public List<int> TelevisionShowIds { get; set; } = new(); |
|
||||||
public List<int> TelevisionSeasonIds { get; set; } = new(); |
|
||||||
public List<int> TelevisionEpisodeIds { get; set; } = new(); |
|
||||||
} |
|
||||||
} |
|
@ -1,74 +0,0 @@ |
|||||||
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.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public class |
|
||||||
RemoveItemsFromSimpleMediaCollectionHandler : MediatR.IRequestHandler< |
|
||||||
RemoveItemsFromSimpleMediaCollection, |
|
||||||
Either<BaseError, Unit>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
|
|
||||||
public RemoveItemsFromSimpleMediaCollectionHandler( |
|
||||||
IMediaCollectionRepository mediaCollectionRepository) => |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle( |
|
||||||
RemoveItemsFromSimpleMediaCollection request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
Validate(request) |
|
||||||
.MapT(collection => ApplyAddTelevisionEpisodeRequest(request, collection)) |
|
||||||
.Bind(v => v.ToEitherAsync()); |
|
||||||
|
|
||||||
private Task<Unit> ApplyAddTelevisionEpisodeRequest( |
|
||||||
RemoveItemsFromSimpleMediaCollection request, |
|
||||||
SimpleMediaCollection collection) |
|
||||||
{ |
|
||||||
var moviesToRemove = collection.Movies |
|
||||||
.Filter(m => request.MovieIds.Contains(m.Id)) |
|
||||||
.ToList(); |
|
||||||
|
|
||||||
moviesToRemove.ForEach(m => collection.Movies.Remove(m)); |
|
||||||
|
|
||||||
var showsToRemove = collection.TelevisionShows |
|
||||||
.Filter(s => request.TelevisionShowIds.Contains(s.Id)) |
|
||||||
.ToList(); |
|
||||||
|
|
||||||
showsToRemove.ForEach(s => collection.TelevisionShows.Remove(s)); |
|
||||||
|
|
||||||
var seasonsToRemove = collection.TelevisionSeasons |
|
||||||
.Filter(s => request.TelevisionSeasonIds.Contains(s.Id)) |
|
||||||
.ToList(); |
|
||||||
|
|
||||||
seasonsToRemove.ForEach(s => collection.TelevisionSeasons.Remove(s)); |
|
||||||
|
|
||||||
var episodesToRemove = collection.TelevisionEpisodes |
|
||||||
.Filter(e => request.TelevisionEpisodeIds.Contains(e.Id)) |
|
||||||
.ToList(); |
|
||||||
|
|
||||||
episodesToRemove.ForEach(e => collection.TelevisionEpisodes.Remove(e)); |
|
||||||
|
|
||||||
if (moviesToRemove.Any() || showsToRemove.Any() || seasonsToRemove.Any() || episodesToRemove.Any()) |
|
||||||
{ |
|
||||||
return _mediaCollectionRepository.Update(collection).ToUnit(); |
|
||||||
} |
|
||||||
|
|
||||||
return Task.FromResult(Unit.Default); |
|
||||||
} |
|
||||||
|
|
||||||
private Task<Validation<BaseError, SimpleMediaCollection>> Validate( |
|
||||||
RemoveItemsFromSimpleMediaCollection request) => |
|
||||||
SimpleMediaCollectionMustExist(request); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist( |
|
||||||
RemoveItemsFromSimpleMediaCollection updateSimpleMediaCollection) => |
|
||||||
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) |
|
||||||
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist.")); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,8 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record UpdateCollection |
||||||
|
(int CollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public UpdateCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
UpdateCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(c => ApplyUpdateRequest(c, request)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection update) |
||||||
|
{ |
||||||
|
c.Name = update.Name; |
||||||
|
await _mediaCollectionRepository.Update(c); |
||||||
|
return Unit.Default; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Collection>> |
||||||
|
Validate(UpdateCollection request) => |
||||||
|
(await CollectionMustExist(request), ValidateName(request)) |
||||||
|
.Apply((collectionToUpdate, _) => collectionToUpdate); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Collection>> CollectionMustExist( |
||||||
|
UpdateCollection updateCollection) => |
||||||
|
_mediaCollectionRepository.Get(updateCollection.CollectionId) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); |
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidateName(UpdateCollection updateSimpleMediaCollection) => |
||||||
|
updateSimpleMediaCollection.NotEmpty(c => c.Name) |
||||||
|
.Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name)); |
||||||
|
} |
||||||
|
} |
@ -1,8 +0,0 @@ |
|||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public record UpdateSimpleMediaCollection |
|
||||||
(int MediaCollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>; |
|
||||||
} |
|
@ -1,47 +0,0 @@ |
|||||||
using System.Threading; |
|
||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Core; |
|
||||||
using ErsatzTV.Core.Domain; |
|
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
|
||||||
using LanguageExt; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public class |
|
||||||
UpdateSimpleMediaCollectionHandler : MediatR.IRequestHandler<UpdateSimpleMediaCollection, |
|
||||||
Either<BaseError, Unit>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
|
|
||||||
public UpdateSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) => |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle( |
|
||||||
UpdateSimpleMediaCollection request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
Validate(request) |
|
||||||
.MapT(c => ApplyUpdateRequest(c, request)) |
|
||||||
.Bind(v => v.ToEitherAsync()); |
|
||||||
|
|
||||||
private async Task<Unit> ApplyUpdateRequest(SimpleMediaCollection c, UpdateSimpleMediaCollection update) |
|
||||||
{ |
|
||||||
c.Name = update.Name; |
|
||||||
await _mediaCollectionRepository.Update(c); |
|
||||||
return Unit.Default; |
|
||||||
} |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, SimpleMediaCollection>> |
|
||||||
Validate(UpdateSimpleMediaCollection request) => |
|
||||||
(await SimpleMediaCollectionMustExist(request), ValidateName(request)) |
|
||||||
.Apply((simpleMediaCollectionToUpdate, _) => simpleMediaCollectionToUpdate); |
|
||||||
|
|
||||||
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist( |
|
||||||
UpdateSimpleMediaCollection updateSimpleMediaCollection) => |
|
||||||
_mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId) |
|
||||||
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist.")); |
|
||||||
|
|
||||||
private Validation<BaseError, string> ValidateName(UpdateSimpleMediaCollection updateSimpleMediaCollection) => |
|
||||||
updateSimpleMediaCollection.NotEmpty(c => c.Name) |
|
||||||
.Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name)); |
|
||||||
} |
|
||||||
} |
|
@ -1,7 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Queries |
|
||||||
{ |
|
||||||
public record GetAllSimpleMediaCollections : IRequest<List<MediaCollectionViewModel>>; |
|
||||||
} |
|
@ -1,25 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using System.Linq; |
|
||||||
using System.Threading; |
|
||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
|
||||||
using MediatR; |
|
||||||
using static ErsatzTV.Application.MediaCollections.Mapper; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Queries |
|
||||||
{ |
|
||||||
public class |
|
||||||
GetAllSimpleMediaCollectionsHandler : IRequestHandler<GetAllSimpleMediaCollections, |
|
||||||
List<MediaCollectionViewModel>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
|
|
||||||
public GetAllSimpleMediaCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) => |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
|
|
||||||
public async Task<List<MediaCollectionViewModel>> Handle( |
|
||||||
GetAllSimpleMediaCollections request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
(await _mediaCollectionRepository.GetSimpleMediaCollections()).Map(ProjectToViewModel).ToList(); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,7 @@ |
|||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public record GetCollectionById(int Id) : IRequest<Option<MediaCollectionViewModel>>; |
||||||
|
} |
@ -1,7 +0,0 @@ |
|||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Queries |
|
||||||
{ |
|
||||||
public record GetSimpleMediaCollectionById(int Id) : IRequest<Option<MediaCollectionViewModel>>; |
|
||||||
} |
|
@ -1,4 +1,4 @@ |
|||||||
namespace ErsatzTV.Application.MediaItems |
namespace ErsatzTV.Application.MediaItems |
||||||
{ |
{ |
||||||
public record MediaItemViewModel(int Id, int MediaSourceId, string Path); |
public record MediaItemViewModel(int Id, int LibraryPathId); |
||||||
} |
} |
||||||
|
@ -0,0 +1,4 @@ |
|||||||
|
namespace ErsatzTV.Application.MediaItems |
||||||
|
{ |
||||||
|
public record NamedMediaItemViewModel(int MediaItemId, string Name); |
||||||
|
} |
@ -1,10 +0,0 @@ |
|||||||
using ErsatzTV.Core; |
|
||||||
using ErsatzTV.Core.Domain; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaSources.Commands |
|
||||||
{ |
|
||||||
public record CreateLocalMediaSource |
|
||||||
(string Name, MediaType MediaType, string Folder) : IRequest<Either<BaseError, MediaSourceViewModel>>; |
|
||||||
} |
|
@ -1,78 +0,0 @@ |
|||||||
using System; |
|
||||||
using System.Collections.Generic; |
|
||||||
using System.IO; |
|
||||||
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 static ErsatzTV.Application.MediaSources.Mapper; |
|
||||||
using static LanguageExt.Prelude; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaSources.Commands |
|
||||||
{ |
|
||||||
public class CreateLocalMediaSourceHandler : IRequestHandler<CreateLocalMediaSource, |
|
||||||
Either<BaseError, MediaSourceViewModel>> |
|
||||||
{ |
|
||||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
|
||||||
|
|
||||||
public CreateLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) => |
|
||||||
_mediaSourceRepository = mediaSourceRepository; |
|
||||||
|
|
||||||
public Task<Either<BaseError, MediaSourceViewModel>> Handle( |
|
||||||
CreateLocalMediaSource request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
Validate(request).MapT(PersistLocalMediaSource).Bind(v => v.ToEitherAsync()); |
|
||||||
|
|
||||||
private Task<MediaSourceViewModel> PersistLocalMediaSource(LocalMediaSource c) => |
|
||||||
_mediaSourceRepository.Add(c).Map(ProjectToViewModel); |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, LocalMediaSource>> Validate(CreateLocalMediaSource request) => |
|
||||||
(await ValidateName(request), await ValidateFolder(request)) |
|
||||||
.Apply( |
|
||||||
(name, folder) => |
|
||||||
new LocalMediaSource |
|
||||||
{ |
|
||||||
Name = name, |
|
||||||
MediaType = request.MediaType, |
|
||||||
Folder = folder |
|
||||||
}); |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, string>> ValidateName(CreateLocalMediaSource request) |
|
||||||
{ |
|
||||||
List<string> allNames = await _mediaSourceRepository.GetAll() |
|
||||||
.Map(list => list.Map(c => c.Name).ToList()); |
|
||||||
|
|
||||||
Validation<BaseError, string> result1 = request.NotEmpty(c => c.Name) |
|
||||||
.Bind(_ => request.NotLongerThan(50)(c => c.Name)); |
|
||||||
|
|
||||||
var result2 = Optional(request.Name) |
|
||||||
.Filter(name => !allNames.Contains(name)) |
|
||||||
.ToValidation<BaseError>("Media source name must be unique"); |
|
||||||
|
|
||||||
return (result1, result2).Apply((_, _) => request.Name); |
|
||||||
} |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalMediaSource request) |
|
||||||
{ |
|
||||||
List<string> allFolders = await _mediaSourceRepository.GetAll() |
|
||||||
.Map(list => list.OfType<LocalMediaSource>().Map(c => c.Folder).ToList()); |
|
||||||
|
|
||||||
|
|
||||||
return Optional(request.Folder) |
|
||||||
.Filter(folder => allFolders.ForAll(f => !AreSubPaths(f, folder))) |
|
||||||
.ToValidation<BaseError>("Folder must not belong to another media source"); |
|
||||||
} |
|
||||||
|
|
||||||
private static bool AreSubPaths(string path1, string path2) |
|
||||||
{ |
|
||||||
string one = path1 + Path.DirectorySeparatorChar; |
|
||||||
string two = path2 + Path.DirectorySeparatorChar; |
|
||||||
return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) || |
|
||||||
two.StartsWith(one, StringComparison.OrdinalIgnoreCase); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,9 +0,0 @@ |
|||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaSources.Commands |
|
||||||
{ |
|
||||||
public record DeleteLocalMediaSource(int LocalMediaSourceId) : IRequest<Either<BaseError, Task>>; |
|
||||||
} |
|
@ -1,38 +0,0 @@ |
|||||||
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; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaSources.Commands |
|
||||||
{ |
|
||||||
public class |
|
||||||
DeleteLocalMediaSourceHandler : IRequestHandler<DeleteLocalMediaSource, Either<BaseError, Task>> |
|
||||||
{ |
|
||||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
|
||||||
|
|
||||||
public DeleteLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) => |
|
||||||
_mediaSourceRepository = mediaSourceRepository; |
|
||||||
|
|
||||||
public async Task<Either<BaseError, Task>> Handle( |
|
||||||
DeleteLocalMediaSource request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
(await MediaSourceMustExist(request)) |
|
||||||
.Map(DoDeletion) |
|
||||||
.ToEither<Task>(); |
|
||||||
|
|
||||||
private async Task DoDeletion(LocalMediaSource mediaSource) => |
|
||||||
await _mediaSourceRepository.Delete(mediaSource.Id); |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, LocalMediaSource>> MediaSourceMustExist( |
|
||||||
DeleteLocalMediaSource deleteMediaSource) => |
|
||||||
(await _mediaSourceRepository.Get(deleteMediaSource.LocalMediaSourceId)) |
|
||||||
.OfType<LocalMediaSource>() |
|
||||||
.HeadOrNone() |
|
||||||
.ToValidation<BaseError>( |
|
||||||
$"Local media source {deleteMediaSource.LocalMediaSourceId} does not exist."); |
|
||||||
} |
|
||||||
} |
|
@ -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; |
||||||
|
|
||||||
|
var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero); |
||||||
|
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 = DateTime.UtcNow; |
||||||
|
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 @@ |
|||||||
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,4 @@ |
|||||||
using ErsatzTV.Core.Domain; |
namespace ErsatzTV.Application.MediaSources |
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaSources |
|
||||||
{ |
{ |
||||||
public record LocalMediaSourceViewModel(int Id, string Name, string Folder) |
public record LocalMediaSourceViewModel(int Id) : MediaSourceViewModel(Id, "Local"); |
||||||
: MediaSourceViewModel(Id, Name, MediaSourceType.Local); |
|
||||||
} |
} |
||||||
|
@ -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 @@ |
|||||||
using ErsatzTV.Core.Domain; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaSources |
|
||||||
{ |
|
||||||
public record PlexMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel( |
|
||||||
Id, |
|
||||||
Name, |
|
||||||
MediaSourceType.Plex); |
|
||||||
} |
|
@ -1,10 +1,20 @@ |
|||||||
using ErsatzTV.Core.Domain; |
using System.Linq; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
namespace ErsatzTV.Application.Movies |
namespace ErsatzTV.Application.Movies |
||||||
{ |
{ |
||||||
internal static class Mapper |
internal static class Mapper |
||||||
{ |
{ |
||||||
internal static MovieViewModel ProjectToViewModel(MovieMediaItem movie) => |
internal static MovieViewModel ProjectToViewModel(Movie movie) |
||||||
new(movie.Metadata.Title, movie.Metadata.Year?.ToString(), movie.Metadata.Plot, movie.Poster); |
{ |
||||||
|
MovieMetadata metadata = Optional(movie.MovieMetadata).Flatten().Head(); |
||||||
|
return new MovieViewModel( |
||||||
|
metadata.Title, |
||||||
|
metadata.Year?.ToString(), |
||||||
|
metadata.Plot, |
||||||
|
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster)) |
||||||
|
.Match(a => a.Path, string.Empty)); |
||||||
|
} |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -1,7 +1,7 @@ |
|||||||
using ErsatzTV.Core; |
using ErsatzTV.Core; |
||||||
using LanguageExt; |
using LanguageExt; |
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaSources.Commands |
namespace ErsatzTV.Application.Plex.Commands |
||||||
{ |
{ |
||||||
public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>; |
public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
} |
} |
@ -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 @@ |
|||||||
|
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) |
||||||
|
{ |
||||||
|
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 _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 = DateTime.UtcNow; |
||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Plex |
||||||
|
{ |
||||||
|
public record PlexLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems); |
||||||
|
} |
@ -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 @@ |
|||||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||||
using MediatR; |
using MediatR; |
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaSources.Queries |
namespace ErsatzTV.Application.Plex.Queries |
||||||
{ |
{ |
||||||
public record GetAllPlexMediaSources : IRequest<List<PlexMediaSourceViewModel>>; |
public record GetAllPlexMediaSources : IRequest<List<PlexMediaSourceViewModel>>; |
||||||
} |
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue