From a6d5df3ca63f63fff75c74186ad635df54268b8f Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sun, 28 Apr 2024 08:59:14 -0500 Subject: [PATCH] more add to playlist buttons; fix playlist preview (#1693) --- .../Commands/AddEpisodeToPlaylist.cs | 5 ++ .../Commands/AddEpisodeToPlaylistHandler.cs | 59 +++++++++++++++++++ .../Commands/AddSeasonToPlaylist.cs | 5 ++ .../Commands/AddSeasonToPlaylistHandler.cs | 59 +++++++++++++++++++ .../Commands/AddShowToPlaylist.cs | 5 ++ .../Commands/AddShowToPlaylistHandler.cs | 59 +++++++++++++++++++ .../Fakes/FakeMediaCollectionRepository.cs | 6 ++ .../IMediaCollectionRepository.cs | 6 ++ ErsatzTV.Core/Scheduling/CollectionKey.cs | 30 ++++++++++ .../Scheduling/MediaItemsForCollection.cs | 36 +++++++++++ .../Repositories/MediaCollectionRepository.cs | 37 +++++++++++- ErsatzTV/Pages/EpisodeList.razor | 7 +++ ErsatzTV/Pages/ImageList.razor | 7 +++ ErsatzTV/Pages/MovieList.razor | 7 +++ ErsatzTV/Pages/MusicVideoList.razor | 7 +++ ErsatzTV/Pages/OtherVideoList.razor | 7 +++ ErsatzTV/Pages/SongList.razor | 7 +++ ErsatzTV/Pages/TelevisionEpisodeList.razor | 49 +++++++++++++++ ErsatzTV/Pages/TelevisionSeasonList.razor | 21 +++++++ ErsatzTV/Pages/TelevisionShowList.razor | 7 +++ 20 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylist.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylistHandler.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylist.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylistHandler.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylist.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylistHandler.cs diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylist.cs b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylist.cs new file mode 100644 index 000000000..b5127a804 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylist.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.MediaCollections; + +public record AddEpisodeToPlaylist(int PlaylistId, int EpisodeId) : IRequest>; diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylistHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylistHandler.cs new file mode 100644 index 000000000..2a93cbea6 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylistHandler.cs @@ -0,0 +1,59 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.MediaCollections; + +public class AddEpisodeToPlaylistHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + AddEpisodeToPlaylist request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await validation.Apply(parameters => ApplyAddEpisodeRequest(dbContext, parameters)); + } + + private static async Task ApplyAddEpisodeRequest(TvContext dbContext, Parameters parameters) + { + var playlistItem = new PlaylistItem + { + Index = parameters.Playlist.Items.Max(i => i.Index) + 1, + CollectionType = ProgramScheduleItemCollectionType.Episode, + MediaItemId = parameters.Episode.Id, + PlaybackOrder = PlaybackOrder.Shuffle, + IncludeInProgramGuide = true + }; + + parameters.Playlist.Items.Add(playlistItem); + await dbContext.SaveChangesAsync(); + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddEpisodeToPlaylist request) => + (await PlaylistMustExist(dbContext, request), await ValidateEpisode(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> PlaylistMustExist( + TvContext dbContext, + AddEpisodeToPlaylist request) => + dbContext.Playlists + .Include(c => c.Items) + .SelectOneAsync(c => c.Id, c => c.Id == request.PlaylistId) + .Map(o => o.ToValidation("Playlist does not exist.")); + + private static Task> ValidateEpisode( + TvContext dbContext, + AddEpisodeToPlaylist request) => + dbContext.Episodes + .SelectOneAsync(m => m.Id, e => e.Id == request.EpisodeId) + .Map(o => o.ToValidation("Episode does not exist")); + + private sealed record Parameters(Playlist Playlist, Episode Episode); +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylist.cs b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylist.cs new file mode 100644 index 000000000..9ea48b024 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylist.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.MediaCollections; + +public record AddSeasonToPlaylist(int PlaylistId, int SeasonId) : IRequest>; diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylistHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylistHandler.cs new file mode 100644 index 000000000..7b9b2af20 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylistHandler.cs @@ -0,0 +1,59 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.MediaCollections; + +public class AddSeasonToPlaylistHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + AddSeasonToPlaylist request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await validation.Apply(parameters => ApplyAddSeasonRequest(dbContext, parameters)); + } + + private static async Task ApplyAddSeasonRequest(TvContext dbContext, Parameters parameters) + { + var playlistItem = new PlaylistItem + { + Index = parameters.Playlist.Items.Max(i => i.Index) + 1, + CollectionType = ProgramScheduleItemCollectionType.TelevisionSeason, + MediaItemId = parameters.Season.Id, + PlaybackOrder = PlaybackOrder.Shuffle, + IncludeInProgramGuide = true + }; + + parameters.Playlist.Items.Add(playlistItem); + await dbContext.SaveChangesAsync(); + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddSeasonToPlaylist request) => + (await PlaylistMustExist(dbContext, request), await ValidateSeason(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> PlaylistMustExist( + TvContext dbContext, + AddSeasonToPlaylist request) => + dbContext.Playlists + .Include(c => c.Items) + .SelectOneAsync(c => c.Id, c => c.Id == request.PlaylistId) + .Map(o => o.ToValidation("Playlist does not exist.")); + + private static Task> ValidateSeason( + TvContext dbContext, + AddSeasonToPlaylist request) => + dbContext.Seasons + .SelectOneAsync(m => m.Id, e => e.Id == request.SeasonId) + .Map(o => o.ToValidation("Season does not exist")); + + private sealed record Parameters(Playlist Playlist, Season Season); +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylist.cs b/ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylist.cs new file mode 100644 index 000000000..2b998e5df --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylist.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.MediaCollections; + +public record AddShowToPlaylist(int PlaylistId, int ShowId) : IRequest>; diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylistHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylistHandler.cs new file mode 100644 index 000000000..c4c25a868 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylistHandler.cs @@ -0,0 +1,59 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.MediaCollections; + +public class AddShowToPlaylistHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + AddShowToPlaylist request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await validation.Apply(parameters => ApplyAddShowRequest(dbContext, parameters)); + } + + private static async Task ApplyAddShowRequest(TvContext dbContext, Parameters parameters) + { + var playlistItem = new PlaylistItem + { + Index = parameters.Playlist.Items.Max(i => i.Index) + 1, + CollectionType = ProgramScheduleItemCollectionType.TelevisionShow, + MediaItemId = parameters.Show.Id, + PlaybackOrder = PlaybackOrder.Shuffle, + IncludeInProgramGuide = true + }; + + parameters.Playlist.Items.Add(playlistItem); + await dbContext.SaveChangesAsync(); + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddShowToPlaylist request) => + (await PlaylistMustExist(dbContext, request), await ValidateShow(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> PlaylistMustExist( + TvContext dbContext, + AddShowToPlaylist request) => + dbContext.Playlists + .Include(c => c.Items) + .SelectOneAsync(c => c.Id, c => c.Id == request.PlaylistId) + .Map(o => o.ToValidation("Playlist does not exist.")); + + private static Task> ValidateShow( + TvContext dbContext, + AddShowToPlaylist request) => + dbContext.Shows + .SelectOneAsync(m => m.Id, e => e.Id == request.ShowId) + .Map(o => o.ToValidation("Show does not exist")); + + private sealed record Parameters(Playlist Playlist, Show Show); +} diff --git a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs index 86de5bd26..64152516b 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs @@ -20,6 +20,12 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository public Task> GetMultiCollectionItems(int id) => throw new NotSupportedException(); public Task> GetSmartCollectionItems(int id) => _data[id].ToList().AsTask(); public Task> GetPlaylistItems(int id) => throw new NotSupportedException(); + public Task> GetMovie(int id) => throw new NotSupportedException(); + public Task> GetEpisode(int id) => throw new NotSupportedException(); + public Task> GetMusicVideo(int id) => throw new NotSupportedException(); + public Task> GetOtherVideo(int id) => throw new NotSupportedException(); + public Task> GetSong(int id) => throw new NotSupportedException(); + public Task> GetImage(int id) => throw new NotSupportedException(); public Task> GetMultiCollectionCollections(int id) => throw new NotSupportedException(); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs index 479ee6c95..60cd49cbf 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs @@ -11,6 +11,12 @@ public interface IMediaCollectionRepository Task> GetMultiCollectionItems(int id); Task> GetSmartCollectionItems(int id); Task> GetPlaylistItems(int id); + Task> GetMovie(int id); + Task> GetEpisode(int id); + Task> GetMusicVideo(int id); + Task> GetOtherVideo(int id); + Task> GetSong(int id); + Task> GetImage(int id); Task> GetMultiCollectionCollections(int id); Task> GetFakeMultiCollectionCollections(int? collectionId, int? smartCollectionId); Task> PlayoutIdsUsingCollection(int collectionId); diff --git a/ErsatzTV.Core/Scheduling/CollectionKey.cs b/ErsatzTV.Core/Scheduling/CollectionKey.cs index 711f356ca..aaad9cbf3 100644 --- a/ErsatzTV.Core/Scheduling/CollectionKey.cs +++ b/ErsatzTV.Core/Scheduling/CollectionKey.cs @@ -121,6 +121,36 @@ public class CollectionKey : Record { CollectionType = item.CollectionType }, + ProgramScheduleItemCollectionType.Movie => new CollectionKey + { + CollectionType = item.CollectionType, + MediaItemId = item.MediaItemId + }, + ProgramScheduleItemCollectionType.Episode => new CollectionKey + { + CollectionType = item.CollectionType, + MediaItemId = item.MediaItemId + }, + ProgramScheduleItemCollectionType.MusicVideo => new CollectionKey + { + CollectionType = item.CollectionType, + MediaItemId = item.MediaItemId + }, + ProgramScheduleItemCollectionType.OtherVideo => new CollectionKey + { + CollectionType = item.CollectionType, + MediaItemId = item.MediaItemId + }, + ProgramScheduleItemCollectionType.Song => new CollectionKey + { + CollectionType = item.CollectionType, + MediaItemId = item.MediaItemId + }, + ProgramScheduleItemCollectionType.Image => new CollectionKey + { + CollectionType = item.CollectionType, + MediaItemId = item.MediaItemId + }, _ => throw new ArgumentOutOfRangeException(nameof(item)) }; diff --git a/ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs b/ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs index 201fe12d7..413149192 100644 --- a/ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs +++ b/ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs @@ -41,6 +41,42 @@ public static class MediaItemsForCollection result.AddRange( await mediaCollectionRepository.GetPlaylistItems(collectionKey.PlaylistId ?? 0)); break; + case ProgramScheduleItemCollectionType.Movie: + foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) + { + result.AddRange(await mediaCollectionRepository.GetMovie(mediaItemId)); + } + break; + case ProgramScheduleItemCollectionType.Episode: + foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) + { + result.AddRange(await mediaCollectionRepository.GetEpisode(mediaItemId)); + } + break; + case ProgramScheduleItemCollectionType.MusicVideo: + foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) + { + result.AddRange(await mediaCollectionRepository.GetMusicVideo(mediaItemId)); + } + break; + case ProgramScheduleItemCollectionType.OtherVideo: + foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) + { + result.AddRange(await mediaCollectionRepository.GetOtherVideo(mediaItemId)); + } + break; + case ProgramScheduleItemCollectionType.Song: + foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) + { + result.AddRange(await mediaCollectionRepository.GetSong(mediaItemId)); + } + break; + case ProgramScheduleItemCollectionType.Image: + foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) + { + result.AddRange(await mediaCollectionRepository.GetImage(mediaItemId)); + } + break; default: throw new ArgumentOutOfRangeException(nameof(collectionKey)); } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 5e09c2490..718900d7f 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -501,6 +501,41 @@ public class MediaCollectionRepository : IMediaCollectionRepository return result.DistinctBy(x => x.Id).ToList(); } + public async Task> GetMovie(int id) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await GetMovieItems(dbContext, [id]); + } + + public async Task> GetEpisode(int id) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await GetEpisodeItems(dbContext, [id]); + } + + public async Task> GetMusicVideo(int id) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await GetMusicVideoItems(dbContext, [id]); + } + + public async Task> GetOtherVideo(int id) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await GetOtherVideoItems(dbContext, [id]); + } + + public async Task> GetSong(int id) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await GetSongItems(dbContext, [id]); + } + + public async Task> GetImage(int id) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await GetImageItems(dbContext, [id]); + } public async Task> GetFakeMultiCollectionCollections( int? collectionId, @@ -738,7 +773,7 @@ public class MediaCollectionRepository : IMediaCollectionRepository return await GetMovieItems(dbContext, ids); } - + private static Task> GetMovieItems(TvContext dbContext, IEnumerable movieIds) => dbContext.Movies .Include(m => m.MovieMetadata) diff --git a/ErsatzTV/Pages/EpisodeList.razor b/ErsatzTV/Pages/EpisodeList.razor index 969e3d281..92661f5ef 100644 --- a/ErsatzTV/Pages/EpisodeList.razor +++ b/ErsatzTV/Pages/EpisodeList.razor @@ -20,6 +20,13 @@ OnClick="@(_ => AddSelectionToCollection())"> Add To Collection + + Add To Playlist + Add To Collection + + Add To Playlist + Add To Collection + + Add To Playlist + Add To Collection + + Add To Playlist + Add To Collection + + Add To Playlist + Add To Collection + + Add To Playlist + Add To Collection + + Add To Playlist + +
+ + Add To Playlist + +
("Add To Playlist", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Canceled && result.Data is PlaylistViewModel playlist) + { + await _mediator.Send(new AddSeasonToPlaylist(playlist.Id, SeasonId), _cts.Token); + _navigationManager.NavigateTo($"media/playlists/{playlist.Id}"); + } + } private async Task AddToSchedule() { @@ -266,6 +294,27 @@ Right: _ => _snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success)); } } + + private async Task AddEpisodeToPlaylist(TelevisionEpisodeCardViewModel episode) + { + var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", episode.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = await _dialog.ShowAsync("Add To Playlist", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Canceled && result.Data is PlaylistViewModel playlist) + { + var request = new AddEpisodeToPlaylist(playlist.Id, episode.EpisodeId); + Either addResult = await _mediator.Send(request, _cts.Token); + addResult.Match( + Left: error => + { + _snackbar.Add($"Unexpected error adding episode to playlist: {error.Value}"); + _logger.LogError("Unexpected error adding episode to playlist: {Error}", error.Value); + }, + Right: _ => _snackbar.Add($"Added {episode.Title} to playlist {playlist.Name}", Severity.Success)); + } + } private async Task ShowInfo(TelevisionEpisodeCardViewModel episode) { diff --git a/ErsatzTV/Pages/TelevisionSeasonList.razor b/ErsatzTV/Pages/TelevisionSeasonList.razor index 1f16c8139..c9fc623bd 100644 --- a/ErsatzTV/Pages/TelevisionSeasonList.razor +++ b/ErsatzTV/Pages/TelevisionSeasonList.razor @@ -62,6 +62,13 @@ OnClick="@AddToCollection"> Add To Collection + + Add To Playlist + ("Add To Playlist", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Canceled && result.Data is PlaylistViewModel playlist) + { + await _mediator.Send(new AddShowToPlaylist(playlist.Id, ShowId), _cts.Token); + _navigationManager.NavigateTo($"media/playlists/{playlist.Id}"); + } + } + private async Task AddToSchedule() { var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.Title } }; diff --git a/ErsatzTV/Pages/TelevisionShowList.razor b/ErsatzTV/Pages/TelevisionShowList.razor index d4d2259c0..2f16693d8 100644 --- a/ErsatzTV/Pages/TelevisionShowList.razor +++ b/ErsatzTV/Pages/TelevisionShowList.razor @@ -21,6 +21,13 @@ OnClick="@(_ => AddSelectionToCollection())"> Add To Collection + + Add To Playlist +