Browse Source

Remove missing media (#40)

* remove movies that are no longer present on disk

* remove missing episodes, empty seasons, empty shows
pull/42/head
Jason Dove 4 years ago committed by GitHub
parent
commit
51cdb372b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs
  2. 52
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  3. 2
      ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs
  4. 5
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  5. 10
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  6. 12
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  7. 32
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  8. 50
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

9
ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs

@ -57,11 +57,12 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -57,11 +57,12 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task<Either<BaseError, Episode>> GetOrAddEpisode(Season season, LibraryPath libraryPath, string path) =>
throw new NotSupportedException();
public Task<Unit> DeleteEmptyShows() => throw new NotSupportedException();
public Task<IEnumerable<string>> FindEpisodePaths(LibraryPath libraryPath) => throw new NotSupportedException();
public Task<Option<Show>> GetShowByPath(int mediaSourceId, string path) => throw new NotSupportedException();
public Task<Unit> DeleteByPath(LibraryPath libraryPath, string path) => throw new NotSupportedException();
public Task<Unit> DeleteMissingSources(int localMediaSourceId, List<string> allFolders) =>
throw new NotSupportedException();
public Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath) => throw new NotSupportedException();
public Task<Unit> DeleteEmptyShows(LibraryPath libraryPath) => throw new NotSupportedException();
}
}

52
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -1,6 +1,7 @@ @@ -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 @@ -44,6 +45,8 @@ namespace ErsatzTV.Core.Tests.Metadata
_movieRepository.Setup(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()))
.Returns(
(LibraryPath _, string path) => Right<BaseError, Movie>(new FakeMovieWithPath(path)).AsTask());
_movieRepository.Setup(x => x.FindMoviePaths(It.IsAny<LibraryPath>()))
.Returns(new List<string>().AsEnumerable().AsTask());
_localStatisticsProvider = new Mock<ILocalStatisticsProvider>();
_localMetadataProvider = new Mock<ILocalMetadataProvider>();
@ -359,6 +362,55 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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<LibraryPath>()))
.Returns(new List<string> { 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<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.DeleteByPath(It.IsAny<LibraryPath>(), It.IsAny<string>()), 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<LibraryPath>()))
.Returns(new List<string> { 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<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.DeleteByPath(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.DeleteByPath(libraryPath, oldMoviePath), Times.Once);
}
private MovieFolderScanner GetService(params FakeFileEntry[] files) =>
new(
new FakeLocalFileSystem(new List<FakeFileEntry>(files)),

2
ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs

@ -13,5 +13,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -13,5 +13,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<bool> Update(Movie movie);
Task<int> GetMovieCount();
Task<List<MovieMetadata>> GetPagedMovies(int pageNumber, int pageSize);
Task<IEnumerable<string>> FindMoviePaths(LibraryPath libraryPath);
Task<Unit> DeleteByPath(LibraryPath libraryPath, string path);
}
}

5
ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs

@ -27,6 +27,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -27,6 +27,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Either<BaseError, Show>> AddShow(int libraryPathId, string showFolder, ShowMetadata metadata);
Task<Either<BaseError, Season>> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber);
Task<Either<BaseError, Episode>> GetOrAddEpisode(Season season, LibraryPath libraryPath, string path);
Task<Unit> DeleteEmptyShows();
Task<IEnumerable<string>> FindEpisodePaths(LibraryPath libraryPath);
Task<Unit> DeleteByPath(LibraryPath libraryPath, string path);
Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath);
Task<Unit> DeleteEmptyShows(LibraryPath libraryPath);
}
}

10
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -71,6 +71,7 @@ namespace ErsatzTV.Core.Metadata @@ -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 @@ -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;
}

12
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -63,7 +63,17 @@ namespace ErsatzTV.Core.Metadata @@ -63,7 +63,17 @@ namespace ErsatzTV.Core.Metadata
_ => Task.FromResult(Unit.Default));
}
await _televisionRepository.DeleteEmptyShows();
foreach (string path in await _televisionRepository.FindEpisodePaths(libraryPath))
{
if (!_localFileSystem.FileExists(path))
{
_logger.LogInformation("Removing missing episode at {Path}", path);
await _televisionRepository.DeleteByPath(libraryPath, path);
}
}
await _televisionRepository.DeleteEmptySeasons(libraryPath);
await _televisionRepository.DeleteEmptyShows(libraryPath);
return Unit.Default;
}

32
ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs

@ -93,6 +93,38 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -93,6 +93,38 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.OrderBy(mm => mm.SortTitle)
.ToListAsync();
public Task<IEnumerable<string>> FindMoviePaths(LibraryPath libraryPath) =>
_dbConnection.QueryAsync<string>(
@"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<Unit> DeleteByPath(LibraryPath libraryPath, string path)
{
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"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 });
foreach (int movieId in ids)
{
Movie movie = await _dbContext.Movies.FindAsync(movieId);
_dbContext.Movies.Remove(movie);
}
await _dbContext.SaveChangesAsync();
return Unit.Default;
}
private async Task<Either<BaseError, Movie>> AddMovie(int libraryPathId, string path)
{
try

50
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -230,9 +230,55 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -230,9 +230,55 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
() => AddEpisode(season, libraryPath.Id, path));
}
public Task<Unit> DeleteEmptyShows() =>
public Task<IEnumerable<string>> FindEpisodePaths(LibraryPath libraryPath) =>
_dbConnection.QueryAsync<string>(
@"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<Unit> DeleteByPath(LibraryPath libraryPath, string path)
{
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"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 });
foreach (int episodeId in ids)
{
Episode episode = await _dbContext.Episodes.FindAsync(episodeId);
_dbContext.Episodes.Remove(episode);
}
await _dbContext.SaveChangesAsync();
return Unit.Default;
}
public Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath) =>
_dbContext.Seasons
.Filter(s => s.LibraryPathId == libraryPath.Id)
.Filter(s => s.Episodes.Count == 0)
.ToListAsync()
.Bind(
list =>
{
_dbContext.Seasons.RemoveRange(list);
return _dbContext.SaveChangesAsync();
})
.ToUnit();
public Task<Unit> DeleteEmptyShows(LibraryPath libraryPath) =>
_dbContext.Shows
.Where(s => s.Seasons.Count == 0)
.Filter(s => s.LibraryPathId == libraryPath.Id)
.Filter(s => s.Seasons.Count == 0)
.ToListAsync()
.Bind(
list =>

Loading…
Cancel
Save