diff --git a/ErsatzTV.Application/MediaCards/Mapper.cs b/ErsatzTV.Application/MediaCards/Mapper.cs index 8e5336b4b..0a683c872 100644 --- a/ErsatzTV.Application/MediaCards/Mapper.cs +++ b/ErsatzTV.Application/MediaCards/Mapper.cs @@ -28,6 +28,7 @@ namespace ErsatzTV.Application.MediaCards TelevisionEpisodeMediaItem televisionEpisode) => new( televisionEpisode.Id, + televisionEpisode.Season.TelevisionShow.Metadata.Title, televisionEpisode.Metadata.Title, $"Episode {televisionEpisode.Metadata.Episode}", televisionEpisode.Metadata.Episode.ToString(), diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs index fde927d3f..6ad21ff0c 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs @@ -3,6 +3,7 @@ public record TelevisionEpisodeCardViewModel ( int EpisodeId, + string ShowTitle, string Title, string Subtitle, string SortTitle, diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs new file mode 100644 index 000000000..290d8d177 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddTelevisionEpisodeToSimpleMediaCollection + (int MediaCollectionId, int TelevisionEpisodeId) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs new file mode 100644 index 000000000..9604b9ba3 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs @@ -0,0 +1,68 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + AddTelevisionEpisodeToSimpleMediaCollectionHandler : MediatR.IRequestHandler< + AddTelevisionEpisodeToSimpleMediaCollection, + Either> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly ITelevisionRepository _televisionRepository; + + public AddTelevisionEpisodeToSimpleMediaCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + ITelevisionRepository televisionRepository) + { + _mediaCollectionRepository = mediaCollectionRepository; + _televisionRepository = televisionRepository; + } + + public Task> Handle( + AddTelevisionEpisodeToSimpleMediaCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(ApplyAddTelevisionEpisodeRequest) + .Bind(v => v.ToEitherAsync()); + + private async Task ApplyAddTelevisionEpisodeRequest(RequestParameters parameters) + { + if (parameters.Collection.TelevisionEpisodes.All(s => s.Id != parameters.EpisodeToAdd.Id)) + { + parameters.Collection.TelevisionEpisodes.Add(parameters.EpisodeToAdd); + await _mediaCollectionRepository.Update(parameters.Collection); + } + + return Unit.Default; + } + + private async Task> + Validate(AddTelevisionEpisodeToSimpleMediaCollection request) => + (await SimpleMediaCollectionMustExist(request), await ValidateEpisode(request)) + .Apply( + (simpleMediaCollectionToUpdate, episode) => + new RequestParameters(simpleMediaCollectionToUpdate, episode)); + + private Task> SimpleMediaCollectionMustExist( + AddTelevisionEpisodeToSimpleMediaCollection updateSimpleMediaCollection) => + _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) + .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); + + private Task> ValidateEpisode( + AddTelevisionEpisodeToSimpleMediaCollection request) => + LoadTelevisionEpisode(request) + .Map(v => v.ToValidation("TelevisionEpisode does not exist")); + + private Task> LoadTelevisionEpisode( + AddTelevisionEpisodeToSimpleMediaCollection request) => + _televisionRepository.GetEpisode(request.TelevisionEpisodeId); + + private record RequestParameters(SimpleMediaCollection Collection, TelevisionEpisodeMediaItem EpisodeToAdd); + } +} diff --git a/ErsatzTV.Application/Television/Mapper.cs b/ErsatzTV.Application/Television/Mapper.cs index 2ec9fbc63..37366fa1b 100644 --- a/ErsatzTV.Application/Television/Mapper.cs +++ b/ErsatzTV.Application/Television/Mapper.cs @@ -12,7 +12,16 @@ namespace ErsatzTV.Application.Television season.TelevisionShowId, season.TelevisionShow.Metadata.Title, season.TelevisionShow.Metadata.Year?.ToString(), - $"Season {season.Number}", + season.Number == 0 ? "Specials" : $"Season {season.Number}", season.Poster); + + internal static TelevisionEpisodeViewModel ProjectToViewModel(TelevisionEpisodeMediaItem episode) => + new( + episode.Season.TelevisionShowId, + episode.SeasonId, + episode.Metadata.Episode, + episode.Metadata.Title, + episode.Metadata.Plot, + episode.Poster); } } diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs new file mode 100644 index 000000000..2e6cc0330 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs @@ -0,0 +1,7 @@ +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.Television.Queries +{ + public record GetTelevisionEpisodeById(int EpisodeId) : IRequest>; +} diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs new file mode 100644 index 000000000..69a0fc33a --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using MediatR; +using static ErsatzTV.Application.Television.Mapper; + +namespace ErsatzTV.Application.Television.Queries +{ + public class + GetTelevisionEpisodeByIdHandler : IRequestHandler> + { + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionEpisodeByIdHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public Task> Handle( + GetTelevisionEpisodeById request, + CancellationToken cancellationToken) => + _televisionRepository.GetEpisode(request.EpisodeId) + .MapT(ProjectToViewModel); + } +} diff --git a/ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs b/ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs new file mode 100644 index 000000000..937a943fa --- /dev/null +++ b/ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs @@ -0,0 +1,10 @@ +namespace ErsatzTV.Application.Television +{ + public record TelevisionEpisodeViewModel( + int ShowId, + int SeasonId, + int Episode, + string Title, + string Plot, + string Poster); +} diff --git a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs index effa1726b..d6687fc66 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs @@ -16,6 +16,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task> GetSeason(int televisionSeasonId); Task GetSeasonCount(int televisionShowId); Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize); + Task> GetEpisode(int televisionEpisodeId); Task GetEpisodeCount(int televisionSeasonId); Task> GetPagedEpisodes(int televisionSeasonId, int pageNumber, int pageSize); Task> GetShowByPath(int mediaSourceId, string path); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index ac08c343c..ae8c79729 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -55,6 +55,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(s => s.Metadata) .Include(s => s.TelevisionEpisodes) .ThenInclude(s => s.Metadata) + .Include(s => s.TelevisionEpisodes) + .ThenInclude(e => e.Season) + .ThenInclude(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) .SingleOrDefaultAsync(c => c.Id == id) .Map(Optional); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index dcaa425e4..ec5078c66 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -82,6 +82,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Take(pageSize) .ToListAsync(); + public Task> GetEpisode(int televisionEpisodeId) => + _dbContext.TelevisionEpisodeMediaItems + .AsNoTracking() + .Include(s => s.Season) + .Include(s => s.Metadata) + .SingleOrDefaultAsync(s => s.Id == televisionEpisodeId) + .Map(Optional); + public Task GetEpisodeCount(int televisionSeasonId) => _dbContext.TelevisionEpisodeMediaItems .AsNoTracking() @@ -95,6 +103,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories _dbContext.TelevisionEpisodeMediaItems .AsNoTracking() .Include(e => e.Metadata) + .Include(e => e.Season) + .ThenInclude(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) .Where(e => e.SeasonId == televisionSeasonId) .OrderBy(s => s.Metadata.Episode) .Skip((pageNumber - 1) * pageSize) diff --git a/ErsatzTV/Pages/MediaCollectionItems.razor b/ErsatzTV/Pages/MediaCollectionItems.razor index 9bb4a0ea2..695e0506c 100644 --- a/ErsatzTV/Pages/MediaCollectionItems.razor +++ b/ErsatzTV/Pages/MediaCollectionItems.razor @@ -45,19 +45,29 @@ @foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards) { } } -@* *@ -@* @foreach (var card in _data.Cards) *@ -@* { *@ -@* *@ -@* } *@ -@* *@ +@if (_data.EpisodeCards.Any()) +{ + Television Episodes + + + @foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards) + { + + } + +} @code { diff --git a/ErsatzTV/Pages/MediaCollections.razor b/ErsatzTV/Pages/MediaCollections.razor index 83551d187..240efc819 100644 --- a/ErsatzTV/Pages/MediaCollections.razor +++ b/ErsatzTV/Pages/MediaCollections.razor @@ -23,13 +23,18 @@ @foreach (MediaCollectionViewModel card in _data) { - + } - - Add Media Collection - + + + Add Media Collection + + @code { private List _data; diff --git a/ErsatzTV/Pages/TelevisionEpisode.razor b/ErsatzTV/Pages/TelevisionEpisode.razor new file mode 100644 index 000000000..fbf3af3a2 --- /dev/null +++ b/ErsatzTV/Pages/TelevisionEpisode.razor @@ -0,0 +1,82 @@ +@page "/media/tv/episodes/{EpisodeId:int}" +@using ErsatzTV.Application.Television +@using ErsatzTV.Application.Television.Queries +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Commands +@inject IMediator Mediator +@inject IDialogService Dialog +@inject NavigationManager NavigationManager + + + + +
+ @if (!string.IsNullOrWhiteSpace(_episode.Poster)) + { + + + + } + +
+ @_episode.Title + @_season.Plot + @_episode.Plot +
+ + Add To Collection + +
+
+
+
+
+
+ +@code { + + [Parameter] + public int EpisodeId { get; set; } + + private TelevisionEpisodeViewModel _episode; + private TelevisionSeasonViewModel _season; + + private List _breadcrumbs; + + protected override Task OnParametersSetAsync() => RefreshData(); + + private async Task RefreshData() + { + await Mediator.Send(new GetTelevisionEpisodeById(EpisodeId)) + .IfSomeAsync(vm => _episode = vm); + + await Mediator.Send(new GetTelevisionSeasonById(_episode.SeasonId)) + .IfSomeAsync(vm => _season = vm); + + _breadcrumbs = new List + { + new("TV Shows", "/media/tv/shows"), + new($"{_season.Title} ({_season.Year})", $"/media/tv/shows/{_season.ShowId}"), + new(_season.Plot, $"/media/tv/seasons/{_episode.SeasonId}"), + new($"Episode {_episode.Episode}", null, true) + }; + } + + private async Task AddToCollection() + { + var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", _episode.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + await Mediator.Send(new AddTelevisionEpisodeToSimpleMediaCollection(collection.Id, EpisodeId)); + NavigationManager.NavigateTo($"/media/collections/{collection.Id}"); + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/TelevisionEpisodes.razor b/ErsatzTV/Pages/TelevisionEpisodeList.razor similarity index 90% rename from ErsatzTV/Pages/TelevisionEpisodes.razor rename to ErsatzTV/Pages/TelevisionEpisodeList.razor index 1a7ba8caf..7a2875388 100644 --- a/ErsatzTV/Pages/TelevisionEpisodes.razor +++ b/ErsatzTV/Pages/TelevisionEpisodeList.razor @@ -13,15 +13,18 @@
- - - - + @if (!string.IsNullOrWhiteSpace(_season.Poster)) + { + + + + } +
@_season.Title @_season.Year @_season.Plot -
+
- - - - + @if (!string.IsNullOrWhiteSpace(_show.Poster)) + { + + + + } +
@_show.Title @_show.Year diff --git a/ErsatzTV/Pages/TelevisionShows.razor b/ErsatzTV/Pages/TelevisionShowList.razor similarity index 100% rename from ErsatzTV/Pages/TelevisionShows.razor rename to ErsatzTV/Pages/TelevisionShowList.razor diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index 929f3a9e0..768503083 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -6,7 +6,7 @@ .media-card-container { width: 152px; } -.media-card-episode-container { width: 291px; } +.media-card-episode-container { width: 392px; } .media-card { display: flex; @@ -28,7 +28,7 @@ width: 152px; } -.media-card-episode { width: 291px; } +.media-card-episode { width: 392px; } .media-card:hover { filter: brightness(75%); }