using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; using Dapper; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Search; using LanguageExt; using Microsoft.EntityFrameworkCore; using static LanguageExt.Prelude; namespace ErsatzTV.Infrastructure.Data.Repositories { public class MediaCollectionRepository : IMediaCollectionRepository { private readonly IDbConnection _dbConnection; private readonly ISearchIndex _searchIndex; private readonly IDbContextFactory _dbContextFactory; public MediaCollectionRepository( ISearchIndex searchIndex, IDbContextFactory dbContextFactory, IDbConnection dbConnection) { _searchIndex = searchIndex; _dbContextFactory = dbContextFactory; _dbConnection = dbConnection; } public async Task> GetCollectionWithCollectionItemsUntracked(int id) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Collections .Include(c => c.CollectionItems) .OrderBy(c => c.Id) .SingleOrDefaultAsync(c => c.Id == id) .Map(Optional); } public async Task> GetItems(int collectionId) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); var result = new List(); result.AddRange(await GetMovieItems(dbContext, collectionId)); result.AddRange(await GetShowItems(dbContext, collectionId)); result.AddRange(await GetSeasonItems(dbContext, collectionId)); result.AddRange(await GetEpisodeItems(dbContext, collectionId)); result.AddRange(await GetArtistItems(dbContext, collectionId)); result.AddRange(await GetMusicVideoItems(dbContext, collectionId)); result.AddRange(await GetOtherVideoItems(dbContext, collectionId)); result.AddRange(await GetSongItems(dbContext, collectionId)); return result.Distinct().ToList(); } public async Task> GetMultiCollectionItems(int id) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); var result = new List(); Option maybeMultiCollection = await dbContext.MultiCollections .Include(mc => mc.Collections) .Include(mc => mc.SmartCollections) .SelectOneAsync(mc => mc.Id, mc => mc.Id == id); foreach (MultiCollection multiCollection in maybeMultiCollection) { foreach (int collectionId in multiCollection.Collections.Map(c => c.Id)) { result.AddRange(await GetMovieItems(dbContext, collectionId)); result.AddRange(await GetShowItems(dbContext, collectionId)); result.AddRange(await GetSeasonItems(dbContext, collectionId)); result.AddRange(await GetEpisodeItems(dbContext, collectionId)); result.AddRange(await GetArtistItems(dbContext, collectionId)); result.AddRange(await GetMusicVideoItems(dbContext, collectionId)); result.AddRange(await GetOtherVideoItems(dbContext, collectionId)); result.AddRange(await GetSongItems(dbContext, collectionId)); } foreach (int smartCollectionId in multiCollection.SmartCollections.Map(c => c.Id)) { result.AddRange(await GetSmartCollectionItems(smartCollectionId)); } } return result.Distinct().ToList(); } public async Task> GetSmartCollectionItems(int id) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); var result = new List(); Option maybeCollection = await dbContext.SmartCollections .SelectOneAsync(sc => sc.Id, sc => sc.Id == id); foreach (SmartCollection collection in maybeCollection) { SearchResult searchResults = await _searchIndex.Search(collection.Query, 0, 0); var movieIds = searchResults.Items .Filter(i => i.Type == SearchIndex.MovieType) .Map(i => i.Id) .ToList(); result.AddRange(await GetMovieItems(dbContext, movieIds)); foreach (int showId in searchResults.Items.Filter(i => i.Type == SearchIndex.ShowType).Map(i => i.Id)) { result.AddRange(await GetShowItemsFromShowId(dbContext, showId)); } foreach (int seasonId in searchResults.Items.Filter(i => i.Type == SearchIndex.SeasonType) .Map(i => i.Id)) { result.AddRange(await GetSeasonItemsFromSeasonId(dbContext, seasonId)); } foreach (int artistId in searchResults.Items.Filter(i => i.Type == SearchIndex.ArtistType) .Map(i => i.Id)) { result.AddRange(await GetArtistItemsFromArtistId(dbContext, artistId)); } var musicVideoIds = searchResults.Items .Filter(i => i.Type == SearchIndex.MusicVideoType) .Map(i => i.Id) .ToList(); result.AddRange(await GetMusicVideoItems(dbContext, musicVideoIds)); var episodeIds = searchResults.Items .Filter(i => i.Type == SearchIndex.EpisodeType) .Map(i => i.Id) .ToList(); result.AddRange(await GetEpisodeItems(dbContext, episodeIds)); var otherVideoIds = searchResults.Items .Filter(i => i.Type == SearchIndex.OtherVideoType) .Map(i => i.Id) .ToList(); result.AddRange(await GetOtherVideoItems(dbContext, otherVideoIds)); var songIds = searchResults.Items .Filter(i => i.Type == SearchIndex.SongType) .Map(i => i.Id) .ToList(); result.AddRange(await GetSongItems(dbContext, songIds)); } return result; } public async Task> GetMultiCollectionCollections(int id) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); var result = new List(); Option maybeMultiCollection = await dbContext.MultiCollections .Include(mc => mc.Collections) .Include(mc => mc.SmartCollections) .Include(mc => mc.MultiCollectionItems) .ThenInclude(mci => mci.Collection) .Include(mc => mc.MultiCollectionSmartItems) .ThenInclude(mci => mci.SmartCollection) .SelectOneAsync(mc => mc.Id, mc => mc.Id == id); foreach (MultiCollection multiCollection in maybeMultiCollection) { foreach (MultiCollectionItem multiCollectionItem in multiCollection.MultiCollectionItems) { List items = await GetItems(multiCollectionItem.CollectionId); if (multiCollectionItem.Collection.UseCustomPlaybackOrder) { foreach (Collection collection in await GetCollectionWithCollectionItemsUntracked( multiCollectionItem.CollectionId)) { var sortedItems = collection.CollectionItems .OrderBy(ci => ci.CustomIndex) .Map(ci => items.First(i => i.Id == ci.MediaItemId)) .ToList(); result.Add( new CollectionWithItems( multiCollectionItem.CollectionId, sortedItems, multiCollectionItem.ScheduleAsGroup, multiCollectionItem.PlaybackOrder, multiCollectionItem.Collection.UseCustomPlaybackOrder)); } } else { result.Add( new CollectionWithItems( multiCollectionItem.CollectionId, items, multiCollectionItem.ScheduleAsGroup, multiCollectionItem.PlaybackOrder, multiCollectionItem.Collection.UseCustomPlaybackOrder)); } } foreach (MultiCollectionSmartItem multiCollectionSmartItem in multiCollection.MultiCollectionSmartItems) { List items = await GetSmartCollectionItems(multiCollectionSmartItem.SmartCollectionId); result.Add( new CollectionWithItems( multiCollectionSmartItem.SmartCollectionId, items, multiCollectionSmartItem.ScheduleAsGroup, multiCollectionSmartItem.PlaybackOrder, false)); } } // remove duplicate items from ungrouped collections var toRemoveFrom = result.Filter(c => !c.ScheduleAsGroup).ToList(); var toRemove = result.Filter(c => c.ScheduleAsGroup) .SelectMany(c => c.MediaItems.Map(i => i.Id)) .Distinct() .ToList(); foreach (CollectionWithItems collection in toRemoveFrom) { collection.MediaItems.RemoveAll(mi => toRemove.Contains(mi.Id)); } return result; } public async Task> GetFakeMultiCollectionCollections( int? collectionId, int? smartCollectionId) { var items = new List(); if (collectionId.HasValue) { items = await GetItems(collectionId.Value); } if (smartCollectionId.HasValue) { items = await GetSmartCollectionItems(smartCollectionId.Value); } return GroupIntoFakeCollections(items); } private static List GroupIntoFakeCollections(List items) { int id = -1; var result = new List(); var showCollections = new Dictionary>(); foreach (Episode episode in items.OfType()) { List list = showCollections.ContainsKey(episode.Season.ShowId) ? showCollections[episode.Season.ShowId] : new List(); if (list.All(i => i.Id != episode.Id)) { list.Add(episode); } showCollections[episode.Season.ShowId] = list; } foreach ((int _, List list) in showCollections) { result.Add( new CollectionWithItems( id--, list, true, PlaybackOrder.Chronological, false)); } var artistCollections = new Dictionary>(); foreach (MusicVideo musicVideo in items.OfType()) { List list = artistCollections.ContainsKey(musicVideo.ArtistId) ? artistCollections[musicVideo.ArtistId] : new List(); if (list.All(i => i.Id != musicVideo.Id)) { list.Add(musicVideo); } artistCollections[musicVideo.ArtistId] = list; } foreach ((int _, List list) in artistCollections) { result.Add( new CollectionWithItems( id--, list, true, PlaybackOrder.Chronological, false)); } result.Add( new CollectionWithItems( id, items.OfType().Cast().ToList(), true, PlaybackOrder.Chronological, false)); return result; } public Task> PlayoutIdsUsingCollection(int collectionId) => _dbConnection.QueryAsync( @"SELECT DISTINCT p.PlayoutId FROM PlayoutProgramScheduleAnchor p WHERE p.CollectionId = @CollectionId", new { CollectionId = collectionId }) .Map(result => result.ToList()); public Task> PlayoutIdsUsingMultiCollection(int multiCollectionId) => _dbConnection.QueryAsync( @"SELECT DISTINCT p.PlayoutId FROM PlayoutProgramScheduleAnchor p WHERE p.MultiCollectionId = @MultiCollectionId", new { MultiCollectionId = multiCollectionId }) .Map(result => result.ToList()); public Task> PlayoutIdsUsingSmartCollection(int smartCollectionId) => _dbConnection.QueryAsync( @"SELECT DISTINCT p.PlayoutId FROM PlayoutProgramScheduleAnchor p WHERE p.SmartCollectionId = @SmartCollectionId", new { SmartCollectionId = smartCollectionId }) .Map(result => result.ToList()); public Task IsCustomPlaybackOrder(int collectionId) => _dbConnection.QuerySingleAsync( @"SELECT IFNULL(MIN(UseCustomPlaybackOrder), 0) FROM Collection WHERE Id = @CollectionId", new { CollectionId = collectionId }); private async Task> GetMovieItems(TvContext dbContext, int collectionId) { IEnumerable ids = await _dbConnection.QueryAsync( @"SELECT m.Id FROM CollectionItem ci INNER JOIN Movie m ON m.Id = ci.MediaItemId WHERE ci.CollectionId = @CollectionId", new { CollectionId = collectionId }); return await GetMovieItems(dbContext, ids); } private static Task> GetMovieItems(TvContext dbContext, IEnumerable movieIds) => dbContext.Movies .Include(m => m.MovieMetadata) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Chapters) .Filter(m => movieIds.Contains(m.Id)) .ToListAsync(); private async Task> GetArtistItems(TvContext dbContext, int collectionId) { IEnumerable ids = await _dbConnection.QueryAsync( @"SELECT MusicVideo.Id FROM CollectionItem ci INNER JOIN Artist on Artist.Id = ci.MediaItemId INNER JOIN MusicVideo on Artist.Id = MusicVideo.ArtistId WHERE ci.CollectionId = @CollectionId", new { CollectionId = collectionId }); return await GetArtistItemsFromMusicVideoIds(dbContext, ids); } private static Task> GetArtistItemsFromMusicVideoIds( TvContext dbContext, IEnumerable musicVideoIds) => dbContext.MusicVideos .Include(m => m.Artist) .ThenInclude(a => a.ArtistMetadata) .Include(m => m.MusicVideoMetadata) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Chapters) .Filter(m => musicVideoIds.Contains(m.Id)) .ToListAsync(); private async Task> GetArtistItemsFromArtistId(TvContext dbContext, int artistId) { IEnumerable ids = await _dbConnection.QueryAsync( @"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> GetMusicVideoItems(TvContext dbContext, int collectionId) { IEnumerable ids = await _dbConnection.QueryAsync( @"SELECT m.Id FROM CollectionItem ci INNER JOIN MusicVideo m ON m.Id = ci.MediaItemId WHERE ci.CollectionId = @CollectionId", new { CollectionId = collectionId }); return await GetMusicVideoItems(dbContext, ids); } private static Task> GetMusicVideoItems(TvContext dbContext, IEnumerable musicVideoIds) => dbContext.MusicVideos .Include(m => m.Artist) .ThenInclude(a => a.ArtistMetadata) .Include(m => m.MusicVideoMetadata) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Chapters) .Filter(m => musicVideoIds.Contains(m.Id)) .ToListAsync(); private async Task> GetOtherVideoItems(TvContext dbContext, int collectionId) { IEnumerable ids = await _dbConnection.QueryAsync( @"SELECT o.Id FROM CollectionItem ci INNER JOIN OtherVideo o ON o.Id = ci.MediaItemId WHERE ci.CollectionId = @CollectionId", new { CollectionId = collectionId }); return await GetOtherVideoItems(dbContext, ids); } private static Task> GetOtherVideoItems(TvContext dbContext, IEnumerable otherVideoIds) => dbContext.OtherVideos .Include(m => m.OtherVideoMetadata) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Chapters) .Filter(m => otherVideoIds.Contains(m.Id)) .ToListAsync(); private async Task> GetSongItems(TvContext dbContext, int collectionId) { IEnumerable ids = await _dbConnection.QueryAsync( @"SELECT s.Id FROM CollectionItem ci INNER JOIN Song s ON s.Id = ci.MediaItemId WHERE ci.CollectionId = @CollectionId", new { CollectionId = collectionId }); return await GetSongItems(dbContext, ids); } private static Task> GetSongItems(TvContext dbContext, IEnumerable songIds) => dbContext.Songs .Include(m => m.SongMetadata) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Chapters) .Filter(m => songIds.Contains(m.Id)) .ToListAsync(); private async Task> GetShowItems(TvContext dbContext, int collectionId) { IEnumerable ids = await _dbConnection.QueryAsync( @"SELECT Episode.Id FROM CollectionItem ci INNER JOIN Show ON Show.Id = ci.MediaItemId INNER JOIN Season ON Season.ShowId = Show.Id INNER JOIN Episode ON Episode.SeasonId = Season.Id WHERE ci.CollectionId = @CollectionId", new { CollectionId = collectionId }); return await GetShowItemsFromEpisodeIds(dbContext, ids); } private static Task> GetShowItemsFromEpisodeIds(TvContext dbContext, IEnumerable episodeIds) => dbContext.Episodes .Include(e => e.EpisodeMetadata) .Include(e => e.MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(e => e.Season) .ThenInclude(s => s.Show) .ThenInclude(s => s.ShowMetadata) .Filter(e => episodeIds.Contains(e.Id)) .ToListAsync(); private async Task> GetShowItemsFromShowId(TvContext dbContext, int showId) { IEnumerable ids = await _dbConnection.QueryAsync( @"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> GetSeasonItems(TvContext dbContext, int collectionId) { IEnumerable ids = await _dbConnection.QueryAsync( @"SELECT Episode.Id FROM CollectionItem ci INNER JOIN Season ON Season.Id = ci.MediaItemId INNER JOIN Episode ON Episode.SeasonId = Season.Id WHERE ci.CollectionId = @CollectionId", new { CollectionId = collectionId }); return await GetSeasonItemsFromEpisodeIds(dbContext, ids); } private static Task> GetSeasonItemsFromEpisodeIds(TvContext dbContext, IEnumerable episodeIds) => dbContext.Episodes .Include(e => e.EpisodeMetadata) .Include(e => e.MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(e => e.Season) .ThenInclude(s => s.Show) .ThenInclude(s => s.ShowMetadata) .Filter(e => episodeIds.Contains(e.Id)) .ToListAsync(); private async Task> GetSeasonItemsFromSeasonId(TvContext dbContext, int seasonId) { IEnumerable ids = await _dbConnection.QueryAsync( @"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> GetEpisodeItems(TvContext dbContext, int collectionId) { IEnumerable ids = await _dbConnection.QueryAsync( @"SELECT Episode.Id FROM CollectionItem ci INNER JOIN Episode ON Episode.Id = ci.MediaItemId WHERE ci.CollectionId = @CollectionId", new { CollectionId = collectionId }); return await GetEpisodeItems(dbContext, ids); } private static Task> GetEpisodeItems(TvContext dbContext, IEnumerable episodeIds) => dbContext.Episodes .Include(e => e.EpisodeMetadata) .Include(e => e.MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(e => e.Season) .ThenInclude(s => s.Show) .ThenInclude(s => s.ShowMetadata) .Filter(e => episodeIds.Contains(e.Id)) .ToListAsync(); } }