diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a77b360..68c0a8f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Add artists directly to schedules + +### Fixed +- Ignore unsupported plex guids (this prevented some libraries from scanning correctly) ## [0.0.43-prealpha] - 2021-06-05 ### Added diff --git a/ErsatzTV.Application/Artists/Queries/GetAllArtists.cs b/ErsatzTV.Application/Artists/Queries/GetAllArtists.cs new file mode 100644 index 00000000..b58873fa --- /dev/null +++ b/ErsatzTV.Application/Artists/Queries/GetAllArtists.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +using ErsatzTV.Application.MediaItems; +using MediatR; + +namespace ErsatzTV.Application.Artists.Queries +{ + public record GetAllArtists : IRequest>; +} diff --git a/ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs b/ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs new file mode 100644 index 00000000..344f8a05 --- /dev/null +++ b/ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Application.MediaItems; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using MediatR; +using static ErsatzTV.Application.MediaItems.Mapper; + +namespace ErsatzTV.Application.Artists.Queries +{ + public class GetAllArtistsHandler : IRequestHandler> + { + private readonly IArtistRepository _artistRepository; + + public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository; + + public Task> Handle( + GetAllArtists request, + CancellationToken cancellationToken) => + _artistRepository.GetAllArtists().Map(list => list.Map(ProjectToViewModel).ToList()); + } +} diff --git a/ErsatzTV.Application/MediaItems/Mapper.cs b/ErsatzTV.Application/MediaItems/Mapper.cs index 175f69ac..793378a7 100644 --- a/ErsatzTV.Application/MediaItems/Mapper.cs +++ b/ErsatzTV.Application/MediaItems/Mapper.cs @@ -7,12 +7,15 @@ namespace ErsatzTV.Application.MediaItems internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) => new(mediaItem.Id, mediaItem.LibraryPathId); - public static NamedMediaItemViewModel ProjectToViewModel(Show show) => + internal static NamedMediaItemViewModel ProjectToViewModel(Show show) => new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???")); - public static NamedMediaItemViewModel ProjectToViewModel(Season season) => + internal static NamedMediaItemViewModel ProjectToViewModel(Season season) => new(season.Id, $"{ShowTitle(season)} ({SeasonDescription(season)})"); + internal static NamedMediaItemViewModel ProjectToViewModel(Artist artist) => + new(artist.Id, artist.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => "???")); + private static string ShowTitle(Season season) => season.Show.ShowMetadata.HeadOrNone().Map(sm => sm.Title).IfNone("???"); diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs index b77171bc..998fbd00 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs @@ -79,6 +79,13 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'"); } + break; + case ProgramScheduleItemCollectionType.Artist: + if (item.MediaItemId is null) + { + return BaseError.New("[MediaItem] is required for collection type 'Artist'"); + } + break; default: return BaseError.New("[CollectionType] is invalid"); diff --git a/ErsatzTV.Application/ProgramSchedules/Mapper.cs b/ErsatzTV.Application/ProgramSchedules/Mapper.cs index 198b1b94..8fec8b96 100644 --- a/ErsatzTV.Application/ProgramSchedules/Mapper.cs +++ b/ErsatzTV.Application/ProgramSchedules/Mapper.cs @@ -30,6 +30,7 @@ namespace ErsatzTV.Application.ProgramSchedules { Show show => MediaItems.Mapper.ProjectToViewModel(show), Season season => MediaItems.Mapper.ProjectToViewModel(season), + Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), _ => null }, duration.PlayoutDuration, @@ -49,6 +50,7 @@ namespace ErsatzTV.Application.ProgramSchedules { Show show => MediaItems.Mapper.ProjectToViewModel(show), Season season => MediaItems.Mapper.ProjectToViewModel(season), + Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), _ => null }, flood.CustomTitle), @@ -66,6 +68,7 @@ namespace ErsatzTV.Application.ProgramSchedules { Show show => MediaItems.Mapper.ProjectToViewModel(show), Season season => MediaItems.Mapper.ProjectToViewModel(season), + Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), _ => null }, multiple.Count, @@ -84,6 +87,7 @@ namespace ErsatzTV.Application.ProgramSchedules { Show show => MediaItems.Mapper.ProjectToViewModel(show), Season season => MediaItems.Mapper.ProjectToViewModel(season), + Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), _ => null }, one.CustomTitle), diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs index 195be833..64a1e013 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs @@ -19,10 +19,12 @@ namespace ErsatzTV.Application.ProgramSchedules public string Name => CollectionType switch { ProgramScheduleItemCollectionType.Collection => Collection?.Name, - ProgramScheduleItemCollectionType - .TelevisionShow => MediaItem?.Name, // $"{TelevisionShow?.Title} ({TelevisionShow?.Year})", - ProgramScheduleItemCollectionType - .TelevisionSeason => MediaItem?.Name, // $"{TelevisionSeason?.Title} ({TelevisionSeason?.Plot})", + ProgramScheduleItemCollectionType.TelevisionShow => + MediaItem?.Name, // $"{TelevisionShow?.Title} ({TelevisionShow?.Year})", + ProgramScheduleItemCollectionType.TelevisionSeason => + MediaItem?.Name, // $"{TelevisionSeason?.Title} ({TelevisionSeason?.Plot})", + ProgramScheduleItemCollectionType.Artist => + MediaItem?.Name, _ => string.Empty }; } diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 431f677e..ff23215e 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Tests.Fakes; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Serilog; using static LanguageExt.Prelude; @@ -349,7 +351,8 @@ namespace ErsatzTV.Core.Tests.Scheduling }; var televisionRepo = new FakeTelevisionRepository(); - var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); @@ -429,7 +432,8 @@ namespace ErsatzTV.Core.Tests.Scheduling }; var televisionRepo = new FakeTelevisionRepository(); - var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(7); @@ -515,7 +519,8 @@ namespace ErsatzTV.Core.Tests.Scheduling }; var televisionRepo = new FakeTelevisionRepository(); - var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); @@ -605,7 +610,8 @@ namespace ErsatzTV.Core.Tests.Scheduling }; var televisionRepo = new FakeTelevisionRepository(); - var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); @@ -699,7 +705,8 @@ namespace ErsatzTV.Core.Tests.Scheduling }; var televisionRepo = new FakeTelevisionRepository(); - var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(5); @@ -792,7 +799,8 @@ namespace ErsatzTV.Core.Tests.Scheduling }; var televisionRepo = new FakeTelevisionRepository(); - var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder(fakeRepository, televisionRepo, artistRepo.Object, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(5); @@ -851,7 +859,8 @@ namespace ErsatzTV.Core.Tests.Scheduling var collectionRepo = new FakeMediaCollectionRepository(Map((mediaCollection.Id, mediaItems))); var televisionRepo = new FakeTelevisionRepository(); - var builder = new PlayoutBuilder(collectionRepo, televisionRepo, _logger); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder(collectionRepo, televisionRepo, artistRepo.Object, _logger); var items = new List { Flood(mediaCollection) }; diff --git a/ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs b/ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs index e0dcd6cf..2abbd3d9 100644 --- a/ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs +++ b/ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs @@ -4,6 +4,7 @@ { Collection = 0, TelevisionShow = 1, - TelevisionSeason = 2 + TelevisionSeason = 2, + Artist = 3 } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IArtistRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IArtistRepository.cs index 0c5411ab..648bcb92 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IArtistRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IArtistRepository.cs @@ -21,5 +21,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task AddGenre(ArtistMetadata metadata, Genre genre); Task AddStyle(ArtistMetadata metadata, Style style); Task AddMood(ArtistMetadata metadata, Mood mood); + Task> GetArtistItems(int artistId); + Task> GetAllArtists(); } } diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 9b905698..b6c7c7d7 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -18,6 +18,7 @@ namespace ErsatzTV.Core.Scheduling public class PlayoutBuilder : IPlayoutBuilder { private static readonly Random Random = new(); + private readonly IArtistRepository _artistRepository; private readonly ILogger _logger; private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly ITelevisionRepository _televisionRepository; @@ -25,10 +26,12 @@ namespace ErsatzTV.Core.Scheduling public PlayoutBuilder( IMediaCollectionRepository mediaCollectionRepository, ITelevisionRepository televisionRepository, + IArtistRepository artistRepository, ILogger logger) { _mediaCollectionRepository = mediaCollectionRepository; _televisionRepository = televisionRepository; + _artistRepository = artistRepository; _logger = logger; } @@ -66,6 +69,10 @@ namespace ErsatzTV.Core.Scheduling List seasonItems = await _televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0); return Tuple(collectionKey, seasonItems.Cast().ToList()); + case ProgramScheduleItemCollectionType.Artist: + List artistItems = + await _artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0); + return Tuple(collectionKey, artistItems.Cast().ToList()); default: return Tuple(collectionKey, new List()); } @@ -555,6 +562,11 @@ namespace ErsatzTV.Core.Scheduling CollectionType = item.CollectionType, MediaItemId = item.MediaItemId }, + ProgramScheduleItemCollectionType.Artist => new CollectionKey + { + CollectionType = item.CollectionType, + MediaItemId = item.MediaItemId + }, _ => throw new ArgumentOutOfRangeException(nameof(item)) }; diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ArtistRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ArtistRepository.cs index 3777d8d5..0b1e833c 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ArtistRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ArtistRepository.cs @@ -146,5 +146,28 @@ namespace ErsatzTV.Infrastructure.Data.Repositories _dbConnection.ExecuteAsync( "INSERT INTO Mood (Name, ArtistMetadataId) VALUES (@Name, @MetadataId)", new { mood.Name, MetadataId = metadata.Id }).Map(result => result > 0); + + public async Task> GetArtistItems(int artistId) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.MusicVideos + .AsNoTracking() + .Include(mv => mv.MusicVideoMetadata) + .Include(mv => mv.MediaVersions) + .Include(mv => mv.Artist) + .ThenInclude(a => a.ArtistMetadata) + .Filter(mv => mv.ArtistId == artistId) + .ToListAsync(); + } + + public async Task> GetAllArtists() + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.Artists + .AsNoTracking() + .Include(a => a.ArtistMetadata) + .ThenInclude(am => am.Artwork) + .ToListAsync(); + } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs index a4899c77..79541735 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs @@ -76,6 +76,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(i => i.MediaItem) .ThenInclude(i => (i as Show).ShowMetadata) .ThenInclude(sm => sm.Artwork) + .Include(i => i.MediaItem) + .ThenInclude(i => (i as Artist).ArtistMetadata) + .ThenInclude(am => am.Artwork) .LoadAsync(); return programSchedule.Items; }).Sequence(); diff --git a/ErsatzTV/Pages/Artist.razor b/ErsatzTV/Pages/Artist.razor index 02a9472d..aa37e6e0 100644 --- a/ErsatzTV/Pages/Artist.razor +++ b/ErsatzTV/Pages/Artist.razor @@ -5,6 +5,8 @@ @using ErsatzTV.Application.MediaCards.Queries @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands +@using ErsatzTV.Application.ProgramSchedules +@using ErsatzTV.Application.ProgramSchedules.Commands @using System.Globalization @using Unit = LanguageExt.Unit @inject IMediator _mediator @@ -55,6 +57,13 @@ OnClick="@AddToCollection"> Add To Collection + + Add To Schedule + @@ -187,6 +196,20 @@ } } + private async Task AddToSchedule() + { + var parameters = new DialogParameters { { "EntityType", "artist" }, { "EntityName", _artist.Name } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = _dialog.Show("Add To Schedule", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule) + { + await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, ArtistId, null, null, null, null)); + _navigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); + } + } + private async Task AddMusicVideoToCollection(MusicVideoCardViewModel musicVideo) { var parameters = new DialogParameters { { "EntityType", "music video" }, { "EntityName", musicVideo.Title } }; diff --git a/ErsatzTV/Pages/ScheduleItemsEditor.razor b/ErsatzTV/Pages/ScheduleItemsEditor.razor index 98e1fefd..ee63d70a 100644 --- a/ErsatzTV/Pages/ScheduleItemsEditor.razor +++ b/ErsatzTV/Pages/ScheduleItemsEditor.razor @@ -6,6 +6,7 @@ @using ErsatzTV.Application.ProgramSchedules.Commands @using ErsatzTV.Application.ProgramSchedules.Queries @using ErsatzTV.Application.Television.Queries +@using ErsatzTV.Application.Artists.Queries @inject NavigationManager _navigationManager @inject ILogger _logger @inject ISnackbar _snackbar @@ -129,6 +130,15 @@ SearchFunc="@SearchTelevisionSeasons" ToStringFunc="@(s => s?.Name)"/> } + @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Artist) + { + + } @foreach (PlayoutMode playoutMode in Enum.GetValues()) { @@ -177,6 +187,7 @@ private List _mediaCollections; private List _televisionShows; private List _televisionSeasons; + private List _artists; private ProgramScheduleItemEditViewModel _selectedItem; @@ -184,9 +195,11 @@ private async Task LoadScheduleItems() { + // TODO: fix performance _mediaCollections = await _mediator.Send(new GetAllCollections()); _televisionShows = await _mediator.Send(new GetAllTelevisionShows()); _televisionSeasons = await _mediator.Send(new GetAllTelevisionSeasons()); + _artists = await _mediator.Send(new GetAllArtists()); string name = string.Empty; Option maybeSchedule = await _mediator.Send(new GetProgramScheduleById(Id)); @@ -276,6 +289,9 @@ private Task> SearchTelevisionSeasons(string value) => _televisionSeasons.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask(); + private Task> SearchArtists(string value) => + _artists.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask(); + private async Task SaveChanges() { var items = _schedule.Items.Map(item => new ReplaceProgramScheduleItem( diff --git a/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs b/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs index c037ba5e..1cb7dd74 100644 --- a/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs +++ b/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs @@ -54,6 +54,7 @@ namespace ErsatzTV.ViewModels ProgramScheduleItemCollectionType.Collection => Collection?.Name, ProgramScheduleItemCollectionType.TelevisionShow => MediaItem?.Name, ProgramScheduleItemCollectionType.TelevisionSeason => MediaItem?.Name, + ProgramScheduleItemCollectionType.Artist => MediaItem?.Name, _ => string.Empty };