mirror of https://github.com/ErsatzTV/ErsatzTV.git
16 changed files with 51 additions and 244 deletions
@ -1,76 +0,0 @@
@@ -1,76 +0,0 @@
|
||||
using System.Threading.Channels; |
||||
using ErsatzTV.Application.MediaSources; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Locking; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using static ErsatzTV.Application.Libraries.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Libraries; |
||||
|
||||
public class CreateLocalLibraryHandler : LocalLibraryHandlerBase, |
||||
IRequestHandler<CreateLocalLibrary, Either<BaseError, LocalLibraryViewModel>> |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
private readonly IEntityLocker _entityLocker; |
||||
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel; |
||||
|
||||
public CreateLocalLibraryHandler( |
||||
ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel, |
||||
IEntityLocker entityLocker, |
||||
IDbContextFactory<TvContext> dbContextFactory) |
||||
{ |
||||
_scannerWorkerChannel = scannerWorkerChannel; |
||||
_entityLocker = entityLocker; |
||||
_dbContextFactory = dbContextFactory; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, LocalLibraryViewModel>> Handle( |
||||
CreateLocalLibrary request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, LocalLibrary> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary)); |
||||
} |
||||
|
||||
private async Task<LocalLibraryViewModel> PersistLocalLibrary( |
||||
TvContext dbContext, |
||||
LocalLibrary localLibrary) |
||||
{ |
||||
await dbContext.LocalLibraries.AddAsync(localLibrary); |
||||
await dbContext.SaveChangesAsync(); |
||||
|
||||
if (_entityLocker.LockLibrary(localLibrary.Id)) |
||||
{ |
||||
await _scannerWorkerChannel.WriteAsync(new ForceScanLocalLibrary(localLibrary.Id)); |
||||
} |
||||
|
||||
return ProjectToViewModel(localLibrary); |
||||
} |
||||
|
||||
private static Task<Validation<BaseError, LocalLibrary>> Validate( |
||||
TvContext dbContext, |
||||
CreateLocalLibrary request) => |
||||
MediaSourceMustExist(dbContext, request) |
||||
.BindT(localLibrary => NameMustBeValid(request, localLibrary)) |
||||
.BindT(localLibrary => PathsMustBeValid(dbContext, localLibrary)); |
||||
|
||||
private static Task<Validation<BaseError, LocalLibrary>> MediaSourceMustExist( |
||||
TvContext dbContext, |
||||
CreateLocalLibrary request) => |
||||
dbContext.LocalMediaSources |
||||
.OrderBy(lms => lms.Id) |
||||
.FirstOrDefaultAsync() |
||||
.Map(Optional) |
||||
.MapT( |
||||
lms => new LocalLibrary |
||||
{ |
||||
Name = request.Name, |
||||
Paths = request.Paths.Map(p => new LibraryPath { Path = p }).ToList(), |
||||
MediaKind = request.MediaKind, |
||||
MediaSourceId = lms.Id |
||||
}) |
||||
.Map(o => o.ToValidation<BaseError>("LocalMediaSource does not exist.")); |
||||
} |
||||
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Libraries; |
||||
|
||||
public record CreateLocalLibraryPath(int LibraryId, string Path) |
||||
: IRequest<Either<BaseError, LocalLibraryPathViewModel>>; |
||||
@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using static ErsatzTV.Application.Libraries.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Libraries; |
||||
|
||||
public class CreateLocalLibraryPathHandler : IRequestHandler<CreateLocalLibraryPath, |
||||
Either<BaseError, LocalLibraryPathViewModel>> |
||||
{ |
||||
private readonly ILibraryRepository _libraryRepository; |
||||
|
||||
public CreateLocalLibraryPathHandler(ILibraryRepository libraryRepository) => |
||||
_libraryRepository = libraryRepository; |
||||
|
||||
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) |
||||
.Where(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,91 +0,0 @@
@@ -1,91 +0,0 @@
|
||||
@page "/media/libraries/local/{Id:int}/add" |
||||
@using ErsatzTV.Application.Libraries |
||||
@using ErsatzTV.Application.MediaSources |
||||
@implements IDisposable |
||||
@inject NavigationManager NavigationManager |
||||
@inject ILogger<LocalLibraryPathEditor> Logger |
||||
@inject ISnackbar Snackbar |
||||
@inject IMediator Mediator |
||||
@inject IEntityLocker Locker |
||||
@inject ChannelWriter<IScannerBackgroundServiceRequest> ScannerWorkerChannel |
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> |
||||
<MudText Typo="Typo.h4" Class="mb-4">@_library.Name - Add Local Library Path</MudText> |
||||
<div style="max-width: 400px;"> |
||||
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> |
||||
<FluentValidationValidator/> |
||||
<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 { |
||||
private readonly CancellationTokenSource _cts = new(); |
||||
|
||||
[Parameter] |
||||
public int Id { get; set; } |
||||
|
||||
private readonly LocalLibraryPathEditViewModel _model = new(); |
||||
private EditContext _editContext; |
||||
private ValidationMessageStore _messageStore; |
||||
|
||||
private LocalLibraryViewModel _library; |
||||
|
||||
public void Dispose() |
||||
{ |
||||
_cts.Cancel(); |
||||
_cts.Dispose(); |
||||
} |
||||
|
||||
protected override async Task OnParametersSetAsync() |
||||
{ |
||||
Option<LocalLibraryViewModel> maybeLibrary = await Mediator.Send(new GetLocalLibraryById(Id), _cts.Token); |
||||
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, _cts.Token); |
||||
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 _ => |
||||
{ |
||||
if (Locker.LockLibrary(_library.Id)) |
||||
{ |
||||
await ScannerWorkerChannel.WriteAsync(new ScanLocalLibraryIfNeeded(_library.Id), _cts.Token); |
||||
NavigationManager.NavigateTo("media/libraries"); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
|
||||
} |
||||
Loading…
Reference in new issue