Browse Source

add movies to collections

pull/26/head
Jason Dove 6 years ago
parent
commit
348cc37d8b
  1. 8
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollection.cs
  2. 62
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs
  3. 9
      ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollection.cs
  4. 79
      ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollectionHandler.cs
  5. 10
      ErsatzTV.Application/Movies/Mapper.cs
  6. 4
      ErsatzTV.Application/Movies/MovieViewModel.cs
  7. 7
      ErsatzTV.Application/Movies/Queries/GetMovieById.cs
  8. 22
      ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs
  9. 1
      ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs
  10. 3
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  11. 4
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  12. 8
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  13. 4
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  14. 6
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  15. 1
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  16. 76
      ErsatzTV/Pages/Movie.razor
  17. 4
      ErsatzTV/Pages/MovieList.razor

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

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

62
ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
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
AddMovieToSimpleMediaCollectionHandler : MediatR.IRequestHandler<AddMovieToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository;
public AddMovieToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
IMovieRepository movieRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_movieRepository = movieRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddMovieToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddMoviesRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddMoviesRequest(RequestParameters parameters)
{
parameters.Collection.Movies.Add(parameters.MovieToAdd);
await _mediaCollectionRepository.Update(parameters.Collection);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddMovieToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateMovies(request))
.Apply(
(simpleMediaCollectionToUpdate, movieToAdd) =>
new RequestParameters(simpleMediaCollectionToUpdate, movieToAdd));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddMovieToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, MovieMediaItem>> ValidateMovies(
AddMovieToSimpleMediaCollection request) =>
LoadMovie(request)
.Map(v => v.ToValidation<BaseError>("MovieMediaItem does not exist"));
private Task<Option<MovieMediaItem>> LoadMovie(AddMovieToSimpleMediaCollection request) =>
_movieRepository.GetMovie(request.MovieId);
private record RequestParameters(SimpleMediaCollection Collection, MovieMediaItem MovieToAdd);
}
}

9
ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollection.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record AddMoviesToSimpleMediaCollection
(int MediaCollectionId, List<int> MovieIds) : MediatR.IRequest<Either<BaseError, Unit>>;
}

79
ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollectionHandler.cs

@ -1,79 +0,0 @@ @@ -1,79 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddMoviesToSimpleMediaCollectionHandler : MediatR.IRequestHandler<AddMoviesToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMediaItemRepository _mediaItemRepository;
public AddMoviesToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
IMediaItemRepository mediaItemRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_mediaItemRepository = mediaItemRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddMoviesToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddMoviesRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddMoviesRequest(RequestParameters parameters)
{
foreach (MovieMediaItem item in parameters.MoviesToAdd.Where(
item => parameters.Collection.Movies.All(i => i.Id != item.Id)))
{
parameters.Collection.Movies.Add(item);
}
await _mediaCollectionRepository.Update(parameters.Collection);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddMoviesToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateMovies(request))
.Apply(
(simpleMediaCollectionToUpdate, moviesToAdd) =>
new RequestParameters(simpleMediaCollectionToUpdate, moviesToAdd));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddMoviesToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, List<MovieMediaItem>>> ValidateMovies(
AddMoviesToSimpleMediaCollection request) =>
LoadAllMovies(request)
.Map(v => v.ToValidation<BaseError>("MovieMediaItem does not exist"));
private async Task<Option<List<MovieMediaItem>>> LoadAllMovies(AddMoviesToSimpleMediaCollection request)
{
var items = (await request.MovieIds.Map(async id => await _mediaItemRepository.Get(id)).Sequence())
.ToList();
if (items.Any(i => i.IsNone))
{
return None;
}
return items.Somes().OfType<MovieMediaItem>().ToList();
}
private record RequestParameters(SimpleMediaCollection Collection, List<MovieMediaItem> MoviesToAdd);
}
}

10
ErsatzTV.Application/Movies/Mapper.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Movies
{
internal static class Mapper
{
internal static MovieViewModel ProjectToViewModel(MovieMediaItem movie) =>
new(movie.Metadata.Title, movie.Metadata.Year?.ToString(), movie.Metadata.Plot, movie.Poster);
}
}

4
ErsatzTV.Application/Movies/MovieViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Movies
{
public record MovieViewModel(string Title, string Year, string Plot, string Poster);
}

7
ErsatzTV.Application/Movies/Queries/GetMovieById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Movies.Queries
{
public record GetMovieById(int Id) : IRequest<Option<MovieViewModel>>;
}

22
ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Movies.Mapper;
namespace ErsatzTV.Application.Movies.Queries
{
public class GetMovieByIdHandler : IRequestHandler<GetMovieById, Option<MovieViewModel>>
{
private readonly IMovieRepository _movieRepository;
public GetMovieByIdHandler(IMovieRepository movieRepository) =>
_movieRepository = movieRepository;
public Task<Option<MovieViewModel>> Handle(
GetMovieById request,
CancellationToken cancellationToken) =>
_movieRepository.GetMovie(request.Id).MapT(ProjectToViewModel);
}
}

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

@ -7,6 +7,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -7,6 +7,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{
public interface IMovieRepository
{
Task<Option<MovieMediaItem>> GetMovie(int movieId);
Task<Either<BaseError, MovieMediaItem>> GetOrAdd(int mediaSourceId, string path);
Task<bool> Update(MovieMediaItem movie);
Task<int> GetMovieCount();

3
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -64,7 +64,8 @@ namespace ErsatzTV.Core.Metadata @@ -64,7 +64,8 @@ namespace ErsatzTV.Core.Metadata
try
{
if (mediaItem.Statistics is null ||
mediaItem.LastWriteTime < _localFileSystem.GetLastWriteTime(mediaItem.Path))
(mediaItem.Statistics.LastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(mediaItem.Path))
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", mediaItem.Path);
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);

4
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -15,14 +15,17 @@ namespace ErsatzTV.Core.Metadata @@ -15,14 +15,17 @@ namespace ErsatzTV.Core.Metadata
{
public class LocalStatisticsProvider : ILocalStatisticsProvider
{
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<LocalStatisticsProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository;
public LocalStatisticsProvider(
IMediaItemRepository mediaItemRepository,
ILocalFileSystem localFileSystem,
ILogger<LocalStatisticsProvider> logger)
{
_mediaItemRepository = mediaItemRepository;
_localFileSystem = localFileSystem;
_logger = logger;
}
@ -52,6 +55,7 @@ namespace ErsatzTV.Core.Metadata @@ -52,6 +55,7 @@ namespace ErsatzTV.Core.Metadata
bool durationChange = mediaItem.Statistics.Duration != statistics.Duration;
mediaItem.Statistics.LastWriteTime = _localFileSystem.GetLastWriteTime(mediaItem.Path);
mediaItem.Statistics.Duration = statistics.Duration;
mediaItem.Statistics.AudioCodec = statistics.AudioCodec;
mediaItem.Statistics.SampleAspectRatio = statistics.SampleAspectRatio;

8
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -71,7 +71,8 @@ namespace ErsatzTV.Core.Metadata @@ -71,7 +71,8 @@ namespace ErsatzTV.Core.Metadata
async nfoFile =>
{
if (movie.Metadata == null || movie.Metadata.Source == MetadataSource.Fallback ||
movie.Metadata.LastWriteTime < _localFileSystem.GetLastWriteTime(nfoFile))
(movie.Metadata.LastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(nfoFile))
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile);
await _localMetadataProvider.RefreshSidecarMetadata(movie, nfoFile);
@ -102,10 +103,11 @@ namespace ErsatzTV.Core.Metadata @@ -102,10 +103,11 @@ namespace ErsatzTV.Core.Metadata
async posterFile =>
{
if (string.IsNullOrWhiteSpace(movie.Poster) ||
movie.PosterLastWriteTime < _localFileSystem.GetLastWriteTime(posterFile))
(movie.PosterLastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(posterFile))
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile);
await SavePosterToDisk(movie, posterFile, _movieRepository.Update);
await SavePosterToDisk(movie, posterFile, _movieRepository.Update, 440);
}
});

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

@ -34,7 +34,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -34,7 +34,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
_dbContext.SimpleMediaCollections
.Include(s => s.Movies)
.ThenInclude(i => i.Source)
.ThenInclude(m => m.Source)
.Include(s => s.TelevisionShows)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionSeasons)
@ -48,6 +48,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -48,6 +48,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.AsNoTracking()
.Include(s => s.Movies)
.ThenInclude(i => i.Source)
.Include(s => s.Movies)
.ThenInclude(m => m.Metadata)
.Include(s => s.TelevisionShows)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionSeasons)

6
ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs

@ -17,6 +17,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -17,6 +17,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public MovieRepository(TvContext dbContext) => _dbContext = dbContext;
public Task<Option<MovieMediaItem>> GetMovie(int movieId) =>
_dbContext.MovieMediaItems
.Include(m => m.Metadata)
.SingleOrDefaultAsync(m => m.Id == movieId)
.Map(Optional);
public async Task<Either<BaseError, MovieMediaItem>> GetOrAdd(int mediaSourceId, string path)
{
Option<MovieMediaItem> maybeExisting = await _dbContext.MovieMediaItems

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

@ -198,6 +198,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -198,6 +198,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
Option<TelevisionEpisodeMediaItem> maybeExisting = await _dbContext.TelevisionEpisodeMediaItems
.Include(i => i.Metadata)
.Include(i => i.Statistics)
.SingleOrDefaultAsync(i => i.Path == path);
return await maybeExisting.Match(

76
ErsatzTV/Pages/Movie.razor

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
@page "/media/movies/{MovieId:int}"
@using ErsatzTV.Application.Movies
@using ErsatzTV.Application.Movies.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(_movie.Poster))
{
<MudPaper Style="flex-shrink: 0;">
<MudCardMedia Image="@($"/posters/{_movie.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">@_movie.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_movie.Year</MudText>
<MudText Style="flex-grow: 1">@_movie.Plot</MudText>
<div>
<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 MovieId { get; set; }
private MovieViewModel _movie;
private List<BreadcrumbItem> _breadcrumbs;
protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData()
{
await Mediator.Send(new GetMovieById(MovieId))
.IfSomeAsync(vm => _movie = vm);
_breadcrumbs = new List<BreadcrumbItem>
{
new("Movies", "/media/movies"),
new($"{_movie.Title} ({_movie.Year})", null, true)
};
}
private async Task AddToCollection()
{
var parameters = new DialogParameters { { "EntityType", "movie" }, { "EntityName", _movie.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 AddMovieToSimpleMediaCollection(collection.Id, MovieId));
NavigationManager.NavigateTo($"/media/collections/{collection.Id}");
}
}
}

4
ErsatzTV/Pages/Movies.razor → ErsatzTV/Pages/MovieList.razor

@ -22,7 +22,9 @@ @@ -22,7 +22,9 @@
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MovieCardViewModel card in _data.Cards)
{
<MediaCard Data="@card" DataRefreshed="@(() => RefreshData())"/>
<MediaCard Data="@card"
DataRefreshed="@(() => RefreshData())"
Link="@($"/media/movies/{card.MovieId}")"/>
}
</MudContainer>
Loading…
Cancel
Save