diff --git a/ErsatzTV.Application/MediaCards/Mapper.cs b/ErsatzTV.Application/MediaCards/Mapper.cs index 671d17169..b8a3148cd 100644 --- a/ErsatzTV.Application/MediaCards/Mapper.cs +++ b/ErsatzTV.Application/MediaCards/Mapper.cs @@ -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 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 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}"; } diff --git a/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs new file mode 100644 index 000000000..d99fc0ea0 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core; +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public record GetSimpleMediaCollectionCards + (int Id) : IRequest>; +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs new file mode 100644 index 000000000..07393be8a --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs @@ -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> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public GetSimpleMediaCollectionCardsHandler(IMediaCollectionRepository mediaCollectionRepository) => + _mediaCollectionRepository = mediaCollectionRepository; + + public async Task> Handle( + GetSimpleMediaCollectionCards request, + CancellationToken cancellationToken) => + (await _mediaCollectionRepository.GetSimpleMediaCollectionWithItemsUntracked(request.Id)) + .ToEither(BaseError.New("Unable to load collection")) + .Map(ProjectToViewModel); + } +} diff --git a/ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs new file mode 100644 index 000000000..0ac4bebb9 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Application.MediaCards +{ + public record SimpleMediaCollectionCardResultsViewModel( + string Name, + List MovieCards, + List ShowCards, + List SeasonCards, + List EpisodeCards); +} diff --git a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs index 3229aaf5f..ff714980c 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs @@ -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) { } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs deleted file mode 100644 index a3d1a2d77..000000000 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; - -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddItemsToSimpleMediaCollection - (int MediaCollectionId, List ItemIds) : MediatR.IRequest>; -} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollection.cs new file mode 100644 index 000000000..8ca7687a5 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollection.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddMoviesToSimpleMediaCollection + (int MediaCollectionId, List MovieIds) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollectionHandler.cs similarity index 57% rename from ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs rename to ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollectionHandler.cs index a94211d8e..f56730eec 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMoviesToSimpleMediaCollectionHandler.cs @@ -11,13 +11,13 @@ using static LanguageExt.Prelude; namespace ErsatzTV.Application.MediaCollections.Commands { public class - AddItemsToSimpleMediaCollectionHandler : MediatR.IRequestHandler> { 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 } public Task> Handle( - AddItemsToSimpleMediaCollection request, + AddMoviesToSimpleMediaCollection request, CancellationToken cancellationToken) => Validate(request) - .MapT(ApplyAddItemsRequest) + .MapT(ApplyAddMoviesRequest) .Bind(v => v.ToEitherAsync()); - private async Task ApplyAddItemsRequest(RequestParameters parameters) + private async Task 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 } private async Task> - 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> SimpleMediaCollectionMustExist( - AddItemsToSimpleMediaCollection updateSimpleMediaCollection) => + AddMoviesToSimpleMediaCollection updateSimpleMediaCollection) => _mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId) .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); - private Task>> ValidateItems( - AddItemsToSimpleMediaCollection request) => - LoadAllMediaItems(request) - .Map(v => v.ToValidation("MediaItem does not exist")); + private Task>> ValidateMovies( + AddMoviesToSimpleMediaCollection request) => + LoadAllMovies(request) + .Map(v => v.ToValidation("MovieMediaItem does not exist")); - private async Task>> LoadAllMediaItems(AddItemsToSimpleMediaCollection request) + private async Task>> 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().ToList(); } - private record RequestParameters(SimpleMediaCollection Collection, List ItemsToAdd); + private record RequestParameters(SimpleMediaCollection Collection, List MoviesToAdd); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs new file mode 100644 index 000000000..faf5eb3f5 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddTelevisionShowToSimpleMediaCollection + (int MediaCollectionId, int TelevisionShowId) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs new file mode 100644 index 000000000..77197f624 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs @@ -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> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly ITelevisionRepository _televisionRepository; + + public AddTelevisionShowToSimpleMediaCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + ITelevisionRepository televisionRepository) + { + _mediaCollectionRepository = mediaCollectionRepository; + _televisionRepository = televisionRepository; + } + + public Task> Handle( + AddTelevisionShowToSimpleMediaCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(ApplyAddTelevisionShowRequest) + .Bind(v => v.ToEitherAsync()); + + private async Task 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> + Validate(AddTelevisionShowToSimpleMediaCollection request) => + (await SimpleMediaCollectionMustExist(request), await ValidateShow(request)) + .Apply( + (simpleMediaCollectionToUpdate, show) => + new RequestParameters(simpleMediaCollectionToUpdate, show)); + + private Task> SimpleMediaCollectionMustExist( + AddTelevisionShowToSimpleMediaCollection updateSimpleMediaCollection) => + _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) + .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); + + private Task> ValidateShow( + AddTelevisionShowToSimpleMediaCollection request) => + LoadTelevisionShow(request) + .Map(v => v.ToValidation("TelevisionShow does not exist")); + + private Task> LoadTelevisionShow(AddTelevisionShowToSimpleMediaCollection request) => + _televisionRepository.GetShow(request.TelevisionShowId); + + private record RequestParameters(SimpleMediaCollection Collection, TelevisionShow ShowToAdd); + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs b/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs deleted file mode 100644 index 84c4c14c4..000000000 --- a/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs +++ /dev/null @@ -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 MediaItemIds) : IRequest>>; -} diff --git a/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs deleted file mode 100644 index 5c7c79965..000000000 --- a/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs +++ /dev/null @@ -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>> - { - private readonly IMediaCollectionRepository _mediaCollectionRepository; - private readonly IMediaItemRepository _mediaItemRepository; - - public ReplaceSimpleMediaCollectionItemsHandler( - IMediaCollectionRepository mediaCollectionRepository, - IMediaItemRepository mediaItemRepository) - { - _mediaCollectionRepository = mediaCollectionRepository; - _mediaItemRepository = mediaItemRepository; - } - - public Task>> Handle( - ReplaceSimpleMediaCollectionItems request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(mediaItems => PersistItems(request, mediaItems)) - .Bind(v => v.ToEitherAsync()); - - private async Task> PersistItems( - ReplaceSimpleMediaCollectionItems request, - List mediaItems) - { - await _mediaCollectionRepository.ReplaceItems(request.MediaCollectionId, mediaItems); - return mediaItems.Map(MediaItems.Mapper.ProjectToViewModel).ToList(); - } - - private Task>> Validate(ReplaceSimpleMediaCollectionItems request) => - MediaCollectionMustExist(request).BindT(_ => MediaItemsMustExist(request)); - - private async Task> MediaCollectionMustExist( - ReplaceSimpleMediaCollectionItems request) => - (await _mediaCollectionRepository.GetSimpleMediaCollection(request.MediaCollectionId)) - .ToValidation("[MediaCollectionId] does not exist."); - - private async Task>> 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(); - } - } -} diff --git a/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs b/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs index add855bf0..7bfe6e0c4 100644 --- a/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs @@ -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); } diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs b/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs deleted file mode 100644 index d48c716dc..000000000 --- a/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Collections.Generic; -using MediatR; - -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetMediaCollectionSummaries(string SearchString) : IRequest>; -} diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs deleted file mode 100644 index b08800aeb..000000000 --- a/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs +++ /dev/null @@ -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> - { - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public GetMediaCollectionSummariesHandler(IMediaCollectionRepository mediaCollectionRepository) => - _mediaCollectionRepository = mediaCollectionRepository; - - public Task> Handle( - GetMediaCollectionSummaries request, - CancellationToken cancellationToken) => - _mediaCollectionRepository.GetSummaries(request.SearchString) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs b/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs deleted file mode 100644 index 470ff51a2..000000000 --- a/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs +++ /dev/null @@ -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>>>; -} diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs deleted file mode 100644 index 11f9f79b3..000000000 --- a/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs +++ /dev/null @@ -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>>> - { - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public GetSimpleMediaCollectionWithItemsByIdHandler(IMediaCollectionRepository mediaCollectionRepository) => - _mediaCollectionRepository = mediaCollectionRepository; - - public async Task>>> Handle( - GetSimpleMediaCollectionWithItemsById request, - CancellationToken cancellationToken) - { - Option maybeCollection = - await _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(request.Id); - - return maybeCollection.Match>>>( - c => Tuple(ProjectToViewModel(c), c.Items.Map(ProjectToSearchViewModel).ToList()), - None); - } - } -} diff --git a/ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs b/ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs index 8e9fd604e..26eff7b38 100644 --- a/ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs @@ -12,16 +12,10 @@ namespace ErsatzTV.Application.MediaSources.Commands public class DeleteLocalMediaSourceHandler : IRequestHandler> { - 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> Handle( DeleteLocalMediaSource request, @@ -30,14 +24,8 @@ namespace ErsatzTV.Application.MediaSources.Commands .Map(DoDeletion) .ToEither(); - 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> MediaSourceMustExist( DeleteLocalMediaSource deleteMediaSource) => diff --git a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs index 45f90a965..62142b4b7 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs @@ -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 { public class FakeMediaCollectionRepository : IMediaCollectionRepository { - private readonly Map> _data; + private readonly Map> _data; - public FakeMediaCollectionRepository(Map> data) => _data = data; + public FakeMediaCollectionRepository(Map> data) => _data = data; public Task Add(SimpleMediaCollection collection) => throw new NotSupportedException(); @@ -25,32 +25,20 @@ namespace ErsatzTV.Core.Tests.Fakes public Task> GetSimpleMediaCollectionWithItems(int id) => throw new NotSupportedException(); - public Task> GetTelevisionMediaCollection(int id) => + public Task> GetSimpleMediaCollectionWithItemsUntracked(int id) => throw new NotSupportedException(); public Task> GetSimpleMediaCollections() => throw new NotSupportedException(); public Task> GetAll() => throw new NotSupportedException(); - public Task> GetSummaries(string searchString) => - throw new NotSupportedException(); - - public Task>> GetItems(int id) => Some(_data[id]).AsTask(); + public Task>> GetItems(int id) => Some(_data[id].OfType().ToList()).AsTask(); public Task>> GetSimpleMediaCollectionItems(int id) => throw new NotSupportedException(); - public Task>> GetTelevisionMediaCollectionItems(int id) => - throw new NotSupportedException(); - public Task Update(SimpleMediaCollection collection) => throw new NotSupportedException(); - public Task InsertOrIgnore(TelevisionMediaCollection collection) => throw new NotSupportedException(); - - public Task ReplaceItems(int collectionId, List mediaItems) => - throw new NotSupportedException(); - public Task Delete(int mediaCollectionId) => throw new NotSupportedException(); - public Task DeleteEmptyTelevisionCollections() => throw new NotSupportedException(); } } diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 1e5d45013..5638f92c2 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task InitialFlood_Should_StartAtMidnight() { - var mediaItems = new List + var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) }; @@ -59,7 +59,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task InitialFlood_Should_StartAtMidnight_With_LateStart() { - var mediaItems = new List + var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) }; @@ -79,7 +79,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task ChronologicalContent_Should_CreateChronologicalItems() { - var mediaItems = new List + var mediaItems = new List { 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 [Test] public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() { - var mediaItems = new List + var mediaItems = new List { 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 [Test] public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems() { - var mediaItems = new List + var mediaItems = new List { 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 [Test] public async Task ShuffleFloodRebuild_Should_IgnoreAnchors() { - var mediaItems = new List + var mediaItems = new List { 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 [Test] public async Task ShuffleFlood_Should_MaintainRandomSeed() { - var mediaItems = new List + var mediaItems = new List { 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 { Id = 1, Name = "Flood Items", - Items = new List + Movies = new List { 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 { Id = 2, Name = "Fixed Items", - Items = new List + Movies = new List { TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)) } @@ -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 { @@ -336,7 +336,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 1, Name = "Flood Items", - Items = new List + Movies = new List { 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 { Id = 2, Name = "Fixed Items", - Items = new List + Movies = new List { 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 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 { @@ -420,7 +420,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 1, Name = "Flood Items", - Items = new List + Movies = new List { 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 { Id = 2, Name = "Fixed Items", - Items = new List + Movies = new List { 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 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 { @@ -508,7 +508,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 1, Name = "Multiple Items", - Items = new List + Movies = new List { 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 { Id = 2, Name = "Dynamic Items", - Items = new List + Movies = new List { 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 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 { @@ -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 mediaItems, PlaybackOrder playbackOrder) + private TestData TestDataFloodForItems(List mediaItems, PlaybackOrder playbackOrder) { var mediaCollection = new SimpleMediaCollection { Id = 1, - Items = mediaItems + Movies = mediaItems }; var collectionRepo = new FakeMediaCollectionRepository(Map((mediaCollection.Id, mediaItems))); diff --git a/ErsatzTV.Core/Domain/MediaItem.cs b/ErsatzTV.Core/Domain/MediaItem.cs index 4501a4a38..a17b305e4 100644 --- a/ErsatzTV.Core/Domain/MediaItem.cs +++ b/ErsatzTV.Core/Domain/MediaItem.cs @@ -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 { 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 SimpleMediaCollections { get; set; } public string Path { get; set; } public string Poster { get; set; } public DateTime? PosterLastWriteTime { get; set; } diff --git a/ErsatzTV.Core/Domain/MovieMediaItem.cs b/ErsatzTV.Core/Domain/MovieMediaItem.cs index b521540fc..9b2d44fee 100644 --- a/ErsatzTV.Core/Domain/MovieMediaItem.cs +++ b/ErsatzTV.Core/Domain/MovieMediaItem.cs @@ -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 SimpleMediaCollections { get; set; } } } diff --git a/ErsatzTV.Core/Domain/SimpleMediaCollection.cs b/ErsatzTV.Core/Domain/SimpleMediaCollection.cs index 6f7a1b134..f475a4adc 100644 --- a/ErsatzTV.Core/Domain/SimpleMediaCollection.cs +++ b/ErsatzTV.Core/Domain/SimpleMediaCollection.cs @@ -4,6 +4,9 @@ namespace ErsatzTV.Core.Domain { public class SimpleMediaCollection : MediaCollection { - public IList Items { get; set; } + public List Movies { get; set; } + public List TelevisionShows { get; set; } + public List TelevisionSeasons { get; set; } + public List TelevisionEpisodes { get; set; } } } diff --git a/ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs b/ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs index b86731ec5..bd2d66bc7 100644 --- a/ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs +++ b/ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs @@ -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 SimpleMediaCollections { get; set; } } } diff --git a/ErsatzTV.Core/Domain/TelevisionMediaCollection.cs b/ErsatzTV.Core/Domain/TelevisionMediaCollection.cs deleted file mode 100644 index 3d872c9b0..000000000 --- a/ErsatzTV.Core/Domain/TelevisionMediaCollection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ErsatzTV.Core.Domain -{ - public class TelevisionMediaCollection : MediaCollection - { - public string ShowTitle { get; set; } - public int? SeasonNumber { get; set; } - } -} diff --git a/ErsatzTV.Core/Domain/TelevisionSeason.cs b/ErsatzTV.Core/Domain/TelevisionSeason.cs index 600a2aa11..009bbf3ce 100644 --- a/ErsatzTV.Core/Domain/TelevisionSeason.cs +++ b/ErsatzTV.Core/Domain/TelevisionSeason.cs @@ -11,6 +11,7 @@ namespace ErsatzTV.Core.Domain public TelevisionShow TelevisionShow { get; set; } public int Number { get; set; } public List Episodes { get; set; } + public List SimpleMediaCollections { get; set; } public string Path { get; set; } public string Poster { get; set; } public DateTime? PosterLastWriteTime { get; set; } diff --git a/ErsatzTV.Core/Domain/TelevisionShow.cs b/ErsatzTV.Core/Domain/TelevisionShow.cs index 233c5bcb5..e50793d2a 100644 --- a/ErsatzTV.Core/Domain/TelevisionShow.cs +++ b/ErsatzTV.Core/Domain/TelevisionShow.cs @@ -11,5 +11,6 @@ namespace ErsatzTV.Core.Domain public List Seasons { get; set; } public string Poster { get; set; } public DateTime? PosterLastWriteTime { get; set; } + public List SimpleMediaCollections { get; set; } } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs index 624b037be..4dd81dfbd 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs @@ -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 Task> Get(int id); Task> GetSimpleMediaCollection(int id); Task> GetSimpleMediaCollectionWithItems(int id); - Task> GetTelevisionMediaCollection(int id); + Task> GetSimpleMediaCollectionWithItemsUntracked(int id); Task> GetSimpleMediaCollections(); Task> GetAll(); - Task> GetSummaries(string searchString); Task>> GetItems(int id); Task>> GetSimpleMediaCollectionItems(int id); - Task>> GetTelevisionMediaCollectionItems(int id); Task Update(SimpleMediaCollection collection); - Task InsertOrIgnore(TelevisionMediaCollection collection); - Task ReplaceItems(int collectionId, List mediaItems); Task Delete(int mediaCollectionId); - Task DeleteEmptyTelevisionCollections(); } } diff --git a/ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs b/ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs deleted file mode 100644 index c17262354..000000000 --- a/ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs +++ /dev/null @@ -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 RefreshSmartCollections(TelevisionEpisodeMediaItem mediaItem) - { - var results = new List(); - - foreach (TelevisionMediaCollection collection in GetTelevisionCollections(mediaItem)) - { - results.Add(await _mediaCollectionRepository.InsertOrIgnore(collection)); - } - - return results.Any(identity); - } - - private IEnumerable GetTelevisionCollections(TelevisionEpisodeMediaItem mediaItem) - { - var televisionMediaItems = new List { mediaItem }; - - IEnumerable televisionShowCollections = televisionMediaItems - .Map(c => c.Metadata.Title) - .Distinct().Map( - t => new TelevisionMediaCollection - { - Name = $"{t} - All Seasons", - ShowTitle = t, - SeasonNumber = null - }); - - IEnumerable 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(); - } - } -} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs index bd1c81b89..dcb5df392 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs @@ -6,7 +6,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations { public class MediaItemConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) => + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("MediaItems"); builder.OwnsOne(c => c.Statistics).WithOwner(); + } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/SimpleMediaCollectionConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/SimpleMediaCollectionConfiguration.cs index 66d11f585..5f79f77a2 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/SimpleMediaCollectionConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/SimpleMediaCollectionConfiguration.cs @@ -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")); } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionMediaCollectionConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionMediaCollectionConfiguration.cs deleted file mode 100644 index e2345daf5..000000000 --- a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionMediaCollectionConfiguration.cs +++ /dev/null @@ -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 - { - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("TelevisionMediaCollections"); - - builder.HasIndex(c => new { c.ShowTitle, c.SeasonNumber }) - .IsUnique(); - } - } -} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 26e1378be..b0806ca9c 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -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 _dbContext.MediaCollections.SingleOrDefaultAsync(c => c.Id == id).Map(Optional); public Task> GetSimpleMediaCollection(int id) => - Get(id).Map(c => c.OfType().HeadOrNone()); + _dbContext.SimpleMediaCollections + .SingleOrDefaultAsync(c => c.Id == id) + .Map(Optional); public Task> 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> GetTelevisionMediaCollection(int id) => - Get(id).Map(c => c.OfType().HeadOrNone()); + public Task> 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> GetSimpleMediaCollections() => _dbContext.SimpleMediaCollections.ToListAsync(); @@ -46,112 +62,98 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public Task> GetAll() => _dbContext.MediaCollections.ToListAsync(); - public Task> 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>> 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>> GetSimpleMediaCollectionItems(int id) => GetSimpleMediaCollection(id).MapT(SimpleItems).Bind(x => x.Sequence()); - public Task>> 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 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 ReplaceItems(int collectionId, List mediaItems) => - GetSimpleMediaCollection(collectionId).IfSomeAsync( - async c => - { - await SimpleItems(c); + private async Task> SimpleItems(SimpleMediaCollection collection) + { + var result = new List(); - 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> GetTelevisionShowItems(SimpleMediaCollection collection) { - List 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 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 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> SimpleItems(SimpleMediaCollection collection) + private async Task> 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 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> TelevisionItems(TelevisionMediaCollection collection) => - new List().AsTask(); + private async Task> GetTelevisionEpisodeItems(SimpleMediaCollection collection) + { + // TODO: would be nice to get the media items in one go, but ef... + List 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(); + } } } diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index ebab22ffe..688a8e885 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -23,7 +23,6 @@ namespace ErsatzTV.Infrastructure.Data public DbSet TelevisionEpisodeMediaItems { get; set; } public DbSet MediaCollections { get; set; } public DbSet SimpleMediaCollections { get; set; } - public DbSet TelevisionMediaCollections { get; set; } public DbSet ProgramSchedules { get; set; } public DbSet Playouts { get; set; } public DbSet PlayoutItems { get; set; } diff --git a/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.Designer.cs new file mode 100644 index 000000000..ee59dd786 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.Designer.cs @@ -0,0 +1,1289 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20210220003018_CollectionsRework")] + partial class CollectionsRework + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.3"); + + modelBuilder.Entity("ErsatzTV.Core.AggregateModels.GenericIntegerId", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.ToView("No table or view exists for GenericIntegerId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaCollectionSummary", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("IsSimple") + .HasColumnType("INTEGER"); + + b.Property("ItemCount") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.ToView("No table or view exists for MediaCollectionSummary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaItemSummary", b => + { + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Subtitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.ToView("No table or view exists for MediaItemSummary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("Number") + .IsUnique(); + + b.ToTable("Channels"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("ConfigElements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioBitrate") + .HasColumnType("INTEGER"); + + b.Property("AudioBufferSize") + .HasColumnType("INTEGER"); + + b.Property("AudioChannels") + .HasColumnType("INTEGER"); + + b.Property("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("AudioVolume") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .HasColumnType("INTEGER"); + + b.Property("NormalizeAudioCodec") + .HasColumnType("INTEGER"); + + b.Property("NormalizeResolution") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideoCodec") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("Transcode") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfiles"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("PosterLastWriteTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaSourceId"); + + b.ToTable("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("Premiered") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId") + .IsUnique(); + + b.ToTable("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId"); + + b.HasIndex("MediaCollectionId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleItemAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexMediaSourceConnections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MediaType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexMediaSourceLibraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedules"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaCollectionId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolutions"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionEpisodeMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Aired") + .HasColumnType("TEXT"); + + b.Property("Episode") + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("Season") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("TelevisionEpisodeId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionEpisodeId") + .IsUnique(); + + b.ToTable("TelevisionEpisodeMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionSeason", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("PosterLastWriteTime") + .HasColumnType("TEXT"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionShowId"); + + b.ToTable("TelevisionSeasons"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("PosterLastWriteTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TelevisionShows"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionShowId") + .IsUnique(); + + b.ToTable("TelevisionShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionShowId"); + + b.ToTable("TelevisionShowSource"); + + b.HasDiscriminator("Discriminator").HasValue("TelevisionShowSource"); + }); + + modelBuilder.Entity("MovieMediaItemSimpleMediaCollection", b => + { + b.Property("MoviesId") + .HasColumnType("INTEGER"); + + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.HasKey("MoviesId", "SimpleMediaCollectionsId"); + + b.HasIndex("SimpleMediaCollectionsId"); + + b.ToTable("SimpleMediaCollectionMovies"); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionEpisodeMediaItem", b => + { + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionEpisodesId") + .HasColumnType("INTEGER"); + + b.HasKey("SimpleMediaCollectionsId", "TelevisionEpisodesId"); + + b.HasIndex("TelevisionEpisodesId"); + + b.ToTable("SimpleMediaCollectionEpisodes"); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionSeason", b => + { + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionSeasonsId") + .HasColumnType("INTEGER"); + + b.HasKey("SimpleMediaCollectionsId", "TelevisionSeasonsId"); + + b.HasIndex("TelevisionSeasonsId"); + + b.ToTable("SimpleMediaCollectionSeasons"); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionShow", b => + { + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.Property("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("ErsatzTV.Core.Domain.MovieMediaItem", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("MetadataId") + .HasColumnType("INTEGER"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("TelevisionEpisodes"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("Folder") + .HasColumnType("TEXT"); + + b.Property("MediaType") + .HasColumnType("INTEGER"); + + b.ToTable("LocalMediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("OfflineTail") + .HasColumnType("INTEGER"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.ToTable("ProgramScheduleDurationItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalTelevisionShowSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.TelevisionShowSource"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasIndex("MediaSourceId"); + + b.HasDiscriminator().HasValue("LocalTelevisionShowSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source") + .WithMany() + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaItemStatistics", "Statistics", b1 => + { + b1.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b1.Property("AudioCodec") + .HasColumnType("TEXT"); + + b1.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("Duration") + .HasColumnType("TEXT"); + + b1.Property("Height") + .HasColumnType("INTEGER"); + + b1.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b1.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("VideoCodec") + .HasColumnType("TEXT"); + + b1.Property("VideoScanType") + .HasColumnType("INTEGER"); + + b1.Property("Width") + .HasColumnType("INTEGER"); + + b1.HasKey("MediaItemId"); + + b1.ToTable("MediaItems"); + + b1.WithOwner() + .HasForeignKey("MediaItemId"); + }); + + b.Navigation("Source"); + + b.Navigation("Statistics"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.MovieMediaItem", "Movie") + .WithOne("Metadata") + .HasForeignKey("ErsatzTV.Core.Domain.MovieMetadata", "MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") + .WithMany("Playouts") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("NextScheduleItemId") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.HasIndex("NextScheduleItemId"); + + b1.ToTable("Playouts"); + + b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem") + .WithMany() + .HasForeignKey("NextScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.Navigation("NextScheduleItem"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection") + .WithMany() + .HasForeignKey("MediaCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany() + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaCollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorPlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorProgramScheduleId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorMediaCollectionId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + + b1.ToTable("PlayoutProgramScheduleItemAnchors"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + }); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaCollection"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null) + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null) + .WithMany("Libraries") + .HasForeignKey("PlexMediaSourceId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection") + .WithMany() + .HasForeignKey("MediaCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaCollection"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionEpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", "TelevisionEpisode") + .WithOne("Metadata") + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionEpisodeMetadata", "TelevisionEpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TelevisionEpisode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") + .WithMany("Seasons") + .HasForeignKey("TelevisionShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TelevisionShow"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") + .WithOne("Metadata") + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionShowMetadata", "TelevisionShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TelevisionShow"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") + .WithMany("Sources") + .HasForeignKey("TelevisionShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TelevisionShow"); + }); + + modelBuilder.Entity("MovieMediaItemSimpleMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MovieMediaItem", null) + .WithMany() + .HasForeignKey("MoviesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null) + .WithMany() + .HasForeignKey("SimpleMediaCollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionEpisodeMediaItem", b => + { + 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("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.SimpleMediaCollection", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.MovieMediaItem", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.TelevisionSeason", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalTelevisionShowSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.LocalMediaSource", "MediaSource") + .WithMany() + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionSeason", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShow", b => + { + b.Navigation("Metadata"); + + b.Navigation("Seasons"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMediaItem", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("Libraries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.cs b/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.cs new file mode 100644 index 000000000..c27d17cc6 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.cs @@ -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("INTEGER", nullable: false), + TelevisionEpisodesId = table.Column("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("INTEGER", nullable: false), + SimpleMediaCollectionsId = table.Column("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("INTEGER", nullable: false), + TelevisionSeasonsId = table.Column("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("INTEGER", nullable: false), + TelevisionShowsId = table.Column("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("INTEGER", nullable: false), + SimpleMediaCollectionsId = table.Column("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("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SeasonNumber = table.Column("INTEGER", nullable: true), + ShowTitle = table.Column("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); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 8ce8c69ab..1b09d5314 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -666,47 +666,80 @@ namespace ErsatzTV.Infrastructure.Migrations }); modelBuilder.Entity( - "MediaItemSimpleMediaCollection", + "MovieMediaItemSimpleMediaCollection", b => { - b.Property("ItemsId") + b.Property("MoviesId") .HasColumnType("INTEGER"); b.Property("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("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); - b.ToTable("SimpleMediaCollections"); + b.Property("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("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); - b.Property("SeasonNumber") + b.Property("TelevisionSeasonsId") .HasColumnType("INTEGER"); - b.Property("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("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.Property("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 }); 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 }); 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(); }); diff --git a/ErsatzTV/Controllers/Api/MediaCollectionsController.cs b/ErsatzTV/Controllers/Api/MediaCollectionsController.cs index dfda3dc4f..5c2e6c1c1 100644 --- a/ErsatzTV/Controllers/Api/MediaCollectionsController.cs +++ b/ErsatzTV/Controllers/Api/MediaCollectionsController.cs @@ -44,14 +44,5 @@ namespace ErsatzTV.Controllers.Api [ProducesResponseType(404)] public Task GetItems(int id) => _mediator.Send(new GetSimpleMediaCollectionItems(id)).ToActionResult(); - - [HttpPut("{id}/items")] - [ProducesResponseType(typeof(IEnumerable), 200)] - [ProducesResponseType(404)] - public Task PutItems( - int id, - [Required] [FromBody] - List mediaItemIds) => - _mediator.Send(new ReplaceSimpleMediaCollectionItems(id, mediaItemIds)).ToActionResult(); } } diff --git a/ErsatzTV/Pages/MediaCollectionEditor.razor b/ErsatzTV/Pages/MediaCollectionEditor.razor index 2bd73f8a1..db154d6cb 100644 --- a/ErsatzTV/Pages/MediaCollectionEditor.razor +++ b/ErsatzTV/Pages/MediaCollectionEditor.razor @@ -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 @@ 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}")); } } diff --git a/ErsatzTV/Pages/MediaCollectionItems.razor b/ErsatzTV/Pages/MediaCollectionItems.razor new file mode 100644 index 000000000..2795f409c --- /dev/null +++ b/ErsatzTV/Pages/MediaCollectionItems.razor @@ -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 Logger +@inject ISnackbar Snackbar + +
+ @_data.Name + +
+ +@if (_data.MovieCards.Any()) +{ + Movies + + + @foreach (MovieCardViewModel card in _data.MovieCards) + { + + } + +} + +@if (_data.ShowCards.Any()) +{ + Television Shows + + + @foreach (TelevisionShowCardViewModel card in _data.ShowCards) + { + + } + +} + +@* *@ +@* @foreach (var card in _data.Cards) *@ +@* { *@ +@* *@ +@* } *@ +@* *@ + +@code { + + [Parameter] + public int Id { get; set; } + + private SimpleMediaCollectionCardResultsViewModel _data; + + protected override async Task OnParametersSetAsync() => await RefreshData(); + + private async Task RefreshData() + { + Either maybeResult = + await Mediator.Send(new GetSimpleMediaCollectionCards(Id)); + + maybeResult.Match( + result => _data = result, + error => NavigationManager.NavigateTo("404")); + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/MediaCollectionItemsEditor.razor b/ErsatzTV/Pages/MediaCollectionItemsEditor.razor deleted file mode 100644 index fbe2df0ce..000000000 --- a/ErsatzTV/Pages/MediaCollectionItemsEditor.razor +++ /dev/null @@ -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 Logger -@inject ISnackbar Snackbar - - - - @_mediaCollection.Name Media Items - - - Source - Type - Title - Duration - - - @context.Source - @context.MediaType - @context.Title - @context.Duration - - - @if (_collectionItems.Any()) - { - - } - - - - - - All Media Items - - - - - - Source - Type - Title - Duration - - - @context.Source - @context.MediaType - @context.Title - @context.Duration - - - - - - - Add Results - - -@code { - - [Parameter] - public int Id { get; set; } - - private MediaCollectionViewModel _mediaCollection; - private IEnumerable _collectionItems; - - protected override async Task OnParametersSetAsync() => await LoadMediaCollectionAsync(); - - private List _mediaItemIds; - private IEnumerable _pagedData; - private MudTable _table; - - private int _totalItems; - private string _searchString; - - private async Task> ServerReload(TableState state) - { - List 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 { TotalItems = _totalItems, Items = _pagedData }; - } - - private async Task OnSearch(string text) - { - _searchString = text; - await _table.ReloadServerData(); - } - - private async Task AddResultsAsync() - { - Either 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>> maybeResult = - await Mediator.Send(new GetSimpleMediaCollectionWithItemsById(Id)); - maybeResult.Match( - result => (_mediaCollection, _collectionItems) = result, - () => NavigationManager.NavigateTo("404")); - } - -} \ No newline at end of file diff --git a/ErsatzTV/Pages/MediaCollections.razor b/ErsatzTV/Pages/MediaCollections.razor index d34c2523a..83551d187 100644 --- a/ErsatzTV/Pages/MediaCollections.razor +++ b/ErsatzTV/Pages/MediaCollections.razor @@ -5,67 +5,39 @@ @inject IDialogService Dialog @inject IMediator Mediator - - - Media Collections - - - - - - - - - - - Name - Media Items - - - - - @if (context.IsSimple) - { - @context.Name - } - else - { - @context.Name - } - - @context.ItemCount - - @if (context.IsSimple) - { - - - Edit Properties - - - Edit Media Items - - - Delete - - - } - - - - - - +@* @if (context.IsSimple) *@ +@* { *@ +@* *@ +@* *@ +@* Edit Properties *@ +@* *@ +@* *@ +@* Edit Media Items *@ +@* *@ +@* *@ +@* Delete *@ +@* *@ +@* *@ +@* } *@ + + + @foreach (MediaCollectionViewModel card in _data) + { + + } + + Add Media Collection @code { - private IEnumerable _pagedData; - private MudTable _table; + private List _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 @@ if (!result.Cancelled) { await Mediator.Send(new DeleteSimpleMediaCollection(mediaCollection.Id)); - await _table.ReloadServerData(); + await RefreshData(); } } } - private async Task> ServerReload(TableState state) - { - List 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 { TotalItems = _totalItems, Items = _pagedData }; - } - - private async Task OnSearch(string text) - { - _searchString = text; - await _table.ReloadServerData(); - } - } \ No newline at end of file diff --git a/ErsatzTV/Pages/TelevisionSeasons.razor b/ErsatzTV/Pages/TelevisionSeasons.razor index 3a314728b..2119882ad 100644 --- a/ErsatzTV/Pages/TelevisionSeasons.razor +++ b/ErsatzTV/Pages/TelevisionSeasons.razor @@ -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 @@ -18,9 +22,15 @@ @_show.Year @_show.Plot
- - Add To Schedule + + Add To Collection + @* *@ + @* Add To Schedule *@ + @* *@
@@ -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("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}"); + } + } + } \ No newline at end of file diff --git a/ErsatzTV/Shared/AddToCollectionDialog.razor b/ErsatzTV/Shared/AddToCollectionDialog.razor new file mode 100644 index 000000000..b76f9d3e9 --- /dev/null +++ b/ErsatzTV/Shared/AddToCollectionDialog.razor @@ -0,0 +1,58 @@ +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Queries +@inject IMediator Mediator + + + + + + + + @foreach (MediaCollectionViewModel collection in _collections) + { + @collection.Name + } + + + + Cancel + + Add To Collection + + + + +@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 _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(); + +} \ No newline at end of file diff --git a/ErsatzTV/Shared/MediaCard.razor b/ErsatzTV/Shared/MediaCard.razor index 5b8b9c992..779ee504c 100644 --- a/ErsatzTV/Shared/MediaCard.razor +++ b/ErsatzTV/Shared/MediaCard.razor @@ -45,7 +45,7 @@ [Parameter] public EventCallback DataRefreshed { get; set; } - + [Parameter] public Func CustomPlaceholder { get; set; } @@ -55,7 +55,7 @@ { return CustomPlaceholder(sortTitle); } - + string first = sortTitle.Substring(0, 1).ToUpperInvariant(); return int.TryParse(first, out _) ? "#" : first; } diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index ef11f331a..4a8560af6 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -174,7 +174,6 @@ namespace ErsatzTV services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();