Initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
public record AggregateMediaItemViewModel(string Source, string Title, int Count, string Duration);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public record CreateMediaItem(int MediaSourceId, string Path) : IRequest<Either<BaseError, MediaItemViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static LanguageExt.Prelude;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public class CreateMediaItemHandler : IRequestHandler<CreateMediaItem, Either<BaseError, MediaItemViewModel>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILocalMetadataProvider _localMetadataProvider;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly ISmartCollectionBuilder _smartCollectionBuilder;
|
||||
|
||||
public CreateMediaItemHandler(
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ISmartCollectionBuilder smartCollectionBuilder,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
ILocalStatisticsProvider localStatisticsProvider)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_configElementRepository = configElementRepository;
|
||||
_smartCollectionBuilder = smartCollectionBuilder;
|
||||
_localMetadataProvider = localMetadataProvider;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, MediaItemViewModel>> Handle(
|
||||
CreateMediaItem request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(PersistMediaItem)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<MediaItemViewModel> PersistMediaItem(RequestParameters parameters)
|
||||
{
|
||||
await _mediaItemRepository.Add(parameters.MediaItem);
|
||||
|
||||
await _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem);
|
||||
await _localMetadataProvider.RefreshMetadata(parameters.MediaItem);
|
||||
await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem);
|
||||
|
||||
return ProjectToViewModel(parameters.MediaItem);
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(CreateMediaItem request) =>
|
||||
(await ValidateMediaSource(request), PathMustExist(request), await ValidateFFprobePath())
|
||||
.Apply(
|
||||
(mediaSourceId, path, ffprobePath) => new RequestParameters(
|
||||
ffprobePath,
|
||||
new MediaItem
|
||||
{
|
||||
MediaSourceId = mediaSourceId,
|
||||
Path = path
|
||||
}));
|
||||
|
||||
private async Task<Validation<BaseError, int>> ValidateMediaSource(CreateMediaItem createMediaItem) =>
|
||||
(await MediaSourceMustExist(createMediaItem)).Bind(MediaSourceMustBeLocal);
|
||||
|
||||
private async Task<Validation<BaseError, MediaSource>> MediaSourceMustExist(CreateMediaItem createMediaItem) =>
|
||||
(await _mediaSourceRepository.Get(createMediaItem.MediaSourceId))
|
||||
.ToValidation<BaseError>($"[MediaSource] {createMediaItem.MediaSourceId} does not exist.");
|
||||
|
||||
private Validation<BaseError, int> MediaSourceMustBeLocal(MediaSource mediaSource) =>
|
||||
Some(mediaSource)
|
||||
.Filter(ms => ms is LocalMediaSource)
|
||||
.ToValidation<BaseError>($"[MediaSource] {mediaSource.Id} must be a local media source")
|
||||
.Map(ms => ms.Id);
|
||||
|
||||
private Validation<BaseError, string> PathMustExist(CreateMediaItem createMediaItem) =>
|
||||
Some(createMediaItem.Path)
|
||||
.Filter(File.Exists)
|
||||
.ToValidation<BaseError>("[Path] does not exist on the file system");
|
||||
|
||||
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(string FFprobePath, MediaItem MediaItem);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public record DeleteMediaItem(int MediaItemId) : IRequest<Either<BaseError, Task>>;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public class DeleteMediaItemHandler : IRequestHandler<DeleteMediaItem, Either<BaseError, Task>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public DeleteMediaItemHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task<Either<BaseError, Task>> Handle(
|
||||
DeleteMediaItem request,
|
||||
CancellationToken cancellationToken) =>
|
||||
(await MediaItemMustExist(request))
|
||||
.Map(DoDeletion)
|
||||
.ToEither<Task>();
|
||||
|
||||
private Task DoDeletion(int mediaItemId) => _mediaItemRepository.Delete(mediaItemId);
|
||||
|
||||
private async Task<Validation<BaseError, int>> MediaItemMustExist(DeleteMediaItem deleteMediaItem) =>
|
||||
(await _mediaItemRepository.Get(deleteMediaItem.MediaItemId))
|
||||
.ToValidation<BaseError>($"MediaItem {deleteMediaItem.MediaItemId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public record RefreshMediaItem(int MediaItemId) : MediatR.IRequest<Either<BaseError, Unit>>,
|
||||
IBackgroundServiceRequest;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public record RefreshMediaItemCollections : RefreshMediaItem
|
||||
{
|
||||
public RefreshMediaItemCollections(int mediaItemId) : base(mediaItemId)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public class
|
||||
RefreshMediaItemCollectionsHandler : MediatR.IRequestHandler<RefreshMediaItemCollections,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
private readonly ISmartCollectionBuilder _smartCollectionBuilder;
|
||||
|
||||
public RefreshMediaItemCollectionsHandler(
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
ISmartCollectionBuilder smartCollectionBuilder)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_smartCollectionBuilder = smartCollectionBuilder;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
RefreshMediaItemCollections request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(RefreshCollections)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemCollections request) =>
|
||||
MediaItemMustExist(request);
|
||||
|
||||
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
|
||||
RefreshMediaItemCollections refreshMediaItemCollections) =>
|
||||
_mediaItemRepository.Get(refreshMediaItemCollections.MediaItemId)
|
||||
.Map(
|
||||
maybeItem => maybeItem.ToValidation<BaseError>(
|
||||
$"[MediaItem] {refreshMediaItemCollections.MediaItemId} does not exist."));
|
||||
|
||||
private Task<Unit> RefreshCollections(MediaItem mediaItem) =>
|
||||
_smartCollectionBuilder.RefreshSmartCollections(mediaItem).ToUnit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public record RefreshMediaItemMetadata : RefreshMediaItem
|
||||
{
|
||||
public RefreshMediaItemMetadata(int mediaItemId) : base(mediaItemId)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public class
|
||||
RefreshMediaItemMetadataHandler : MediatR.IRequestHandler<RefreshMediaItemMetadata, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ILocalMetadataProvider _localMetadataProvider;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public RefreshMediaItemMetadataHandler(
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
ILocalMetadataProvider localMetadataProvider)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_localMetadataProvider = localMetadataProvider;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
RefreshMediaItemMetadata request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(RefreshMetadata)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemMetadata request) =>
|
||||
MediaItemMustExist(request).BindT(PathMustExist);
|
||||
|
||||
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
|
||||
RefreshMediaItemMetadata refreshMediaItemMetadata) =>
|
||||
_mediaItemRepository.Get(refreshMediaItemMetadata.MediaItemId)
|
||||
.Map(
|
||||
maybeItem => maybeItem.ToValidation<BaseError>(
|
||||
$"[MediaItem] {refreshMediaItemMetadata.MediaItemId} does not exist."));
|
||||
|
||||
private Validation<BaseError, MediaItem> PathMustExist(MediaItem mediaItem) =>
|
||||
Some(mediaItem)
|
||||
.Filter(item => File.Exists(item.Path))
|
||||
.ToValidation<BaseError>($"[Path] '{mediaItem.Path}' does not exist on the file system");
|
||||
|
||||
private Task<Unit> RefreshMetadata(MediaItem mediaItem) =>
|
||||
_localMetadataProvider.RefreshMetadata(mediaItem).ToUnit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public record RefreshMediaItemStatistics : RefreshMediaItem
|
||||
{
|
||||
public RefreshMediaItemStatistics(int mediaItemId) : base(mediaItemId)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public class
|
||||
RefreshMediaItemStatisticsHandler : MediatR.IRequestHandler<RefreshMediaItemStatistics, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public RefreshMediaItemStatisticsHandler(
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ILocalStatisticsProvider localStatisticsProvider)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_configElementRepository = configElementRepository;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
RefreshMediaItemStatistics request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(RefreshStatistics)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Validation<BaseError, RefreshParameters>> Validate(RefreshMediaItemStatistics request) =>
|
||||
(await MediaItemMustExist(request).BindT(PathMustExist), await ValidateFFprobePath())
|
||||
.Apply((mediaItem, ffprobePath) => new RefreshParameters(mediaItem, ffprobePath));
|
||||
|
||||
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
|
||||
RefreshMediaItemStatistics refreshMediaItemStatistics) =>
|
||||
_mediaItemRepository.Get(refreshMediaItemStatistics.MediaItemId)
|
||||
.Map(
|
||||
maybeItem => maybeItem.ToValidation<BaseError>(
|
||||
$"[MediaItem] {refreshMediaItemStatistics.MediaItemId} does not exist."));
|
||||
|
||||
private Validation<BaseError, MediaItem> PathMustExist(MediaItem mediaItem) =>
|
||||
Some(mediaItem)
|
||||
.Filter(item => File.Exists(item.Path))
|
||||
.ToValidation<BaseError>($"[Path] '{mediaItem.Path}' does not exist on the file system");
|
||||
|
||||
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 Task<Unit> RefreshStatistics(RefreshParameters parameters) =>
|
||||
_localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem).ToUnit();
|
||||
|
||||
private record RefreshParameters(MediaItem MediaItem, string FFprobePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) =>
|
||||
new(
|
||||
mediaItem.Id,
|
||||
mediaItem.MediaSourceId,
|
||||
mediaItem.Path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
public record MediaItemViewModel(int Id, int MediaSourceId, string Path);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record GetAggregateMediaItems
|
||||
(MediaType MediaType, string SearchString) : IRequest<List<AggregateMediaItemViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class
|
||||
GetAggregateMediaItemsHandler : IRequestHandler<GetAggregateMediaItems, List<AggregateMediaItemViewModel>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetAggregateMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task<List<AggregateMediaItemViewModel>> Handle(
|
||||
GetAggregateMediaItems request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<MediaItem> allItems = await _mediaItemRepository.GetAll(request.MediaType);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.SearchString))
|
||||
{
|
||||
allItems = allItems.Filter(i => i.Metadata?.Title.Contains(request.SearchString) == true);
|
||||
}
|
||||
|
||||
return allItems.GroupBy(c => new { c.Source.Name, c.Metadata.Title }).Map(
|
||||
group => new AggregateMediaItemViewModel(
|
||||
group.Key.Name,
|
||||
group.Key.Title,
|
||||
group.Count(),
|
||||
group.Count() == 1 ? DisplayDuration(group.Head()) : string.Empty))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string DisplayDuration(MediaItem mediaItem) => string.Format(
|
||||
mediaItem.Metadata?.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||
mediaItem.Metadata?.Duration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record GetAllMediaItems : IRequest<List<MediaItemViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class GetAllMediaItemsHandler : IRequestHandler<GetAllMediaItems, List<MediaItemViewModel>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task<List<MediaItemViewModel>> Handle(
|
||||
GetAllMediaItems request,
|
||||
CancellationToken cancellationToken) =>
|
||||
(await _mediaItemRepository.GetAll()).Map(ProjectToViewModel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record GetMediaItemById(int Id) : IRequest<Option<MediaItemViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class GetMediaItemByIdHandler : IRequestHandler<GetMediaItemById, Option<MediaItemViewModel>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetMediaItemByIdHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public Task<Option<MediaItemViewModel>> Handle(
|
||||
GetMediaItemById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaItemRepository.Get(request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user