From 3280b2229965829629dc9793bfdfb54415706ee4 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Fri, 19 Feb 2021 21:05:00 -0600 Subject: [PATCH] add television seasons to collections --- ErsatzTV.Application/MediaCards/Mapper.cs | 10 +- .../Queries/GetTelevisionEpisodeCards.cs | 7 ++ .../GetTelevisionEpisodeCardsHandler.cs | 34 +++++++ .../TelevisionEpisodeCardResultsViewModel.cs | 6 ++ .../TelevisionEpisodeCardViewModel.cs | 16 +++- .../TelevisionSeasonCardViewModel.cs | 2 + ...TelevisionSeasonToSimpleMediaCollection.cs | 8 ++ ...ionSeasonToSimpleMediaCollectionHandler.cs | 68 +++++++++++++ ErsatzTV.Application/Television/Mapper.cs | 8 ++ .../Queries/GetTelevisionSeasonById.cs | 7 ++ .../Queries/GetTelevisionSeasonByIdHandler.cs | 24 +++++ .../Television/TelevisionSeasonViewModel.cs | 4 + .../Repositories/ITelevisionRepository.cs | 3 + ErsatzTV.Core/Metadata/LocalFolderScanner.cs | 1 + .../Metadata/LocalMetadataProvider.cs | 2 +- .../Metadata/TelevisionFolderScanner.cs | 18 ++-- .../Repositories/MediaCollectionRepository.cs | 2 + .../Data/Repositories/TelevisionRepository.cs | 68 +++++++------ ErsatzTV/Pages/MediaCollectionItems.razor | 15 +++ ErsatzTV/Pages/TelevisionEpisodes.razor | 95 +++++++++++++++++++ ErsatzTV/Pages/TelevisionSeasons.razor | 21 ++-- ErsatzTV/Shared/MediaCard.razor | 34 ++++--- ErsatzTV/wwwroot/css/site.css | 14 +++ 23 files changed, 401 insertions(+), 66 deletions(-) create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs create mode 100644 ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs create mode 100644 ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs create mode 100644 ErsatzTV/Pages/TelevisionEpisodes.razor diff --git a/ErsatzTV.Application/MediaCards/Mapper.cs b/ErsatzTV.Application/MediaCards/Mapper.cs index b8a3148cd..8e5336b4b 100644 --- a/ErsatzTV.Application/MediaCards/Mapper.cs +++ b/ErsatzTV.Application/MediaCards/Mapper.cs @@ -15,7 +15,9 @@ namespace ErsatzTV.Application.MediaCards internal static TelevisionSeasonCardViewModel ProjectToViewModel(TelevisionSeason televisionSeason) => new( + televisionSeason.TelevisionShow.Metadata.Title, televisionSeason.Id, + televisionSeason.Number, GetSeasonName(televisionSeason.Number), string.Empty, GetSeasonName(televisionSeason.Number), @@ -25,10 +27,12 @@ namespace ErsatzTV.Application.MediaCards internal static TelevisionEpisodeCardViewModel ProjectToViewModel( TelevisionEpisodeMediaItem televisionEpisode) => new( + televisionEpisode.Id, televisionEpisode.Metadata.Title, - string.Empty, - televisionEpisode.Metadata.SortTitle, - televisionEpisode.Poster); + $"Episode {televisionEpisode.Metadata.Episode}", + televisionEpisode.Metadata.Episode.ToString(), + televisionEpisode.Poster, + televisionEpisode.Metadata.Episode.ToString()); internal static MovieCardViewModel ProjectToViewModel(MovieMediaItem movie) => new( diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs new file mode 100644 index 000000000..b6aa3cc51 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public record GetTelevisionEpisodeCards + (int TelevisionSeasonId, int PageNumber, int PageSize) : IRequest; +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs new file mode 100644 index 000000000..942a15415 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs @@ -0,0 +1,34 @@ +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.MediaCards.Mapper; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public class + GetTelevisionEpisodeCardsHandler : IRequestHandler + { + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionEpisodeCardsHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public async Task Handle( + GetTelevisionEpisodeCards request, + CancellationToken cancellationToken) + { + int count = await _televisionRepository.GetEpisodeCount(request.TelevisionSeasonId); + + List results = await _televisionRepository + .GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new TelevisionEpisodeCardResultsViewModel(count, results); + } + } +} diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs new file mode 100644 index 000000000..348b7f9e4 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Application.MediaCards +{ + public record TelevisionEpisodeCardResultsViewModel(int Count, List Cards); +} diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs index 1c8ed8348..fde927d3f 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs @@ -1,11 +1,17 @@ namespace ErsatzTV.Application.MediaCards { public record TelevisionEpisodeCardViewModel - (string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel( - Title, - Subtitle, - SortTitle, - Poster) + ( + int EpisodeId, + string Title, + string Subtitle, + string SortTitle, + string Poster, + string Placeholder) : MediaCardViewModel( + Title, + Subtitle, + SortTitle, + Poster) { } } diff --git a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs index ff714980c..0cc0b4d42 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs @@ -2,7 +2,9 @@ { public record TelevisionSeasonCardViewModel ( + string ShowTitle, int TelevisionSeasonId, + int TelevisionSeasonNumber, string Title, string Subtitle, string SortTitle, diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs new file mode 100644 index 000000000..54cde04ad --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddTelevisionSeasonToSimpleMediaCollection + (int MediaCollectionId, int TelevisionSeasonId) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs new file mode 100644 index 000000000..fb4ada450 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.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 + AddTelevisionSeasonToSimpleMediaCollectionHandler : MediatR.IRequestHandler< + AddTelevisionSeasonToSimpleMediaCollection, + Either> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly ITelevisionRepository _televisionRepository; + + public AddTelevisionSeasonToSimpleMediaCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + ITelevisionRepository televisionRepository) + { + _mediaCollectionRepository = mediaCollectionRepository; + _televisionRepository = televisionRepository; + } + + public Task> Handle( + AddTelevisionSeasonToSimpleMediaCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(ApplyAddTelevisionSeasonRequest) + .Bind(v => v.ToEitherAsync()); + + private async Task ApplyAddTelevisionSeasonRequest(RequestParameters parameters) + { + if (parameters.Collection.TelevisionSeasons.All(s => s.Id != parameters.SeasonToAdd.Id)) + { + parameters.Collection.TelevisionSeasons.Add(parameters.SeasonToAdd); + await _mediaCollectionRepository.Update(parameters.Collection); + } + + return Unit.Default; + } + + private async Task> + Validate(AddTelevisionSeasonToSimpleMediaCollection request) => + (await SimpleMediaCollectionMustExist(request), await ValidateSeason(request)) + .Apply( + (simpleMediaCollectionToUpdate, season) => + new RequestParameters(simpleMediaCollectionToUpdate, season)); + + private Task> SimpleMediaCollectionMustExist( + AddTelevisionSeasonToSimpleMediaCollection updateSimpleMediaCollection) => + _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) + .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); + + private Task> ValidateSeason( + AddTelevisionSeasonToSimpleMediaCollection request) => + LoadTelevisionSeason(request) + .Map(v => v.ToValidation("TelevisionSeason does not exist")); + + private Task> LoadTelevisionSeason( + AddTelevisionSeasonToSimpleMediaCollection request) => + _televisionRepository.GetSeason(request.TelevisionSeasonId); + + private record RequestParameters(SimpleMediaCollection Collection, TelevisionSeason SeasonToAdd); + } +} diff --git a/ErsatzTV.Application/Television/Mapper.cs b/ErsatzTV.Application/Television/Mapper.cs index 630254d1f..2ec9fbc63 100644 --- a/ErsatzTV.Application/Television/Mapper.cs +++ b/ErsatzTV.Application/Television/Mapper.cs @@ -6,5 +6,13 @@ namespace ErsatzTV.Application.Television { internal static TelevisionShowViewModel ProjectToViewModel(TelevisionShow show) => new(show.Metadata.Title, show.Metadata.Year?.ToString(), show.Metadata.Plot, show.Poster); + + internal static TelevisionSeasonViewModel ProjectToViewModel(TelevisionSeason season) => + new( + season.TelevisionShowId, + season.TelevisionShow.Metadata.Title, + season.TelevisionShow.Metadata.Year?.ToString(), + $"Season {season.Number}", + season.Poster); } } diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs new file mode 100644 index 000000000..e551e2168 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs @@ -0,0 +1,7 @@ +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.Television.Queries +{ + public record GetTelevisionSeasonById(int SeasonId) : IRequest>; +} diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs new file mode 100644 index 000000000..6adea60a4 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.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 + GetTelevisionSeasonByIdHandler : IRequestHandler> + { + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionSeasonByIdHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public Task> Handle( + GetTelevisionSeasonById request, + CancellationToken cancellationToken) => + _televisionRepository.GetSeason(request.SeasonId) + .MapT(ProjectToViewModel); + } +} diff --git a/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs b/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs new file mode 100644 index 000000000..b7abbc2a6 --- /dev/null +++ b/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Application.Television +{ + public record TelevisionSeasonViewModel(int ShowId, string Title, string Year, string Plot, string Poster); +} diff --git a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs index 25ade2b15..effa1726b 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs @@ -13,8 +13,11 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task> GetShow(int televisionShowId); Task GetShowCount(); Task> GetPagedShows(int pageNumber, int pageSize); + Task> GetSeason(int televisionSeasonId); Task GetSeasonCount(int televisionShowId); Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize); + Task GetEpisodeCount(int televisionSeasonId); + Task> GetPagedEpisodes(int televisionSeasonId, int pageNumber, int pageSize); Task> GetShowByPath(int mediaSourceId, string path); Task> GetShowByMetadata(TelevisionShowMetadata metadata); diff --git a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs index ad258aa05..1d0ddcde9 100644 --- a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs @@ -90,6 +90,7 @@ namespace ErsatzTV.Core.Metadata hash => { show.Poster = hash; + show.PosterLastWriteTime = _localFileSystem.GetLastWriteTime(posterPath); return update(show); }, error => diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index 1d8ae20df..d6d03994a 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -220,7 +220,7 @@ namespace ErsatzTV.Core.Metadata { Source = MetadataSource.Sidecar, LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName), - Title = nfo.ShowTitle, + Title = nfo.Title, Aired = GetAired(nfo.Aired), Episode = nfo.Episode, Season = nfo.Season, diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index 01264fb1e..d1fe84f2f 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -158,7 +158,8 @@ namespace ErsatzTV.Core.Metadata async nfoFile => { if (show.Metadata == null || show.Metadata.Source == MetadataSource.Fallback || - show.Metadata.LastWriteTime < _localFileSystem.GetLastWriteTime(nfoFile)) + (show.Metadata.LastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(nfoFile)) { _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); await _localMetadataProvider.RefreshSidecarMetadata(show, nfoFile); @@ -190,7 +191,8 @@ namespace ErsatzTV.Core.Metadata async nfoFile => { if (episode.Metadata == null || episode.Metadata.Source == MetadataSource.Fallback || - episode.Metadata.LastWriteTime < _localFileSystem.GetLastWriteTime(nfoFile)) + (episode.Metadata.LastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(nfoFile)) { _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); await _localMetadataProvider.RefreshSidecarMetadata(episode, nfoFile); @@ -223,7 +225,8 @@ namespace ErsatzTV.Core.Metadata async posterFile => { if (string.IsNullOrWhiteSpace(show.Poster) || - show.PosterLastWriteTime < _localFileSystem.GetLastWriteTime(posterFile)) + (show.PosterLastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(posterFile)) { _logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile); Either maybePoster = await SavePosterToDisk(posterFile, 440); @@ -231,6 +234,7 @@ namespace ErsatzTV.Core.Metadata poster => { show.Poster = poster; + show.PosterLastWriteTime = _localFileSystem.GetLastWriteTime(posterFile); return _televisionRepository.Update(show); }, error => @@ -260,10 +264,11 @@ namespace ErsatzTV.Core.Metadata async posterFile => { if (string.IsNullOrWhiteSpace(season.Poster) || - season.PosterLastWriteTime < _localFileSystem.GetLastWriteTime(posterFile)) + (season.PosterLastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(posterFile)) { _logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile); - await SavePosterToDisk(season, posterFile, _televisionRepository.Update); + await SavePosterToDisk(season, posterFile, _televisionRepository.Update, 440); } }); @@ -284,7 +289,8 @@ namespace ErsatzTV.Core.Metadata async posterFile => { if (string.IsNullOrWhiteSpace(episode.Poster) || - episode.PosterLastWriteTime < _localFileSystem.GetLastWriteTime(posterFile)) + (episode.PosterLastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(posterFile)) { _logger.LogDebug("Refreshing {Attribute} from {Path}", "Thumbnail", posterFile); await SavePosterToDisk(episode, posterFile, _televisionRepository.Update); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index b0806ca9c..ac08c343c 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -51,6 +51,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(s => s.TelevisionShows) .ThenInclude(s => s.Metadata) .Include(s => s.TelevisionSeasons) + .ThenInclude(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) .Include(s => s.TelevisionEpisodes) .ThenInclude(s => s.Metadata) .SingleOrDefaultAsync(c => c.Id == id) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index ada1daa76..dcaa425e4 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -46,7 +46,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public Task GetShowCount() => _dbContext.TelevisionShows .AsNoTracking() - .GroupBy(s => new { s.Metadata.Title, s.Metadata.Year }) .CountAsync(); public Task> GetPagedShows(int pageNumber, int pageSize) => @@ -58,42 +57,49 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Take(pageSize) .ToListAsync(); - public async Task GetSeasonCount(int televisionShowId) - { - Option metadata = await _dbContext.TelevisionShowMetadata + public Task> GetSeason(int televisionSeasonId) => + _dbContext.TelevisionSeasons .AsNoTracking() - .Where(m => m.TelevisionShowId == televisionShowId) - .SingleOrDefaultAsync() + .Include(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) + .SingleOrDefaultAsync(s => s.Id == televisionSeasonId) .Map(Optional); - return await metadata.Match( - m => _dbContext.TelevisionShows - .AsNoTracking() - .Where(s => s.Metadata.Title == m.Title && s.Metadata.Year == m.Year) - .SelectMany(s => s.Seasons) - .CountAsync(), - Task.FromResult(0)); - } + public Task GetSeasonCount(int televisionShowId) => + _dbContext.TelevisionSeasons + .AsNoTracking() + .Where(s => s.TelevisionShowId == televisionShowId) + .CountAsync(); - public async Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) - { - Option metadata = await _dbContext.TelevisionShowMetadata + public Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) => + _dbContext.TelevisionSeasons .AsNoTracking() - .Where(m => m.TelevisionShowId == televisionShowId) - .SingleOrDefaultAsync() - .Map(Optional); + .Where(s => s.TelevisionShowId == televisionShowId) + .Include(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) + .OrderBy(s => s.Number) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); - return await metadata.Match( - m => _dbContext.TelevisionShows - .AsNoTracking() - .Where(s => s.Metadata.Title == m.Title && s.Metadata.Year == m.Year) - .SelectMany(s => s.Seasons) - .OrderBy(s => s.Number) - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize) - .ToListAsync(), - Task.FromResult(new List())); - } + public Task GetEpisodeCount(int televisionSeasonId) => + _dbContext.TelevisionEpisodeMediaItems + .AsNoTracking() + .Where(e => e.SeasonId == televisionSeasonId) + .CountAsync(); + + public Task> GetPagedEpisodes( + int televisionSeasonId, + int pageNumber, + int pageSize) => + _dbContext.TelevisionEpisodeMediaItems + .AsNoTracking() + .Include(e => e.Metadata) + .Where(e => e.SeasonId == televisionSeasonId) + .OrderBy(s => s.Metadata.Episode) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); public async Task> GetShowByPath(int mediaSourceId, string path) { diff --git a/ErsatzTV/Pages/MediaCollectionItems.razor b/ErsatzTV/Pages/MediaCollectionItems.razor index 2795f409c..9bb4a0ea2 100644 --- a/ErsatzTV/Pages/MediaCollectionItems.razor +++ b/ErsatzTV/Pages/MediaCollectionItems.razor @@ -37,6 +37,21 @@ } +@if (_data.SeasonCards.Any()) +{ + Television Seasons + + + @foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards) + { + + } + +} + @* *@ @* @foreach (var card in _data.Cards) *@ @* { *@ diff --git a/ErsatzTV/Pages/TelevisionEpisodes.razor b/ErsatzTV/Pages/TelevisionEpisodes.razor new file mode 100644 index 000000000..1a7ba8caf --- /dev/null +++ b/ErsatzTV/Pages/TelevisionEpisodes.razor @@ -0,0 +1,95 @@ +@page "/media/tv/seasons/{SeasonId:int}" +@using ErsatzTV.Application.Television +@using ErsatzTV.Application.Television.Queries +@using ErsatzTV.Application.MediaCards +@using ErsatzTV.Application.MediaCards.Queries +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Commands +@inject IMediator Mediator +@inject IDialogService Dialog +@inject NavigationManager NavigationManager + + + + +
+ + + + +
+ @_season.Title + @_season.Year + @_season.Plot +
+ + Add To Collection + +
+
+
+
+
+
+ + + @foreach (TelevisionEpisodeCardViewModel card in _data.Cards) + { + + } + + +@code { + + [Parameter] + public int SeasonId { get; set; } + + private TelevisionSeasonViewModel _season; + + private int _pageSize => 100; + private readonly int _pageNumber = 1; + + private TelevisionEpisodeCardResultsViewModel _data; + + private List _breadcrumbs; + + protected override Task OnParametersSetAsync() => RefreshData(); + + private async Task RefreshData() + { + await Mediator.Send(new GetTelevisionSeasonById(SeasonId)) + .IfSomeAsync(vm => _season = vm); + + _data = await Mediator.Send(new GetTelevisionEpisodeCards(SeasonId, _pageNumber, _pageSize)); + + _breadcrumbs = new List + { + new("TV Shows", "/media/tv/shows"), + new($"{_season.Title} ({_season.Year})", $"/media/tv/shows/{_season.ShowId}"), + new(_season.Plot, null, true) + }; + } + + private async Task AddToCollection() + { + var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Plot}" } }; + 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 AddTelevisionSeasonToSimpleMediaCollection(collection.Id, SeasonId)); + NavigationManager.NavigateTo($"/media/collections/{collection.Id}"); + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/TelevisionSeasons.razor b/ErsatzTV/Pages/TelevisionSeasons.razor index 2119882ad..7fb9e04e7 100644 --- a/ErsatzTV/Pages/TelevisionSeasons.razor +++ b/ErsatzTV/Pages/TelevisionSeasons.razor @@ -1,5 +1,4 @@ @page "/media/tv/shows/{ShowId:int}" -@using static LanguageExt.Prelude @using ErsatzTV.Application.Television @using ErsatzTV.Application.Television.Queries @using ErsatzTV.Application.MediaCards @@ -11,6 +10,7 @@ @inject NavigationManager NavigationManager +
@@ -28,9 +28,6 @@ OnClick="@AddToCollection"> Add To Collection - @* *@ - @* Add To Schedule *@ - @* *@
@@ -41,7 +38,9 @@ @foreach (TelevisionSeasonCardViewModel card in _data.Cards) { - + } @@ -57,6 +56,8 @@ private TelevisionSeasonCardResultsViewModel _data; + private List _breadcrumbs; + protected override Task OnParametersSetAsync() => RefreshData(); private async Task RefreshData() @@ -65,11 +66,13 @@ .IfSomeAsync(vm => _show = vm); _data = await Mediator.Send(new GetTelevisionSeasonCards(ShowId, _pageNumber, _pageSize)); - } - private string SeasonPlaceholder(string sortTitle) => - Optional(_data.Cards.SingleOrDefault(d => d.SortTitle == sortTitle)) - .Match(vm => vm.Placeholder, () => sortTitle.Substring(0, 1).ToUpperInvariant()); + _breadcrumbs = new List + { + new("TV Shows", "/media/tv/shows"), + new($"{_show.Title} ({_show.Year})", null, true) + }; + } private async Task AddToCollection() { diff --git a/ErsatzTV/Shared/MediaCard.razor b/ErsatzTV/Shared/MediaCard.razor index 779ee504c..7d1dac30f 100644 --- a/ErsatzTV/Shared/MediaCard.razor +++ b/ErsatzTV/Shared/MediaCard.razor @@ -2,15 +2,15 @@ @using Unit = LanguageExt.Unit @inject IMediator Mediator -
+
@if (!string.IsNullOrWhiteSpace(Link)) { - + @if (string.IsNullOrWhiteSpace(Data.Poster)) { - @Placeholder(Data.SortTitle) + @GetPlaceholder(Data.SortTitle) } @@ -18,20 +18,20 @@ } else { - + @if (string.IsNullOrWhiteSpace(Data.Poster)) { - @Placeholder(Data.SortTitle) + @GetPlaceholder(Data.SortTitle) } } - @Data.Title + @(Title ?? Data.Title) - @Data.Subtitle + @(Subtitle ?? Data.Subtitle)
@@ -47,13 +47,25 @@ public EventCallback DataRefreshed { get; set; } [Parameter] - public Func CustomPlaceholder { get; set; } + public string Placeholder { get; set; } - private string Placeholder(string sortTitle) + [Parameter] + public string Title { get; set; } + + [Parameter] + public string Subtitle { get; set; } + + [Parameter] + public string ContainerClass { get; set; } + + [Parameter] + public string CardClass { get; set; } + + private string GetPlaceholder(string sortTitle) { - if (CustomPlaceholder != null) + if (Placeholder != null) { - return CustomPlaceholder(sortTitle); + return Placeholder; } string first = sortTitle.Substring(0, 1).ToUpperInvariant(); diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index d643912f5..929f3a9e0 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -6,6 +6,18 @@ .media-card-container { width: 152px; } +.media-card-episode-container { width: 291px; } + +.media-card { + display: flex; + filter: brightness(100%); + flex-direction: column; + height: 220px; + justify-content: center; + transition: all 0.2s ease; + width: 152px; +} + .media-card { display: flex; filter: brightness(100%); @@ -16,6 +28,8 @@ width: 152px; } +.media-card-episode { width: 291px; } + .media-card:hover { filter: brightness(75%); } .media-card-title {