more add to playlist buttons; fix playlist preview (#1693)

This commit is contained in:
Jason Dove
2024-04-28 08:59:14 -05:00
committed by GitHub
parent 202ae33e37
commit a6d5df3ca6
20 changed files with 425 additions and 1 deletions
@@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.MediaCollections;
public record AddEpisodeToPlaylist(int PlaylistId, int EpisodeId) : IRequest<Either<BaseError, Unit>>;
@@ -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<TvContext> dbContextFactory)
: IRequestHandler<AddEpisodeToPlaylist, Either<BaseError, Unit>>
{
public async Task<Either<BaseError, Unit>> Handle(
AddEpisodeToPlaylist request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => ApplyAddEpisodeRequest(dbContext, parameters));
}
private static async Task<Unit> 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<Validation<BaseError, Parameters>> Validate(
TvContext dbContext,
AddEpisodeToPlaylist request) =>
(await PlaylistMustExist(dbContext, request), await ValidateEpisode(dbContext, request))
.Apply((collection, episode) => new Parameters(collection, episode));
private static Task<Validation<BaseError, Playlist>> PlaylistMustExist(
TvContext dbContext,
AddEpisodeToPlaylist request) =>
dbContext.Playlists
.Include(c => c.Items)
.SelectOneAsync(c => c.Id, c => c.Id == request.PlaylistId)
.Map(o => o.ToValidation<BaseError>("Playlist does not exist."));
private static Task<Validation<BaseError, Episode>> ValidateEpisode(
TvContext dbContext,
AddEpisodeToPlaylist request) =>
dbContext.Episodes
.SelectOneAsync(m => m.Id, e => e.Id == request.EpisodeId)
.Map(o => o.ToValidation<BaseError>("Episode does not exist"));
private sealed record Parameters(Playlist Playlist, Episode Episode);
}
@@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.MediaCollections;
public record AddSeasonToPlaylist(int PlaylistId, int SeasonId) : IRequest<Either<BaseError, Unit>>;
@@ -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<TvContext> dbContextFactory)
: IRequestHandler<AddSeasonToPlaylist, Either<BaseError, Unit>>
{
public async Task<Either<BaseError, Unit>> Handle(
AddSeasonToPlaylist request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => ApplyAddSeasonRequest(dbContext, parameters));
}
private static async Task<Unit> 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<Validation<BaseError, Parameters>> Validate(
TvContext dbContext,
AddSeasonToPlaylist request) =>
(await PlaylistMustExist(dbContext, request), await ValidateSeason(dbContext, request))
.Apply((collection, episode) => new Parameters(collection, episode));
private static Task<Validation<BaseError, Playlist>> PlaylistMustExist(
TvContext dbContext,
AddSeasonToPlaylist request) =>
dbContext.Playlists
.Include(c => c.Items)
.SelectOneAsync(c => c.Id, c => c.Id == request.PlaylistId)
.Map(o => o.ToValidation<BaseError>("Playlist does not exist."));
private static Task<Validation<BaseError, Season>> ValidateSeason(
TvContext dbContext,
AddSeasonToPlaylist request) =>
dbContext.Seasons
.SelectOneAsync(m => m.Id, e => e.Id == request.SeasonId)
.Map(o => o.ToValidation<BaseError>("Season does not exist"));
private sealed record Parameters(Playlist Playlist, Season Season);
}
@@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.MediaCollections;
public record AddShowToPlaylist(int PlaylistId, int ShowId) : IRequest<Either<BaseError, Unit>>;
@@ -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<TvContext> dbContextFactory)
: IRequestHandler<AddShowToPlaylist, Either<BaseError, Unit>>
{
public async Task<Either<BaseError, Unit>> Handle(
AddShowToPlaylist request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => ApplyAddShowRequest(dbContext, parameters));
}
private static async Task<Unit> 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<Validation<BaseError, Parameters>> Validate(
TvContext dbContext,
AddShowToPlaylist request) =>
(await PlaylistMustExist(dbContext, request), await ValidateShow(dbContext, request))
.Apply((collection, episode) => new Parameters(collection, episode));
private static Task<Validation<BaseError, Playlist>> PlaylistMustExist(
TvContext dbContext,
AddShowToPlaylist request) =>
dbContext.Playlists
.Include(c => c.Items)
.SelectOneAsync(c => c.Id, c => c.Id == request.PlaylistId)
.Map(o => o.ToValidation<BaseError>("Playlist does not exist."));
private static Task<Validation<BaseError, Show>> ValidateShow(
TvContext dbContext,
AddShowToPlaylist request) =>
dbContext.Shows
.SelectOneAsync(m => m.Id, e => e.Id == request.ShowId)
.Map(o => o.ToValidation<BaseError>("Show does not exist"));
private sealed record Parameters(Playlist Playlist, Show Show);
}
@@ -20,6 +20,12 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository
public Task<List<MediaItem>> GetMultiCollectionItems(int id) => throw new NotSupportedException();
public Task<List<MediaItem>> GetSmartCollectionItems(int id) => _data[id].ToList().AsTask();
public Task<List<MediaItem>> GetPlaylistItems(int id) => throw new NotSupportedException();
public Task<List<Movie>> GetMovie(int id) => throw new NotSupportedException();
public Task<List<Episode>> GetEpisode(int id) => throw new NotSupportedException();
public Task<List<MusicVideo>> GetMusicVideo(int id) => throw new NotSupportedException();
public Task<List<OtherVideo>> GetOtherVideo(int id) => throw new NotSupportedException();
public Task<List<Song>> GetSong(int id) => throw new NotSupportedException();
public Task<List<Image>> GetImage(int id) => throw new NotSupportedException();
public Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id) =>
throw new NotSupportedException();
@@ -11,6 +11,12 @@ public interface IMediaCollectionRepository
Task<List<MediaItem>> GetMultiCollectionItems(int id);
Task<List<MediaItem>> GetSmartCollectionItems(int id);
Task<List<MediaItem>> GetPlaylistItems(int id);
Task<List<Movie>> GetMovie(int id);
Task<List<Episode>> GetEpisode(int id);
Task<List<MusicVideo>> GetMusicVideo(int id);
Task<List<OtherVideo>> GetOtherVideo(int id);
Task<List<Song>> GetSong(int id);
Task<List<Image>> GetImage(int id);
Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id);
Task<List<CollectionWithItems>> GetFakeMultiCollectionCollections(int? collectionId, int? smartCollectionId);
Task<List<int>> PlayoutIdsUsingCollection(int collectionId);
+30
View File
@@ -121,6 +121,36 @@ public class CollectionKey : Record<CollectionKey>
{
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))
};
@@ -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));
}
@@ -501,6 +501,41 @@ public class MediaCollectionRepository : IMediaCollectionRepository
return result.DistinctBy(x => x.Id).ToList();
}
public async Task<List<Movie>> GetMovie(int id)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await GetMovieItems(dbContext, [id]);
}
public async Task<List<Episode>> GetEpisode(int id)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await GetEpisodeItems(dbContext, [id]);
}
public async Task<List<MusicVideo>> GetMusicVideo(int id)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await GetMusicVideoItems(dbContext, [id]);
}
public async Task<List<OtherVideo>> GetOtherVideo(int id)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await GetOtherVideoItems(dbContext, [id]);
}
public async Task<List<Song>> GetSong(int id)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await GetSongItems(dbContext, [id]);
}
public async Task<List<Image>> GetImage(int id)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await GetImageItems(dbContext, [id]);
}
public async Task<List<CollectionWithItems>> GetFakeMultiCollectionCollections(
int? collectionId,
+7
View File
@@ -20,6 +20,13 @@
OnClick="@(_ => AddSelectionToCollection())">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddSelectionToPlaylist())">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
+7
View File
@@ -19,6 +19,13 @@
OnClick="@(_ => AddSelectionToCollection())">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddSelectionToPlaylist())">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
+7
View File
@@ -21,6 +21,13 @@
OnClick="@(_ => AddSelectionToCollection())">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddSelectionToPlaylist())">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
+7
View File
@@ -20,6 +20,13 @@
OnClick="@(_ => AddSelectionToCollection())">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddSelectionToPlaylist())">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
+7
View File
@@ -19,6 +19,13 @@
OnClick="@(_ => AddSelectionToCollection())">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddSelectionToPlaylist())">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
+7
View File
@@ -20,6 +20,13 @@
OnClick="@(_ => AddSelectionToCollection())">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddSelectionToPlaylist())">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
@@ -56,6 +56,13 @@
OnClick="@AddToCollection">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@AddToPlaylist">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
@@ -109,6 +116,14 @@
Add To Collection
</MudButton>
</div>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddEpisodeToPlaylist(episode))">
Add To Playlist
</MudButton>
</div>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
@@ -231,6 +246,19 @@
}
}
private async Task AddToPlaylist()
{
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Name}" } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await _dialog.ShowAsync<AddToPlaylistDialog>("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()
{
@@ -267,6 +295,27 @@
}
}
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<AddToPlaylistDialog>("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<BaseError, Unit> 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)
{
Either<BaseError, MediaItemInfo> maybeInfo = await _mediator.Send(new GetMediaItemInfo(episode.EpisodeId));
+21
View File
@@ -62,6 +62,13 @@
OnClick="@AddToCollection">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@AddToPlaylist">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
@@ -220,6 +227,20 @@
}
}
private async Task AddToPlaylist()
{
var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.Title } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await _dialog.ShowAsync<AddToPlaylistDialog>("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 } };
+7
View File
@@ -21,6 +21,13 @@
OnClick="@(_ => AddSelectionToCollection())">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddSelectionToPlaylist())">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"