Browse Source

add shuffle-in-order support to all collections (#356)

pull/357/head
Jason Dove 4 years ago committed by GitHub
parent
commit
a076b3eb30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 4
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  3. 1
      ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs
  4. 3
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  5. 1
      ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs
  6. 6
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  7. 163
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  8. 29
      ErsatzTV/Pages/ScheduleItemsEditor.razor

4
CHANGELOG.md

@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add Smart Collections - Add Smart Collections
- Smart Collections use search queries and can be created from the search result page - Smart Collections use search queries and can be created from the search result page
- Smart Collections are re-evaluated every time playouts are extended or rebuilt to automatically include newly-matching items - Smart Collections are re-evaluated every time playouts are extended or rebuilt to automatically include newly-matching items
- Allow `Shuffle In Order` with Collections and Smart Collections
- Episodes will be grouped by show, and music videos will be grouped by artist
- All movies will be a single group (multi-collections are probably better if `Shuffle In Order` is desired for movies)
- All groups will be be ordered chronologically (custom ordering is only supported in multi-collections)
### Fixed ### Fixed
- Generate XMLTV that validates successfully - Generate XMLTV that validates successfully

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

@ -36,10 +36,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
break; break;
} }
} }
else if (item.PlaybackOrder == PlaybackOrder.ShuffleInOrder)
{
return BaseError.New("Invalid playback order: 'Shuffle In Order'");
}
switch (item.PlayoutMode) switch (item.PlayoutMode)
{ {

1
ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs

@ -28,6 +28,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
.Filter(psi => psi.ProgramScheduleId == request.Id) .Filter(psi => psi.ProgramScheduleId == request.Id)
.Include(i => i.Collection) .Include(i => i.Collection)
.Include(i => i.MultiCollection) .Include(i => i.MultiCollection)
.Include(i => i.SmartCollection)
.Include(i => i.MediaItem) .Include(i => i.MediaItem)
.ThenInclude(i => (i as Movie).MovieMetadata) .ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork) .ThenInclude(mm => mm.Artwork)

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

@ -25,6 +25,9 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id) => public Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id) =>
throw new NotSupportedException(); throw new NotSupportedException();
public Task<List<CollectionWithItems>> GetFakeMultiCollectionCollections(int? collectionId, int? smartCollectionId) =>
throw new NotSupportedException();
public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException(); public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException();
public Task<List<int>> PlayoutIdsUsingMultiCollection(int multiCollectionId) => public Task<List<int>> PlayoutIdsUsingMultiCollection(int multiCollectionId) =>

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

@ -13,6 +13,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<List<MediaItem>> GetMultiCollectionItems(int id); Task<List<MediaItem>> GetMultiCollectionItems(int id);
Task<List<MediaItem>> GetSmartCollectionItems(int id); Task<List<MediaItem>> GetSmartCollectionItems(int id);
Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id); Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id);
Task<List<CollectionWithItems>> GetFakeMultiCollectionCollections(int? collectionId, int? smartCollectionId);
Task<List<int>> PlayoutIdsUsingCollection(int collectionId); Task<List<int>> PlayoutIdsUsingCollection(int collectionId);
Task<List<int>> PlayoutIdsUsingMultiCollection(int multiCollectionId); Task<List<int>> PlayoutIdsUsingMultiCollection(int multiCollectionId);
Task<List<int>> PlayoutIdsUsingSmartCollection(int smartCollectionId); Task<List<int>> PlayoutIdsUsingSmartCollection(int smartCollectionId);

6
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -613,6 +613,12 @@ namespace ErsatzTV.Core.Scheduling
result = await _mediaCollectionRepository.GetMultiCollectionCollections( result = await _mediaCollectionRepository.GetMultiCollectionCollections(
collectionKey.MultiCollectionId.Value); collectionKey.MultiCollectionId.Value);
} }
else
{
result = await _mediaCollectionRepository.GetFakeMultiCollectionCollections(
collectionKey.CollectionId,
collectionKey.SmartCollectionId);
}
return result; return result;
} }

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

@ -103,18 +103,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ToList(); .ToList();
result.AddRange(await GetMovieItems(dbContext, movieIds)); result.AddRange(await GetMovieItems(dbContext, movieIds));
var showIds = searchResults.Items foreach (int showId in searchResults.Items.Filter(i => i.Type == SearchIndex.ShowType).Map(i => i.Id))
.Filter(i => i.Type == SearchIndex.ShowType) {
.Map(i => i.Id) result.AddRange(await GetShowItemsFromShowId(dbContext, showId));
.ToList(); }
result.AddRange(await GetShowItems(dbContext, showIds));
foreach (int artistId in searchResults.Items.Filter(i => i.Type == SearchIndex.ArtistType)
.Map(i => i.Id))
{
result.AddRange(await GetArtistItemsFromArtistId(dbContext, artistId));
}
var artistIds = searchResults.Items
.Filter(i => i.Type == SearchIndex.ArtistType)
.Map(i => i.Id)
.ToList();
result.AddRange(await GetArtistItems(dbContext, artistIds));
var musicVideoIds = searchResults.Items var musicVideoIds = searchResults.Items
.Filter(i => i.Type == SearchIndex.MusicVideoType) .Filter(i => i.Type == SearchIndex.MusicVideoType)
.Map(i => i.Id) .Map(i => i.Id)
@ -196,6 +195,93 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return result; return result;
} }
public async Task<List<CollectionWithItems>> GetFakeMultiCollectionCollections(
int? collectionId,
int? smartCollectionId)
{
var items = new List<MediaItem>();
if (collectionId.HasValue)
{
items = await GetItems(collectionId.Value);
}
if (smartCollectionId.HasValue)
{
items = await GetSmartCollectionItems(smartCollectionId.Value);
}
return GroupIntoFakeCollections(items);
}
private static List<CollectionWithItems> GroupIntoFakeCollections(List<MediaItem> items)
{
int id = -1;
var result = new List<CollectionWithItems>();
var showCollections = new Dictionary<int, List<MediaItem>>();
foreach (Episode episode in items.OfType<Episode>())
{
List<MediaItem> list = showCollections.ContainsKey(episode.Season.ShowId)
? showCollections[episode.Season.ShowId]
: new List<MediaItem>();
if (list.All(i => i.Id != episode.Id))
{
list.Add(episode);
}
showCollections[episode.Season.ShowId] = list;
}
foreach ((int _, List<MediaItem> list) in showCollections)
{
result.Add(
new CollectionWithItems(
id--,
list,
true,
PlaybackOrder.Chronological,
false));
}
var artistCollections = new Dictionary<int, List<MediaItem>>();
foreach (MusicVideo musicVideo in items.OfType<MusicVideo>())
{
List<MediaItem> list = artistCollections.ContainsKey(musicVideo.ArtistId)
? artistCollections[musicVideo.ArtistId]
: new List<MediaItem>();
if (list.All(i => i.Id != musicVideo.Id))
{
list.Add(musicVideo);
}
artistCollections[musicVideo.ArtistId] = list;
}
foreach ((int _, List<MediaItem> list) in artistCollections)
{
result.Add(
new CollectionWithItems(
id--,
list,
true,
PlaybackOrder.Chronological,
false));
}
result.Add(
new CollectionWithItems(
id,
items.OfType<Movie>().Cast<MediaItem>().ToList(),
true,
PlaybackOrder.Chronological,
false));
return result;
}
public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) =>
_dbConnection.QueryAsync<int>( _dbConnection.QueryAsync<int>(
@"SELECT DISTINCT p.PlayoutId @"SELECT DISTINCT p.PlayoutId
@ -252,17 +338,30 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE ci.CollectionId = @CollectionId", WHERE ci.CollectionId = @CollectionId",
new { CollectionId = collectionId }); new { CollectionId = collectionId });
return await GetArtistItems(dbContext, ids); return await GetArtistItemsFromMusicVideoIds(dbContext, ids);
} }
private static Task<List<MusicVideo>> GetArtistItems(TvContext dbContext, IEnumerable<int> artistIds) => private static Task<List<MusicVideo>> GetArtistItemsFromMusicVideoIds(
TvContext dbContext,
IEnumerable<int> musicVideoIds) =>
dbContext.MusicVideos dbContext.MusicVideos
.Include(m => m.Artist) .Include(m => m.Artist)
.ThenInclude(a => a.ArtistMetadata) .ThenInclude(a => a.ArtistMetadata)
.Include(m => m.MusicVideoMetadata) .Include(m => m.MusicVideoMetadata)
.Include(m => m.MediaVersions) .Include(m => m.MediaVersions)
.Filter(m => artistIds.Contains(m.Id)) .Filter(m => musicVideoIds.Contains(m.Id))
.ToListAsync(); .ToListAsync();
private async Task<List<MusicVideo>> GetArtistItemsFromArtistId(TvContext dbContext, int artistId)
{
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT MusicVideo.Id FROM Artist
INNER JOIN MusicVideo on Artist.Id = MusicVideo.ArtistId
WHERE Artist.Id = @ArtistId",
new { ArtistId = artistId });
return await GetArtistItemsFromMusicVideoIds(dbContext, ids);
}
private async Task<List<MusicVideo>> GetMusicVideoItems(TvContext dbContext, int collectionId) private async Task<List<MusicVideo>> GetMusicVideoItems(TvContext dbContext, int collectionId)
{ {
@ -294,19 +393,31 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE ci.CollectionId = @CollectionId", WHERE ci.CollectionId = @CollectionId",
new { CollectionId = collectionId }); new { CollectionId = collectionId });
return await GetShowItems(dbContext, ids); return await GetShowItemsFromEpisodeIds(dbContext, ids);
} }
private static Task<List<Episode>> GetShowItems(TvContext dbContext, IEnumerable<int> showIds) => private static Task<List<Episode>> GetShowItemsFromEpisodeIds(TvContext dbContext, IEnumerable<int> episodeIds) =>
dbContext.Episodes dbContext.Episodes
.Include(e => e.EpisodeMetadata) .Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions) .Include(e => e.MediaVersions)
.Include(e => e.Season) .Include(e => e.Season)
.ThenInclude(s => s.Show) .ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata) .ThenInclude(s => s.ShowMetadata)
.Filter(e => showIds.Contains(e.Id)) .Filter(e => episodeIds.Contains(e.Id))
.ToListAsync(); .ToListAsync();
private async Task<List<Episode>> GetShowItemsFromShowId(TvContext dbContext, int showId)
{
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT Episode.Id FROM Show
INNER JOIN Season ON Season.ShowId = Show.Id
INNER JOIN Episode ON Episode.SeasonId = Season.Id
WHERE Show.Id = @ShowId",
new { ShowId = showId });
return await GetShowItemsFromEpisodeIds(dbContext, ids);
}
private async Task<List<Episode>> GetSeasonItems(TvContext dbContext, int collectionId) private async Task<List<Episode>> GetSeasonItems(TvContext dbContext, int collectionId)
{ {
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>( IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@ -316,14 +427,28 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE ci.CollectionId = @CollectionId", WHERE ci.CollectionId = @CollectionId",
new { CollectionId = collectionId }); new { CollectionId = collectionId });
return await dbContext.Episodes return await GetSeasonItemsFromEpisodeIds(dbContext, ids);
}
private static Task<List<Episode>> GetSeasonItemsFromEpisodeIds(TvContext dbContext, IEnumerable<int> episodeIds) =>
dbContext.Episodes
.Include(e => e.EpisodeMetadata) .Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions) .Include(e => e.MediaVersions)
.Include(e => e.Season) .Include(e => e.Season)
.ThenInclude(s => s.Show) .ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata) .ThenInclude(s => s.ShowMetadata)
.Filter(e => ids.Contains(e.Id)) .Filter(e => episodeIds.Contains(e.Id))
.ToListAsync(); .ToListAsync();
private async Task<List<Episode>> GetSeasonItemsFromSeasonId(TvContext dbContext, int seasonId)
{
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT Episode.Id FROM Season
INNER JOIN Episode ON Episode.SeasonId = Season.Id
WHERE Season.Id = @SeasonId",
new { SeasonId = seasonId });
return await GetSeasonItemsFromEpisodeIds(dbContext, ids);
} }
private async Task<List<Episode>> GetEpisodeItems(TvContext dbContext, int collectionId) private async Task<List<Episode>> GetEpisodeItems(TvContext dbContext, int collectionId)

29
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -154,18 +154,25 @@
SearchFunc="@SearchArtists" SearchFunc="@SearchArtists"
ToStringFunc="@(s => s?.Name)"/> ToStringFunc="@(s => s?.Name)"/>
} }
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)"> <MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.MultiCollection) @switch (_selectedItem.CollectionType)
{ {
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> case ProgramScheduleItemCollectionType.MultiCollection:
<MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
} <MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem>
else break;
{ case ProgramScheduleItemCollectionType.Collection:
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem> case ProgramScheduleItemCollectionType.SmartCollection:
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
@*<MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem>*@ <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem>
break;
default:
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
break;
} }
</MudSelect> </MudSelect>
<MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)"> <MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)">

Loading…
Cancel
Save