From 40cb8b07a23b4ef0f75b4e28dd69c12991e35f29 Mon Sep 17 00:00:00 2001
From: ShukeBta <272197458+ShukeBta@users.noreply.github.com>
Date: Wed, 24 Jun 2026 15:02:48 +0800
Subject: [PATCH] refactor: split local metadata tests
---
internal/service/local_metadata_adult_test.go | 205 +++++++++
internal/service/local_metadata_test.go | 414 ------------------
.../service/scanner_local_metadata_test.go | 223 ++++++++++
3 files changed, 428 insertions(+), 414 deletions(-)
create mode 100644 internal/service/local_metadata_adult_test.go
create mode 100644 internal/service/scanner_local_metadata_test.go
diff --git a/internal/service/local_metadata_adult_test.go b/internal/service/local_metadata_adult_test.go
new file mode 100644
index 0000000..1cf5f5f
--- /dev/null
+++ b/internal/service/local_metadata_adult_test.go
@@ -0,0 +1,205 @@
+package service
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestReadAdultLocalMetadataAndArtwork(t *testing.T) {
+ root := t.TempDir()
+ dir := filepath.Join(root, "SSIS-001")
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ mediaPath := filepath.Join(dir, "SSIS-001.mp4")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ nfo := `
+
+ 成人影片标题
+ SSIS-001
+ SSIS-001
+ 2024-05-01
+ 本地简介
+ SSIS-001-poster.jpg
+ SSIS-001-fanart.jpg
+ 测试片商
+ 剧情
+ 中文字幕
+ 演员A
+`
+ if err := os.WriteFile(nfoPath(mediaPath), []byte(nfo), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ poster := filepath.Join(dir, "SSIS-001-poster.jpg")
+ if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ fanart := filepath.Join(dir, "SSIS-001-fanart.jpg")
+ if err := os.WriteFile(fanart, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := ReadLocalMetadata(mediaPath, root, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got == nil || got.Title != "成人影片标题" || got.AdultCode != "SSIS-001" || !got.NSFW {
+ t.Fatalf("unexpected adult metadata: %+v", got)
+ }
+ if got.OriginalName != "SSIS-001" || got.Year != 2024 || got.Overview != "本地简介" {
+ t.Fatalf("unexpected adult fields: %+v", got)
+ }
+ if got.PosterURL != poster || got.BackdropURL != fanart {
+ t.Fatalf("artwork poster=%q fanart=%q", got.PosterURL, got.BackdropURL)
+ }
+ if got.Genres != "剧情,中文字幕,测试片商,演员A" {
+ t.Fatalf("genres = %q", got.Genres)
+ }
+}
+
+func TestReadAdultMovieNFOFallbackInSingleMovieFolder(t *testing.T) {
+ root := t.TempDir()
+ dir := filepath.Join(root, "MIDV-123")
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ mediaPath := filepath.Join(dir, "video.mp4")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "movie.nfo"), []byte(`本地番号电影MIDV-123`), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ poster := filepath.Join(dir, "video-cover.jpg")
+ if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := ReadLocalMetadata(mediaPath, root, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got == nil || got.Title != "本地番号电影" || got.AdultCode != "MIDV-123" || got.PosterURL != poster {
+ t.Fatalf("unexpected fallback metadata: %+v", got)
+ }
+}
+
+func TestReadAdultNFOByCodeForStackedFile(t *testing.T) {
+ root := t.TempDir()
+ mediaPath := filepath.Join(root, "SSIS-001-C.mp4")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(root, "SSIS-001.nfo"), []byte(`
+ 按番号命中的本地 NFO
+ SSIS-001
+ SSIS-001-poster.jpgSSIS-001-fanart.jpg
+`), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ poster := filepath.Join(root, "SSIS-001-poster.jpg")
+ fanart := filepath.Join(root, "SSIS-001-fanart.jpg")
+ if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(fanart, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := ReadLocalMetadata(mediaPath, root, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got == nil || got.Title != "按番号命中的本地 NFO" || got.AdultCode != "SSIS-001" || !got.HasNFO {
+ t.Fatalf("unexpected metadata: %+v", got)
+ }
+ if got.PosterURL != poster || got.BackdropURL != fanart {
+ t.Fatalf("artwork poster=%q fanart=%q", got.PosterURL, got.BackdropURL)
+ }
+}
+
+func TestReadAdultMetadataPrefersLocalDMMPosterOverRemoteNFO(t *testing.T) {
+ root := t.TempDir()
+ dir := filepath.Join(root, "IPX-641-C")
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ mediaPath := filepath.Join(dir, "ipx-641-C.mp4")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(nfoPath(mediaPath), []byte(`
+ 本地标题
+ IPX-641
+ https://www.javbus.com/pics/cover/remote.jpg
+ https://pics.dmm.co.jp/digital/video/ipx00641/ipx00641jp-1.jpg
+`), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ poster := filepath.Join(dir, "ipx00641pl.jpg")
+ if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := ReadLocalMetadata(mediaPath, root, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got == nil || got.PosterURL != poster {
+ t.Fatalf("poster_url = %q, want local %q", got.PosterURL, poster)
+ }
+ if got.BackdropURL != "https://pics.dmm.co.jp/digital/video/ipx00641/ipx00641jp-1.jpg" {
+ t.Fatalf("backdrop should keep remote NFO fallback, got %q", got.BackdropURL)
+ }
+}
+
+func TestReadAdultMetadataDerivesDMMPosterFromRemoteFanart(t *testing.T) {
+ root := t.TempDir()
+ dir := filepath.Join(root, "NACR-833")
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ mediaPath := filepath.Join(dir, "NACR-833.mp4")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(nfoPath(mediaPath), []byte(`
+ 本地标题
+ NACR-833
+ https://www.javbus.com/pics/cover/an5p_b.jpg
+ https://pics.dmm.co.jp/digital/video/h_237nacr00833/h_237nacr00833jp-1.jpg
+`), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := ReadLocalMetadata(mediaPath, root, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got == nil || got.PosterURL != "https://pics.dmm.co.jp/digital/video/h_237nacr00833/h_237nacr00833pl.jpg" {
+ t.Fatalf("unexpected metadata: %+v", got)
+ }
+}
+
+func TestReadAdultArtworkByCodeWithoutNFO(t *testing.T) {
+ root := t.TempDir()
+ mediaPath := filepath.Join(root, "SSIS-001-CD1.mp4")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ poster := filepath.Join(root, "SSIS-001-poster.jpg")
+ if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := ReadLocalMetadata(mediaPath, root, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got == nil || got.PosterURL != poster || !got.HasArtwork || got.HasNFO {
+ t.Fatalf("unexpected artwork metadata: %+v", got)
+ }
+}
diff --git a/internal/service/local_metadata_test.go b/internal/service/local_metadata_test.go
index 11abc26..c4636e2 100644
--- a/internal/service/local_metadata_test.go
+++ b/internal/service/local_metadata_test.go
@@ -4,12 +4,6 @@ import (
"os"
"path/filepath"
"testing"
-
- "go.uber.org/zap"
-
- "github.com/ShukeBta/MediaStationGo/internal/config"
- "github.com/ShukeBta/MediaStationGo/internal/model"
- "github.com/ShukeBta/MediaStationGo/internal/repository"
)
func TestReadLocalMovieMetadata(t *testing.T) {
@@ -228,411 +222,3 @@ func TestReadLocalMetadataWithoutNFOStillFindsArtwork(t *testing.T) {
t.Fatalf("unexpected artwork metadata: %+v", got)
}
}
-
-func TestReadAdultLocalMetadataAndArtwork(t *testing.T) {
- root := t.TempDir()
- dir := filepath.Join(root, "SSIS-001")
- if err := os.MkdirAll(dir, 0o755); err != nil {
- t.Fatal(err)
- }
- mediaPath := filepath.Join(dir, "SSIS-001.mp4")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
- nfo := `
-
- 成人影片标题
- SSIS-001
- SSIS-001
- 2024-05-01
- 本地简介
- SSIS-001-poster.jpg
- SSIS-001-fanart.jpg
- 测试片商
- 剧情
- 中文字幕
- 演员A
-`
- if err := os.WriteFile(nfoPath(mediaPath), []byte(nfo), 0o644); err != nil {
- t.Fatal(err)
- }
- poster := filepath.Join(dir, "SSIS-001-poster.jpg")
- if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
- fanart := filepath.Join(dir, "SSIS-001-fanart.jpg")
- if err := os.WriteFile(fanart, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
-
- got, err := ReadLocalMetadata(mediaPath, root, false)
- if err != nil {
- t.Fatal(err)
- }
- if got == nil || got.Title != "成人影片标题" || got.AdultCode != "SSIS-001" || !got.NSFW {
- t.Fatalf("unexpected adult metadata: %+v", got)
- }
- if got.OriginalName != "SSIS-001" || got.Year != 2024 || got.Overview != "本地简介" {
- t.Fatalf("unexpected adult fields: %+v", got)
- }
- if got.PosterURL != poster || got.BackdropURL != fanart {
- t.Fatalf("artwork poster=%q fanart=%q", got.PosterURL, got.BackdropURL)
- }
- if got.Genres != "剧情,中文字幕,测试片商,演员A" {
- t.Fatalf("genres = %q", got.Genres)
- }
-}
-
-func TestReadAdultMovieNFOFallbackInSingleMovieFolder(t *testing.T) {
- root := t.TempDir()
- dir := filepath.Join(root, "MIDV-123")
- if err := os.MkdirAll(dir, 0o755); err != nil {
- t.Fatal(err)
- }
- mediaPath := filepath.Join(dir, "video.mp4")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(filepath.Join(dir, "movie.nfo"), []byte(`本地番号电影MIDV-123`), 0o644); err != nil {
- t.Fatal(err)
- }
- poster := filepath.Join(dir, "video-cover.jpg")
- if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
-
- got, err := ReadLocalMetadata(mediaPath, root, false)
- if err != nil {
- t.Fatal(err)
- }
- if got == nil || got.Title != "本地番号电影" || got.AdultCode != "MIDV-123" || got.PosterURL != poster {
- t.Fatalf("unexpected fallback metadata: %+v", got)
- }
-}
-
-func TestReadAdultNFOByCodeForStackedFile(t *testing.T) {
- root := t.TempDir()
- mediaPath := filepath.Join(root, "SSIS-001-C.mp4")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(filepath.Join(root, "SSIS-001.nfo"), []byte(`
- 按番号命中的本地 NFO
- SSIS-001
- SSIS-001-poster.jpgSSIS-001-fanart.jpg
-`), 0o644); err != nil {
- t.Fatal(err)
- }
- poster := filepath.Join(root, "SSIS-001-poster.jpg")
- fanart := filepath.Join(root, "SSIS-001-fanart.jpg")
- if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(fanart, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
-
- got, err := ReadLocalMetadata(mediaPath, root, false)
- if err != nil {
- t.Fatal(err)
- }
- if got == nil || got.Title != "按番号命中的本地 NFO" || got.AdultCode != "SSIS-001" || !got.HasNFO {
- t.Fatalf("unexpected metadata: %+v", got)
- }
- if got.PosterURL != poster || got.BackdropURL != fanart {
- t.Fatalf("artwork poster=%q fanart=%q", got.PosterURL, got.BackdropURL)
- }
-}
-
-func TestReadAdultMetadataPrefersLocalDMMPosterOverRemoteNFO(t *testing.T) {
- root := t.TempDir()
- dir := filepath.Join(root, "IPX-641-C")
- if err := os.MkdirAll(dir, 0o755); err != nil {
- t.Fatal(err)
- }
- mediaPath := filepath.Join(dir, "ipx-641-C.mp4")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(nfoPath(mediaPath), []byte(`
- 本地标题
- IPX-641
- https://www.javbus.com/pics/cover/remote.jpg
- https://pics.dmm.co.jp/digital/video/ipx00641/ipx00641jp-1.jpg
-`), 0o644); err != nil {
- t.Fatal(err)
- }
- poster := filepath.Join(dir, "ipx00641pl.jpg")
- if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
-
- got, err := ReadLocalMetadata(mediaPath, root, false)
- if err != nil {
- t.Fatal(err)
- }
- if got == nil || got.PosterURL != poster {
- t.Fatalf("poster_url = %q, want local %q", got.PosterURL, poster)
- }
- if got.BackdropURL != "https://pics.dmm.co.jp/digital/video/ipx00641/ipx00641jp-1.jpg" {
- t.Fatalf("backdrop should keep remote NFO fallback, got %q", got.BackdropURL)
- }
-}
-
-func TestReadAdultMetadataDerivesDMMPosterFromRemoteFanart(t *testing.T) {
- root := t.TempDir()
- dir := filepath.Join(root, "NACR-833")
- if err := os.MkdirAll(dir, 0o755); err != nil {
- t.Fatal(err)
- }
- mediaPath := filepath.Join(dir, "NACR-833.mp4")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(nfoPath(mediaPath), []byte(`
- 本地标题
- NACR-833
- https://www.javbus.com/pics/cover/an5p_b.jpg
- https://pics.dmm.co.jp/digital/video/h_237nacr00833/h_237nacr00833jp-1.jpg
-`), 0o644); err != nil {
- t.Fatal(err)
- }
-
- got, err := ReadLocalMetadata(mediaPath, root, false)
- if err != nil {
- t.Fatal(err)
- }
- if got == nil || got.PosterURL != "https://pics.dmm.co.jp/digital/video/h_237nacr00833/h_237nacr00833pl.jpg" {
- t.Fatalf("unexpected metadata: %+v", got)
- }
-}
-
-func TestReadAdultArtworkByCodeWithoutNFO(t *testing.T) {
- root := t.TempDir()
- mediaPath := filepath.Join(root, "SSIS-001-CD1.mp4")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
- poster := filepath.Join(root, "SSIS-001-poster.jpg")
- if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
-
- got, err := ReadLocalMetadata(mediaPath, root, false)
- if err != nil {
- t.Fatal(err)
- }
- if got == nil || got.PosterURL != poster || !got.HasArtwork || got.HasNFO {
- t.Fatalf("unexpected artwork metadata: %+v", got)
- }
-}
-
-func TestScanLibraryUsesLocalMetadata(t *testing.T) {
- root := t.TempDir()
- seasonDir := filepath.Join(root, "Show", "Season 02")
- if err := os.MkdirAll(seasonDir, 0o755); err != nil {
- t.Fatal(err)
- }
- mediaPath := filepath.Join(seasonDir, "Show - EP03.mkv")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(filepath.Join(root, "Show", "tvshow.nfo"), []byte(`本地剧名2025`), 0o644); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(nfoPath(mediaPath), []byte(`本地第三集23`), 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)
- }
-
- scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
- res, err := scanner.ScanLibrary(t.Context(), lib.ID)
- if err != nil {
- t.Fatal(err)
- }
- if res.LocalMetadata != 1 {
- t.Fatalf("LocalMetadata = %d, want 1", res.LocalMetadata)
- }
- if res.Added != 1 || res.Updated != 0 {
- t.Fatalf("scan counts added=%d updated=%d, want 1/0", res.Added, res.Updated)
- }
- var media model.Media
- if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
- t.Fatal(err)
- }
- // 单集名(本地第三集)不应写入 OriginalName(整剧原名,合集分组键)。
- // tvshow.nfo 未提供 originaltitle, 故 OriginalName 应为空。
- if media.Title != "本地剧名" || media.OriginalName != "" || media.SeasonNum != 2 || media.EpisodeNum != 3 || media.ScrapeStatus != "matched" {
- t.Fatalf("unexpected scanned media: %+v", media)
- }
- if media.EpisodeTitle != "本地第三集" {
- t.Fatalf("episode_title = %q, want 本地第三集", media.EpisodeTitle)
- }
-
- res, err = scanner.ScanLibrary(t.Context(), lib.ID)
- if err != nil {
- t.Fatal(err)
- }
- if res.Added != 0 || res.Updated != 0 || res.Skipped != 1 {
- t.Fatalf("repeat scan counts added=%d updated=%d skipped=%d, want 0/0/1", res.Added, res.Updated, res.Skipped)
- }
-}
-
-func TestScanLibraryDoesNotMarkArtworkOnlyAsMatched(t *testing.T) {
- root := t.TempDir()
- mediaPath := filepath.Join(root, "SSIS-001-CD1.mp4")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
- poster := filepath.Join(root, "SSIS-001-poster.jpg")
- if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
-
- db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
- repos := repository.New(db)
- lib := model.Library{Name: "Adult", Path: root, Type: "movie", Enabled: true}
- if err := repos.Library.Create(t.Context(), &lib); err != nil {
- t.Fatal(err)
- }
-
- scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
- res, err := scanner.ScanLibrary(t.Context(), lib.ID)
- if err != nil {
- t.Fatal(err)
- }
- if res.LocalMetadata != 1 {
- t.Fatalf("LocalMetadata = %d, want 1", res.LocalMetadata)
- }
- var media model.Media
- if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
- t.Fatal(err)
- }
- if media.PosterURL != poster {
- t.Fatalf("poster_url = %q, want %q", media.PosterURL, poster)
- }
- if media.ScrapeStatus == "matched" {
- t.Fatalf("artwork-only media should remain enrichable, got status %q", media.ScrapeStatus)
- }
-}
-
-func TestScanLibraryRefreshesArtworkOnlyMetadata(t *testing.T) {
- root := t.TempDir()
- mediaPath := filepath.Join(root, "Movie.mkv")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
- oldPoster := filepath.Join(root, "Movie-thumb.jpg")
- if err := os.WriteFile(oldPoster, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
-
- db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
- repos := repository.New(db)
- lib := model.Library{Name: "Movies", Path: root, Type: "movie", Enabled: true}
- if err := repos.Library.Create(t.Context(), &lib); 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)
- }
-
- newPoster := filepath.Join(root, "poster.jpg")
- if err := os.WriteFile(newPoster, []byte("jpg"), 0o644); err != nil {
- t.Fatal(err)
- }
- if _, err := scanner.ScanLibrary(t.Context(), lib.ID); err != nil {
- t.Fatal(err)
- }
-
- var media model.Media
- if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
- t.Fatal(err)
- }
- if media.PosterURL != newPoster {
- t.Fatalf("poster_url = %q, want refreshed local poster %q", media.PosterURL, newPoster)
- }
- if media.ScrapeStatus == "matched" {
- t.Fatalf("artwork-only refresh should keep media enrichable, got %q", media.ScrapeStatus)
- }
-}
-
-func TestScanLibraryParsesEpisodesForMovieTypedLibrary(t *testing.T) {
- root := t.TempDir()
- seasonDir := filepath.Join(root, "哈哈哈哈哈 (2020)", "Season 06")
- if err := os.MkdirAll(seasonDir, 0o755); err != nil {
- t.Fatal(err)
- }
- mediaPath := filepath.Join(seasonDir, "哈哈哈哈哈 - S06E17 - 第 17 集.mkv")
- if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
-
- db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
- repos := repository.New(db)
- lib := model.Library{Name: "综艺", Path: root, Type: "movie", Enabled: true}
- if err := repos.Library.Create(t.Context(), &lib); 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, "path = ?", mediaPath).Error; err != nil {
- t.Fatal(err)
- }
- if media.SeasonNum != 6 || media.EpisodeNum != 17 {
- t.Fatalf("season/episode = %d/%d, want 6/17", media.SeasonNum, media.EpisodeNum)
- }
-}
-
-func TestScanLibraryPrunesMissingMedia(t *testing.T) {
- root := t.TempDir()
- mediaPath := filepath.Join(root, "Show.S02E03.mkv")
- if err := os.WriteFile(mediaPath, []byte("x"), 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)
- }
- stale := model.Media{
- LibraryID: lib.ID,
- Title: "Show",
- Path: filepath.Join(root, "old", "Show.S02E03.mkv"),
- SizeBytes: 123,
- ScrapeStatus: "pending",
- }
- if err := db.Create(&stale).Error; err != nil {
- t.Fatal(err)
- }
-
- scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
- res, err := scanner.ScanLibrary(t.Context(), lib.ID)
- if err != nil {
- t.Fatal(err)
- }
- if res.Removed != 1 {
- t.Fatalf("Removed = %d, want 1", res.Removed)
- }
- var count int64
- if err := db.Model(&model.Media{}).Where("library_id = ?", lib.ID).Count(&count).Error; err != nil {
- t.Fatal(err)
- }
- if count != 1 {
- t.Fatalf("media count = %d, want 1", count)
- }
-}
diff --git a/internal/service/scanner_local_metadata_test.go b/internal/service/scanner_local_metadata_test.go
new file mode 100644
index 0000000..f9f8ad7
--- /dev/null
+++ b/internal/service/scanner_local_metadata_test.go
@@ -0,0 +1,223 @@
+package service
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "go.uber.org/zap"
+
+ "github.com/ShukeBta/MediaStationGo/internal/config"
+ "github.com/ShukeBta/MediaStationGo/internal/model"
+ "github.com/ShukeBta/MediaStationGo/internal/repository"
+)
+
+func TestScanLibraryUsesLocalMetadata(t *testing.T) {
+ root := t.TempDir()
+ seasonDir := filepath.Join(root, "Show", "Season 02")
+ if err := os.MkdirAll(seasonDir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ mediaPath := filepath.Join(seasonDir, "Show - EP03.mkv")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(root, "Show", "tvshow.nfo"), []byte(`本地剧名2025`), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(nfoPath(mediaPath), []byte(`本地第三集23`), 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)
+ }
+
+ scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
+ res, err := scanner.ScanLibrary(t.Context(), lib.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.LocalMetadata != 1 {
+ t.Fatalf("LocalMetadata = %d, want 1", res.LocalMetadata)
+ }
+ if res.Added != 1 || res.Updated != 0 {
+ t.Fatalf("scan counts added=%d updated=%d, want 1/0", res.Added, res.Updated)
+ }
+ var media model.Media
+ if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
+ t.Fatal(err)
+ }
+ // 单集名(本地第三集)不应写入 OriginalName(整剧原名,合集分组键)。
+ // tvshow.nfo 未提供 originaltitle, 故 OriginalName 应为空。
+ if media.Title != "本地剧名" || media.OriginalName != "" || media.SeasonNum != 2 || media.EpisodeNum != 3 || media.ScrapeStatus != "matched" {
+ t.Fatalf("unexpected scanned media: %+v", media)
+ }
+ if media.EpisodeTitle != "本地第三集" {
+ t.Fatalf("episode_title = %q, want 本地第三集", media.EpisodeTitle)
+ }
+
+ res, err = scanner.ScanLibrary(t.Context(), lib.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.Added != 0 || res.Updated != 0 || res.Skipped != 1 {
+ t.Fatalf("repeat scan counts added=%d updated=%d skipped=%d, want 0/0/1", res.Added, res.Updated, res.Skipped)
+ }
+}
+
+func TestScanLibraryDoesNotMarkArtworkOnlyAsMatched(t *testing.T) {
+ root := t.TempDir()
+ mediaPath := filepath.Join(root, "SSIS-001-CD1.mp4")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ poster := filepath.Join(root, "SSIS-001-poster.jpg")
+ if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
+ repos := repository.New(db)
+ lib := model.Library{Name: "Adult", Path: root, Type: "movie", Enabled: true}
+ if err := repos.Library.Create(t.Context(), &lib); err != nil {
+ t.Fatal(err)
+ }
+
+ scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
+ res, err := scanner.ScanLibrary(t.Context(), lib.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.LocalMetadata != 1 {
+ t.Fatalf("LocalMetadata = %d, want 1", res.LocalMetadata)
+ }
+ var media model.Media
+ if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
+ t.Fatal(err)
+ }
+ if media.PosterURL != poster {
+ t.Fatalf("poster_url = %q, want %q", media.PosterURL, poster)
+ }
+ if media.ScrapeStatus == "matched" {
+ t.Fatalf("artwork-only media should remain enrichable, got status %q", media.ScrapeStatus)
+ }
+}
+
+func TestScanLibraryRefreshesArtworkOnlyMetadata(t *testing.T) {
+ root := t.TempDir()
+ mediaPath := filepath.Join(root, "Movie.mkv")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ oldPoster := filepath.Join(root, "Movie-thumb.jpg")
+ if err := os.WriteFile(oldPoster, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
+ repos := repository.New(db)
+ lib := model.Library{Name: "Movies", Path: root, Type: "movie", Enabled: true}
+ if err := repos.Library.Create(t.Context(), &lib); 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)
+ }
+
+ newPoster := filepath.Join(root, "poster.jpg")
+ if err := os.WriteFile(newPoster, []byte("jpg"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := scanner.ScanLibrary(t.Context(), lib.ID); err != nil {
+ t.Fatal(err)
+ }
+
+ var media model.Media
+ if err := db.First(&media, "path = ?", mediaPath).Error; err != nil {
+ t.Fatal(err)
+ }
+ if media.PosterURL != newPoster {
+ t.Fatalf("poster_url = %q, want refreshed local poster %q", media.PosterURL, newPoster)
+ }
+ if media.ScrapeStatus == "matched" {
+ t.Fatalf("artwork-only refresh should keep media enrichable, got %q", media.ScrapeStatus)
+ }
+}
+
+func TestScanLibraryParsesEpisodesForMovieTypedLibrary(t *testing.T) {
+ root := t.TempDir()
+ seasonDir := filepath.Join(root, "哈哈哈哈哈 (2020)", "Season 06")
+ if err := os.MkdirAll(seasonDir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ mediaPath := filepath.Join(seasonDir, "哈哈哈哈哈 - S06E17 - 第 17 集.mkv")
+ if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.Setting{})
+ repos := repository.New(db)
+ lib := model.Library{Name: "综艺", Path: root, Type: "movie", Enabled: true}
+ if err := repos.Library.Create(t.Context(), &lib); 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, "path = ?", mediaPath).Error; err != nil {
+ t.Fatal(err)
+ }
+ if media.SeasonNum != 6 || media.EpisodeNum != 17 {
+ t.Fatalf("season/episode = %d/%d, want 6/17", media.SeasonNum, media.EpisodeNum)
+ }
+}
+
+func TestScanLibraryPrunesMissingMedia(t *testing.T) {
+ root := t.TempDir()
+ mediaPath := filepath.Join(root, "Show.S02E03.mkv")
+ if err := os.WriteFile(mediaPath, []byte("x"), 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)
+ }
+ stale := model.Media{
+ LibraryID: lib.ID,
+ Title: "Show",
+ Path: filepath.Join(root, "old", "Show.S02E03.mkv"),
+ SizeBytes: 123,
+ ScrapeStatus: "pending",
+ }
+ if err := db.Create(&stale).Error; err != nil {
+ t.Fatal(err)
+ }
+
+ scanner := NewScannerService(&config.Config{}, zap.NewNop(), repos, NewHub(zap.NewNop()), nil, nil)
+ res, err := scanner.ScanLibrary(t.Context(), lib.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.Removed != 1 {
+ t.Fatalf("Removed = %d, want 1", res.Removed)
+ }
+ var count int64
+ if err := db.Model(&model.Media{}).Where("library_id = ?", lib.ID).Count(&count).Error; err != nil {
+ t.Fatal(err)
+ }
+ if count != 1 {
+ t.Fatalf("media count = %d, want 1", count)
+ }
+}