mirror of https://github.com/ErsatzTV/ErsatzTV.git
19 changed files with 263 additions and 243 deletions
@ -0,0 +1,9 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
||||
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Application.Libraries |
||||
{ |
||||
public record LocalLibraryPathViewModel(int Id, string Path); |
||||
public record LocalLibraryPathViewModel(int Id, int LibraryId, string Path); |
||||
} |
||||
|
||||
@ -1,4 +1,6 @@
@@ -1,4 +1,6 @@
|
||||
namespace ErsatzTV.Application.Libraries |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.Libraries |
||||
{ |
||||
public record LocalLibraryViewModel(int Id, string Name, string MediaKind); |
||||
public record LocalLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind); |
||||
} |
||||
|
||||
@ -0,0 +1,7 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -1,12 +0,0 @@
@@ -1,12 +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,80 +0,0 @@
@@ -1,80 +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);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
@page "/media/libraries/local/{Id:int}/add" |
||||
@using ErsatzTV.Application.Libraries |
||||
@using ErsatzTV.Application.Libraries.Commands |
||||
@using ErsatzTV.Application.Libraries.Queries |
||||
@using ErsatzTV.Application.MediaSources.Commands |
||||
@inject NavigationManager NavigationManager |
||||
@inject ILogger<LocalLibraryPathEditor> Logger |
||||
@inject ISnackbar Snackbar |
||||
@inject IMediator Mediator |
||||
@inject IEntityLocker Locker |
||||
@inject ChannelWriter<IBackgroundServiceRequest> Channel |
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> |
||||
<MudText Typo="Typo.h4" Class="mb-4">@_library.Name - Add Local Library Path</MudText> |
||||
<div style="max-width: 400px;"> |
||||
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> |
||||
<FluentValidator/> |
||||
<MudCard> |
||||
<MudCardContent> |
||||
<MudTextField T="string" Label="Media Kind" Disabled="true" Value="@(Enum.GetName(typeof(LibraryMediaKind), _library.MediaKind))"/> |
||||
@* TODO: replace this with a folder picker *@ |
||||
<MudTextField Label="Path" @bind-Value="_model.Path" For="@(() => _model.Path)"/> |
||||
</MudCardContent> |
||||
<MudCardActions> |
||||
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto"> |
||||
Add Local Library Path |
||||
</MudButton> |
||||
</MudCardActions> |
||||
</MudCard> |
||||
</EditForm> |
||||
</div> |
||||
</MudContainer> |
||||
|
||||
@code { |
||||
|
||||
[Parameter] |
||||
public int Id { get; set; } |
||||
|
||||
private readonly LocalLibraryPathEditViewModel _model = new(); |
||||
private EditContext _editContext; |
||||
private ValidationMessageStore _messageStore; |
||||
|
||||
private LocalLibraryViewModel _library; |
||||
|
||||
protected override async Task OnParametersSetAsync() |
||||
{ |
||||
Option<LocalLibraryViewModel> maybeLibrary = await Mediator.Send(new GetLocalLibraryById(Id)); |
||||
maybeLibrary.Match( |
||||
library => _library = library, |
||||
() => NavigationManager.NavigateTo("404")); |
||||
} |
||||
|
||||
protected override void OnInitialized() |
||||
{ |
||||
_editContext = new EditContext(_model); |
||||
_messageStore = new ValidationMessageStore(_editContext); |
||||
} |
||||
|
||||
private async Task HandleSubmitAsync() |
||||
{ |
||||
_messageStore.Clear(); |
||||
if (_editContext.Validate()) |
||||
{ |
||||
var command = new CreateLocalLibraryPath(_library.Id, _model.Path); |
||||
Either<BaseError, LocalLibraryPathViewModel> result = await Mediator.Send(command); |
||||
await result.Match( |
||||
Left: error => |
||||
{ |
||||
Snackbar.Add(error.Value, Severity.Error); |
||||
Logger.LogError("Unexpected error saving local library path: {Error}", error.Value); |
||||
return Task.CompletedTask; |
||||
}, |
||||
Right: async vm => |
||||
{ |
||||
if (Locker.LockLibrary(_library.Id)) |
||||
{ |
||||
await Channel.WriteAsync(new ForceScanLocalLibrary(_library.Id)); |
||||
NavigationManager.NavigateTo("/media/libraries"); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
} |
||||
@ -1,99 +0,0 @@
@@ -1,99 +0,0 @@
|
||||
@* @page "/media/sources/local/add" *@ |
||||
@* @using ErsatzTV.Application.MediaSources.Commands *@ |
||||
@* @using ErsatzTV.Application.MediaSources *@ |
||||
@* @inject NavigationManager NavigationManager *@ |
||||
@* @inject ILogger<LocalMediaSourceEditor> Logger *@ |
||||
@* @inject ISnackbar Snackbar *@ |
||||
@* @inject IMediator Mediator *@ |
||||
@* @inject IEntityLocker Locker *@ |
||||
@* @inject ChannelWriter<IBackgroundServiceRequest> Channel *@ |
||||
@* *@ |
||||
@* <div style="max-width: 400px;"> *@ |
||||
@* <MudText Typo="Typo.h4" Class="mb-4">Add Local Media Source</MudText> *@ |
||||
@* *@ |
||||
@* <EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> *@ |
||||
@* <FluentValidator/> *@ |
||||
@* <MudCard> *@ |
||||
@* <MudCardContent> *@ |
||||
@* <MudSelect Class="mt-3" Label="Media Type" @bind-Value="_model.MediaType" For="@(() => _model.MediaType)"> *@ |
||||
@* @foreach (MediaType mediaType in new[] { MediaType.TvShow, MediaType.Movie }) *@ |
||||
@* { *@ |
||||
@* <MudSelectItem Value="@mediaType">@mediaType</MudSelectItem> *@ |
||||
@* } *@ |
||||
@* </MudSelect> *@ |
||||
@* $1$ TODO: replace this with a folder picker #1# *@ |
||||
@* <MudTextField Label="Folder" @bind-Value="_model.Folder" For="@(() => _model.Folder)"/> *@ |
||||
@* </MudCardContent> *@ |
||||
@* <MudCardActions> *@ |
||||
@* <MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto"> *@ |
||||
@* Add Local Media Source *@ |
||||
@* </MudButton> *@ |
||||
@* </MudCardActions> *@ |
||||
@* </MudCard> *@ |
||||
@* </EditForm> *@ |
||||
@* </div> *@ |
||||
@* *@ |
||||
@* @code { *@ |
||||
@* *@ |
||||
@* [Parameter] *@ |
||||
@* public int Id { get; set; } *@ |
||||
@* *@ |
||||
@* private readonly LocalMediaSourceEditViewModel _model = new(); *@ |
||||
@* private EditContext _editContext; *@ |
||||
@* private ValidationMessageStore _messageStore; *@ |
||||
@* *@ |
||||
@* private LocalMediaSource _mediaSource; *@ |
||||
@* *@ |
||||
@* protected override Task OnParametersSetAsync() *@ |
||||
@* { *@ |
||||
@* _mediaSource = new LocalMediaSource *@ |
||||
@* { *@ |
||||
@* MediaType = MediaType.TvShow *@ |
||||
@* }; *@ |
||||
@* *@ |
||||
@* _model.MediaType = _mediaSource.MediaType; *@ |
||||
@* _model.Folder = _mediaSource.Folder; *@ |
||||
@* *@ |
||||
@* return Task.CompletedTask; *@ |
||||
@* } *@ |
||||
@* *@ |
||||
@* protected override void OnInitialized() *@ |
||||
@* { *@ |
||||
@* _editContext = new EditContext(_model); *@ |
||||
@* _messageStore = new ValidationMessageStore(_editContext); *@ |
||||
@* } *@ |
||||
@* *@ |
||||
@* private async Task HandleSubmitAsync() *@ |
||||
@* { *@ |
||||
@* _messageStore.Clear(); *@ |
||||
@* if (_editContext.Validate()) *@ |
||||
@* { *@ |
||||
@* var command = new CreateLocalMediaSource( *@ |
||||
@* Convert.ToBase64String(Guid.NewGuid().ToByteArray()) *@ |
||||
@* .TrimEnd('=') *@ |
||||
@* .Replace("/", "_") *@ |
||||
@* .Replace("+", "-"), *@ |
||||
@* _model.MediaType, *@ |
||||
@* _model.Folder); *@ |
||||
@* Either<BaseError, MediaSourceViewModel> result = await Mediator.Send(command); *@ |
||||
@* await result.Match( *@ |
||||
@* Left: error => *@ |
||||
@* { *@ |
||||
@* Snackbar.Add(error.Value, Severity.Error); *@ |
||||
@* Logger.LogError("Unexpected error saving collection: {Error}", error.Value); *@ |
||||
@* return Task.CompletedTask; *@ |
||||
@* }, *@ |
||||
@* Right: async vm => *@ |
||||
@* { *@ |
||||
@* if (Locker.LockLibrary(vm.Id)) *@ |
||||
@* { *@ |
||||
@* await Channel.WriteAsync(new ForceScanLocalLibrary(vm.Id)); *@ |
||||
@* NavigationManager.NavigateTo("/media/sources"); *@ |
||||
@* } *@ |
||||
@* }); *@ |
||||
@* } *@ |
||||
@* } *@ |
||||
@* *@ |
||||
@* *@ |
||||
@* *@ |
||||
@* } *@ |
||||
@ -1,25 +1,25 @@
@@ -1,25 +1,25 @@
|
||||
@page "/media/sources" |
||||
@page "/media/sources/{PanelIndex:int}" |
||||
@inject NavigationManager NavigationManager |
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> |
||||
<MudTabs @ref="_tabs" Elevation="1"> |
||||
<MudTabPanel Text="Local" OnClick="@(() => NavigationManager.NavigateTo("/media/sources/0"))"> |
||||
<LocalMediaSources></LocalMediaSources> |
||||
</MudTabPanel> |
||||
<MudTabPanel Text="Plex" OnClick="@(() => NavigationManager.NavigateTo("/media/sources/1"))"> |
||||
<PlexMediaSources></PlexMediaSources> |
||||
</MudTabPanel> |
||||
</MudTabs> |
||||
</MudContainer> |
||||
|
||||
@code { |
||||
|
||||
[Parameter] |
||||
public int PanelIndex { get; set; } |
||||
|
||||
private MudTabs _tabs; |
||||
|
||||
protected override void OnAfterRender(bool firstRender) => _tabs.ActivatePanel(PanelIndex); |
||||
|
||||
} |
||||
@* @page "/media/sources" *@ |
||||
@* @page "/media/sources/{PanelIndex:int}" *@ |
||||
@* @inject NavigationManager NavigationManager *@ |
||||
@* *@ |
||||
@* <MudContainer MaxWidth="MaxWidth.ExtraLarge"> *@ |
||||
@* <MudTabs @ref="_tabs" Elevation="1"> *@ |
||||
@* <MudTabPanel Text="Local" OnClick="@(() => NavigationManager.NavigateTo("/media/sources/0"))"> *@ |
||||
@* <LocalMediaSources></LocalMediaSources> *@ |
||||
@* </MudTabPanel> *@ |
||||
@* <MudTabPanel Text="Plex" OnClick="@(() => NavigationManager.NavigateTo("/media/sources/1"))"> *@ |
||||
@* <PlexMediaSources></PlexMediaSources> *@ |
||||
@* </MudTabPanel> *@ |
||||
@* </MudTabs> *@ |
||||
@* </MudContainer> *@ |
||||
@* *@ |
||||
@* @code { *@ |
||||
@* *@ |
||||
@* [Parameter] *@ |
||||
@* public int PanelIndex { get; set; } *@ |
||||
@* *@ |
||||
@* private MudTabs _tabs; *@ |
||||
@* *@ |
||||
@* protected override void OnAfterRender(bool firstRender) => _tabs.ActivatePanel(PanelIndex); *@ |
||||
@* *@ |
||||
@* } *@ |
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using System.IO; |
||||
using ErsatzTV.ViewModels; |
||||
using FluentValidation; |
||||
|
||||
namespace ErsatzTV.Validators |
||||
{ |
||||
public class LocalLibraryPathEditViewModelValidator : AbstractValidator<LocalLibraryPathEditViewModel> |
||||
{ |
||||
public LocalLibraryPathEditViewModelValidator() |
||||
{ |
||||
RuleFor(x => x.Path).NotEmpty(); |
||||
RuleFor(x => x.Path).Must(Directory.Exists).WithMessage("Path must exist on filesystem"); |
||||
} |
||||
} |
||||
} |
||||
@ -1,15 +0,0 @@
@@ -1,15 +0,0 @@
|
||||
using System.IO; |
||||
using ErsatzTV.ViewModels; |
||||
using FluentValidation; |
||||
|
||||
namespace ErsatzTV.Validators |
||||
{ |
||||
public class LocalMediaSourceEditViewModelValidator : AbstractValidator<LocalMediaSourceEditViewModel> |
||||
{ |
||||
public LocalMediaSourceEditViewModelValidator() |
||||
{ |
||||
RuleFor(x => x.Folder).NotEmpty(); |
||||
RuleFor(x => x.Folder).Must(Directory.Exists).WithMessage("Folder must exist on filesystem"); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.ViewModels |
||||
{ |
||||
public class LocalLibraryPathEditViewModel |
||||
{ |
||||
public string Path { get; set; } |
||||
} |
||||
} |
||||
Loading…
Reference in new issue