From 548b3844471670fdc0680f722f6beceb6e327eb2 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:26:09 -0600 Subject: [PATCH] support "backdrop" files as local movie fanart fallback --- CHANGELOG.md | 1 + .../Core/Metadata/MovieFolderScannerTests.cs | 54 +++++++++++++++++++ .../Core/Metadata/MovieFolderScanner.cs | 30 +++++------ 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cff333a93..8b96a8344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs index 533af09e5..6a7167cfa 100644 --- a/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs @@ -511,6 +511,60 @@ public class MovieFolderScannerTests 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() }; + + Either result = await service.ScanFolder( + libraryPath, + FFmpegPath, + FFprobePath, + 0, + 1, + CancellationToken.None); + + result.IsRight.ShouldBeTrue(); + + await _movieRepository.Received(1).GetOrAdd( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + await _movieRepository.Received(1).GetOrAdd( + libraryPath, + Arg.Any(), + moviePath, + Arg.Any()); + + await _localStatisticsProvider.Received(1).RefreshStatistics( + FFmpegPath, + FFprobePath, + Arg.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)); + + await _localMetadataProvider.Received(1).RefreshFallbackMetadata( + Arg.Is(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))] diff --git a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs index 4afe697c2..1bcb56b2f 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs @@ -381,26 +381,22 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner private Option LocateArtwork(Movie movie, ArtworkKind artworkKind) { - string segment = artworkKind switch - { - ArtworkKind.Poster => "poster", - ArtworkKind.FanArt => "fanart", - _ => throw new ArgumentOutOfRangeException(nameof(artworkKind)) - }; - string path = movie.MediaVersions.Head().MediaFiles.Head().Path; + string fileName = Path.GetFileNameWithoutExtension(path); string folder = Path.GetDirectoryName(path) ?? string.Empty; - IEnumerable possibleMoviePosters = ImageFileExtensions.Collect(ext => - new[] { $"{segment}.{ext}", Path.GetFileNameWithoutExtension(path) + $"-{segment}.{ext}" }) - .Map(f => Path.Combine(folder, f)); - Option result = possibleMoviePosters.Filter(p => _fileSystem.File.Exists(p)).HeadOrNone(); - if (result.IsNone && artworkKind == ArtworkKind.Poster) + + string[] segments = artworkKind switch { - IEnumerable possibleFolderPosters = ImageFileExtensions.Collect(ext => new[] { $"folder.{ext}" }) - .Map(f => Path.Combine(folder, f)); - result = possibleFolderPosters.Filter(p => _fileSystem.File.Exists(p)).HeadOrNone(); - } + ArtworkKind.Poster => ["poster", $"{fileName}-poster", "folder"], + ArtworkKind.FanArt => ["fanart", $"{fileName}-fanart", "backdrop"], + _ => throw new ArgumentOutOfRangeException(nameof(artworkKind)) + }; - 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(); } }