detect more local movie artwork (#2804)
* expand test coverage * support "backdrop" files as local movie fanart fallback
This commit is contained in:
@@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Add chapter `title` to filler expression
|
||||
- This can be used to include or exclude chapters with specific (case-insensitive) titles
|
||||
- E.g. `title == 'here'`, `title != 'not here'`, `title like '%here%'`
|
||||
- Local movie libraries: load fanart from `backdrop` files (created by Jellyfin)
|
||||
|
||||
### Changed
|
||||
- Disable automatic artwork database cleanup
|
||||
|
||||
@@ -79,7 +79,7 @@ public class MovieFolderScannerTests
|
||||
_localMetadataProvider.RefreshFallbackMetadata(Arg.Any<Movie>())
|
||||
.Returns(arg =>
|
||||
{
|
||||
((Movie)arg.Arg<MediaItem>()).MovieMetadata = new List<MovieMetadata> { new() };
|
||||
((Movie)arg.Arg<MediaItem>()).MovieMetadata = [new MovieMetadata { Artwork = [] }];
|
||||
return Task.FromResult(true);
|
||||
});
|
||||
|
||||
@@ -457,6 +457,310 @@ public class MovieFolderScannerTests
|
||||
await _imageCache.Received(1).CopyArtworkToCache(posterPath, ArtworkKind.Poster);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NewMovie_Statistics_And_FallbackMetadata_And_FanArt(
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
|
||||
string videoExtension,
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))]
|
||||
string imageExtension)
|
||||
{
|
||||
string moviePath = Path.Combine(
|
||||
FakeRoot,
|
||||
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
|
||||
|
||||
string fanArtPath = Path.Combine(
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"fanart.{imageExtension}");
|
||||
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(fanArtPath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
0,
|
||||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
result.IsRight.ShouldBeTrue();
|
||||
|
||||
await _movieRepository.Received(1).GetOrAdd(
|
||||
Arg.Any<LibraryPath>(),
|
||||
Arg.Any<LibraryFolder>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
await _movieRepository.Received(1).GetOrAdd(
|
||||
libraryPath,
|
||||
Arg.Any<LibraryFolder>(),
|
||||
moviePath,
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _localStatisticsProvider.Received(1).RefreshStatistics(
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
Arg.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath));
|
||||
|
||||
await _localMetadataProvider.Received(1).RefreshFallbackMetadata(
|
||||
Arg.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath));
|
||||
|
||||
await _imageCache.Received(1).CopyArtworkToCache(fanArtPath, ArtworkKind.FanArt);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NewMovie_Statistics_And_FallbackMetadata_And_FanArt_Backdrop(
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
|
||||
string videoExtension,
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))]
|
||||
string imageExtension)
|
||||
{
|
||||
string moviePath = Path.Combine(
|
||||
FakeRoot,
|
||||
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
|
||||
|
||||
string fanArtPath = Path.Combine(
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"backdrop.{imageExtension}");
|
||||
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(fanArtPath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
0,
|
||||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
result.IsRight.ShouldBeTrue();
|
||||
|
||||
await _movieRepository.Received(1).GetOrAdd(
|
||||
Arg.Any<LibraryPath>(),
|
||||
Arg.Any<LibraryFolder>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
await _movieRepository.Received(1).GetOrAdd(
|
||||
libraryPath,
|
||||
Arg.Any<LibraryFolder>(),
|
||||
moviePath,
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _localStatisticsProvider.Received(1).RefreshStatistics(
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
Arg.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath));
|
||||
|
||||
await _localMetadataProvider.Received(1).RefreshFallbackMetadata(
|
||||
Arg.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath));
|
||||
|
||||
await _imageCache.Received(1).CopyArtworkToCache(fanArtPath, ArtworkKind.FanArt);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NewMovie_Statistics_And_FallbackMetadata_And_MovieNameFanArt(
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
|
||||
string videoExtension,
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))]
|
||||
string imageExtension)
|
||||
{
|
||||
string moviePath = Path.Combine(
|
||||
FakeRoot,
|
||||
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
|
||||
|
||||
string fanArtPath = Path.Combine(
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"Movie (2020)-fanart.{imageExtension}");
|
||||
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(fanArtPath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
0,
|
||||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
result.IsRight.ShouldBeTrue();
|
||||
|
||||
await _movieRepository.Received(1).GetOrAdd(
|
||||
Arg.Any<LibraryPath>(),
|
||||
Arg.Any<LibraryFolder>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
await _movieRepository.Received(1).GetOrAdd(
|
||||
libraryPath,
|
||||
Arg.Any<LibraryFolder>(),
|
||||
moviePath,
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _localStatisticsProvider.Received(1).RefreshStatistics(
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
Arg.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath));
|
||||
|
||||
await _localMetadataProvider.Received(1).RefreshFallbackMetadata(
|
||||
Arg.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath));
|
||||
|
||||
await _imageCache.Received(1).CopyArtworkToCache(fanArtPath, ArtworkKind.FanArt);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NewMovie_Statistics_And_FallbackMetadata_FanArt_Ignores_Folder_Image(
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
|
||||
string videoExtension,
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))]
|
||||
string imageExtension)
|
||||
{
|
||||
string moviePath = Path.Combine(
|
||||
FakeRoot,
|
||||
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
|
||||
|
||||
string folderImagePath = Path.Combine(
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"folder.{imageExtension}");
|
||||
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(folderImagePath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
0,
|
||||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
result.IsRight.ShouldBeTrue();
|
||||
|
||||
await _movieRepository.Received(1).GetOrAdd(
|
||||
Arg.Any<LibraryPath>(),
|
||||
Arg.Any<LibraryFolder>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
await _movieRepository.Received(1).GetOrAdd(
|
||||
libraryPath,
|
||||
Arg.Any<LibraryFolder>(),
|
||||
moviePath,
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _localStatisticsProvider.Received(1).RefreshStatistics(
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
Arg.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath));
|
||||
|
||||
await _localMetadataProvider.Received(1).RefreshFallbackMetadata(
|
||||
Arg.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath));
|
||||
|
||||
// Should receive Poster call (as folder.ext is valid for poster)
|
||||
await _imageCache.Received(1).CopyArtworkToCache(folderImagePath, ArtworkKind.Poster);
|
||||
|
||||
// Should NOT receive FanArt call
|
||||
await _imageCache.DidNotReceive().CopyArtworkToCache(Arg.Any<string>(), ArtworkKind.FanArt);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NewMovie_Statistics_And_FallbackMetadata_Poster_Priority(
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
|
||||
string videoExtension,
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))]
|
||||
string imageExtension)
|
||||
{
|
||||
string moviePath = Path.Combine(
|
||||
FakeRoot,
|
||||
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
|
||||
|
||||
string posterPath = Path.Combine(
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"poster.{imageExtension}");
|
||||
|
||||
string moviePosterPath = Path.Combine(
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"Movie (2020)-poster.{imageExtension}");
|
||||
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(moviePosterPath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
0,
|
||||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
result.IsRight.ShouldBeTrue();
|
||||
|
||||
// Should prefer "poster.ext" over "MovieName-poster.ext"
|
||||
await _imageCache.Received(1).CopyArtworkToCache(posterPath, ArtworkKind.Poster);
|
||||
await _imageCache.DidNotReceive().CopyArtworkToCache(moviePosterPath, ArtworkKind.Poster);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NewMovie_Statistics_And_FallbackMetadata_FanArt_Priority(
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
|
||||
string videoExtension,
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))]
|
||||
string imageExtension)
|
||||
{
|
||||
string moviePath = Path.Combine(
|
||||
FakeRoot,
|
||||
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
|
||||
|
||||
string fanArtPath = Path.Combine(
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"fanart.{imageExtension}");
|
||||
|
||||
string movieFanArtPath = Path.Combine(
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"Movie (2020)-fanart.{imageExtension}");
|
||||
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(fanArtPath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(movieFanArtPath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath
|
||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFmpegPath,
|
||||
FFprobePath,
|
||||
0,
|
||||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
result.IsRight.ShouldBeTrue();
|
||||
|
||||
// Should prefer "fanart.ext" over "MovieName-fanart.ext"
|
||||
await _imageCache.Received(1).CopyArtworkToCache(fanArtPath, ArtworkKind.FanArt);
|
||||
await _imageCache.DidNotReceive().CopyArtworkToCache(movieFanArtPath, ArtworkKind.FanArt);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Ignore_Extra_Files(
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
|
||||
|
||||
@@ -381,26 +381,22 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
||||
|
||||
private Option<string> LocateArtwork(Movie movie, ArtworkKind artworkKind)
|
||||
{
|
||||
string segment = artworkKind switch
|
||||
string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
string fileName = Path.GetFileNameWithoutExtension(path);
|
||||
string folder = Path.GetDirectoryName(path) ?? string.Empty;
|
||||
|
||||
string[] segments = artworkKind switch
|
||||
{
|
||||
ArtworkKind.Poster => "poster",
|
||||
ArtworkKind.FanArt => "fanart",
|
||||
ArtworkKind.Poster => ["poster", $"{fileName}-poster", "folder"],
|
||||
ArtworkKind.FanArt => ["fanart", $"{fileName}-fanart", "backdrop"],
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(artworkKind))
|
||||
};
|
||||
|
||||
string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
string folder = Path.GetDirectoryName(path) ?? string.Empty;
|
||||
IEnumerable<string> possibleMoviePosters = ImageFileExtensions.Collect(ext =>
|
||||
new[] { $"{segment}.{ext}", Path.GetFileNameWithoutExtension(path) + $"-{segment}.{ext}" })
|
||||
.Map(f => Path.Combine(folder, f));
|
||||
Option<string> result = possibleMoviePosters.Filter(p => _fileSystem.File.Exists(p)).HeadOrNone();
|
||||
if (result.IsNone && artworkKind == ArtworkKind.Poster)
|
||||
{
|
||||
IEnumerable<string> possibleFolderPosters = ImageFileExtensions.Collect(ext => new[] { $"folder.{ext}" })
|
||||
.Map(f => Path.Combine(folder, f));
|
||||
result = possibleFolderPosters.Filter(p => _fileSystem.File.Exists(p)).HeadOrNone();
|
||||
}
|
||||
|
||||
return result;
|
||||
return ImageFileExtensions
|
||||
.Map(ext => segments.Map(segment => $"{segment}.{ext}"))
|
||||
.Flatten()
|
||||
.Map(f => Path.Combine(folder, f))
|
||||
.Filter(s => _fileSystem.File.Exists(s))
|
||||
.HeadOrNone();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user