Browse Source

add television episodes to collections

pull/26/head
Jason Dove 6 years ago
parent
commit
32c72f5ac4
  1. 1
      ErsatzTV.Application/MediaCards/Mapper.cs
  2. 1
      ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs
  3. 8
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs
  4. 68
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs
  5. 11
      ErsatzTV.Application/Television/Mapper.cs
  6. 7
      ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs
  7. 24
      ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs
  8. 10
      ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs
  9. 1
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  10. 4
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  11. 11
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  12. 24
      ErsatzTV/Pages/MediaCollectionItems.razor
  13. 13
      ErsatzTV/Pages/MediaCollections.razor
  14. 82
      ErsatzTV/Pages/TelevisionEpisode.razor
  15. 13
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  16. 11
      ErsatzTV/Pages/TelevisionSeasonList.razor
  17. 0
      ErsatzTV/Pages/TelevisionShowList.razor
  18. 4
      ErsatzTV/wwwroot/css/site.css

1
ErsatzTV.Application/MediaCards/Mapper.cs

@ -28,6 +28,7 @@ namespace ErsatzTV.Application.MediaCards @@ -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(),

1
ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
public record TelevisionEpisodeCardViewModel
(
int EpisodeId,
string ShowTitle,
string Title,
string Subtitle,
string SortTitle,

8
ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record AddTelevisionEpisodeToSimpleMediaCollection
(int MediaCollectionId, int TelevisionEpisodeId) : MediatR.IRequest<Either<BaseError, Unit>>;
}

68
ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs

@ -0,0 +1,68 @@ @@ -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<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddTelevisionEpisodeToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddTelevisionEpisodeToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddTelevisionEpisodeRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> 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<Validation<BaseError, RequestParameters>>
Validate(AddTelevisionEpisodeToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateEpisode(request))
.Apply(
(simpleMediaCollectionToUpdate, episode) =>
new RequestParameters(simpleMediaCollectionToUpdate, episode));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddTelevisionEpisodeToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, TelevisionEpisodeMediaItem>> ValidateEpisode(
AddTelevisionEpisodeToSimpleMediaCollection request) =>
LoadTelevisionEpisode(request)
.Map(v => v.ToValidation<BaseError>("TelevisionEpisode does not exist"));
private Task<Option<TelevisionEpisodeMediaItem>> LoadTelevisionEpisode(
AddTelevisionEpisodeToSimpleMediaCollection request) =>
_televisionRepository.GetEpisode(request.TelevisionEpisodeId);
private record RequestParameters(SimpleMediaCollection Collection, TelevisionEpisodeMediaItem EpisodeToAdd);
}
}

11
ErsatzTV.Application/Television/Mapper.cs

@ -12,7 +12,16 @@ namespace ErsatzTV.Application.Television @@ -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);
}
}

7
ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetTelevisionEpisodeById(int EpisodeId) : IRequest<Option<TelevisionEpisodeViewModel>>;
}

24
ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs

@ -0,0 +1,24 @@ @@ -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<GetTelevisionEpisodeById, Option<TelevisionEpisodeViewModel>>
{
private readonly ITelevisionRepository _televisionRepository;
public GetTelevisionEpisodeByIdHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public Task<Option<TelevisionEpisodeViewModel>> Handle(
GetTelevisionEpisodeById request,
CancellationToken cancellationToken) =>
_televisionRepository.GetEpisode(request.EpisodeId)
.MapT(ProjectToViewModel);
}
}

10
ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionEpisodeViewModel(
int ShowId,
int SeasonId,
int Episode,
string Title,
string Plot,
string Poster);
}

1
ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs

@ -16,6 +16,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -16,6 +16,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Option<TelevisionSeason>> GetSeason(int televisionSeasonId);
Task<int> GetSeasonCount(int televisionShowId);
Task<List<TelevisionSeason>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize);
Task<Option<TelevisionEpisodeMediaItem>> GetEpisode(int televisionEpisodeId);
Task<int> GetEpisodeCount(int televisionSeasonId);
Task<List<TelevisionEpisodeMediaItem>> GetPagedEpisodes(int televisionSeasonId, int pageNumber, int pageSize);
Task<Option<TelevisionShow>> GetShowByPath(int mediaSourceId, string path);

4
ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs

@ -55,6 +55,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -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);

11
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -82,6 +82,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -82,6 +82,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Take(pageSize)
.ToListAsync();
public Task<Option<TelevisionEpisodeMediaItem>> GetEpisode(int televisionEpisodeId) =>
_dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(s => s.Season)
.Include(s => s.Metadata)
.SingleOrDefaultAsync(s => s.Id == televisionEpisodeId)
.Map(Optional);
public Task<int> GetEpisodeCount(int televisionSeasonId) =>
_dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
@ -95,6 +103,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -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)

24
ErsatzTV/Pages/MediaCollectionItems.razor

@ -45,19 +45,29 @@ @@ -45,19 +45,29 @@
@foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards)
{
<MediaCard Data="@card"
Link="@($"/media/tv/Seasons/{card.TelevisionSeasonId}")"
Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")"
Title="@card.ShowTitle"
Subtitle="@card.Title"/>
}
</MudContainer>
}
@* <MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid"> *@
@* @foreach (var card in _data.Cards) *@
@* { *@
@* <MediaCard Data="@card" CustomPlaceholder="@SeasonPlaceholder" Link="@($"/media/tv/shows/{ShowId}/seasons/{card.TelevisionSeasonId}")" DataRefreshed="@(() => RefreshData())"/> *@
@* } *@
@* </MudContainer> *@
@if (_data.EpisodeCards.Any())
{
<MudText GutterBottom="true" Typo="Typo.h4">Television Episodes</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards)
{
<MediaCard Data="@card"
Link="@($"/media/tv/episodes/{card.EpisodeId}")"
Title="@card.ShowTitle"
Subtitle="@card.Title"
ContainerClass="media-card-episode-container mx-2"
CardClass="media-card-episode"/>
}
</MudContainer>
}
@code {

13
ErsatzTV/Pages/MediaCollections.razor

@ -23,13 +23,18 @@ @@ -23,13 +23,18 @@
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MediaCollectionViewModel card in _data)
{
<MediaCard Data="@card" Link="@($"/media/collections/{card.Id}")"/>
<MediaCard Data="@card"
Link="@($"/media/collections/{card.Id}")"
ContainerClass="media-card-episode-container mr-4"
CardClass="media-card-episode"/>
}
</MudContainer>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/media/collections/add" Class="mt-4">
Add Media Collection
</MudButton>
<MudContainer MaxWidth="MaxWidth.False">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/media/collections/add" Class="mt-4">
Add Media Collection
</MudButton>
</MudContainer>
@code {
private List<MediaCollectionViewModel> _data;

82
ErsatzTV/Pages/TelevisionEpisode.razor

@ -0,0 +1,82 @@ @@ -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
<MudContainer MaxWidth="MaxWidth.Large">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<MudCard Class="mb-6">
<div style="display: flex; flex-direction: row;">
@if (!string.IsNullOrWhiteSpace(_episode.Poster))
{
<MudPaper style="display: flex; flex-direction: column">
<MudCardMedia Image="@($"/posters/{_episode.Poster}")" Style="flex-grow: 1; height: 220px; width: 392px;"/>
</MudPaper>
}
<MudCardContent Class="ml-3">
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h4">@_episode.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Plot</MudText>
<MudText Style="flex-grow: 1">@_episode.Plot</MudText>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
</div>
</div>
</MudCardContent>
</div>
</MudCard>
</MudContainer>
@code {
[Parameter]
public int EpisodeId { get; set; }
private TelevisionEpisodeViewModel _episode;
private TelevisionSeasonViewModel _season;
private List<BreadcrumbItem> _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<BreadcrumbItem>
{
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<AddToCollectionDialog>("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}");
}
}
}

13
ErsatzTV/Pages/TelevisionEpisodes.razor → ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -13,15 +13,18 @@ @@ -13,15 +13,18 @@
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<MudCard Class="mb-6">
<div style="display: flex; flex-direction: row;">
<MudPaper Style="flex-shrink: 0;">
<MudCardMedia Image="@($"/posters/{_season.Poster}")" Style="height: 440px; width: 304px;"/>
</MudPaper>
<MudCardContent>
@if (!string.IsNullOrWhiteSpace(_season.Poster))
{
<MudPaper Style="flex-shrink: 0;">
<MudCardMedia Image="@($"/posters/{_season.Poster}")" Style="height: 440px; width: 304px;"/>
</MudPaper>
}
<MudCardContent Class="mx-3 my-3">
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h3">@_season.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Year</MudText>
<MudText Style="flex-grow: 1">@_season.Plot</MudText>
<div>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"

11
ErsatzTV/Pages/TelevisionSeasons.razor → ErsatzTV/Pages/TelevisionSeasonList.razor

@ -13,10 +13,13 @@ @@ -13,10 +13,13 @@
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<MudCard Class="mb-6">
<div style="display: flex; flex-direction: row;">
<MudPaper Style="flex-shrink: 0;">
<MudCardMedia Image="@($"/posters/{_show.Poster}")" Style="height: 440px; width: 304px;"/>
</MudPaper>
<MudCardContent>
@if (!string.IsNullOrWhiteSpace(_show.Poster))
{
<MudPaper Style="flex-shrink: 0;">
<MudCardMedia Image="@($"/posters/{_show.Poster}")" Style="height: 440px; width: 304px;"/>
</MudPaper>
}
<MudCardContent Class="mx-3 my-3">
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h3">@_show.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_show.Year</MudText>

0
ErsatzTV/Pages/TelevisionShows.razor → ErsatzTV/Pages/TelevisionShowList.razor

4
ErsatzTV/wwwroot/css/site.css

@ -6,7 +6,7 @@ @@ -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 @@ @@ -28,7 +28,7 @@
width: 152px;
}
.media-card-episode { width: 291px; }
.media-card-episode { width: 392px; }
.media-card:hover { filter: brightness(75%); }

Loading…
Cancel
Save