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) + } +}