Browse Source

add artists directly to schedules (#248)

pull/249/head
Jason Dove 4 years ago committed by GitHub
parent
commit
db24ba84f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 8
      ErsatzTV.Application/Artists/Queries/GetAllArtists.cs
  3. 24
      ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs
  4. 7
      ErsatzTV.Application/MediaItems/Mapper.cs
  5. 7
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  6. 4
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  7. 10
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  8. 23
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  9. 3
      ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs
  10. 2
      ErsatzTV.Core/Interfaces/Repositories/IArtistRepository.cs
  11. 12
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  12. 23
      ErsatzTV.Infrastructure/Data/Repositories/ArtistRepository.cs
  13. 3
      ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs
  14. 23
      ErsatzTV/Pages/Artist.razor
  15. 16
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  16. 1
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

5
CHANGELOG.md

@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. @@ -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

8
ErsatzTV.Application/Artists/Queries/GetAllArtists.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using MediatR;
namespace ErsatzTV.Application.Artists.Queries
{
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
}

24
ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs

@ -0,0 +1,24 @@ @@ -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<GetAllArtists, List<NamedMediaItemViewModel>>
{
private readonly IArtistRepository _artistRepository;
public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository;
public Task<List<NamedMediaItemViewModel>> Handle(
GetAllArtists request,
CancellationToken cancellationToken) =>
_artistRepository.GetAllArtists().Map(list => list.Map(ProjectToViewModel).ToList());
}
}

7
ErsatzTV.Application/MediaItems/Mapper.cs

@ -7,12 +7,15 @@ namespace ErsatzTV.Application.MediaItems @@ -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("???");

7
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -79,6 +79,13 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -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");

4
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -30,6 +30,7 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -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 @@ -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 @@ -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 @@ -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),

10
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -19,10 +19,12 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -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
};
}

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

@ -3,11 +3,13 @@ using System.Collections.Generic; @@ -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 @@ -349,7 +351,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
};
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
var artistRepo = new Mock<IArtistRepository>();
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 @@ -429,7 +432,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
};
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
var artistRepo = new Mock<IArtistRepository>();
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 @@ -515,7 +519,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
};
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
var artistRepo = new Mock<IArtistRepository>();
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 @@ -605,7 +610,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
};
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
var artistRepo = new Mock<IArtistRepository>();
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 @@ -699,7 +705,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
};
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
var artistRepo = new Mock<IArtistRepository>();
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 @@ -792,7 +799,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
};
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
var artistRepo = new Mock<IArtistRepository>();
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 @@ -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<IArtistRepository>();
var builder = new PlayoutBuilder(collectionRepo, televisionRepo, artistRepo.Object, _logger);
var items = new List<ProgramScheduleItem> { Flood(mediaCollection) };

3
ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
{
Collection = 0,
TelevisionShow = 1,
TelevisionSeason = 2
TelevisionSeason = 2,
Artist = 3
}
}

2
ErsatzTV.Core/Interfaces/Repositories/IArtistRepository.cs

@ -21,5 +21,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -21,5 +21,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<bool> AddGenre(ArtistMetadata metadata, Genre genre);
Task<bool> AddStyle(ArtistMetadata metadata, Style style);
Task<bool> AddMood(ArtistMetadata metadata, Mood mood);
Task<List<MusicVideo>> GetArtistItems(int artistId);
Task<List<Artist>> GetAllArtists();
}
}

12
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -18,6 +18,7 @@ namespace ErsatzTV.Core.Scheduling @@ -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<PlayoutBuilder> _logger;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
@ -25,10 +26,12 @@ namespace ErsatzTV.Core.Scheduling @@ -25,10 +26,12 @@ namespace ErsatzTV.Core.Scheduling
public PlayoutBuilder(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
IArtistRepository artistRepository,
ILogger<PlayoutBuilder> logger)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
_artistRepository = artistRepository;
_logger = logger;
}
@ -66,6 +69,10 @@ namespace ErsatzTV.Core.Scheduling @@ -66,6 +69,10 @@ namespace ErsatzTV.Core.Scheduling
List<Episode> seasonItems =
await _televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0);
return Tuple(collectionKey, seasonItems.Cast<MediaItem>().ToList());
case ProgramScheduleItemCollectionType.Artist:
List<MusicVideo> artistItems =
await _artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0);
return Tuple(collectionKey, artistItems.Cast<MediaItem>().ToList());
default:
return Tuple(collectionKey, new List<MediaItem>());
}
@ -555,6 +562,11 @@ namespace ErsatzTV.Core.Scheduling @@ -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))
};

23
ErsatzTV.Infrastructure/Data/Repositories/ArtistRepository.cs

@ -146,5 +146,28 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -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<List<MusicVideo>> 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<List<Artist>> GetAllArtists()
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Artists
.AsNoTracking()
.Include(a => a.ArtistMetadata)
.ThenInclude(am => am.Artwork)
.ToListAsync();
}
}
}

3
ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs

@ -76,6 +76,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -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();

23
ErsatzTV/Pages/Artist.razor

@ -5,6 +5,8 @@ @@ -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 @@ @@ -55,6 +57,13 @@
OnClick="@AddToCollection">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Schedule"
OnClick="@AddToSchedule">
Add To Schedule
</MudButton>
</div>
</div>
</div>
@ -187,6 +196,20 @@ @@ -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<AddToScheduleDialog>("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 } };

16
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -6,6 +6,7 @@ @@ -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<ScheduleItemsEditor> _logger
@inject ISnackbar _snackbar
@ -129,6 +130,15 @@ @@ -129,6 +130,15 @@
SearchFunc="@SearchTelevisionSeasons"
ToStringFunc="@(s => s?.Name)"/>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Artist"
@bind-value="_selectedItem.MediaItem"
SearchFunc="@SearchArtists"
ToStringFunc="@(s => s?.Name)"/>
}
<MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)">
@foreach (PlayoutMode playoutMode in Enum.GetValues<PlayoutMode>())
{
@ -177,6 +187,7 @@ @@ -177,6 +187,7 @@
private List<MediaCollectionViewModel> _mediaCollections;
private List<NamedMediaItemViewModel> _televisionShows;
private List<NamedMediaItemViewModel> _televisionSeasons;
private List<NamedMediaItemViewModel> _artists;
private ProgramScheduleItemEditViewModel _selectedItem;
@ -184,9 +195,11 @@ @@ -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<ProgramScheduleViewModel> maybeSchedule = await _mediator.Send(new GetProgramScheduleById(Id));
@ -276,6 +289,9 @@ @@ -276,6 +289,9 @@
private Task<IEnumerable<NamedMediaItemViewModel>> SearchTelevisionSeasons(string value) =>
_televisionSeasons.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<NamedMediaItemViewModel>> 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(

1
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -54,6 +54,7 @@ namespace ErsatzTV.ViewModels @@ -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
};

Loading…
Cancel
Save