diff --git a/ErsatzTV.Application/Libraries/LocalLibraryPathViewModel.cs b/ErsatzTV.Application/Libraries/LocalLibraryPathViewModel.cs new file mode 100644 index 000000000..4f67de281 --- /dev/null +++ b/ErsatzTV.Application/Libraries/LocalLibraryPathViewModel.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Application.Libraries +{ + public record LocalLibraryPathViewModel(int Id, string Path); +} diff --git a/ErsatzTV.Application/Libraries/LocalLibraryViewModel.cs b/ErsatzTV.Application/Libraries/LocalLibraryViewModel.cs new file mode 100644 index 000000000..2cf7b205d --- /dev/null +++ b/ErsatzTV.Application/Libraries/LocalLibraryViewModel.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Application.Libraries +{ + public record LocalLibraryViewModel(int Id, string Name, string MediaKind); +} diff --git a/ErsatzTV.Application/Libraries/Mapper.cs b/ErsatzTV.Application/Libraries/Mapper.cs new file mode 100644 index 000000000..c868045d6 --- /dev/null +++ b/ErsatzTV.Application/Libraries/Mapper.cs @@ -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.ToString()); + + public static LocalLibraryPathViewModel ProjectToViewModel(LibraryPath libraryPath) => + new(libraryPath.Id, libraryPath.Path); + } +} diff --git a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraries.cs b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraries.cs new file mode 100644 index 000000000..6abe74f5c --- /dev/null +++ b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraries.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using MediatR; + +namespace ErsatzTV.Application.Libraries.Queries +{ + public record GetLocalLibraries : IRequest>; +} diff --git a/ErsatzTV.Application/Libraries/Queries/GetLocalLibrariesHandler.cs b/ErsatzTV.Application/Libraries/Queries/GetLocalLibrariesHandler.cs new file mode 100644 index 000000000..0ea7d2752 --- /dev/null +++ b/ErsatzTV.Application/Libraries/Queries/GetLocalLibrariesHandler.cs @@ -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> + { + private readonly ILibraryRepository _libraryRepository; + + public GetLocalLibrariesHandler(ILibraryRepository libraryRepository) => _libraryRepository = libraryRepository; + + public Task> + Handle(GetLocalLibraries request, CancellationToken cancellationToken) => + _libraryRepository.GetAllLocal().Map(list => list.Map(ProjectToViewModel).ToList()); + } +} diff --git a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPaths.cs b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPaths.cs new file mode 100644 index 000000000..1bf34612b --- /dev/null +++ b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPaths.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using MediatR; + +namespace ErsatzTV.Application.Libraries.Queries +{ + public record GetLocalLibraryPaths(int LocalLibraryId) : IRequest>; +} diff --git a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPathsHandler.cs b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPathsHandler.cs new file mode 100644 index 000000000..517985a7f --- /dev/null +++ b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPathsHandler.cs @@ -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> + { + private readonly ILibraryRepository _libraryRepository; + + public GetLocalLibraryPathsHandler(ILibraryRepository libraryRepository) => + _libraryRepository = libraryRepository; + + public Task> Handle( + GetLocalLibraryPaths request, + CancellationToken cancellationToken) => + _libraryRepository.GetLocalPaths(request.LocalLibraryId) + .Map(list => list.Map(ProjectToViewModel).ToList()); + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs index f7355bdcc..2bb747502 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs @@ -46,7 +46,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands var result2 = Optional(createCollection.Name) .Filter(name => !allNames.Contains(name)) - .ToValidation("Media collection name must be unique"); + .ToValidation("Collection name must be unique"); return (result1, result2).Apply((_, _) => createCollection.Name); } diff --git a/ErsatzTV.CommandLine/Commands/Schedules/ScheduleAddItemCommand.cs b/ErsatzTV.CommandLine/Commands/Schedules/ScheduleAddItemCommand.cs index 1a76f9018..9e1c07041 100644 --- a/ErsatzTV.CommandLine/Commands/Schedules/ScheduleAddItemCommand.cs +++ b/ErsatzTV.CommandLine/Commands/Schedules/ScheduleAddItemCommand.cs @@ -97,7 +97,7 @@ namespace ErsatzTV.CommandLine.Commands.Schedules () => { _logger.LogError( - "Unable to locate media collection {MediaCollection}", + "Unable to locate collection {Collection}", CollectionName); return Task.CompletedTask; }); diff --git a/ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs index 740c2e382..03440d5a1 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using ErsatzTV.Core.Domain; using LanguageExt; @@ -7,6 +8,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories public interface ILibraryRepository { Task> Get(int libraryId); + Task> GetAllLocal(); Task UpdateLastScan(Library library); + Task> GetLocalPaths(int libraryId); } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs index 384c49b02..53650e5fa 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs @@ -1,4 +1,5 @@ -using System.Data; +using System.Collections.Generic; +using System.Data; using System.Linq; using System.Threading.Tasks; using Dapper; @@ -31,8 +32,25 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Map(Optional); } + public Task> GetAllLocal() + { + using TvContext context = _dbContextFactory.CreateDbContext(); + return context.LocalLibraries.ToListAsync(); + } + public Task UpdateLastScan(Library library) => _dbConnection.ExecuteAsync( "UPDATE Library SET LastScan = @LastScan WHERE Id = @Id", new { library.LastScan, library.Id }).ToUnit(); + + public Task> GetLocalPaths(int libraryId) + { + using TvContext context = _dbContextFactory.CreateDbContext(); + return context.LocalLibraries + .Include(l => l.Paths) + .OrderBy(l => l.Id) + .SingleOrDefaultAsync(l => l.Id == libraryId) + .Map(Optional) + .Match(l => l.Paths, () => new List()); + } } } diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index ae85e1050..d0b7e369a 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -18,6 +18,7 @@ namespace ErsatzTV.Infrastructure.Data public DbSet LocalMediaSources { get; set; } public DbSet PlexMediaSources { get; set; } public DbSet Libraries { get; set; } + public DbSet LocalLibraries { get; set; } public DbSet PlexLibraries { get; set; } public DbSet MediaItems { get; set; } public DbSet Movies { get; set; } diff --git a/ErsatzTV/Pages/MediaCollectionEditor.razor b/ErsatzTV/Pages/CollectionEditor.razor similarity index 88% rename from ErsatzTV/Pages/MediaCollectionEditor.razor rename to ErsatzTV/Pages/CollectionEditor.razor index fa08b7227..533d7b5e8 100644 --- a/ErsatzTV/Pages/MediaCollectionEditor.razor +++ b/ErsatzTV/Pages/CollectionEditor.razor @@ -4,12 +4,12 @@ @using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Queries @inject NavigationManager NavigationManager -@inject ILogger Logger +@inject ILogger Logger @inject ISnackbar Snackbar @inject IMediator Mediator
- @(IsEdit ? "Edit Media Collection" : "Add Media Collection") + @(IsEdit ? "Edit Collection" : "Add Collection") @@ -19,7 +19,7 @@ - @(IsEdit ? "Save Changes" : "Add Media Collection") + @(IsEdit ? "Save Changes" : "Add Collection") @@ -73,7 +73,7 @@ error => { Snackbar.Add(error.Value, Severity.Error); - Logger.LogError("Error saving simple media collection: {Error}", error.Value); + Logger.LogError("Error saving collection: {Error}", error.Value); }, () => NavigationManager.NavigateTo(_model.Id > 0 ? $"/media/collections/{_model.Id}" : "/media/collections")); } diff --git a/ErsatzTV/Pages/MediaCollectionItems.razor b/ErsatzTV/Pages/CollectionItems.razor similarity index 99% rename from ErsatzTV/Pages/MediaCollectionItems.razor rename to ErsatzTV/Pages/CollectionItems.razor index c4a3e3c71..aefe7aae1 100644 --- a/ErsatzTV/Pages/MediaCollectionItems.razor +++ b/ErsatzTV/Pages/CollectionItems.razor @@ -4,7 +4,7 @@ @using ErsatzTV.Application.MediaCollections.Commands @inject NavigationManager NavigationManager @inject IMediator Mediator -@inject ILogger Logger +@inject ILogger Logger @inject ISnackbar Snackbar @inject IDialogService Dialog diff --git a/ErsatzTV/Pages/MediaCollections.razor b/ErsatzTV/Pages/Collections.razor similarity index 92% rename from ErsatzTV/Pages/MediaCollections.razor rename to ErsatzTV/Pages/Collections.razor index 6510f815d..96dfb3d28 100644 --- a/ErsatzTV/Pages/MediaCollections.razor +++ b/ErsatzTV/Pages/Collections.razor @@ -19,7 +19,7 @@ - Add Media Collection + Add Collection @@ -35,10 +35,10 @@ { if (vm is MediaCollectionViewModel collection) { - var parameters = new DialogParameters { { "EntityType", "media collection" }, { "EntityName", collection.Name } }; + var parameters = new DialogParameters { { "EntityType", "collection" }, { "EntityName", collection.Name } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; - IDialogReference dialog = Dialog.Show("Delete Media Collection", parameters, options); + IDialogReference dialog = Dialog.Show("Delete Collection", parameters, options); DialogResult result = await dialog.Result; if (!result.Cancelled) { diff --git a/ErsatzTV/Pages/Index.razor b/ErsatzTV/Pages/Index.razor index f1c041ec7..aaf05ba3a 100644 --- a/ErsatzTV/Pages/Index.razor +++ b/ErsatzTV/Pages/Index.razor @@ -28,9 +28,9 @@ - Media Collections + Collections - Media collections have a name and contain a logical grouping of media items. + Collections have a name and contain a logical grouping of media items. Collections may contain television shows, television seasons, television episodes or movies. Collections containing television shows and television seasons are automatically updated as media is added or removed from the linked shows and seasons. @@ -47,7 +47,7 @@
  • Chronological - to play collection items sorted by air date and then by season and episode number (for when multiple episodes aired on a single day).
  • - Schedule items have a start type, a start time, a media collection and a playout mode. + Schedule items have a start type, a start time, a collection and a playout mode. A fixed start type requires a start time, while a dynamic start type means the schedule item will start immediately after the preceding schedule item. diff --git a/ErsatzTV/Pages/Libraries.razor b/ErsatzTV/Pages/Libraries.razor new file mode 100644 index 000000000..07ed87a0e --- /dev/null +++ b/ErsatzTV/Pages/Libraries.razor @@ -0,0 +1,82 @@ +@page "/media/libraries" +@using ErsatzTV.Application.Libraries +@using ErsatzTV.Application.Libraries.Queries +@implements IDisposable +@inject IMediator Mediator +@inject IEntityLocker Locker + + + + + Libraries + + + + + + + + + Library Kind + Name + Media Kind + + + + Local + @context.Name + @context.MediaKind + +
    + @if (Locker.IsLibraryLocked(context.Id)) + { +
    + +
    + } + else + { + + + + + } + @if (context is LocalLibraryViewModel) + { + + + + + } +
    +
    +
    +
    +
    + +@code { + private IList _libraries; + + protected override void OnInitialized() => + Locker.OnLibraryChanged += LockChanged; + + protected override async Task OnParametersSetAsync() => await LoadLibraries(); + + private async Task LoadLibraries() => + _libraries = await Mediator.Send(new GetLocalLibraries()); + + private Task ScanLibrary(LocalLibraryViewModel library) + { + return Task.CompletedTask; + } + + private void LockChanged(object sender, EventArgs e) => + InvokeAsync(StateHasChanged); + + void IDisposable.Dispose() => Locker.OnLibraryChanged -= LockChanged; + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/LocalLibraryEditor.razor b/ErsatzTV/Pages/LocalLibraryEditor.razor new file mode 100644 index 000000000..2fe2afd4b --- /dev/null +++ b/ErsatzTV/Pages/LocalLibraryEditor.razor @@ -0,0 +1,48 @@ +@page "/media/libraries/local/{Id:int}" +@using ErsatzTV.Application.Libraries +@using ErsatzTV.Application.Libraries.Queries +@inject IMediator Mediator + + + + + Library Paths + + + + + + + Path + + + + @context.Path + +
    + + + + +
    +
    +
    +
    +
    + +@code { + + [Parameter] + public int Id { get; set; } + + private IList _libraryPaths; + + protected override async Task OnParametersSetAsync() => await LoadLibraryPaths(); + + private async Task LoadLibraryPaths() => + _libraryPaths = await Mediator.Send(new GetLocalLibraryPaths(Id)); + + private Task DeleteLibraryPath(LocalLibraryPathViewModel libraryPath) => Task.CompletedTask; + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/LocalMediaSourceEditor.razor b/ErsatzTV/Pages/LocalMediaSourceEditor.razor index bf61d6bda..b1d62e739 100644 --- a/ErsatzTV/Pages/LocalMediaSourceEditor.razor +++ b/ErsatzTV/Pages/LocalMediaSourceEditor.razor @@ -80,7 +80,7 @@ @* Left: error => *@ @* { *@ @* Snackbar.Add(error.Value, Severity.Error); *@ -@* Logger.LogError("Unexpected error saving simple media collection: {Error}", error.Value); *@ +@* Logger.LogError("Unexpected error saving collection: {Error}", error.Value); *@ @* return Task.CompletedTask; *@ @* }, *@ @* Right: async vm => *@ diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index 914063230..b5af3bcff 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -75,7 +75,7 @@ var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ProgramSchedule.Name} on {playout.Channel.Number} - {playout.Channel.Name}" } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; - IDialogReference dialog = Dialog.Show("Delete Media Collection", parameters, options); + IDialogReference dialog = Dialog.Show("Delete Playout", parameters, options); DialogResult result = await dialog.Result; if (!result.Cancelled) { diff --git a/ErsatzTV/Pages/ScheduleEditor.razor b/ErsatzTV/Pages/ScheduleEditor.razor index 98e148bb7..a20d8e8d3 100644 --- a/ErsatzTV/Pages/ScheduleEditor.razor +++ b/ErsatzTV/Pages/ScheduleEditor.razor @@ -15,7 +15,7 @@ - + @foreach (PlaybackOrder playbackOrder in Enum.GetValues()) { @playbackOrder diff --git a/ErsatzTV/Pages/ScheduleItemsEditor.razor b/ErsatzTV/Pages/ScheduleItemsEditor.razor index 6bb7cd389..1488f26e5 100644 --- a/ErsatzTV/Pages/ScheduleItemsEditor.razor +++ b/ErsatzTV/Pages/ScheduleItemsEditor.razor @@ -25,7 +25,7 @@ Start Time - Media Collection + Collection Playout Mode @@ -104,7 +104,7 @@ { diff --git a/ErsatzTV/Pages/Schedules.razor b/ErsatzTV/Pages/Schedules.razor index 803985768..157b55e81 100644 --- a/ErsatzTV/Pages/Schedules.razor +++ b/ErsatzTV/Pages/Schedules.razor @@ -19,7 +19,7 @@ Id Name - Media Collection Playback Order + Collection Playback Order @@ -54,15 +54,15 @@ Start Time - Media Collection + Collection Playout Mode @(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic") - @context.Name - @context.PlayoutMode + @context.Name + @context.PlayoutMode diff --git a/ErsatzTV/Shared/LocalMediaSources.razor b/ErsatzTV/Shared/LocalMediaSources.razor index ed6d2979f..b8e4dd2e8 100644 --- a/ErsatzTV/Shared/LocalMediaSources.razor +++ b/ErsatzTV/Shared/LocalMediaSources.razor @@ -1,6 +1,7 @@ @using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.MediaSources.Commands @using ErsatzTV.Application.MediaSources.Queries +@using ErsatzTV.Application.Libraries @implements IDisposable @inject IDialogService Dialog @inject IMediator Mediator diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index e0d55ba4a..c0ea170af 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -27,10 +27,10 @@ Channels FFmpeg - Media Sources + Libraries TV Shows Movies - Media Collections + Collections Schedules Playouts