Browse Source

add television shows to collections

pull/26/head
Jason Dove 6 years ago
parent
commit
59863300c5
  1. 20
      ErsatzTV.Application/MediaCards/Mapper.cs
  2. 9
      ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs
  3. 26
      ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs
  4. 11
      ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs
  5. 16
      ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs
  6. 9
      ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs
  7. 9
      ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollection.cs
  8. 42
      ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollectionHandler.cs
  9. 8
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs
  10. 67
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs
  11. 11
      ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs
  12. 65
      ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs
  13. 10
      ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs
  14. 7
      ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs
  15. 27
      ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs
  16. 11
      ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs
  17. 37
      ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs
  18. 16
      ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs
  19. 22
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  20. 54
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  21. 5
      ErsatzTV.Core/Domain/MediaItem.cs
  22. 5
      ErsatzTV.Core/Domain/MovieMediaItem.cs
  23. 5
      ErsatzTV.Core/Domain/SimpleMediaCollection.cs
  24. 5
      ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs
  25. 8
      ErsatzTV.Core/Domain/TelevisionMediaCollection.cs
  26. 1
      ErsatzTV.Core/Domain/TelevisionSeason.cs
  27. 1
      ErsatzTV.Core/Domain/TelevisionShow.cs
  28. 8
      ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs
  29. 63
      ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs
  30. 5
      ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs
  31. 17
      ErsatzTV.Infrastructure/Data/Configurations/SimpleMediaCollectionConfiguration.cs
  32. 17
      ErsatzTV.Infrastructure/Data/Configurations/TelevisionMediaCollectionConfiguration.cs
  33. 158
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  34. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  35. 1289
      ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.Designer.cs
  36. 212
      ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.cs
  37. 121
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  38. 9
      ErsatzTV/Controllers/Api/MediaCollectionsController.cs
  39. 4
      ErsatzTV/Pages/MediaCollectionEditor.razor
  40. 66
      ErsatzTV/Pages/MediaCollectionItems.razor
  41. 121
      ErsatzTV/Pages/MediaCollectionItemsEditor.razor
  42. 105
      ErsatzTV/Pages/MediaCollections.razor
  43. 28
      ErsatzTV/Pages/TelevisionSeasons.razor
  44. 58
      ErsatzTV/Shared/AddToCollectionDialog.razor
  45. 4
      ErsatzTV/Shared/MediaCard.razor
  46. 1
      ErsatzTV/Startup.cs

20
ErsatzTV.Application/MediaCards/Mapper.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain;
using System.Linq;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaCards
{
@ -21,6 +22,14 @@ namespace ErsatzTV.Application.MediaCards @@ -21,6 +22,14 @@ namespace ErsatzTV.Application.MediaCards
televisionSeason.Poster,
televisionSeason.Number == 0 ? "S" : televisionSeason.Number.ToString());
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
TelevisionEpisodeMediaItem televisionEpisode) =>
new(
televisionEpisode.Metadata.Title,
string.Empty,
televisionEpisode.Metadata.SortTitle,
televisionEpisode.Poster);
internal static MovieCardViewModel ProjectToViewModel(MovieMediaItem movie) =>
new(
movie.Id,
@ -29,6 +38,15 @@ namespace ErsatzTV.Application.MediaCards @@ -29,6 +38,15 @@ namespace ErsatzTV.Application.MediaCards
movie.Metadata.SortTitle,
movie.Poster);
internal static SimpleMediaCollectionCardResultsViewModel
ProjectToViewModel(SimpleMediaCollection collection) =>
new(
collection.Name,
collection.Movies.Map(ProjectToViewModel).ToList(),
collection.TelevisionShows.Map(ProjectToViewModel).ToList(),
collection.TelevisionSeasons.Map(ProjectToViewModel).ToList(),
collection.TelevisionEpisodes.Map(ProjectToViewModel).ToList());
private static string GetSeasonName(int number) =>
number == 0 ? "Specials" : $"Season {number}";
}

9
ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetSimpleMediaCollectionCards
(int Id) : IRequest<Either<BaseError, SimpleMediaCollectionCardResultsViewModel>>;
}

26
ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards.Queries
{
public class GetSimpleMediaCollectionCardsHandler : IRequestHandler<GetSimpleMediaCollectionCards,
Either<BaseError, SimpleMediaCollectionCardResultsViewModel>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetSimpleMediaCollectionCardsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public async Task<Either<BaseError, SimpleMediaCollectionCardResultsViewModel>> Handle(
GetSimpleMediaCollectionCards request,
CancellationToken cancellationToken) =>
(await _mediaCollectionRepository.GetSimpleMediaCollectionWithItemsUntracked(request.Id))
.ToEither(BaseError.New("Unable to load collection"))
.Map(ProjectToViewModel);
}
}

11
ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record SimpleMediaCollectionCardResultsViewModel(
string Name,
List<MovieCardViewModel> MovieCards,
List<TelevisionShowCardViewModel> ShowCards,
List<TelevisionSeasonCardViewModel> SeasonCards,
List<TelevisionEpisodeCardViewModel> EpisodeCards);
}

16
ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs

@ -1,11 +1,17 @@ @@ -1,11 +1,17 @@
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionSeasonCardViewModel
(int TelevisionSeasonId, string Title, string Subtitle, string SortTitle, string Poster, string Placeholder) : MediaCardViewModel(
Title,
Subtitle,
SortTitle,
Poster)
(
int TelevisionSeasonId,
string Title,
string Subtitle,
string SortTitle,
string Poster,
string Placeholder) : MediaCardViewModel(
Title,
Subtitle,
SortTitle,
Poster)
{
}
}

9
ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.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 AddItemsToSimpleMediaCollection
(int MediaCollectionId, List<int> ItemIds) : MediatR.IRequest<Either<BaseError, Unit>>;
}

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

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
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>>;
}

42
ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs → ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollectionHandler.cs

@ -11,13 +11,13 @@ using static LanguageExt.Prelude; @@ -11,13 +11,13 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddItemsToSimpleMediaCollectionHandler : MediatR.IRequestHandler<AddItemsToSimpleMediaCollection,
AddMoviesToSimpleMediaCollectionHandler : MediatR.IRequestHandler<AddMoviesToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMediaItemRepository _mediaItemRepository;
public AddItemsToSimpleMediaCollectionHandler(
public AddMoviesToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
IMediaItemRepository mediaItemRepository)
{
@ -26,18 +26,18 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -26,18 +26,18 @@ namespace ErsatzTV.Application.MediaCollections.Commands
}
public Task<Either<BaseError, Unit>> Handle(
AddItemsToSimpleMediaCollection request,
AddMoviesToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddItemsRequest)
.MapT(ApplyAddMoviesRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddItemsRequest(RequestParameters parameters)
private async Task<Unit> ApplyAddMoviesRequest(RequestParameters parameters)
{
foreach (MediaItem item in parameters.ItemsToAdd.Where(
item => parameters.Collection.Items.All(i => i.Id != item.Id)))
foreach (MovieMediaItem item in parameters.MoviesToAdd.Where(
item => parameters.Collection.Movies.All(i => i.Id != item.Id)))
{
parameters.Collection.Items.Add(item);
parameters.Collection.Movies.Add(item);
}
await _mediaCollectionRepository.Update(parameters.Collection);
@ -46,34 +46,34 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -46,34 +46,34 @@ namespace ErsatzTV.Application.MediaCollections.Commands
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddItemsToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateItems(request))
Validate(AddMoviesToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateMovies(request))
.Apply(
(simpleMediaCollectionToUpdate, itemsToAdd) =>
new RequestParameters(simpleMediaCollectionToUpdate, itemsToAdd));
(simpleMediaCollectionToUpdate, moviesToAdd) =>
new RequestParameters(simpleMediaCollectionToUpdate, moviesToAdd));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddItemsToSimpleMediaCollection updateSimpleMediaCollection) =>
AddMoviesToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, List<MediaItem>>> ValidateItems(
AddItemsToSimpleMediaCollection request) =>
LoadAllMediaItems(request)
.Map(v => v.ToValidation<BaseError>("MediaItem 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<MediaItem>>> LoadAllMediaItems(AddItemsToSimpleMediaCollection request)
private async Task<Option<List<MovieMediaItem>>> LoadAllMovies(AddMoviesToSimpleMediaCollection request)
{
var items = (await request.ItemIds.Map(async id => await _mediaItemRepository.Get(id)).Sequence())
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().ToList();
return items.Somes().OfType<MovieMediaItem>().ToList();
}
private record RequestParameters(SimpleMediaCollection Collection, List<MediaItem> ItemsToAdd);
private record RequestParameters(SimpleMediaCollection Collection, List<MovieMediaItem> MoviesToAdd);
}
}

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

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

67
ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
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
AddTelevisionShowToSimpleMediaCollectionHandler : MediatR.IRequestHandler<
AddTelevisionShowToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddTelevisionShowToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddTelevisionShowToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddTelevisionShowRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddTelevisionShowRequest(RequestParameters parameters)
{
if (parameters.Collection.TelevisionShows.All(s => s.Id != parameters.ShowToAdd.Id))
{
parameters.Collection.TelevisionShows.Add(parameters.ShowToAdd);
await _mediaCollectionRepository.Update(parameters.Collection);
}
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddTelevisionShowToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateShow(request))
.Apply(
(simpleMediaCollectionToUpdate, show) =>
new RequestParameters(simpleMediaCollectionToUpdate, show));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddTelevisionShowToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, TelevisionShow>> ValidateShow(
AddTelevisionShowToSimpleMediaCollection request) =>
LoadTelevisionShow(request)
.Map(v => v.ToValidation<BaseError>("TelevisionShow does not exist"));
private Task<Option<TelevisionShow>> LoadTelevisionShow(AddTelevisionShowToSimpleMediaCollection request) =>
_televisionRepository.GetShow(request.TelevisionShowId);
private record RequestParameters(SimpleMediaCollection Collection, TelevisionShow ShowToAdd);
}
}

11
ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs

@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record ReplaceSimpleMediaCollectionItems
(int MediaCollectionId, List<int> MediaItemIds) : IRequest<Either<BaseError, List<MediaItemViewModel>>>;
}

65
ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs

@ -1,65 +0,0 @@ @@ -1,65 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using LanguageExt.UnsafeValueAccess;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class ReplaceSimpleMediaCollectionItemsHandler : IRequestHandler<ReplaceSimpleMediaCollectionItems,
Either<BaseError, List<MediaItemViewModel>>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMediaItemRepository _mediaItemRepository;
public ReplaceSimpleMediaCollectionItemsHandler(
IMediaCollectionRepository mediaCollectionRepository,
IMediaItemRepository mediaItemRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_mediaItemRepository = mediaItemRepository;
}
public Task<Either<BaseError, List<MediaItemViewModel>>> Handle(
ReplaceSimpleMediaCollectionItems request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(mediaItems => PersistItems(request, mediaItems))
.Bind(v => v.ToEitherAsync());
private async Task<List<MediaItemViewModel>> PersistItems(
ReplaceSimpleMediaCollectionItems request,
List<MediaItem> mediaItems)
{
await _mediaCollectionRepository.ReplaceItems(request.MediaCollectionId, mediaItems);
return mediaItems.Map(MediaItems.Mapper.ProjectToViewModel).ToList();
}
private Task<Validation<BaseError, List<MediaItem>>> Validate(ReplaceSimpleMediaCollectionItems request) =>
MediaCollectionMustExist(request).BindT(_ => MediaItemsMustExist(request));
private async Task<Validation<BaseError, SimpleMediaCollection>> MediaCollectionMustExist(
ReplaceSimpleMediaCollectionItems request) =>
(await _mediaCollectionRepository.GetSimpleMediaCollection(request.MediaCollectionId))
.ToValidation<BaseError>("[MediaCollectionId] does not exist.");
private async Task<Validation<BaseError, List<MediaItem>>> MediaItemsMustExist(
ReplaceSimpleMediaCollectionItems replaceItems)
{
var allMediaItems = (await replaceItems.MediaItemIds.Map(i => _mediaItemRepository.Get(i)).Sequence())
.ToList();
if (allMediaItems.Any(x => x.IsNone))
{
return BaseError.New("[MediaItemId] does not exist");
}
return allMediaItems.Sequence().ValueUnsafe().ToList();
}
}
}

10
ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs

@ -1,4 +1,10 @@ @@ -1,4 +1,10 @@
namespace ErsatzTV.Application.MediaCollections
using ErsatzTV.Application.MediaCards;
namespace ErsatzTV.Application.MediaCollections
{
public record MediaCollectionViewModel(int Id, string Name);
public record MediaCollectionViewModel(int Id, string Name) : MediaCardViewModel(
Name,
string.Empty,
Name,
string.Empty);
}

7
ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public record GetMediaCollectionSummaries(string SearchString) : IRequest<List<MediaCollectionSummaryViewModel>>;
}

27
ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs

@ -1,27 +0,0 @@ @@ -1,27 +0,0 @@
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.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public class
GetMediaCollectionSummariesHandler : IRequestHandler<GetMediaCollectionSummaries,
List<MediaCollectionSummaryViewModel>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetMediaCollectionSummariesHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<List<MediaCollectionSummaryViewModel>> Handle(
GetMediaCollectionSummaries request,
CancellationToken cancellationToken) =>
_mediaCollectionRepository.GetSummaries(request.SearchString)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

11
ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs

@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public record GetSimpleMediaCollectionWithItemsById
(int Id) : IRequest<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>;
}

37
ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs

@ -1,37 +0,0 @@ @@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.MediaCollections.Mapper;
using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public class GetSimpleMediaCollectionWithItemsByIdHandler : IRequestHandler<GetSimpleMediaCollectionWithItemsById,
Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetSimpleMediaCollectionWithItemsByIdHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public async Task<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>> Handle(
GetSimpleMediaCollectionWithItemsById request,
CancellationToken cancellationToken)
{
Option<SimpleMediaCollection> maybeCollection =
await _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(request.Id);
return maybeCollection.Match<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>(
c => Tuple(ProjectToViewModel(c), c.Items.Map(ProjectToSearchViewModel).ToList()),
None);
}
}
}

16
ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs

@ -12,16 +12,10 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -12,16 +12,10 @@ namespace ErsatzTV.Application.MediaSources.Commands
public class
DeleteLocalMediaSourceHandler : IRequestHandler<DeleteLocalMediaSource, Either<BaseError, Task>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMediaSourceRepository _mediaSourceRepository;
public DeleteLocalMediaSourceHandler(
IMediaSourceRepository mediaSourceRepository,
IMediaCollectionRepository mediaCollectionRepository)
{
public DeleteLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
_mediaCollectionRepository = mediaCollectionRepository;
}
public async Task<Either<BaseError, Task>> Handle(
DeleteLocalMediaSource request,
@ -30,14 +24,8 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -30,14 +24,8 @@ namespace ErsatzTV.Application.MediaSources.Commands
.Map(DoDeletion)
.ToEither<Task>();
private async Task DoDeletion(LocalMediaSource mediaSource)
{
private async Task DoDeletion(LocalMediaSource mediaSource) =>
await _mediaSourceRepository.Delete(mediaSource.Id);
if (mediaSource.MediaType == MediaType.TvShow)
{
await _mediaCollectionRepository.DeleteEmptyTelevisionCollections();
}
}
private async Task<Validation<BaseError, LocalMediaSource>> MediaSourceMustExist(
DeleteLocalMediaSource deleteMediaSource) =>

22
ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
@ -11,9 +11,9 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -11,9 +11,9 @@ namespace ErsatzTV.Core.Tests.Fakes
{
public class FakeMediaCollectionRepository : IMediaCollectionRepository
{
private readonly Map<int, List<MediaItem>> _data;
private readonly Map<int, List<MovieMediaItem>> _data;
public FakeMediaCollectionRepository(Map<int, List<MediaItem>> data) => _data = data;
public FakeMediaCollectionRepository(Map<int, List<MovieMediaItem>> data) => _data = data;
public Task<SimpleMediaCollection> Add(SimpleMediaCollection collection) => throw new NotSupportedException();
@ -25,32 +25,20 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -25,32 +25,20 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
throw new NotSupportedException();
public Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id) =>
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItemsUntracked(int id) =>
throw new NotSupportedException();
public Task<List<SimpleMediaCollection>> GetSimpleMediaCollections() => throw new NotSupportedException();
public Task<List<MediaCollection>> GetAll() => throw new NotSupportedException();
public Task<List<MediaCollectionSummary>> GetSummaries(string searchString) =>
throw new NotSupportedException();
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id]).AsTask();
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].OfType<MediaItem>().ToList()).AsTask();
public Task<Option<List<MediaItem>>> GetSimpleMediaCollectionItems(int id) =>
throw new NotSupportedException();
public Task<Option<List<MediaItem>>> GetTelevisionMediaCollectionItems(int id) =>
throw new NotSupportedException();
public Task Update(SimpleMediaCollection collection) => throw new NotSupportedException();
public Task<bool> InsertOrIgnore(TelevisionMediaCollection collection) => throw new NotSupportedException();
public Task<Unit> ReplaceItems(int collectionId, List<MediaItem> mediaItems) =>
throw new NotSupportedException();
public Task Delete(int mediaCollectionId) => throw new NotSupportedException();
public Task DeleteEmptyTelevisionCollections() => throw new NotSupportedException();
}
}

54
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task InitialFlood_Should_StartAtMidnight()
{
var mediaItems = new List<MediaItem>
var mediaItems = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today)
};
@ -59,7 +59,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -59,7 +59,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task InitialFlood_Should_StartAtMidnight_With_LateStart()
{
var mediaItems = new List<MediaItem>
var mediaItems = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today)
};
@ -79,7 +79,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -79,7 +79,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ChronologicalContent_Should_CreateChronologicalItems()
{
var mediaItems = new List<MediaItem>
var mediaItems = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -105,7 +105,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -105,7 +105,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout()
{
var mediaItems = new List<MediaItem>
var mediaItems = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today),
TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1))
@ -142,7 +142,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -142,7 +142,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems()
{
var mediaItems = new List<MediaItem>
var mediaItems = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today),
TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1))
@ -180,7 +180,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -180,7 +180,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ShuffleFloodRebuild_Should_IgnoreAnchors()
{
var mediaItems = new List<MediaItem>
var mediaItems = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(1), DateTime.Today),
TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)),
@ -223,7 +223,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -223,7 +223,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ShuffleFlood_Should_MaintainRandomSeed()
{
var mediaItems = new List<MediaItem>
var mediaItems = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(1), DateTime.Today),
TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)),
@ -259,7 +259,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -259,7 +259,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 1,
Name = "Flood Items",
Items = new List<MediaItem>
Movies = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -270,7 +270,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -270,7 +270,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 2,
Name = "Fixed Items",
Items = new List<MediaItem>
Movies = new List<MovieMediaItem>
{
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1))
}
@ -278,8 +278,8 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -278,8 +278,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
var fakeRepository = new FakeMediaCollectionRepository(
Map(
(floodCollection.Id, floodCollection.Items.ToList()),
(fixedCollection.Id, fixedCollection.Items.ToList())));
(floodCollection.Id, floodCollection.Movies.ToList()),
(fixedCollection.Id, fixedCollection.Movies.ToList())));
var items = new List<ProgramScheduleItem>
{
@ -336,7 +336,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -336,7 +336,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 1,
Name = "Flood Items",
Items = new List<MediaItem>
Movies = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -347,7 +347,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -347,7 +347,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 2,
Name = "Fixed Items",
Items = new List<MediaItem>
Movies = new List<MovieMediaItem>
{
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2))
@ -356,8 +356,8 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -356,8 +356,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
var fakeRepository = new FakeMediaCollectionRepository(
Map(
(floodCollection.Id, floodCollection.Items.ToList()),
(fixedCollection.Id, fixedCollection.Items.ToList())));
(floodCollection.Id, floodCollection.Movies.ToList()),
(fixedCollection.Id, fixedCollection.Movies.ToList())));
var items = new List<ProgramScheduleItem>
{
@ -420,7 +420,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -420,7 +420,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 1,
Name = "Flood Items",
Items = new List<MediaItem>
Movies = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -431,7 +431,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -431,7 +431,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 2,
Name = "Fixed Items",
Items = new List<MediaItem>
Movies = new List<MovieMediaItem>
{
TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)),
TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2))
@ -440,8 +440,8 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -440,8 +440,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
var fakeRepository = new FakeMediaCollectionRepository(
Map(
(floodCollection.Id, floodCollection.Items.ToList()),
(fixedCollection.Id, fixedCollection.Items.ToList())));
(floodCollection.Id, floodCollection.Movies.ToList()),
(fixedCollection.Id, fixedCollection.Movies.ToList())));
var items = new List<ProgramScheduleItem>
{
@ -508,7 +508,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -508,7 +508,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 1,
Name = "Multiple Items",
Items = new List<MediaItem>
Movies = new List<MovieMediaItem>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -519,7 +519,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -519,7 +519,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 2,
Name = "Dynamic Items",
Items = new List<MediaItem>
Movies = new List<MovieMediaItem>
{
TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)),
TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2))
@ -528,8 +528,8 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -528,8 +528,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
var fakeRepository = new FakeMediaCollectionRepository(
Map(
(multipleCollection.Id, multipleCollection.Items.ToList()),
(dynamicCollection.Id, dynamicCollection.Items.ToList())));
(multipleCollection.Id, multipleCollection.Movies.ToList()),
(dynamicCollection.Id, dynamicCollection.Movies.ToList())));
var items = new List<ProgramScheduleItem>
{
@ -603,20 +603,20 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -603,20 +603,20 @@ namespace ErsatzTV.Core.Tests.Scheduling
StartTime = null
};
private static MediaItem TestMovie(int id, TimeSpan duration, DateTime aired) =>
new MovieMediaItem
private static MovieMediaItem TestMovie(int id, TimeSpan duration, DateTime aired) =>
new()
{
Id = id,
Metadata = new MovieMetadata { Premiered = aired },
Statistics = new MediaItemStatistics { Duration = duration }
};
private TestData TestDataFloodForItems(List<MediaItem> mediaItems, PlaybackOrder playbackOrder)
private TestData TestDataFloodForItems(List<MovieMediaItem> mediaItems, PlaybackOrder playbackOrder)
{
var mediaCollection = new SimpleMediaCollection
{
Id = 1,
Items = mediaItems
Movies = mediaItems
};
var collectionRepo = new FakeMediaCollectionRepository(Map((mediaCollection.Id, mediaItems)));

5
ErsatzTV.Core/Domain/MediaItem.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Interfaces.Domain;
namespace ErsatzTV.Core.Domain
@ -8,13 +7,9 @@ namespace ErsatzTV.Core.Domain @@ -8,13 +7,9 @@ namespace ErsatzTV.Core.Domain
{
public int Id { get; set; }
public int MediaSourceId { get; set; }
public MediaSource Source { get; set; }
// public MediaMetadata Metadata { get; set; }
public MediaItemStatistics Statistics { get; set; }
public DateTime? LastWriteTime { get; set; }
public IList<SimpleMediaCollection> SimpleMediaCollections { get; set; }
public string Path { get; set; }
public string Poster { get; set; }
public DateTime? PosterLastWriteTime { get; set; }

5
ErsatzTV.Core/Domain/MovieMediaItem.cs

@ -1,8 +1,11 @@ @@ -1,8 +1,11 @@
namespace ErsatzTV.Core.Domain
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class MovieMediaItem : MediaItem
{
public int MetadataId { get; set; }
public MovieMetadata Metadata { get; set; }
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; }
}
}

5
ErsatzTV.Core/Domain/SimpleMediaCollection.cs

@ -4,6 +4,9 @@ namespace ErsatzTV.Core.Domain @@ -4,6 +4,9 @@ namespace ErsatzTV.Core.Domain
{
public class SimpleMediaCollection : MediaCollection
{
public IList<MediaItem> Items { get; set; }
public List<MovieMediaItem> Movies { get; set; }
public List<TelevisionShow> TelevisionShows { get; set; }
public List<TelevisionSeason> TelevisionSeasons { get; set; }
public List<TelevisionEpisodeMediaItem> TelevisionEpisodes { get; set; }
}
}

5
ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs

@ -1,9 +1,12 @@ @@ -1,9 +1,12 @@
namespace ErsatzTV.Core.Domain
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class TelevisionEpisodeMediaItem : MediaItem
{
public int SeasonId { get; set; }
public TelevisionSeason Season { get; set; }
public TelevisionEpisodeMetadata Metadata { get; set; }
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; }
}
}

8
ErsatzTV.Core/Domain/TelevisionMediaCollection.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
namespace ErsatzTV.Core.Domain
{
public class TelevisionMediaCollection : MediaCollection
{
public string ShowTitle { get; set; }
public int? SeasonNumber { get; set; }
}
}

1
ErsatzTV.Core/Domain/TelevisionSeason.cs

@ -11,6 +11,7 @@ namespace ErsatzTV.Core.Domain @@ -11,6 +11,7 @@ namespace ErsatzTV.Core.Domain
public TelevisionShow TelevisionShow { get; set; }
public int Number { get; set; }
public List<TelevisionEpisodeMediaItem> Episodes { get; set; }
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; }
public string Path { get; set; }
public string Poster { get; set; }
public DateTime? PosterLastWriteTime { get; set; }

1
ErsatzTV.Core/Domain/TelevisionShow.cs

@ -11,5 +11,6 @@ namespace ErsatzTV.Core.Domain @@ -11,5 +11,6 @@ namespace ErsatzTV.Core.Domain
public List<TelevisionSeason> Seasons { get; set; }
public string Poster { get; set; }
public DateTime? PosterLastWriteTime { get; set; }
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; }
}
}

8
ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using LanguageExt;
@ -12,17 +11,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -12,17 +11,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Option<MediaCollection>> Get(int id);
Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id);
Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id);
Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id);
Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItemsUntracked(int id);
Task<List<SimpleMediaCollection>> GetSimpleMediaCollections();
Task<List<MediaCollection>> GetAll();
Task<List<MediaCollectionSummary>> GetSummaries(string searchString);
Task<Option<List<MediaItem>>> GetItems(int id);
Task<Option<List<MediaItem>>> GetSimpleMediaCollectionItems(int id);
Task<Option<List<MediaItem>>> GetTelevisionMediaCollectionItems(int id);
Task Update(SimpleMediaCollection collection);
Task<bool> InsertOrIgnore(TelevisionMediaCollection collection);
Task<Unit> ReplaceItems(int collectionId, List<MediaItem> mediaItems);
Task Delete(int mediaCollectionId);
Task DeleteEmptyTelevisionCollections();
}
}

63
ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs

@ -1,63 +0,0 @@ @@ -1,63 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Metadata
{
public class SmartCollectionBuilder : ISmartCollectionBuilder
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public SmartCollectionBuilder(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public async Task<bool> RefreshSmartCollections(TelevisionEpisodeMediaItem mediaItem)
{
var results = new List<bool>();
foreach (TelevisionMediaCollection collection in GetTelevisionCollections(mediaItem))
{
results.Add(await _mediaCollectionRepository.InsertOrIgnore(collection));
}
return results.Any(identity);
}
private IEnumerable<TelevisionMediaCollection> GetTelevisionCollections(TelevisionEpisodeMediaItem mediaItem)
{
var televisionMediaItems = new List<TelevisionEpisodeMediaItem> { mediaItem };
IEnumerable<TelevisionMediaCollection> televisionShowCollections = televisionMediaItems
.Map(c => c.Metadata.Title)
.Distinct().Map(
t => new TelevisionMediaCollection
{
Name = $"{t} - All Seasons",
ShowTitle = t,
SeasonNumber = null
});
IEnumerable<TelevisionMediaCollection> televisionShowSeasonCollections = televisionMediaItems
.Map(c => new { c.Metadata.Title, c.Metadata.Season }).Distinct()
.Map(
ts =>
{
return Optional(ts.Season).Map(
sn => new TelevisionMediaCollection
{
Name = $"{ts.Title} - Season {sn:00}",
ShowTitle = ts.Title,
SeasonNumber = sn
});
})
.Sequence().Flatten();
return Seq(televisionShowCollections, televisionShowSeasonCollections).Flatten();
}
}
}

5
ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs

@ -6,7 +6,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -6,7 +6,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MediaItemConfiguration : IEntityTypeConfiguration<MediaItem>
{
public void Configure(EntityTypeBuilder<MediaItem> builder) =>
public void Configure(EntityTypeBuilder<MediaItem> builder)
{
builder.ToTable("MediaItems");
builder.OwnsOne(c => c.Statistics).WithOwner();
}
}
}

17
ErsatzTV.Infrastructure/Data/Configurations/SimpleMediaCollectionConfiguration.cs

@ -10,8 +10,21 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -10,8 +10,21 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
builder.ToTable("SimpleMediaCollections");
builder.HasMany(cg => cg.Items)
.WithMany(c => c.SimpleMediaCollections);
builder.HasMany(c => c.Movies)
.WithMany(m => m.SimpleMediaCollections)
.UsingEntity(join => join.ToTable("SimpleMediaCollectionMovies"));
builder.HasMany(c => c.TelevisionShows)
.WithMany(s => s.SimpleMediaCollections)
.UsingEntity(join => join.ToTable("SimpleMediaCollectionShows"));
builder.HasMany(c => c.TelevisionSeasons)
.WithMany(s => s.SimpleMediaCollections)
.UsingEntity(join => join.ToTable("SimpleMediaCollectionSeasons"));
builder.HasMany(c => c.TelevisionEpisodes)
.WithMany(e => e.SimpleMediaCollections)
.UsingEntity(join => join.ToTable("SimpleMediaCollectionEpisodes"));
}
}
}

17
ErsatzTV.Infrastructure/Data/Configurations/TelevisionMediaCollectionConfiguration.cs

@ -1,17 +0,0 @@ @@ -1,17 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class TelevisionMediaCollectionConfiguration : IEntityTypeConfiguration<TelevisionMediaCollection>
{
public void Configure(EntityTypeBuilder<TelevisionMediaCollection> builder)
{
builder.ToTable("TelevisionMediaCollections");
builder.HasIndex(c => new { c.ShowTitle, c.SeasonNumber })
.IsUnique();
}
}
}

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

@ -2,7 +2,6 @@ @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
@ -28,17 +27,34 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -28,17 +27,34 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
_dbContext.MediaCollections.SingleOrDefaultAsync(c => c.Id == id).Map(Optional);
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id) =>
Get(id).Map(c => c.OfType<SimpleMediaCollection>().HeadOrNone());
_dbContext.SimpleMediaCollections
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
_dbContext.SimpleMediaCollections
.Include(s => s.Items)
.Include(s => s.Movies)
.ThenInclude(i => i.Source)
.Include(s => s.TelevisionShows)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionSeasons)
.Include(s => s.TelevisionEpisodes)
.ThenInclude(s => s.Metadata)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id) =>
Get(id).Map(c => c.OfType<TelevisionMediaCollection>().HeadOrNone());
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItemsUntracked(int id) =>
_dbContext.SimpleMediaCollections
.AsNoTracking()
.Include(s => s.Movies)
.ThenInclude(i => i.Source)
.Include(s => s.TelevisionShows)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionSeasons)
.Include(s => s.TelevisionEpisodes)
.ThenInclude(s => s.Metadata)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<List<SimpleMediaCollection>> GetSimpleMediaCollections() =>
_dbContext.SimpleMediaCollections.ToListAsync();
@ -46,112 +62,98 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -46,112 +62,98 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<List<MediaCollection>> GetAll() =>
_dbContext.MediaCollections.ToListAsync();
public Task<List<MediaCollectionSummary>> GetSummaries(string searchString) =>
_dbContext.MediaCollectionSummaries.FromSqlRaw(
@"SELECT mc.Id, mc.Name, Count(mismc.ItemsId) AS ItemCount, true AS IsSimple
FROM MediaCollections mc
INNER JOIN SimpleMediaCollections smc ON smc.Id = mc.Id
LEFT OUTER JOIN MediaItemSimpleMediaCollection mismc ON mismc.SimpleMediaCollectionsId = mc.Id
WHERE mc.Name LIKE {0}
GROUP BY mc.Id, mc.Name
UNION ALL
SELECT mc.Id, mc.Name, Count(tem.Id) AS ItemCount, false AS IsSimple
FROM MediaCollections mc
INNER JOIN TelevisionMediaCollections tmc ON tmc.Id = mc.Id
LEFT OUTER JOIN TelevisionEpisodeMetadata tem ON (tmc.SeasonNumber IS NULL OR tem.Season = tmc.SeasonNumber)
AND tem.Title = tmc.ShowTitle
WHERE mc.Name LIKE {0}
GROUP BY mc.Id, mc.Name",
$"%{searchString}%").ToListAsync();
public Task<Option<List<MediaItem>>> GetItems(int id) =>
Get(id).MapT(
collection => collection switch
{
SimpleMediaCollection s => SimpleItems(s),
TelevisionMediaCollection t => TelevisionItems(t),
_ => throw new NotSupportedException($"Unsupported collection type {collection.GetType().Name}")
}).Bind(x => x.Sequence());
public Task<Option<List<MediaItem>>> GetSimpleMediaCollectionItems(int id) =>
GetSimpleMediaCollection(id).MapT(SimpleItems).Bind(x => x.Sequence());
public Task<Option<List<MediaItem>>> GetTelevisionMediaCollectionItems(int id) =>
GetTelevisionMediaCollection(id).MapT(TelevisionItems).Bind(x => x.Sequence());
public Task Update(SimpleMediaCollection collection)
{
_dbContext.SimpleMediaCollections.Update(collection);
return _dbContext.SaveChangesAsync();
}
public async Task<bool> InsertOrIgnore(TelevisionMediaCollection collection)
public async Task Delete(int mediaCollectionId)
{
if (!_dbContext.TelevisionMediaCollections.Any(
existing => existing.ShowTitle == collection.ShowTitle &&
existing.SeasonNumber == collection.SeasonNumber))
{
await _dbContext.TelevisionMediaCollections.AddAsync(collection);
return await _dbContext.SaveChangesAsync() > 0;
}
// no change
return false;
MediaCollection mediaCollection = await _dbContext.MediaCollections.FindAsync(mediaCollectionId);
_dbContext.MediaCollections.Remove(mediaCollection);
await _dbContext.SaveChangesAsync();
}
public Task<Unit> ReplaceItems(int collectionId, List<MediaItem> mediaItems) =>
GetSimpleMediaCollection(collectionId).IfSomeAsync(
async c =>
{
await SimpleItems(c);
private async Task<List<MediaItem>> SimpleItems(SimpleMediaCollection collection)
{
var result = new List<MediaItem>();
c.Items.Clear();
foreach (MediaItem mediaItem in mediaItems)
{
c.Items.Add(mediaItem);
}
await _dbContext.Entry(collection).Collection(c => c.Movies).LoadAsync();
result.AddRange(collection.Movies);
_dbContext.SimpleMediaCollections.Update(c);
await _dbContext.SaveChangesAsync();
});
result.AddRange(await GetTelevisionShowItems(collection));
result.AddRange(await GetTelevisionSeasonItems(collection));
result.AddRange(await GetTelevisionEpisodeItems(collection));
public async Task Delete(int mediaCollectionId)
{
MediaCollection mediaCollection = await _dbContext.MediaCollections.FindAsync(mediaCollectionId);
_dbContext.MediaCollections.Remove(mediaCollection);
await _dbContext.SaveChangesAsync();
return result.Distinct().ToList();
}
public async Task DeleteEmptyTelevisionCollections()
private async Task<List<MediaItem>> GetTelevisionShowItems(SimpleMediaCollection collection)
{
List<int> ids = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"SELECT mc.Id FROM MediaCollections mc
INNER JOIN TelevisionMediaCollections t on mc.Id = t.Id
WHERE NOT EXISTS
(SELECT 1 FROM MediaItems mi WHERE t.ShowTitle = mi.Metadata_Title AND (t.SeasonNumber IS NULL OR t.SeasonNumber = mi.Metadata_SeasonNumber))")
.Map(i => i.Id)
// TODO: would be nice to get the media items in one go, but ef...
List<int> showItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select tmi.Id
from TelevisionEpisodes tmi
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
inner join TelevisionShows ts on ts.Id = tsn.TelevisionShowId
inner join SimpleMediaCollectionShows s on s.TelevisionShowsId = ts.Id
where s.SimpleMediaCollectionsId = {0}",
collection.Id)
.Select(i => i.Id)
.ToListAsync();
List<MediaCollection> toDelete =
await _dbContext.MediaCollections.Where(mc => ids.Contains(mc.Id)).ToListAsync();
_dbContext.MediaCollections.RemoveRange(toDelete);
await _dbContext.SaveChangesAsync();
return await _dbContext.MediaItems
.AsNoTracking()
.Where(mi => showItemIds.Contains(mi.Id))
.ToListAsync();
}
private async Task<List<MediaItem>> SimpleItems(SimpleMediaCollection collection)
private async Task<List<MediaItem>> GetTelevisionSeasonItems(SimpleMediaCollection collection)
{
await _dbContext.Entry(collection).Collection(c => c.Items).LoadAsync();
return collection.Items.ToList();
// TODO: would be nice to get the media items in one go, but ef...
List<int> seasonItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select tmi.Id
from TelevisionEpisodes tmi
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
inner join SimpleMediaCollectionSeasons s on s.TelevisionSeasonsId = tsn.Id
where s.SimpleMediaCollectionsId = {0}",
collection.Id)
.Select(i => i.Id)
.ToListAsync();
return await _dbContext.MediaItems
.AsNoTracking()
.Where(mi => seasonItemIds.Contains(mi.Id))
.ToListAsync();
}
// TODO: reimplement this
private Task<List<MediaItem>> TelevisionItems(TelevisionMediaCollection collection) =>
new List<MediaItem>().AsTask();
private async Task<List<MediaItem>> GetTelevisionEpisodeItems(SimpleMediaCollection collection)
{
// TODO: would be nice to get the media items in one go, but ef...
List<int> episodeItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select s.TelevisionEpisodesId as Id
from SimpleMediaCollectionEpisodes s
where s.SimpleMediaCollectionsId = {0}",
collection.Id)
.Select(i => i.Id)
.ToListAsync();
// _dbContext.MediaItems
// .Filter(c => c.Metadata.Title == collection.ShowTitle)
// .Filter(c => collection.SeasonNumber == null || c.Metadata.SeasonNumber == collection.SeasonNumber)
// .ToListAsync();
return await _dbContext.MediaItems
.AsNoTracking()
.Where(mi => episodeItemIds.Contains(mi.Id))
.ToListAsync();
}
}
}

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -23,7 +23,6 @@ namespace ErsatzTV.Infrastructure.Data @@ -23,7 +23,6 @@ namespace ErsatzTV.Infrastructure.Data
public DbSet<TelevisionEpisodeMediaItem> TelevisionEpisodeMediaItems { get; set; }
public DbSet<MediaCollection> MediaCollections { get; set; }
public DbSet<SimpleMediaCollection> SimpleMediaCollections { get; set; }
public DbSet<TelevisionMediaCollection> TelevisionMediaCollections { get; set; }
public DbSet<ProgramSchedule> ProgramSchedules { get; set; }
public DbSet<Playout> Playouts { get; set; }
public DbSet<PlayoutItem> PlayoutItems { get; set; }

1289
ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.Designer.cs generated

File diff suppressed because it is too large Load Diff

212
ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.cs

@ -0,0 +1,212 @@ @@ -0,0 +1,212 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class CollectionsRework : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
"MediaItemSimpleMediaCollection");
migrationBuilder.DropTable(
"TelevisionMediaCollections");
migrationBuilder.CreateTable(
"SimpleMediaCollectionEpisodes",
table => new
{
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false),
TelevisionEpisodesId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey(
"PK_SimpleMediaCollectionEpisodes",
x => new { x.SimpleMediaCollectionsId, x.TelevisionEpisodesId });
table.ForeignKey(
"FK_SimpleMediaCollectionEpisodes_SimpleMediaCollections_SimpleMediaCollectionsId",
x => x.SimpleMediaCollectionsId,
"SimpleMediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_SimpleMediaCollectionEpisodes_TelevisionEpisodes_TelevisionEpisodesId",
x => x.TelevisionEpisodesId,
"TelevisionEpisodes",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"SimpleMediaCollectionMovies",
table => new
{
MoviesId = table.Column<int>("INTEGER", nullable: false),
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey(
"PK_SimpleMediaCollectionMovies",
x => new { x.MoviesId, x.SimpleMediaCollectionsId });
table.ForeignKey(
"FK_SimpleMediaCollectionMovies_Movies_MoviesId",
x => x.MoviesId,
"Movies",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_SimpleMediaCollectionMovies_SimpleMediaCollections_SimpleMediaCollectionsId",
x => x.SimpleMediaCollectionsId,
"SimpleMediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"SimpleMediaCollectionSeasons",
table => new
{
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false),
TelevisionSeasonsId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey(
"PK_SimpleMediaCollectionSeasons",
x => new { x.SimpleMediaCollectionsId, x.TelevisionSeasonsId });
table.ForeignKey(
"FK_SimpleMediaCollectionSeasons_SimpleMediaCollections_SimpleMediaCollectionsId",
x => x.SimpleMediaCollectionsId,
"SimpleMediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_SimpleMediaCollectionSeasons_TelevisionSeasons_TelevisionSeasonsId",
x => x.TelevisionSeasonsId,
"TelevisionSeasons",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"SimpleMediaCollectionShows",
table => new
{
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false),
TelevisionShowsId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey(
"PK_SimpleMediaCollectionShows",
x => new { x.SimpleMediaCollectionsId, x.TelevisionShowsId });
table.ForeignKey(
"FK_SimpleMediaCollectionShows_SimpleMediaCollections_SimpleMediaCollectionsId",
x => x.SimpleMediaCollectionsId,
"SimpleMediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_SimpleMediaCollectionShows_TelevisionShows_TelevisionShowsId",
x => x.TelevisionShowsId,
"TelevisionShows",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
"IX_SimpleMediaCollectionEpisodes_TelevisionEpisodesId",
"SimpleMediaCollectionEpisodes",
"TelevisionEpisodesId");
migrationBuilder.CreateIndex(
"IX_SimpleMediaCollectionMovies_SimpleMediaCollectionsId",
"SimpleMediaCollectionMovies",
"SimpleMediaCollectionsId");
migrationBuilder.CreateIndex(
"IX_SimpleMediaCollectionSeasons_TelevisionSeasonsId",
"SimpleMediaCollectionSeasons",
"TelevisionSeasonsId");
migrationBuilder.CreateIndex(
"IX_SimpleMediaCollectionShows_TelevisionShowsId",
"SimpleMediaCollectionShows",
"TelevisionShowsId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
"SimpleMediaCollectionEpisodes");
migrationBuilder.DropTable(
"SimpleMediaCollectionMovies");
migrationBuilder.DropTable(
"SimpleMediaCollectionSeasons");
migrationBuilder.DropTable(
"SimpleMediaCollectionShows");
migrationBuilder.CreateTable(
"MediaItemSimpleMediaCollection",
table => new
{
ItemsId = table.Column<int>("INTEGER", nullable: false),
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey(
"PK_MediaItemSimpleMediaCollection",
x => new { x.ItemsId, x.SimpleMediaCollectionsId });
table.ForeignKey(
"FK_MediaItemSimpleMediaCollection_MediaItems_ItemsId",
x => x.ItemsId,
"MediaItems",
"Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
"FK_MediaItemSimpleMediaCollection_SimpleMediaCollections_SimpleMediaCollectionsId",
x => x.SimpleMediaCollectionsId,
"SimpleMediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"TelevisionMediaCollections",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SeasonNumber = table.Column<int>("INTEGER", nullable: true),
ShowTitle = table.Column<string>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TelevisionMediaCollections", x => x.Id);
table.ForeignKey(
"FK_TelevisionMediaCollections_MediaCollections_Id",
x => x.Id,
"MediaCollections",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
"IX_MediaItemSimpleMediaCollection_SimpleMediaCollectionsId",
"MediaItemSimpleMediaCollection",
"SimpleMediaCollectionsId");
migrationBuilder.CreateIndex(
"IX_TelevisionMediaCollections_ShowTitle_SeasonNumber",
"TelevisionMediaCollections",
new[] { "ShowTitle", "SeasonNumber" },
unique: true);
}
}
}

121
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -666,47 +666,80 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -666,47 +666,80 @@ namespace ErsatzTV.Infrastructure.Migrations
});
modelBuilder.Entity(
"MediaItemSimpleMediaCollection",
"MovieMediaItemSimpleMediaCollection",
b =>
{
b.Property<int>("ItemsId")
b.Property<int>("MoviesId")
.HasColumnType("INTEGER");
b.Property<int>("SimpleMediaCollectionsId")
.HasColumnType("INTEGER");
b.HasKey("ItemsId", "SimpleMediaCollectionsId");
b.HasKey("MoviesId", "SimpleMediaCollectionsId");
b.HasIndex("SimpleMediaCollectionsId");
b.ToTable("MediaItemSimpleMediaCollection");
b.ToTable("SimpleMediaCollectionMovies");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.SimpleMediaCollection",
"SimpleMediaCollectionTelevisionEpisodeMediaItem",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
b.Property<int>("SimpleMediaCollectionsId")
.HasColumnType("INTEGER");
b.ToTable("SimpleMediaCollections");
b.Property<int>("TelevisionEpisodesId")
.HasColumnType("INTEGER");
b.HasKey("SimpleMediaCollectionsId", "TelevisionEpisodesId");
b.HasIndex("TelevisionEpisodesId");
b.ToTable("SimpleMediaCollectionEpisodes");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.TelevisionMediaCollection",
"SimpleMediaCollectionTelevisionSeason",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
b.Property<int>("SimpleMediaCollectionsId")
.HasColumnType("INTEGER");
b.Property<int?>("SeasonNumber")
b.Property<int>("TelevisionSeasonsId")
.HasColumnType("INTEGER");
b.Property<string>("ShowTitle")
.HasColumnType("TEXT");
b.HasKey("SimpleMediaCollectionsId", "TelevisionSeasonsId");
b.HasIndex("ShowTitle", "SeasonNumber")
.IsUnique();
b.HasIndex("TelevisionSeasonsId");
b.ToTable("TelevisionMediaCollections");
b.ToTable("SimpleMediaCollectionSeasons");
});
modelBuilder.Entity(
"SimpleMediaCollectionTelevisionShow",
b =>
{
b.Property<int>("SimpleMediaCollectionsId")
.HasColumnType("INTEGER");
b.Property<int>("TelevisionShowsId")
.HasColumnType("INTEGER");
b.HasKey("SimpleMediaCollectionsId", "TelevisionShowsId");
b.HasIndex("TelevisionShowsId");
b.ToTable("SimpleMediaCollectionShows");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.SimpleMediaCollection",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
b.ToTable("SimpleMediaCollections");
});
modelBuilder.Entity(
@ -1157,12 +1190,12 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1157,12 +1190,12 @@ namespace ErsatzTV.Infrastructure.Migrations
});
modelBuilder.Entity(
"MediaItemSimpleMediaCollection",
"MovieMediaItemSimpleMediaCollection",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
b.HasOne("ErsatzTV.Core.Domain.MovieMediaItem", null)
.WithMany()
.HasForeignKey("ItemsId")
.HasForeignKey("MoviesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -1174,23 +1207,63 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1174,23 +1207,63 @@ namespace ErsatzTV.Infrastructure.Migrations
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.SimpleMediaCollection",
"SimpleMediaCollectionTelevisionEpisodeMediaItem",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.SimpleMediaCollection", "Id")
b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null)
.WithMany()
.HasForeignKey("SimpleMediaCollectionsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", null)
.WithMany()
.HasForeignKey("TelevisionEpisodesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"SimpleMediaCollectionTelevisionSeason",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null)
.WithMany()
.HasForeignKey("SimpleMediaCollectionsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.TelevisionSeason", null)
.WithMany()
.HasForeignKey("TelevisionSeasonsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.TelevisionMediaCollection",
"SimpleMediaCollectionTelevisionShow",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null)
.WithMany()
.HasForeignKey("SimpleMediaCollectionsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", null)
.WithMany()
.HasForeignKey("TelevisionShowsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.SimpleMediaCollection",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.TelevisionMediaCollection", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.SimpleMediaCollection", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

9
ErsatzTV/Controllers/Api/MediaCollectionsController.cs

@ -44,14 +44,5 @@ namespace ErsatzTV.Controllers.Api @@ -44,14 +44,5 @@ namespace ErsatzTV.Controllers.Api
[ProducesResponseType(404)]
public Task<IActionResult> GetItems(int id) =>
_mediator.Send(new GetSimpleMediaCollectionItems(id)).ToActionResult();
[HttpPut("{id}/items")]
[ProducesResponseType(typeof(IEnumerable<MediaItemViewModel>), 200)]
[ProducesResponseType(404)]
public Task<IActionResult> PutItems(
int id,
[Required] [FromBody]
List<int> mediaItemIds) =>
_mediator.Send(new ReplaceSimpleMediaCollectionItems(id, mediaItemIds)).ToActionResult();
}
}

4
ErsatzTV/Pages/MediaCollectionEditor.razor

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
@page "/media/collections/{Id:int}"
@page "/media/collections/{Id:int}/edit"
@page "/media/collections/add"
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands
@ -75,7 +75,7 @@ @@ -75,7 +75,7 @@
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Error saving simple media collection: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("/media/collections"));
() => NavigationManager.NavigateTo($"/media/collections/{Id}"));
}
}

66
ErsatzTV/Pages/MediaCollectionItems.razor

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
@page "/media/collections/{Id:int}"
@using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCards.Queries
@inject NavigationManager NavigationManager
@inject IMediator Mediator
@inject ILogger<MediaCollectionItems> Logger
@inject ISnackbar Snackbar
<div class="mb-6" style="display: flex; flex-direction: row;">
<MudText GutterBottom="true" Typo="Typo.h2">@_data.Name</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Link="@($"/media/collections/{Id}/edit")"
Style="margin-bottom: auto; margin-top: auto;"/>
</div>
@if (_data.MovieCards.Any())
{
<MudText GutterBottom="true" Typo="Typo.h4">Movies</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MovieCardViewModel card in _data.MovieCards)
{
<MediaCard Data="@card" Link="@($"/media/movies/{card.MovieId}")"/>
}
</MudContainer>
}
@if (_data.ShowCards.Any())
{
<MudText GutterBottom="true" Typo="Typo.h4">Television Shows</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionShowCardViewModel card in _data.ShowCards)
{
<MediaCard Data="@card" Link="@($"/media/tv/shows/{card.TelevisionShowId}")"/>
}
</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> *@
@code {
[Parameter]
public int Id { get; set; }
private SimpleMediaCollectionCardResultsViewModel _data;
protected override async Task OnParametersSetAsync() => await RefreshData();
private async Task RefreshData()
{
Either<BaseError, SimpleMediaCollectionCardResultsViewModel> maybeResult =
await Mediator.Send(new GetSimpleMediaCollectionCards(Id));
maybeResult.Match(
result => _data = result,
error => NavigationManager.NavigateTo("404"));
}
}

121
ErsatzTV/Pages/MediaCollectionItemsEditor.razor

@ -1,121 +0,0 @@ @@ -1,121 +0,0 @@
@page "/media/collections/{Id:int}/items"
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.MediaCollections.Queries
@using ErsatzTV.Application.MediaItems
@using ErsatzTV.Application.MediaItems.Queries
@using Unit = LanguageExt.Unit
@inject NavigationManager NavigationManager
@inject IMediator Mediator
@inject ILogger<MediaCollectionItemsEditor> Logger
@inject ISnackbar Snackbar
<MudTable Hover="true" Items="_collectionItems">
<ToolBarContent>
<MudText Typo="Typo.h6">@_mediaCollection.Name Media Items</MudText>
</ToolBarContent>
<HeaderContent>
<MudTh>Source</MudTh>
<MudTh>Type</MudTh>
<MudTh>Title</MudTh>
<MudTh>Duration</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Source">@context.Source</MudTd>
<MudTd DataLabel="Type">@context.MediaType</MudTd>
<MudTd DataLabel="Title">@context.Title</MudTd>
<MudTd DataLabel="Duration">@context.Duration</MudTd>
</RowTemplate>
<PagerContent>
@if (_collectionItems.Any())
{
<MudTablePager/>
}
</PagerContent>
</MudTable>
<MudTable @ref="_table" Hover="true" ServerData="@(new Func<TableState, Task<TableData<MediaItemSearchResultViewModel>>>(ServerReload))" Class="mt-8">
<ToolBarContent>
<MudText Typo="Typo.h6">All Media Items</MudText>
<MudToolBarSpacer/>
<MudTextField T="string" ValueChanged="@OnSearch" Placeholder="Search" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0">
</MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh>Source</MudTh>
<MudTh>Type</MudTh>
<MudTh>Title</MudTh>
<MudTh>Duration</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Source">@context.Source</MudTd>
<MudTd DataLabel="Type">@context.MediaType</MudTd>
<MudTd DataLabel="Title">@context.Title</MudTd>
<MudTd DataLabel="Duration">@context.Duration</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(AddResultsAsync)" Class="mt-4">
Add Results
</MudButton>
@code {
[Parameter]
public int Id { get; set; }
private MediaCollectionViewModel _mediaCollection;
private IEnumerable<MediaItemSearchResultViewModel> _collectionItems;
protected override async Task OnParametersSetAsync() => await LoadMediaCollectionAsync();
private List<int> _mediaItemIds;
private IEnumerable<MediaItemSearchResultViewModel> _pagedData;
private MudTable<MediaItemSearchResultViewModel> _table;
private int _totalItems;
private string _searchString;
private async Task<TableData<MediaItemSearchResultViewModel>> ServerReload(TableState state)
{
List<MediaItemSearchResultViewModel> data = await Mediator.Send(new SearchAllMediaItems(_searchString));
_mediaItemIds = data.Map(c => c.Id).ToList();
_totalItems = data.Count;
_pagedData = data.OrderBy(c => c.Id).Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray();
return new TableData<MediaItemSearchResultViewModel> { TotalItems = _totalItems, Items = _pagedData };
}
private async Task OnSearch(string text)
{
_searchString = text;
await _table.ReloadServerData();
}
private async Task AddResultsAsync()
{
Either<BaseError, Unit> result = await Mediator.Send(new AddItemsToSimpleMediaCollection(Id, _mediaItemIds));
await result.Match(
async _ => await LoadMediaCollectionAsync(),
error =>
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Error adding items to media collection: {Error}", error.Value);
return Task.CompletedTask;
});
}
private async Task LoadMediaCollectionAsync()
{
Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>> maybeResult =
await Mediator.Send(new GetSimpleMediaCollectionWithItemsById(Id));
maybeResult.Match(
result => (_mediaCollection, _collectionItems) = result,
() => NavigationManager.NavigateTo("404"));
}
}

105
ErsatzTV/Pages/MediaCollections.razor

@ -5,67 +5,39 @@ @@ -5,67 +5,39 @@
@inject IDialogService Dialog
@inject IMediator Mediator
<MudTable @ref="_table" Hover="true" ServerData="@(new Func<TableState, Task<TableData<MediaCollectionSummaryViewModel>>>(ServerReload))">
<ToolBarContent>
<MudText Typo="Typo.h6">Media Collections</MudText>
<MudToolBarSpacer/>
<MudTextField T="string" ValueChanged="@OnSearch" Placeholder="Search" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0">
</MudTextField>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
<col style="width: 60px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Media Items</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">
@if (context.IsSimple)
{
@context.Name
}
else
{
<MudText Typo="Typo.body2" Color="Color.Primary">@context.Name</MudText>
}
</MudTd>
<MudTd DataLabel="Media Items">@context.ItemCount</MudTd>
<MudTd>
@if (context.IsSimple)
{
<MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem Icon="@Icons.Material.Filled.Edit" Link="@($"/media/collections/{context.Id}")">
Edit Properties
</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Edit" Link="@($"/media/collections/{context.Id}/items")">
Edit Media Items
</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Delete" OnClick="@(_ => DeleteMediaCollectionAsync(context))">
Delete
</MudMenuItem>
</MudMenu>
}
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
@* @if (context.IsSimple) *@
@* { *@
@* <MudMenu Icon="@Icons.Material.Filled.MoreVert"> *@
@* <MudMenuItem Icon="@Icons.Material.Filled.Edit" Link="@($"/media/collections/{context.Id}")"> *@
@* Edit Properties *@
@* </MudMenuItem> *@
@* <MudMenuItem Icon="@Icons.Material.Filled.Edit" Link="@($"/media/collections/{context.Id}/items")"> *@
@* Edit Media Items *@
@* </MudMenuItem> *@
@* <MudMenuItem Icon="@Icons.Material.Filled.Delete" OnClick="@(_ => DeleteMediaCollectionAsync(context))"> *@
@* Delete *@
@* </MudMenuItem> *@
@* </MudMenu> *@
@* } *@
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MediaCollectionViewModel card in _data)
{
<MediaCard Data="@card" Link="@($"/media/collections/{card.Id}")"/>
}
</MudContainer>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/media/collections/add" Class="mt-4">
Add Media Collection
</MudButton>
@code {
private IEnumerable<MediaCollectionSummaryViewModel> _pagedData;
private MudTable<MediaCollectionSummaryViewModel> _table;
private List<MediaCollectionViewModel> _data;
protected override Task OnParametersSetAsync() => RefreshData();
private int _totalItems;
private string _searchString;
private async Task RefreshData() =>
_data = await Mediator.Send(new GetAllMediaCollections());
private async Task DeleteMediaCollectionAsync(MediaCollectionSummaryViewModel mediaCollection)
{
@ -79,30 +51,9 @@ @@ -79,30 +51,9 @@
if (!result.Cancelled)
{
await Mediator.Send(new DeleteSimpleMediaCollection(mediaCollection.Id));
await _table.ReloadServerData();
await RefreshData();
}
}
}
private async Task<TableData<MediaCollectionSummaryViewModel>> ServerReload(TableState state)
{
List<MediaCollectionSummaryViewModel> aggregateData =
await Mediator.Send(new GetMediaCollectionSummaries(_searchString));
_totalItems = aggregateData.Count;
_pagedData = aggregateData
.Skip(_totalItems <= state.PageSize ? 0 : state.Page * state.PageSize)
.Take(state.PageSize)
.OrderBy(c => c.Name);
return new TableData<MediaCollectionSummaryViewModel> { TotalItems = _totalItems, Items = _pagedData };
}
private async Task OnSearch(string text)
{
_searchString = text;
await _table.ReloadServerData();
}
}

28
ErsatzTV/Pages/TelevisionSeasons.razor

@ -4,7 +4,11 @@ @@ -4,7 +4,11 @@
@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
<MudContainer MaxWidth="MaxWidth.Large">
<MudCard Class="mb-6">
@ -18,9 +22,15 @@ @@ -18,9 +22,15 @@
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_show.Year</MudText>
<MudText Style="flex-grow: 1">@_show.Plot</MudText>
<div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Disabled="true">
Add To Schedule
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
@* <MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Disabled="true"> *@
@* Add To Schedule *@
@* </MudButton> *@
</div>
</div>
</MudCardContent>
@ -61,4 +71,18 @@ @@ -61,4 +71,18 @@
Optional(_data.Cards.SingleOrDefault(d => d.SortTitle == sortTitle))
.Match(vm => vm.Placeholder, () => sortTitle.Substring(0, 1).ToUpperInvariant());
private async Task AddToCollection()
{
var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.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 AddTelevisionShowToSimpleMediaCollection(collection.Id, ShowId));
NavigationManager.NavigateTo($"/media/collections/{collection.Id}");
}
}
}

58
ErsatzTV/Shared/AddToCollectionDialog.razor

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Queries
@inject IMediator Mediator
<MudDialog>
<DialogContent>
<MudContainer Class="mb-6">
<MudHighlighter Class="mud-primary-text"
Style="background-color: transparent; font-weight: bold"
Text="@FormatText()"
HighlightedText="@EntityName"/>
</MudContainer>
<MudSelect Label="Collection" @bind-Value="_selectedCollection" For="@(() => _selectedCollection)" Class="mb-6 mx-4">
@foreach (MediaCollectionViewModel collection in _collections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="@(_selectedCollection == null)" OnClick="Submit">
Add To Collection
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
MudDialogInstance MudDialog { get; set; }
[Parameter]
public string EntityType { get; set; }
[Parameter]
public string EntityName { get; set; }
[Parameter]
public string DetailText { get; set; }
[Parameter]
public string DetailHighlight { get; set; }
private List<MediaCollectionViewModel> _collections;
private MediaCollectionViewModel _selectedCollection;
protected override async Task OnParametersSetAsync() =>
_collections = await Mediator.Send(new GetAllSimpleMediaCollections());
private string FormatText() => $"Select the collection to add the {EntityType} {EntityName}";
private void Submit() => MudDialog.Close(DialogResult.Ok(_selectedCollection));
private void Cancel() => MudDialog.Cancel();
}

4
ErsatzTV/Shared/MediaCard.razor

@ -45,7 +45,7 @@ @@ -45,7 +45,7 @@
[Parameter]
public EventCallback<Unit> DataRefreshed { get; set; }
[Parameter]
public Func<string, string> CustomPlaceholder { get; set; }
@ -55,7 +55,7 @@ @@ -55,7 +55,7 @@
{
return CustomPlaceholder(sortTitle);
}
string first = sortTitle.Substring(0, 1).ToUpperInvariant();
return int.TryParse(first, out _) ? "#" : first;
}

1
ErsatzTV/Startup.cs

@ -174,7 +174,6 @@ namespace ErsatzTV @@ -174,7 +174,6 @@ namespace ErsatzTV
services.AddScoped<ITelevisionRepository, TelevisionRepository>();
services.AddScoped<IMovieRepository, MovieRepository>();
services.AddScoped<IFFmpegLocator, FFmpegLocator>();
services.AddScoped<ISmartCollectionBuilder, SmartCollectionBuilder>();
services.AddScoped<ILocalMetadataProvider, LocalMetadataProvider>();
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>();

Loading…
Cancel
Save