From fd54d42db7b6cacd635784638edc47626e1ee5c5 Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:02:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/service/local_metadata_artwork.go | 14 ++++ internal/service/local_metadata_test.go | 4 +- .../service/scanner_local_metadata_test.go | 68 +++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/internal/service/local_metadata_artwork.go b/internal/service/local_metadata_artwork.go index 0b393bb..e31d38a 100644 --- a/internal/service/local_metadata_artwork.go +++ b/internal/service/local_metadata_artwork.go @@ -101,7 +101,21 @@ func localBackdropCandidates(mediaPath string) []string { seen[key] = struct{}{} names = append(names, name) } + _, episode := ParseEpisode(mediaPath) for _, base := range mediaSidecarBaseVariants(mediaPath) { + // Jellyfin/Emby commonly stores an episode still beside the video as + // "-thumb.jpg" or ".jpg". Episode-specific artwork + // must win over a series-level fanart.jpg in the parent folder, + // otherwise a rescan flattens every episode to the same backdrop. + add(base + "-thumb") + add(base + ".thumb") + if episode > 0 { + add(base + "-still") + add(base + ".still") + add(base + "-scene") + add(base + ".scene") + add(base) + } add(base + "-fanart") add(base + ".fanart") add(base + "-backdrop") diff --git a/internal/service/local_metadata_test.go b/internal/service/local_metadata_test.go index 87f81df..320855e 100644 --- a/internal/service/local_metadata_test.go +++ b/internal/service/local_metadata_test.go @@ -236,8 +236,8 @@ func TestReadLocalVarietyMetadataUsesLocalArtwork(t *testing.T) { if got.PosterURL != showPoster { t.Fatalf("PosterURL = %q, want show poster %q, not episode thumb %q", got.PosterURL, showPoster, episodeThumb) } - if got.BackdropURL != backdrop { - t.Fatalf("BackdropURL = %q, want %q", got.BackdropURL, backdrop) + if got.BackdropURL != episodeThumb { + t.Fatalf("BackdropURL = %q, want episode thumb %q, not series backdrop %q", got.BackdropURL, episodeThumb, backdrop) } } diff --git a/internal/service/scanner_local_metadata_test.go b/internal/service/scanner_local_metadata_test.go index 25d2fd1..7bf6a28 100644 --- a/internal/service/scanner_local_metadata_test.go +++ b/internal/service/scanner_local_metadata_test.go @@ -268,6 +268,74 @@ func TestScanLibraryRefreshesStaleNoMatchDerivedMetadata(t *testing.T) { } } +func TestScanLibraryPrefersEpisodeArtworkOverSeriesFanart(t *testing.T) { + for _, tc := range []struct { + name string + episodeArt string + }{ + {name: "thumb suffix", episodeArt: "Show - S01E01-thumb.jpg"}, + {name: "video stem", episodeArt: "Show - S01E01.jpg"}, + } { + t.Run(tc.name, func(t *testing.T) { + root := t.TempDir() + seasonDir := filepath.Join(root, "Show", "Season 01") + if err := os.MkdirAll(seasonDir, 0o755); err != nil { + t.Fatal(err) + } + mediaPath := filepath.Join(seasonDir, "Show - S01E01.mkv") + if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(nfoPath(mediaPath), []byte( + `第一集11`, + ), 0o644); err != nil { + t.Fatal(err) + } + episodeArt := filepath.Join(seasonDir, tc.episodeArt) + if err := os.WriteFile(episodeArt, []byte("episode still"), 0o644); err != nil { + t.Fatal(err) + } + seriesFanart := filepath.Join(root, "Show", "fanart.jpg") + if err := os.WriteFile(seriesFanart, []byte("series backdrop"), 0o644); err != nil { + t.Fatal(err) + } + + db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{}) + repos := repository.New(db) + lib := model.Library{Name: "TV", Path: root, Type: "tv", Enabled: true} + if err := repos.Library.Create(t.Context(), &lib); err != nil { + t.Fatal(err) + } + existing := model.Media{ + LibraryID: lib.ID, + Title: "Show", + Path: mediaPath, + SizeBytes: 1, + SeasonNum: 1, + EpisodeNum: 1, + BackdropURL: "https://image.example/episode-still.jpg", + ScrapeStatus: "matched", + } + if err := repos.DB.Create(&existing).Error; err != nil { + t.Fatal(err) + } + + scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil) + if _, err := scanner.ScanLibrary(t.Context(), lib.ID); err != nil { + t.Fatal(err) + } + + var media model.Media + if err := db.First(&media, "id = ?", existing.ID).Error; err != nil { + t.Fatal(err) + } + if media.BackdropURL != episodeArt { + t.Fatalf("backdrop_url = %q, want episode artwork %q (series fanart %q must not win)", media.BackdropURL, episodeArt, seriesFanart) + } + }) + } +} + func TestScanLibraryPrunesMissingMedia(t *testing.T) { root := t.TempDir() mediaPath := filepath.Join(root, "Show.S02E03.mkv")