using Dapper; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace ErsatzTV.Infrastructure.Data.Repositories; public class MovieRepository : IMovieRepository { private readonly IDbContextFactory _dbContextFactory; private readonly ILogger _logger; public MovieRepository(IDbContextFactory dbContextFactory, ILogger logger) { _dbContextFactory = dbContextFactory; _logger = logger; } public async Task AllMoviesExist(List movieIds) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QuerySingleAsync( "SELECT COUNT(*) FROM Movie WHERE Id in @MovieIds", new { MovieIds = movieIds }) .Map(c => c == movieIds.Count); } public async Task> GetMovie(int movieId) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Movies .Include(m => m.MovieMetadata) .ThenInclude(m => m.Artwork) .Include(m => m.MovieMetadata) .ThenInclude(m => m.Genres) .Include(m => m.MovieMetadata) .ThenInclude(m => m.Tags) .Include(m => m.MovieMetadata) .ThenInclude(m => m.Studios) .Include(m => m.MovieMetadata) .ThenInclude(m => m.Actors) .ThenInclude(a => a.Artwork) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Directors) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Writers) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Streams) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .OrderBy(m => m.Id) .SingleOrDefaultAsync(m => m.Id == movieId) .Map(Optional); } public async Task>> GetOrAdd( LibraryPath libraryPath, LibraryFolder libraryFolder, string path) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); Option maybeExisting = await dbContext.Movies .Filter(m => !(m is PlexMovie) && !(m is JellyfinMovie) && !(m is EmbyMovie)) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Artwork) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Genres) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Tags) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Studios) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Guids) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Actors) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Actors) .ThenInclude(a => a.Artwork) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Directors) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Writers) .Include(i => i.LibraryPath) .ThenInclude(lp => lp.Library) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.Streams) .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( mediaItem => Right>( new MediaItemScanResult(mediaItem) { IsAdded = false }).AsTask(), async () => await AddMovie(dbContext, libraryPath.Id, libraryFolder.Id, path)); } public async Task> GetMoviesForCards(List ids) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.MovieMetadata .AsNoTracking() .Filter(mm => ids.Contains(mm.MovieId)) .Include(mm => mm.Artwork) .Include(mm => mm.Movie) .ThenInclude(m => m.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .OrderBy(mm => mm.SortTitle) .ToListAsync(); } public async Task> FindMoviePaths(LibraryPath libraryPath) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QueryAsync( @"SELECT MF.Path FROM MediaFile MF INNER JOIN MediaVersion MV on MF.MediaVersionId = MV.Id INNER JOIN Movie M on MV.MovieId = M.Id INNER JOIN MediaItem MI on M.Id = MI.Id WHERE MI.LibraryPathId = @LibraryPathId", new { LibraryPathId = libraryPath.Id }); } public async Task> DeleteByPath(LibraryPath libraryPath, string path) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); List ids = await dbContext.Connection.QueryAsync( @"SELECT M.Id FROM Movie M INNER JOIN MediaItem MI on M.Id = MI.Id INNER JOIN MediaVersion MV on M.Id = MV.MovieId INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path", new { LibraryPathId = libraryPath.Id, Path = path }) .Map(result => result.ToList()); foreach (int movieId in ids) { Movie movie = await dbContext.Movies.FindAsync(movieId); if (movie != null) { dbContext.Movies.Remove(movie); } } bool changed = await dbContext.SaveChangesAsync() > 0; return changed ? ids : new List(); } public async Task AddGenre(MovieMetadata metadata, Genre genre) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.ExecuteAsync( "INSERT INTO Genre (Name, MovieMetadataId) VALUES (@Name, @MetadataId)", new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0); } public async Task AddTag(MovieMetadata metadata, Tag tag) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.ExecuteAsync( "INSERT INTO Tag (Name, MovieMetadataId, ExternalCollectionId) VALUES (@Name, @MetadataId, @ExternalCollectionId)", new { tag.Name, MetadataId = metadata.Id, tag.ExternalCollectionId }).Map(result => result > 0); } public async Task AddStudio(MovieMetadata metadata, Studio studio) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.ExecuteAsync( "INSERT INTO Studio (Name, MovieMetadataId) VALUES (@Name, @MetadataId)", new { studio.Name, MetadataId = metadata.Id }).Map(result => result > 0); } public async Task AddActor(MovieMetadata metadata, Actor actor) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); int? artworkId = null; if (actor.Artwork != null) { artworkId = await dbContext.Connection.QuerySingleAsync( $""" INSERT INTO Artwork (ArtworkKind, DateAdded, DateUpdated, Path) VALUES (@ArtworkKind, @DateAdded, @DateUpdated, @Path); SELECT {TvContext.LastInsertedRowId} """, new { ArtworkKind = (int)actor.Artwork.ArtworkKind, actor.Artwork.DateAdded, actor.Artwork.DateUpdated, actor.Artwork.Path }); } return await dbContext.Connection.ExecuteAsync( "INSERT INTO Actor (Name, Role, `Order`, MovieMetadataId, 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 UpdateSortTitle(MovieMetadata movieMetadata) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.ExecuteAsync( @"UPDATE MovieMetadata SET SortTitle = @SortTitle WHERE Id = @Id", new { movieMetadata.SortTitle, movieMetadata.Id }).Map(result => result > 0); } public async Task AddDirector(MovieMetadata metadata, Director director) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.ExecuteAsync( "INSERT INTO Director (Name, MovieMetadataId) VALUES (@Name, @MetadataId)", new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0); } public async Task AddWriter(MovieMetadata metadata, Writer writer) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.ExecuteAsync( "INSERT INTO Writer (Name, MovieMetadataId) VALUES (@Name, @MetadataId)", new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0); } private async Task>> AddMovie( TvContext dbContext, int libraryPathId, int libraryFolderId, string path) { try { if (await MediaItemRepository.MediaFileAlreadyExists(path, libraryPathId, dbContext, _logger)) { return new MediaFileAlreadyExists(); } var movie = new Movie { LibraryPathId = libraryPathId, MediaVersions = [ new MediaVersion { MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }], Streams = [] } ], TraktListItems = [] }; await dbContext.Movies.AddAsync(movie); await dbContext.SaveChangesAsync(); await dbContext.Entry(movie).Reference(m => m.LibraryPath).LoadAsync(); await dbContext.Entry(movie.LibraryPath).Reference(lp => lp.Library).LoadAsync(); return new MediaItemScanResult(movie) { IsAdded = true }; } catch (Exception ex) { return BaseError.New(ex.Message); } } }