diff --git a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs index a27e6e84d..945baf759 100644 --- a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using ErsatzTV.Core.Domain; @@ -44,6 +45,8 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository.Setup(x => x.GetOrAdd(It.IsAny(), It.IsAny())) .Returns( (LibraryPath _, string path) => Right(new FakeMovieWithPath(path)).AsTask()); + _movieRepository.Setup(x => x.FindMoviePaths(It.IsAny())) + .Returns(new List().AsEnumerable().AsTask()); _localStatisticsProvider = new Mock(); _localMetadataProvider = new Mock(); @@ -359,6 +362,55 @@ namespace ErsatzTV.Core.Tests.Metadata Times.Once); } + [Test] + public async Task RenamedMovie_Should_Delete_Old_Movie() + { + string movieFolder = Path.Combine(FakeRoot, "Movie (2020)"); + string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi"); + + _movieRepository.Setup(x => x.FindMoviePaths(It.IsAny())) + .Returns(new List { oldMoviePath }.AsEnumerable().AsTask()); + + string moviePath = Path.Combine(movieFolder, "Movie (2020).mkv"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } + ); + var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; + + Either result = await service.ScanFolder(libraryPath, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.DeleteByPath(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.DeleteByPath(libraryPath, oldMoviePath), Times.Once); + } + + [Test] + public async Task DeletedMovieAndFolder_Should_Delete_Old_Movie() + { + string movieFolder = Path.Combine(FakeRoot, "Movie (2020)"); + string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi"); + + _movieRepository.Setup(x => x.FindMoviePaths(It.IsAny())) + .Returns(new List { oldMoviePath }.AsEnumerable().AsTask()); + + string moviePath = Path.Combine(movieFolder, "Movie (2020).mkv"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } + ); + var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; + + Either result = await service.ScanFolder(libraryPath, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.DeleteByPath(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.DeleteByPath(libraryPath, oldMoviePath), Times.Once); + } + + private MovieFolderScanner GetService(params FakeFileEntry[] files) => new( new FakeLocalFileSystem(new List(files)), diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs index e7bd50a7f..7b28ea6ac 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs @@ -13,5 +13,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task Update(Movie movie); Task GetMovieCount(); Task> GetPagedMovies(int pageNumber, int pageSize); + Task> FindMoviePaths(LibraryPath libraryPath); + Task DeleteByPath(LibraryPath libraryPath, string path); } } diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs index 7a40a2ddc..c1754f6c5 100644 --- a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -71,6 +71,7 @@ namespace ErsatzTV.Core.Metadata continue; } + foreach (string file in allFiles.OrderBy(identity)) { // TODO: optimize dbcontext use here, do we need tracking? can we make partial updates with dapper? @@ -86,6 +87,15 @@ namespace ErsatzTV.Core.Metadata } } + foreach (string path in await _movieRepository.FindMoviePaths(libraryPath)) + { + if (!_localFileSystem.FileExists(path)) + { + _logger.LogInformation("Removing missing movie at {Path}", path); + await _movieRepository.DeleteByPath(libraryPath, path); + } + } + return Unit.Default; } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs index ad20a7293..5dbf6cbb8 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs @@ -93,6 +93,44 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .OrderBy(mm => mm.SortTitle) .ToListAsync(); + public Task> FindMoviePaths(LibraryPath libraryPath) => + _dbConnection.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) + { + IEnumerable ids = await _dbConnection.QueryAsync( + @"SELECT M.Id + FROM Movie M + INNER JOIN MediaItem MI on M.Id = MI.Id + INNER JOIN MovieMetadata MM on M.Id = MM.MovieId + 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 }); + + foreach (int movieId in ids) + { + await Delete(movieId); + } + + return Unit.Default; + } + + private async Task Delete(int movieId) + { + Movie movie = await _dbContext.Movies.FindAsync(movieId); + _dbContext.Movies.Remove(movie); + await _dbContext.SaveChangesAsync(); + return Unit.Default; + } + private async Task> AddMovie(int libraryPathId, string path) { try