using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; using Dapper; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using LanguageExt; using Microsoft.EntityFrameworkCore; using static LanguageExt.Prelude; namespace ErsatzTV.Infrastructure.Data.Repositories { public class TelevisionRepository : ITelevisionRepository { private readonly IDbConnection _dbConnection; private readonly IDbContextFactory _dbContextFactory; public TelevisionRepository(IDbConnection dbConnection, IDbContextFactory dbContextFactory) { _dbConnection = dbConnection; _dbContextFactory = dbContextFactory; } public Task AllShowsExist(List showIds) => _dbConnection.QuerySingleAsync( "SELECT COUNT(*) FROM Show WHERE Id in @ShowIds", new { ShowIds = showIds }) .Map(c => c == showIds.Count); public Task AllSeasonsExist(List seasonIds) => _dbConnection.QuerySingleAsync( "SELECT COUNT(*) FROM Season WHERE Id in @SeasonIds", new { SeasonIds = seasonIds }) .Map(c => c == seasonIds.Count); public Task AllEpisodesExist(List episodeIds) => _dbConnection.QuerySingleAsync( "SELECT COUNT(*) FROM Episode WHERE Id in @EpisodeIds", new { EpisodeIds = episodeIds }) .Map(c => c == episodeIds.Count); public async Task> GetAllShows() { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Shows .AsNoTracking() .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Artwork) .ToListAsync(); } public async Task> GetShow(int showId) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Shows .AsNoTracking() .Filter(s => s.Id == showId) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Artwork) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Genres) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Tags) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Studios) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Actors) .ThenInclude(a => a.Artwork) .OrderBy(s => s.Id) .SingleOrDefaultAsync() .Map(Optional); } public async Task> GetShowsForCards(List ids) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.ShowMetadata .AsNoTracking() .Filter(sm => ids.Contains(sm.ShowId)) .Include(sm => sm.Artwork) .OrderBy(sm => sm.SortTitle) .ToListAsync(); } public async Task> GetSeasonsForCards(List ids) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.SeasonMetadata .AsNoTracking() .Filter(s => ids.Contains(s.SeasonId)) .Include(s => s.Season.Show) .ThenInclude(s => s.ShowMetadata) .Include(sm => sm.Artwork) .ToListAsync() .Map( list => list .OrderBy(s => s.Season.Show.ShowMetadata.HeadOrNone().Match(sm => sm.SortTitle, () => string.Empty)) .ThenBy(s => s.Season.SeasonNumber) .ToList()); } public async Task> GetEpisodesForCards(List ids) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.EpisodeMetadata .AsNoTracking() .Filter(em => ids.Contains(em.EpisodeId)) .Include(em => em.Artwork) .Include(em => em.Directors) .Include(em => em.Writers) .Include(em => em.Episode) .ThenInclude(e => e.Season) .ThenInclude(s => s.SeasonMetadata) .ThenInclude(sm => sm.Artwork) .Include(em => em.Episode) .ThenInclude(e => e.Season) .ThenInclude(s => s.Show) .ThenInclude(s => s.ShowMetadata) .ThenInclude(sm => sm.Artwork) .OrderBy(em => em.SortTitle) .ToListAsync(); } public async Task> GetAllSeasons() { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Seasons .AsNoTracking() .Include(s => s.SeasonMetadata) .ThenInclude(sm => sm.Artwork) .Include(s => s.Show) .ThenInclude(s => s.ShowMetadata) .ThenInclude(sm => sm.Artwork) .ToListAsync(); } public async Task> GetSeason(int seasonId) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Seasons .AsNoTracking() .Include(s => s.SeasonMetadata) .ThenInclude(sm => sm.Artwork) .Include(s => s.Show) .ThenInclude(s => s.ShowMetadata) .ThenInclude(sm => sm.Artwork) .OrderBy(s => s.Id) .SingleOrDefaultAsync(s => s.Id == seasonId) .Map(Optional); } public async Task GetSeasonCount(int showId) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Seasons .AsNoTracking() .CountAsync(s => s.ShowId == showId); } public async Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) { List showIds = await _dbConnection.QueryAsync( @"SELECT m1.ShowId FROM ShowMetadata m1 LEFT OUTER JOIN ShowMetadata m2 ON m2.ShowId = @ShowId WHERE m1.Title = m2.Title AND m1.Year is m2.Year", new { ShowId = televisionShowId }) .Map(results => results.ToList()); await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Seasons .AsNoTracking() .Where(s => showIds.Contains(s.ShowId)) .Include(s => s.SeasonMetadata) .ThenInclude(sm => sm.Artwork) .Include(s => s.Show) .ThenInclude(s => s.ShowMetadata) .OrderBy(s => s.SeasonNumber) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); } public async Task GetEpisodeCount(int seasonId) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Episodes .AsNoTracking() .CountAsync(e => e.SeasonId == seasonId); } public async Task> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.EpisodeMetadata .AsNoTracking() .Filter(em => em.Episode.SeasonId == seasonId) .Include(em => em.Artwork) .Include(em => em.Directors) .Include(em => em.Writers) .Include(em => em.Episode) .ThenInclude(e => e.Season) .ThenInclude(s => s.Show) .ThenInclude(s => s.ShowMetadata) .ThenInclude(sm => sm.Actors) .ThenInclude(a => a.Artwork) .OrderBy(em => em.EpisodeNumber) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); } public async Task> GetShowByMetadata(int libraryPathId, ShowMetadata metadata) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option maybeId = await dbContext.ShowMetadata .Where(s => s.Title == metadata.Title && s.Year == metadata.Year) .Where(s => s.Show.LibraryPathId == libraryPathId) .SingleOrDefaultAsync() .Map(Optional) .MapT(sm => sm.ShowId); return await maybeId.Match( id => { return dbContext.Shows .AsNoTracking() .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Artwork) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Genres) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Tags) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Studios) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Actors) .ThenInclude(a => a.Artwork) .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Guids) .Include(s => s.LibraryPath) .ThenInclude(lp => lp.Library) .Include(s => s.TraktListItems) .ThenInclude(tli => tli.TraktList) .OrderBy(s => s.Id) .SingleOrDefaultAsync(s => s.Id == id) .Map(Optional); }, () => Option.None.AsTask()); } public async Task>> AddShow( int libraryPathId, string showFolder, ShowMetadata metadata) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); try { metadata.DateAdded = DateTime.UtcNow; metadata.Genres ??= new List(); metadata.Tags ??= new List(); metadata.Studios ??= new List(); metadata.Actors ??= new List(); metadata.Guids ??= new List(); var show = new Show { LibraryPathId = libraryPathId, ShowMetadata = new List { metadata }, Seasons = new List(), TraktListItems = new List() }; await dbContext.Shows.AddAsync(show); await dbContext.SaveChangesAsync(); await dbContext.Entry(show).Reference(s => s.LibraryPath).LoadAsync(); await dbContext.Entry(show.LibraryPath).Reference(lp => lp.Library).LoadAsync(); return new MediaItemScanResult(show) { IsAdded = true }; } catch (Exception ex) { return BaseError.New(ex.Message); } } public async Task> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option maybeExisting = await dbContext.Seasons .Include(s => s.SeasonMetadata) .ThenInclude(sm => sm.Artwork) .Include(s => s.SeasonMetadata) .ThenInclude(sm => sm.Guids) .Include(s => s.LibraryPath) .ThenInclude(lp => lp.Library) .Include(s => s.TraktListItems) .ThenInclude(tli => tli.TraktList) .OrderBy(s => s.ShowId) .ThenBy(s => s.SeasonNumber) .SingleOrDefaultAsync(s => s.ShowId == show.Id && s.SeasonNumber == seasonNumber); return await maybeExisting.Match( season => Right(season).AsTask(), () => AddSeason(dbContext, show, libraryPathId, seasonNumber)); } public async Task> GetOrAddEpisode( Season season, LibraryPath libraryPath, string path) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option maybeExisting = await dbContext.Episodes .Include(i => i.EpisodeMetadata) .ThenInclude(em => em.Artwork) .Include(i => i.EpisodeMetadata) .ThenInclude(em => em.Genres) .Include(i => i.EpisodeMetadata) .ThenInclude(em => em.Tags) .Include(i => i.EpisodeMetadata) .ThenInclude(em => em.Studios) .Include(i => i.EpisodeMetadata) .ThenInclude(em => em.Actors) .ThenInclude(a => a.Artwork) .Include(i => i.EpisodeMetadata) .ThenInclude(em => em.Guids) .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Directors) .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Writers) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.Streams) .Include(i => i.LibraryPath) .ThenInclude(lp => lp.Library) .Include(i => i.Season) .Include(i => i.TraktListItems) .ThenInclude(tli => tli.TraktList) .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path) .SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path); return await maybeExisting.Match>>( async episode => { // move the file to the new season if needed // this can happen when adding NFO metadata to existing content if (episode.SeasonId != season.Id) { episode.SeasonId = season.Id; episode.Season = season; await _dbConnection.ExecuteAsync( @"UPDATE Episode SET SeasonId = @SeasonId WHERE Id = @EpisodeId", new { SeasonId = season.Id, EpisodeId = episode.Id }); } return episode; }, async () => await AddEpisode(dbContext, season, libraryPath.Id, path)); } public Task> FindEpisodePaths(LibraryPath libraryPath) => _dbConnection.QueryAsync( @"SELECT MF.Path FROM MediaFile MF INNER JOIN MediaVersion MV on MF.MediaVersionId = MV.Id INNER JOIN Episode E on MV.EpisodeId = E.Id INNER JOIN MediaItem MI on E.Id = MI.Id WHERE MI.LibraryPathId = @LibraryPathId", new { LibraryPathId = libraryPath.Id }); public async Task DeleteByPath(LibraryPath libraryPath, string path) { IEnumerable ids = await _dbConnection.QueryAsync( @"SELECT E.Id FROM Episode E INNER JOIN MediaItem MI on E.Id = MI.Id INNER JOIN MediaVersion MV on E.Id = MV.EpisodeId INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path", new { LibraryPathId = libraryPath.Id, Path = path }); await using TvContext dbContext = _dbContextFactory.CreateDbContext(); foreach (int episodeId in ids) { Episode episode = await dbContext.Episodes.FindAsync(episodeId); dbContext.Episodes.Remove(episode); } await dbContext.SaveChangesAsync(); return Unit.Default; } public async Task DeleteEmptySeasons(LibraryPath libraryPath) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); List seasons = await dbContext.Seasons .Filter(s => s.LibraryPathId == libraryPath.Id) .Filter(s => s.Episodes.Count == 0) .ToListAsync(); dbContext.Seasons.RemoveRange(seasons); await dbContext.SaveChangesAsync(); return Unit.Default; } public async Task> DeleteEmptyShows(LibraryPath libraryPath) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); List shows = await dbContext.Shows .Filter(s => s.LibraryPathId == libraryPath.Id) .Filter(s => s.Seasons.Count == 0) .ToListAsync(); var ids = shows.Map(s => s.Id).ToList(); dbContext.Shows.RemoveRange(shows); await dbContext.SaveChangesAsync(); return ids; } public async Task>> GetOrAddPlexShow( PlexLibrary library, PlexShow item) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option maybeExisting = await dbContext.PlexShows .AsNoTracking() .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Genres) .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Tags) .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Studios) .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Actors) .ThenInclude(a => a.Artwork) .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Artwork) .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Guids) .Include(i => i.LibraryPath) .ThenInclude(lp => lp.Library) .Include(i => i.TraktListItems) .ThenInclude(tli => tli.TraktList) .OrderBy(i => i.Key) .SingleOrDefaultAsync(i => i.Key == item.Key); return await maybeExisting.Match( plexShow => Right>( new MediaItemScanResult(plexShow) { IsAdded = false }).AsTask(), async () => await AddPlexShow(dbContext, library, item)); } public async Task> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option maybeExisting = await dbContext.PlexSeasons .AsNoTracking() .Include(i => i.SeasonMetadata) .ThenInclude(sm => sm.Artwork) .Include(i => i.SeasonMetadata) .ThenInclude(sm => sm.Guids) .Include(s => s.LibraryPath) .ThenInclude(l => l.Library) .Include(s => s.TraktListItems) .ThenInclude(tli => tli.TraktList) .OrderBy(i => i.Key) .SingleOrDefaultAsync(i => i.Key == item.Key); return await maybeExisting.Match( plexSeason => Right(plexSeason).AsTask(), async () => await AddPlexSeason(dbContext, library, item)); } public async Task> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option maybeExisting = await dbContext.PlexEpisodes .AsNoTracking() .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Artwork) .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Genres) .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Tags) .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Studios) .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Directors) .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Writers) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.Streams) .Include(e => e.EpisodeMetadata) .ThenInclude(em => em.Actors) .ThenInclude(a => a.Artwork) .Include(e => e.EpisodeMetadata) .ThenInclude(em => em.Guids) .Include(i => i.LibraryPath) .ThenInclude(lp => lp.Library) .Include(e => e.Season) .Include(e => e.TraktListItems) .ThenInclude(tli => tli.TraktList) .OrderBy(i => i.Key) .SingleOrDefaultAsync(i => i.Key == item.Key); return await maybeExisting.Match( plexEpisode => Right(plexEpisode).AsTask(), async () => await AddPlexEpisode(dbContext, library, item)); } public Task RemoveMissingPlexSeasons(string showKey, List seasonKeys) => _dbConnection.ExecuteAsync( @"DELETE FROM MediaItem WHERE Id IN (SELECT m.Id FROM MediaItem m INNER JOIN Season s ON m.Id = s.Id INNER JOIN PlexSeason ps ON ps.Id = m.Id INNER JOIN PlexShow P on P.Id = s.ShowId WHERE P.Key = @ShowKey AND ps.Key not in @Keys)", new { ShowKey = showKey, Keys = seasonKeys }).ToUnit(); public async Task> RemoveMissingPlexEpisodes(string seasonKey, List episodeKeys) { List ids = await _dbConnection.QueryAsync( @"SELECT m.Id FROM MediaItem m INNER JOIN Episode e ON m.Id = e.Id INNER JOIN PlexEpisode pe ON pe.Id = m.Id INNER JOIN PlexSeason P on P.Id = e.SeasonId WHERE P.Key = @SeasonKey AND pe.Key not in @Keys", new { SeasonKey = seasonKey, Keys = episodeKeys }).Map(result => result.ToList()); await _dbConnection.ExecuteAsync( "DELETE FROM MediaItem WHERE Id IN @Ids", new { Ids = ids }); return ids; } public async Task RemoveMetadata(Episode episode, EpisodeMetadata metadata) { episode.EpisodeMetadata.Remove(metadata); await _dbConnection.ExecuteAsync( @"DELETE FROM EpisodeMetadata WHERE Id = @MetadataId", new { MetadataId = metadata.Id }); return Unit.Default; } public Task AddDirector(EpisodeMetadata metadata, Director director) => _dbConnection.ExecuteAsync( "INSERT INTO Director (Name, EpisodeMetadataId) VALUES (@Name, @MetadataId)", new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0); public Task AddWriter(EpisodeMetadata metadata, Writer writer) => _dbConnection.ExecuteAsync( "INSERT INTO Writer (Name, EpisodeMetadataId) VALUES (@Name, @MetadataId)", new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0); public Task UpdatePath(int mediaFileId, string path) => _dbConnection.ExecuteAsync( "UPDATE MediaFile SET Path = @Path WHERE Id = @MediaFileId", new { Path = path, MediaFileId = mediaFileId }).Map(_ => Unit.Default); public async Task> GetShowItems(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 }); await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Episodes .AsNoTracking() .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 => ids.Contains(e.Id)) .ToListAsync(); } public async Task> GetSeasonItems(int seasonId) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); return await dbContext.Episodes .AsNoTracking() .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 => e.SeasonId == seasonId) .ToListAsync(); } public Task AddGenre(ShowMetadata metadata, Genre genre) => _dbConnection.ExecuteAsync( "INSERT INTO Genre (Name, ShowMetadataId) VALUES (@Name, @MetadataId)", new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0); public Task AddTag(ShowMetadata metadata, Tag tag) => _dbConnection.ExecuteAsync( "INSERT INTO Tag (Name, ShowMetadataId) VALUES (@Name, @MetadataId)", new { tag.Name, MetadataId = metadata.Id }).Map(result => result > 0); public Task AddStudio(ShowMetadata metadata, Studio studio) => _dbConnection.ExecuteAsync( "INSERT INTO Studio (Name, ShowMetadataId) VALUES (@Name, @MetadataId)", new { studio.Name, MetadataId = metadata.Id }).Map(result => result > 0); public async Task AddActor(ShowMetadata metadata, Actor actor) { int? artworkId = null; if (actor.Artwork != null) { artworkId = await _dbConnection.QuerySingleAsync( @"INSERT INTO Artwork (ArtworkKind, DateAdded, DateUpdated, Path) VALUES (@ArtworkKind, @DateAdded, @DateUpdated, @Path); SELECT last_insert_rowid()", new { ArtworkKind = (int) actor.Artwork.ArtworkKind, actor.Artwork.DateAdded, actor.Artwork.DateUpdated, actor.Artwork.Path }); } return await _dbConnection.ExecuteAsync( "INSERT INTO Actor (Name, Role, \"Order\", ShowMetadataId, ArtworkId) VALUES (@Name, @Role, @Order, @MetadataId, @ArtworkId)", new { actor.Name, actor.Role, actor.Order, MetadataId = metadata.Id, ArtworkId = artworkId }) .Map(result => result > 0); } public async Task AddActor(EpisodeMetadata metadata, Actor actor) { int? artworkId = null; if (actor.Artwork != null) { artworkId = await _dbConnection.QuerySingleAsync( @"INSERT INTO Artwork (ArtworkKind, DateAdded, DateUpdated, Path) VALUES (@ArtworkKind, @DateAdded, @DateUpdated, @Path); SELECT last_insert_rowid()", new { ArtworkKind = (int) actor.Artwork.ArtworkKind, actor.Artwork.DateAdded, actor.Artwork.DateUpdated, actor.Artwork.Path }); } return await _dbConnection.ExecuteAsync( "INSERT INTO Actor (Name, Role, \"Order\", EpisodeMetadataId, ArtworkId) VALUES (@Name, @Role, @Order, @MetadataId, @ArtworkId)", new { actor.Name, actor.Role, actor.Order, MetadataId = metadata.Id, ArtworkId = artworkId }) .Map(result => result > 0); } public async Task> RemoveMissingPlexShows(PlexLibrary library, List showKeys) { List ids = await _dbConnection.QueryAsync( @"SELECT m.Id FROM MediaItem m INNER JOIN PlexShow ps ON ps.Id = m.Id INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId WHERE lp.LibraryId = @LibraryId AND ps.Key not in @Keys", new { LibraryId = library.Id, Keys = showKeys }).Map(result => result.ToList()); await _dbConnection.ExecuteAsync( "DELETE FROM MediaItem WHERE Id IN @Ids", new { Ids = ids }); return ids; } private static async Task> AddSeason( TvContext dbContext, Show show, int libraryPathId, int seasonNumber) { try { var season = new Season { LibraryPathId = libraryPathId, ShowId = show.Id, SeasonNumber = seasonNumber, Episodes = new List(), SeasonMetadata = new List { new() { DateAdded = DateTime.UtcNow, Guids = new List() } }, TraktListItems = new List() }; await dbContext.Seasons.AddAsync(season); await dbContext.SaveChangesAsync(); await dbContext.Entry(season).Reference(s => s.LibraryPath).LoadAsync(); await dbContext.Entry(season.LibraryPath).Reference(lp => lp.Library).LoadAsync(); return season; } catch (Exception ex) { return BaseError.New(ex.Message); } } private static async Task> AddEpisode( TvContext dbContext, Season season, int libraryPathId, string path) { try { if (dbContext.MediaFiles.Any(mf => mf.Path == path)) { return BaseError.New("Multi-episode files are not yet supported"); } var episode = new Episode { LibraryPathId = libraryPathId, SeasonId = season.Id, EpisodeMetadata = new List { new() { DateAdded = DateTime.UtcNow, DateUpdated = SystemTime.MinValueUtc, MetadataKind = MetadataKind.Fallback, Actors = new List(), Guids = new List(), Writers = new List(), Directors = new List(), Genres = new List(), Tags = new List(), Studios = new List() } }, MediaVersions = new List { new() { MediaFiles = new List { new() { Path = path } }, Streams = new List() } }, TraktListItems = new List() }; await dbContext.Episodes.AddAsync(episode); await dbContext.SaveChangesAsync(); await dbContext.Entry(episode).Reference(i => i.LibraryPath).LoadAsync(); await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync(); await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync(); return episode; } catch (Exception ex) { return BaseError.New(ex.Message); } } private static async Task>> AddPlexShow( TvContext dbContext, PlexLibrary library, PlexShow item) { try { item.LibraryPathId = library.Paths.Head().Id; await dbContext.PlexShows.AddAsync(item); await dbContext.SaveChangesAsync(); await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync(); await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync(); return new MediaItemScanResult(item) { IsAdded = true }; } catch (Exception ex) { return BaseError.New(ex.Message); } } private static async Task> AddPlexSeason( TvContext dbContext, PlexLibrary library, PlexSeason item) { try { item.LibraryPathId = library.Paths.Head().Id; await dbContext.PlexSeasons.AddAsync(item); await dbContext.SaveChangesAsync(); await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync(); await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync(); return item; } catch (Exception ex) { return BaseError.New(ex.Message); } } private static async Task> AddPlexEpisode( TvContext dbContext, PlexLibrary library, PlexEpisode item) { try { if (dbContext.MediaFiles.Any(mf => mf.Path == item.MediaVersions.Head().MediaFiles.Head().Path)) { return BaseError.New("Multi-episode files are not yet supported"); } item.LibraryPathId = library.Paths.Head().Id; foreach (EpisodeMetadata metadata in item.EpisodeMetadata) { metadata.Genres ??= new List(); metadata.Tags ??= new List(); metadata.Studios ??= new List(); metadata.Actors ??= new List(); metadata.Directors ??= new List(); metadata.Writers ??= new List(); } await dbContext.PlexEpisodes.AddAsync(item); await dbContext.SaveChangesAsync(); await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync(); await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync(); await dbContext.Entry(item).Reference(e => e.Season).LoadAsync(); return item; } catch (Exception ex) { return BaseError.New(ex.Message); } } } }