From 2b26a5411cb186650553049d74aad81c07cc01bb Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Fri, 9 Apr 2021 05:10:58 -0500 Subject: [PATCH] add artists as owners of music videos (#154) * clean up genre, tag, studio orphans * enforce foreign keys at connection level * wip * fix fragment scroll offset * fix see all link for music videos * add fake artist metadata * not null artist id * add artist scanning * remove improperly named music videos * code cleanup * add artists to search results and collections * clean up music video metadata / artist * add artist view * show music videos on artist page * add music video artwork placeholder --- .../Artists/ArtistViewModel.cs | 14 + ErsatzTV.Application/Artists/Mapper.cs | 27 + .../Artists/Queries/GetArtistById.cs | 7 + .../Artists/Queries/GetArtistByIdHandler.cs | 21 + .../MediaCards/ArtistCardResultsViewModel.cs | 11 + .../MediaCards/ArtistCardViewModel.cs | 12 + .../CollectionCardResultsViewModel.cs | 1 + ErsatzTV.Application/MediaCards/Mapper.cs | 14 +- .../MediaCards/MusicVideoCardViewModel.cs | 2 +- .../MediaCards/Queries/GetMusicVideoCards.cs | 7 + .../Queries/GetMusicVideoCardsHandler.cs | 33 + .../Commands/AddArtistToCollection.cs | 8 + .../Commands/AddArtistToCollectionHandler.cs | 68 + .../Commands/AddItemsToCollection.cs | 1 + .../Commands/AddItemsToCollectionHandler.cs | 10 +- ErsatzTV.Application/Playouts/Mapper.cs | 5 +- .../QuerySearchIndexAllItemsHandler.cs | 4 +- .../Search/Queries/QuerySearchIndexArtists.cs | 8 + .../Queries/QuerySearchIndexArtistsHandler.cs | 44 + .../Search/SearchResultAllItemsViewModel.cs | 2 +- ErsatzTV.Core/Domain/MediaItem/Artist.cs | 10 + ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs | 2 + .../Domain/Metadata/ArtistMetadata.cs | 15 + ErsatzTV.Core/Domain/Metadata/Mood.cs | 8 + .../Domain/Metadata/MusicVideoMetadata.cs | 1 - ErsatzTV.Core/Domain/Metadata/Style.cs | 8 + .../Metadata/IFallbackMetadataProvider.cs | 4 +- .../Metadata/ILocalMetadataProvider.cs | 3 + .../Repositories/IArtistRepository.cs | 25 + .../Repositories/IMetadataRepository.cs | 2 + .../Repositories/IMusicVideoRepository.cs | 9 +- .../Repositories/ISearchRepository.cs | 4 +- ErsatzTV.Core/Iptv/ChannelGuide.cs | 4 +- .../Metadata/FallbackMetadataProvider.cs | 19 +- ErsatzTV.Core/Metadata/LocalFolderScanner.cs | 4 + .../Metadata/LocalMetadataProvider.cs | 164 +- .../Metadata/MusicVideoFolderScanner.cs | 208 +- ErsatzTV.Core/Metadata/Nfo/ArtistNfo.cs | 27 + .../Metadata/TelevisionFolderScanner.cs | 20 +- .../Plex/PlexTelevisionLibraryScanner.cs | 4 +- ErsatzTV.Core/Scheduling/PlayoutBuilder.cs | 34 +- .../MediaItem/ArtistConfiguration.cs | 24 + .../Metadata/ArtistMetadataConfiguration.cs | 30 + .../Data/Repositories/ArtistRepository.cs | 150 ++ .../Repositories/MediaCollectionRepository.cs | 34 + .../Data/Repositories/MetadataRepository.cs | 13 + .../Data/Repositories/MusicVideoRepository.cs | 61 +- .../Data/Repositories/PlayoutRepository.cs | 3 + .../Data/Repositories/SearchRepository.cs | 95 +- ErsatzTV.Infrastructure/Data/TvContext.cs | 2 + ...8_Delete_Orphan_GenreTagStudio.Designer.cs | 1964 +++++++++++++++ ...0407113048_Delete_Orphan_GenreTagStudio.cs | 38 + .../20210407143124_Add_Artist.Designer.cs | 2165 ++++++++++++++++ .../Migrations/20210407143124_Add_Artist.cs | 284 +++ ...10407230353_Add_ArtistMetadata.Designer.cs | 2165 ++++++++++++++++ .../20210407230353_Add_ArtistMetadata.cs | 42 + ...33717_Update_ArtistMetadata_FK.Designer.cs | 2167 +++++++++++++++++ ...20210407233717_Update_ArtistMetadata_FK.cs | 47 + ...lete_MusicVideoMetadata_Artist.Designer.cs | 2164 ++++++++++++++++ ...113508_Delete_MusicVideoMetadata_Artist.cs | 19 + .../Migrations/TvContextModelSnapshot.cs | 226 +- ErsatzTV.Infrastructure/Search/SearchIndex.cs | 62 +- ErsatzTV/Pages/Artist.razor | 176 ++ ErsatzTV/Pages/ArtistList.razor | 163 ++ ErsatzTV/Pages/CollectionItems.razor | 60 +- ErsatzTV/Pages/MultiSelectBase.cs | 13 +- ErsatzTV/Pages/Search.razor | 81 +- ErsatzTV/Shared/FragmentLetterAnchor.razor | 2 +- ErsatzTV/Shared/MainLayout.razor | 2 +- ErsatzTV/Shared/MediaCard.razor | 6 +- ErsatzTV/Startup.cs | 3 +- 71 files changed, 12938 insertions(+), 197 deletions(-) create mode 100644 ErsatzTV.Application/Artists/ArtistViewModel.cs create mode 100644 ErsatzTV.Application/Artists/Mapper.cs create mode 100644 ErsatzTV.Application/Artists/Queries/GetArtistById.cs create mode 100644 ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs create mode 100644 ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCards.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollection.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs create mode 100644 ErsatzTV.Application/Search/Queries/QuerySearchIndexArtists.cs create mode 100644 ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs create mode 100644 ErsatzTV.Core/Domain/MediaItem/Artist.cs create mode 100644 ErsatzTV.Core/Domain/Metadata/ArtistMetadata.cs create mode 100644 ErsatzTV.Core/Domain/Metadata/Mood.cs create mode 100644 ErsatzTV.Core/Domain/Metadata/Style.cs create mode 100644 ErsatzTV.Core/Interfaces/Repositories/IArtistRepository.cs create mode 100644 ErsatzTV.Core/Metadata/Nfo/ArtistNfo.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/MediaItem/ArtistConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Metadata/ArtistMetadataConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Repositories/ArtistRepository.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210407113048_Delete_Orphan_GenreTagStudio.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210407113048_Delete_Orphan_GenreTagStudio.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210407143124_Add_Artist.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210407143124_Add_Artist.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210407230353_Add_ArtistMetadata.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210407230353_Add_ArtistMetadata.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210407233717_Update_ArtistMetadata_FK.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210407233717_Update_ArtistMetadata_FK.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210408113508_Delete_MusicVideoMetadata_Artist.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210408113508_Delete_MusicVideoMetadata_Artist.cs create mode 100644 ErsatzTV/Pages/Artist.razor create mode 100644 ErsatzTV/Pages/ArtistList.razor diff --git a/ErsatzTV.Application/Artists/ArtistViewModel.cs b/ErsatzTV.Application/Artists/ArtistViewModel.cs new file mode 100644 index 000000000..941fae8c2 --- /dev/null +++ b/ErsatzTV.Application/Artists/ArtistViewModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Application.Artists +{ + public record ArtistViewModel( + string Name, + string Disambiguation, + string Biography, + string Thumbnail, + string FanArt, + List Genres, + List Styles, + List Moods); +} diff --git a/ErsatzTV.Application/Artists/Mapper.cs b/ErsatzTV.Application/Artists/Mapper.cs new file mode 100644 index 000000000..ea3c1387c --- /dev/null +++ b/ErsatzTV.Application/Artists/Mapper.cs @@ -0,0 +1,27 @@ +using System.Linq; +using ErsatzTV.Core.Domain; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Application.Artists +{ + internal static class Mapper + { + internal static ArtistViewModel ProjectToViewModel(Artist artist) + { + ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head(); + return new ArtistViewModel( + metadata.Title, + metadata.Disambiguation, + metadata.Biography, + Artwork(metadata, ArtworkKind.Thumbnail), + Artwork(metadata, ArtworkKind.FanArt), + metadata.Genres.Map(g => g.Name).ToList(), + metadata.Styles.Map(s => s.Name).ToList(), + metadata.Moods.Map(m => m.Name).ToList()); + } + + private static string Artwork(Metadata metadata, ArtworkKind artworkKind) => + Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind)) + .Match(a => a.Path, string.Empty); + } +} diff --git a/ErsatzTV.Application/Artists/Queries/GetArtistById.cs b/ErsatzTV.Application/Artists/Queries/GetArtistById.cs new file mode 100644 index 000000000..67d15dd6e --- /dev/null +++ b/ErsatzTV.Application/Artists/Queries/GetArtistById.cs @@ -0,0 +1,7 @@ +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.Artists.Queries +{ + public record GetArtistById(int ArtistId) : IRequest>; +} diff --git a/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs b/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs new file mode 100644 index 000000000..73205bac9 --- /dev/null +++ b/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using MediatR; +using static ErsatzTV.Application.Artists.Mapper; + +namespace ErsatzTV.Application.Artists.Queries +{ + public class GetArtistByIdHandler : IRequestHandler> + { + private readonly IArtistRepository _artistRepository; + + public GetArtistByIdHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository; + + public Task> Handle( + GetArtistById request, + CancellationToken cancellationToken) => + _artistRepository.GetArtist(request.ArtistId).MapT(ProjectToViewModel); + } +} diff --git a/ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs new file mode 100644 index 000000000..cc88a7bec --- /dev/null +++ b/ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using ErsatzTV.Core.Search; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCards +{ + public record ArtistCardResultsViewModel( + int Count, + List Cards, + Option PageMap); +} diff --git a/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs b/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs new file mode 100644 index 000000000..435a02630 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs @@ -0,0 +1,12 @@ +namespace ErsatzTV.Application.MediaCards +{ + public record ArtistCardViewModel + (int ArtistId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel( + ArtistId, + Title, + Subtitle, + SortTitle, + Poster) + { + } +} diff --git a/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs index 91deba3a0..32901297f 100644 --- a/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs @@ -8,6 +8,7 @@ namespace ErsatzTV.Application.MediaCards List ShowCards, List SeasonCards, List EpisodeCards, + List ArtistCards, List MusicVideoCards) { public bool UseCustomPlaybackOrder { get; set; } diff --git a/ErsatzTV.Application/MediaCards/Mapper.cs b/ErsatzTV.Application/MediaCards/Mapper.cs index adcb5df63..153f1f5d9 100644 --- a/ErsatzTV.Application/MediaCards/Mapper.cs +++ b/ErsatzTV.Application/MediaCards/Mapper.cs @@ -55,11 +55,20 @@ namespace ErsatzTV.Application.MediaCards internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) => new( musicVideoMetadata.MusicVideoId, - $"{musicVideoMetadata.Title} ({musicVideoMetadata.Artist})", - musicVideoMetadata.Year?.ToString(), + musicVideoMetadata.Title, + musicVideoMetadata.MusicVideo.Artist.ArtistMetadata.Head().Title, musicVideoMetadata.SortTitle, + musicVideoMetadata.Plot, GetThumbnail(musicVideoMetadata)); + internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) => + new( + artistMetadata.ArtistId, + artistMetadata.Title, + artistMetadata.Disambiguation, + artistMetadata.SortTitle, + GetThumbnail(artistMetadata)); + internal static CollectionCardResultsViewModel ProjectToViewModel(Collection collection) => new( @@ -73,6 +82,7 @@ namespace ErsatzTV.Application.MediaCards collection.MediaItems.OfType().Map(ProjectToViewModel).ToList(), collection.MediaItems.OfType().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head())) .ToList(), + collection.MediaItems.OfType().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(), collection.MediaItems.OfType().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head())) .ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder }; diff --git a/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs b/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs index 5c06ee4d4..eb6a06bd0 100644 --- a/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs @@ -1,7 +1,7 @@ namespace ErsatzTV.Application.MediaCards { public record MusicVideoCardViewModel - (int MusicVideoId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel( + (int MusicVideoId, string Title, string Subtitle, string SortTitle, string Plot, string Poster) : MediaCardViewModel( MusicVideoId, Title, Subtitle, diff --git a/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCards.cs new file mode 100644 index 000000000..71564a568 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCards.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public record GetMusicVideoCards + (int ArtistId, int PageNumber, int PageSize) : IRequest; +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs new file mode 100644 index 000000000..b47c62879 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using MediatR; +using static ErsatzTV.Application.MediaCards.Mapper; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public class GetMusicVideoCardsHandler : IRequestHandler + { + private readonly IMusicVideoRepository _musicVideoRepository; + + public GetMusicVideoCardsHandler(IMusicVideoRepository musicVideoRepository) => + _musicVideoRepository = musicVideoRepository; + + public async Task Handle( + GetMusicVideoCards request, + CancellationToken cancellationToken) + { + int count = await _musicVideoRepository.GetMusicVideoCount(request.ArtistId); + + List results = await _musicVideoRepository + .GetPagedMusicVideos(request.ArtistId, request.PageNumber, request.PageSize) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new MusicVideoCardResultsViewModel(count, results, None); + } + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollection.cs new file mode 100644 index 000000000..328a8af4a --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollection.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddArtistToCollection + (int CollectionId, int ArtistId) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs new file mode 100644 index 000000000..965cfc928 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs @@ -0,0 +1,68 @@ +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using ErsatzTV.Application.Playouts.Commands; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + AddArtistToCollectionHandler : MediatR.IRequestHandler> + { + private readonly ChannelWriter _channel; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly IArtistRepository _artistRepository; + + public AddArtistToCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + IArtistRepository artistRepository, + ChannelWriter channel) + { + _mediaCollectionRepository = mediaCollectionRepository; + _artistRepository = artistRepository; + _channel = channel; + } + + public Task> Handle( + AddArtistToCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(_ => ApplyAddArtistRequest(request)) + .Bind(v => v.ToEitherAsync()); + + private async Task ApplyAddArtistRequest(AddArtistToCollection request) + { + if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ArtistId)) + { + // rebuild all playouts that use this collection + foreach (int playoutId in await _mediaCollectionRepository + .PlayoutIdsUsingCollection(request.CollectionId)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private async Task> Validate(AddArtistToCollection request) => + (await CollectionMustExist(request), await ValidateArtist(request)) + .Apply((_, _) => Unit.Default); + + private Task> CollectionMustExist(AddArtistToCollection request) => + _mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Collection does not exist.")); + + private Task> ValidateArtist(AddArtistToCollection request) => + LoadArtist(request) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Music video does not exist")); + + private Task> LoadArtist(AddArtistToCollection request) => + _artistRepository.GetArtist(request.ArtistId); + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs index 4da9a73d4..94810428a 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs @@ -9,5 +9,6 @@ namespace ErsatzTV.Application.MediaCollections.Commands int CollectionId, List MovieIds, List ShowIds, + List ArtistIds, List MusicVideoIds) : MediatR.IRequest>; } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs index 692ecaebe..31c40dbdc 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs @@ -39,9 +39,13 @@ namespace ErsatzTV.Application.MediaCollections.Commands private async Task ApplyAddItemsRequest(AddItemsToCollection request) { - if (await _mediaCollectionRepository.AddMediaItems( - request.CollectionId, - request.MovieIds.Append(request.ShowIds).Append(request.MusicVideoIds).ToList())) + var allItems = request.MovieIds + .Append(request.ShowIds) + .Append(request.ArtistIds) + .Append(request.MusicVideoIds) + .ToList(); + + if (await _mediaCollectionRepository.AddMediaItems(request.CollectionId, allItems)) { // rebuild all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository diff --git a/ErsatzTV.Application/Playouts/Mapper.cs b/ErsatzTV.Application/Playouts/Mapper.cs index 5bbe18d91..e30fac17d 100644 --- a/ErsatzTV.Application/Playouts/Mapper.cs +++ b/ErsatzTV.Application/Playouts/Mapper.cs @@ -37,7 +37,10 @@ namespace ErsatzTV.Application.Playouts case Movie m: return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"); case MusicVideo mv: - return mv.MusicVideoMetadata.HeadOrNone().Map(mvm => $"{mvm.Artist} - {mvm.Title}") + string artistName = mv.Artist.ArtistMetadata.HeadOrNone() + .Map(am => $"{am.Title} - ").IfNone(string.Empty); + return mv.MusicVideoMetadata.HeadOrNone() + .Map(mvm => $"{artistName}{mvm.Title}") .IfNone("[unknown music video]"); default: return string.Empty; diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs index 220e31d13..d6c2f138a 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs @@ -23,10 +23,12 @@ namespace ErsatzTV.Application.Search.Queries .Map(result => result.Items.Map(i => i.Id).ToList()); List showIds = await _searchIndex.Search($"type:show AND ({request.Query})", 0, 0) .Map(result => result.Items.Map(i => i.Id).ToList()); + List artistIds = await _searchIndex.Search($"type:artist AND ({request.Query})", 0, 0) + .Map(result => result.Items.Map(i => i.Id).ToList()); List musicVideoIds = await _searchIndex.Search($"type:music_video AND ({request.Query})", 0, 0) .Map(result => result.Items.Map(i => i.Id).ToList()); - return new SearchResultAllItemsViewModel(movieIds, showIds, musicVideoIds); + return new SearchResultAllItemsViewModel(movieIds, showIds, artistIds, musicVideoIds); } } } diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtists.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtists.cs new file mode 100644 index 000000000..1ba2b5114 --- /dev/null +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtists.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Application.MediaCards; +using MediatR; + +namespace ErsatzTV.Application.Search.Queries +{ + public record QuerySearchIndexArtists + (string Query, int PageNumber, int PageSize) : IRequest; +} diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs new file mode 100644 index 000000000..126ea4e66 --- /dev/null +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Application.MediaCards; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Search; +using ErsatzTV.Core.Search; +using LanguageExt; +using MediatR; +using static ErsatzTV.Application.MediaCards.Mapper; + +namespace ErsatzTV.Application.Search.Queries +{ + public class + QuerySearchIndexArtistsHandler : IRequestHandler + { + private readonly IArtistRepository _artistRepository; + private readonly ISearchIndex _searchIndex; + + public QuerySearchIndexArtistsHandler(ISearchIndex searchIndex, IArtistRepository artistRepository) + { + _searchIndex = searchIndex; + _artistRepository = artistRepository; + } + + public async Task Handle( + QuerySearchIndexArtists request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + List items = await _artistRepository + .GetArtistsForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new ArtistCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } + } +} diff --git a/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs b/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs index d95c60671..95145009c 100644 --- a/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs +++ b/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs @@ -2,5 +2,5 @@ namespace ErsatzTV.Application.Search { - public record SearchResultAllItemsViewModel(List MovieIds, List ShowIds, List MusicVideoIds); + public record SearchResultAllItemsViewModel(List MovieIds, List ShowIds, List ArtistIds, List MusicVideoIds); } diff --git a/ErsatzTV.Core/Domain/MediaItem/Artist.cs b/ErsatzTV.Core/Domain/MediaItem/Artist.cs new file mode 100644 index 000000000..c30cc2d2a --- /dev/null +++ b/ErsatzTV.Core/Domain/MediaItem/Artist.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Core.Domain +{ + public class Artist : MediaItem + { + public List MusicVideos { get; set; } + public List ArtistMetadata { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs b/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs index 396ffc91c..ae105b8aa 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs @@ -4,6 +4,8 @@ namespace ErsatzTV.Core.Domain { public class MusicVideo : MediaItem { + public int ArtistId { get; set; } + public Artist Artist { get; set; } public List MusicVideoMetadata { get; set; } public List MediaVersions { get; set; } } diff --git a/ErsatzTV.Core/Domain/Metadata/ArtistMetadata.cs b/ErsatzTV.Core/Domain/Metadata/ArtistMetadata.cs new file mode 100644 index 000000000..73ef9c69a --- /dev/null +++ b/ErsatzTV.Core/Domain/Metadata/ArtistMetadata.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Core.Domain +{ + public class ArtistMetadata : Metadata + { + public string Disambiguation { get; set; } + public string Biography { get; set; } + public string Formed { get; set; } + public int ArtistId { get; set; } + public Artist Artist { get; set; } + public List