Initial commit

This commit is contained in:
Jason Dove
2021-02-08 21:13:53 -06:00
commit 4d52e115b5
493 changed files with 31399 additions and 0 deletions
@@ -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);
}
}
+13
View File
@@ -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);
}
}