diff --git a/internal/service/scraper_library_test.go b/internal/service/scraper_library_test.go
new file mode 100644
index 0000000..4089dc3
--- /dev/null
+++ b/internal/service/scraper_library_test.go
@@ -0,0 +1,162 @@
+package service
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/ShukeBta/MediaStationGo/internal/model"
+)
+
+func TestManualEnrichLibraryRetriesNoMatchAndCountsRealMatches(t *testing.T) {
+ scraper, repos, closeServer := newTestScraper(t)
+ defer closeServer()
+
+ lib := model.Library{Name: "番剧", Path: t.TempDir(), Type: "tv", Enabled: true}
+ if err := repos.DB.Create(&lib).Error; err != nil {
+ t.Fatal(err)
+ }
+ mediaPath := filepath.Join(lib.Path, "间谍过家家 - S02E02.mkv")
+ if err := repos.DB.Create(&model.Media{
+ LibraryID: lib.ID,
+ Title: "间谍过家家",
+ Path: mediaPath,
+ SeasonNum: 2,
+ EpisodeNum: 2,
+ ScrapeStatus: "no_match",
+ }).Error; err != nil {
+ t.Fatal(err)
+ }
+
+ if matched, err := scraper.EnrichLibrary(t.Context(), lib.ID); err != nil || matched != 0 {
+ t.Fatalf("default EnrichLibrary matched=%d err=%v, want skipped no_match", matched, err)
+ }
+ if matched, err := scraper.EnrichLibrary(t.Context(), lib.ID, true); err != nil || matched != 1 {
+ t.Fatalf("manual EnrichLibrary matched=%d err=%v, want one real match", matched, err)
+ }
+}
+
+func TestManualEnrichLibraryCanRefreshAlreadyMatchedRows(t *testing.T) {
+ scraper, repos, closeServer := newTestScraper(t)
+ defer closeServer()
+
+ lib := model.Library{Name: "番剧", Path: t.TempDir(), Type: "tv", Enabled: true}
+ if err := repos.DB.Create(&lib).Error; err != nil {
+ t.Fatal(err)
+ }
+ media := model.Media{
+ LibraryID: lib.ID,
+ Title: "间谍过家家",
+ Path: filepath.Join(lib.Path, "间谍过家家 - S02E02.mkv"),
+ SeasonNum: 2,
+ EpisodeNum: 2,
+ ScrapeStatus: "matched",
+ }
+ if err := repos.DB.Create(&media).Error; err != nil {
+ t.Fatal(err)
+ }
+
+ defaultResult, err := scraper.EnrichLibraryDetailedWithOptions(t.Context(), lib.ID, ScrapeOptions{RetryNoMatch: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if defaultResult.Processed != 0 || defaultResult.Candidates != 0 {
+ t.Fatalf("default manual scrape result=%+v, want matched rows skipped without IncludeMatched", defaultResult)
+ }
+
+ refreshResult, err := scraper.EnrichLibraryDetailedWithOptions(t.Context(), lib.ID, ScrapeOptions{
+ RetryNoMatch: true,
+ IncludeMatched: true,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if refreshResult.Processed != 1 || refreshResult.Matched != 1 || refreshResult.Candidates != 1 {
+ t.Fatalf("refresh result=%+v, want already matched row reprocessed", refreshResult)
+ }
+}
+
+func TestScrapeCandidateRowsPrioritizeLibraryArtworkBeforeEpisodes(t *testing.T) {
+ scraper, repos, closeServer := newTestScraper(t)
+ defer closeServer()
+
+ lib := model.Library{Name: "番剧", Path: t.TempDir(), Type: "tv", Enabled: true}
+ if err := repos.DB.Create(&lib).Error; err != nil {
+ t.Fatal(err)
+ }
+ rows := []model.Media{
+ {
+ Base: model.Base{ID: "001-episode"},
+ LibraryID: lib.ID,
+ Title: "间谍过家家 第 1 集",
+ Path: filepath.Join(lib.Path, "间谍过家家 - S02E01.mkv"),
+ SeasonNum: 2,
+ EpisodeNum: 1,
+ ScrapeStatus: "pending",
+ },
+ {
+ Base: model.Base{ID: "999-series"},
+ LibraryID: lib.ID,
+ Title: "间谍过家家",
+ Path: filepath.Join(lib.Path, "间谍过家家.mkv"),
+ ScrapeStatus: "pending",
+ },
+ }
+ if err := repos.DB.Create(&rows).Error; err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := scraper.scrapeCandidateRows(t.Context(), lib.ID, ScrapeOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(got) != 2 {
+ t.Fatalf("candidate rows = %d, want 2", len(got))
+ }
+ if got[0].ID != "999-series" || got[1].ID != "001-episode" {
+ t.Fatalf("scrape order = [%s, %s], want series-level row before episode row", got[0].ID, got[1].ID)
+ }
+}
+
+func TestEnrichLibraryIncludesMergedCloudLibraryMedia(t *testing.T) {
+ scraper, repos, closeServer := newTestScraper(t)
+ defer closeServer()
+
+ local := model.Library{Name: "番剧", Path: t.TempDir(), Type: "tv", Enabled: true}
+ cloud := model.Library{
+ Name: "OpenList · 番剧",
+ Path: BuildCloudLibraryPath("openlist", "/番剧", "/番剧"),
+ Type: "tv",
+ Enabled: true,
+ }
+ if err := repos.DB.Create(&local).Error; err != nil {
+ t.Fatal(err)
+ }
+ if err := repos.DB.Create(&cloud).Error; err != nil {
+ t.Fatal(err)
+ }
+ if err := repos.DB.Create(&model.Media{
+ LibraryID: cloud.ID,
+ Title: "间谍过家家",
+ Path: "cloud://openlist/番剧/间谍过家家 - S02E02.mkv",
+ SeasonNum: 2,
+ EpisodeNum: 2,
+ ScrapeStatus: "pending",
+ }).Error; err != nil {
+ t.Fatal(err)
+ }
+
+ result, err := scraper.EnrichLibraryDetailed(t.Context(), local.ID, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if result.Matched != 1 || result.Processed != 1 || result.Candidates != 1 || result.Failed != 0 {
+ t.Fatalf("result=%+v, want merged cloud media to be scraped once", result)
+ }
+ var got model.Media
+ if err := repos.DB.First(&got, "library_id = ?", cloud.ID).Error; err != nil {
+ t.Fatal(err)
+ }
+ if got.ScrapeStatus != "matched" || got.TMDbID != 12345 {
+ t.Fatalf("merged cloud media was not enriched: status=%q tmdb=%d", got.ScrapeStatus, got.TMDbID)
+ }
+}
diff --git a/internal/service/scraper_local_metadata_test.go b/internal/service/scraper_local_metadata_test.go
new file mode 100644
index 0000000..c8b171b
--- /dev/null
+++ b/internal/service/scraper_local_metadata_test.go
@@ -0,0 +1,151 @@
+package service
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/glebarez/sqlite"
+ "go.uber.org/zap"
+ "gorm.io/gorm"
+
+ "github.com/ShukeBta/MediaStationGo/internal/config"
+ "github.com/ShukeBta/MediaStationGo/internal/model"
+ "github.com/ShukeBta/MediaStationGo/internal/repository"
+)
+
+func TestEnrichOneRejectsWrongYearMatchFromSeriesFolder(t *testing.T) {
+ upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ switch r.URL.Path {
+ case "/search/tv":
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "results": []map[string]any{{
+ "id": 999,
+ "name": "Parade of Stars Auto Show",
+ "first_air_date": "1952-01-01",
+ }},
+ })
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+ defer upstream.Close()
+
+ db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := db.AutoMigrate(&model.Library{}, &model.Series{}, &model.Media{}); err != nil {
+ t.Fatal(err)
+ }
+ repos := repository.New(db)
+ cfg := &config.Config{}
+ cfg.Secrets.TMDbAPIKey = "test-key"
+ cfg.Secrets.TMDbAPIProxy = upstream.URL
+ log := zap.NewNop()
+ scraper := NewScraperService(cfg, log, repos, NewTMDbProvider(cfg, log, nil), nil, nil, nil, NewHub(log))
+
+ root := t.TempDir()
+ mediaPath := filepath.Join(root, "Auto Show (2026)", "Season 1", "Auto Show - S01E03 - 第 3 集.mkv")
+ lib := model.Library{Name: "剧集", Path: root, Type: "tv", Enabled: true}
+ if err := repos.DB.Create(&lib).Error; err != nil {
+ t.Fatal(err)
+ }
+ media := model.Media{
+ LibraryID: lib.ID,
+ Title: "auto show",
+ Path: mediaPath,
+ SeasonNum: 1,
+ EpisodeNum: 3,
+ ScrapeStatus: "pending",
+ }
+ if err := repos.DB.Create(&media).Error; err != nil {
+ t.Fatal(err)
+ }
+
+ if err := scraper.EnrichOne(t.Context(), &media); err != nil {
+ t.Fatal(err)
+ }
+
+ var got model.Media
+ if err := repos.DB.First(&got, "id = ?", media.ID).Error; err != nil {
+ t.Fatal(err)
+ }
+ if got.ScrapeStatus != "no_match" || got.Title != "auto show" || got.Year != 0 || got.TMDbID != 0 {
+ t.Fatalf("wrong-year scrape should be rejected, got status=%q title=%q year=%d tmdb=%d", got.ScrapeStatus, got.Title, got.Year, got.TMDbID)
+ }
+}
+
+func TestEnrichOnePrefersLocalMetadataWithoutProvider(t *testing.T) {
+ db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := db.AutoMigrate(&model.Library{}, &model.Series{}, &model.Media{}); err != nil {
+ t.Fatal(err)
+ }
+ repos := repository.New(db)
+ log := zap.NewNop()
+ scraper := NewScraperService(&config.Config{}, log, repos, nil, nil, nil, nil, NewHub(log))
+
+ root := t.TempDir()
+ showDir := filepath.Join(root, "间谍过家家")
+ seasonDir := filepath.Join(showDir, "Season 02")
+ if err := os.MkdirAll(seasonDir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(showDir, "tvshow.nfo"), []byte(`
+间谍过家家
+2022
+120089
+Animation
+`), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ mediaPath := filepath.Join(seasonDir, "间谍过家家 - S02E12.mkv")
+ if err := os.WriteFile(nfoPath(mediaPath), []byte(`
+企鹅公园
+间谍过家家
+2
+12
+本地剧情
+`), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ lib := model.Library{Name: "番剧", Path: root, Type: "tv", Enabled: true}
+ if err := repos.DB.Create(&lib).Error; err != nil {
+ t.Fatal(err)
+ }
+ media := model.Media{
+ LibraryID: lib.ID,
+ Title: "bad title",
+ Path: mediaPath,
+ ScrapeStatus: "pending",
+ }
+ if err := repos.DB.Create(&media).Error; err != nil {
+ t.Fatal(err)
+ }
+
+ if err := scraper.EnrichOne(t.Context(), &media); err != nil {
+ t.Fatal(err)
+ }
+
+ var got model.Media
+ if err := repos.DB.First(&got, "id = ?", media.ID).Error; err != nil {
+ t.Fatal(err)
+ }
+ if got.ScrapeStatus != "matched" || got.Title != "间谍过家家" || got.TMDbID != 120089 {
+ t.Fatalf("unexpected local scrape: status=%q title=%q tmdb=%d", got.ScrapeStatus, got.Title, got.TMDbID)
+ }
+ if got.SeasonNum != 2 || got.EpisodeNum != 12 || got.Overview != "本地剧情" {
+ t.Fatalf("unexpected local episode data: s=%d e=%d overview=%q", got.SeasonNum, got.EpisodeNum, got.Overview)
+ }
+ if got.EpisodeTitle != "企鹅公园" {
+ t.Fatalf("episode_title = %q, want local episode title", got.EpisodeTitle)
+ }
+}
diff --git a/internal/service/scraper_test.go b/internal/service/scraper_test.go
index 4ba802b..067620e 100644
--- a/internal/service/scraper_test.go
+++ b/internal/service/scraper_test.go
@@ -1,21 +1,11 @@
package service
import (
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "os"
"path/filepath"
"strings"
"testing"
- "github.com/glebarez/sqlite"
- "go.uber.org/zap"
- "gorm.io/gorm"
-
- "github.com/ShukeBta/MediaStationGo/internal/config"
"github.com/ShukeBta/MediaStationGo/internal/model"
- "github.com/ShukeBta/MediaStationGo/internal/repository"
)
func TestEnrichOneUsesExistingTMDbIDForCloudMedia(t *testing.T) {
@@ -275,361 +265,3 @@ func TestApplyManualMatchSkipsTMDbEpisodeStillWhenDisabled(t *testing.T) {
t.Fatalf("series backdrop should still be saved when manual episode artwork is disabled: got %q", got.BackdropURL)
}
}
-
-func TestEnrichOneRejectsWrongYearMatchFromSeriesFolder(t *testing.T) {
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- switch r.URL.Path {
- case "/search/tv":
- _ = json.NewEncoder(w).Encode(map[string]any{
- "results": []map[string]any{{
- "id": 999,
- "name": "Parade of Stars Auto Show",
- "first_air_date": "1952-01-01",
- }},
- })
- default:
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
-
- db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
- if err != nil {
- t.Fatal(err)
- }
- if err := db.AutoMigrate(&model.Library{}, &model.Series{}, &model.Media{}); err != nil {
- t.Fatal(err)
- }
- repos := repository.New(db)
- cfg := &config.Config{}
- cfg.Secrets.TMDbAPIKey = "test-key"
- cfg.Secrets.TMDbAPIProxy = upstream.URL
- log := zap.NewNop()
- scraper := NewScraperService(cfg, log, repos, NewTMDbProvider(cfg, log, nil), nil, nil, nil, NewHub(log))
-
- root := t.TempDir()
- mediaPath := filepath.Join(root, "Auto Show (2026)", "Season 1", "Auto Show - S01E03 - 第 3 集.mkv")
- lib := model.Library{Name: "剧集", Path: root, Type: "tv", Enabled: true}
- if err := repos.DB.Create(&lib).Error; err != nil {
- t.Fatal(err)
- }
- media := model.Media{
- LibraryID: lib.ID,
- Title: "auto show",
- Path: mediaPath,
- SeasonNum: 1,
- EpisodeNum: 3,
- ScrapeStatus: "pending",
- }
- if err := repos.DB.Create(&media).Error; err != nil {
- t.Fatal(err)
- }
-
- if err := scraper.EnrichOne(t.Context(), &media); err != nil {
- t.Fatal(err)
- }
-
- var got model.Media
- if err := repos.DB.First(&got, "id = ?", media.ID).Error; err != nil {
- t.Fatal(err)
- }
- if got.ScrapeStatus != "no_match" || got.Title != "auto show" || got.Year != 0 || got.TMDbID != 0 {
- t.Fatalf("wrong-year scrape should be rejected, got status=%q title=%q year=%d tmdb=%d", got.ScrapeStatus, got.Title, got.Year, got.TMDbID)
- }
-}
-
-func TestEnrichOnePrefersLocalMetadataWithoutProvider(t *testing.T) {
- db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
- if err != nil {
- t.Fatal(err)
- }
- if err := db.AutoMigrate(&model.Library{}, &model.Series{}, &model.Media{}); err != nil {
- t.Fatal(err)
- }
- repos := repository.New(db)
- log := zap.NewNop()
- scraper := NewScraperService(&config.Config{}, log, repos, nil, nil, nil, nil, NewHub(log))
-
- root := t.TempDir()
- showDir := filepath.Join(root, "间谍过家家")
- seasonDir := filepath.Join(showDir, "Season 02")
- if err := os.MkdirAll(seasonDir, 0o755); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(filepath.Join(showDir, "tvshow.nfo"), []byte(`
-间谍过家家
-2022
-120089
-Animation
-`), 0o644); err != nil {
- t.Fatal(err)
- }
- mediaPath := filepath.Join(seasonDir, "间谍过家家 - S02E12.mkv")
- if err := os.WriteFile(nfoPath(mediaPath), []byte(`
-企鹅公园
-间谍过家家
-2
-12
-本地剧情
-`), 0o644); err != nil {
- t.Fatal(err)
- }
-
- lib := model.Library{Name: "番剧", Path: root, Type: "tv", Enabled: true}
- if err := repos.DB.Create(&lib).Error; err != nil {
- t.Fatal(err)
- }
- media := model.Media{
- LibraryID: lib.ID,
- Title: "bad title",
- Path: mediaPath,
- ScrapeStatus: "pending",
- }
- if err := repos.DB.Create(&media).Error; err != nil {
- t.Fatal(err)
- }
-
- if err := scraper.EnrichOne(t.Context(), &media); err != nil {
- t.Fatal(err)
- }
-
- var got model.Media
- if err := repos.DB.First(&got, "id = ?", media.ID).Error; err != nil {
- t.Fatal(err)
- }
- if got.ScrapeStatus != "matched" || got.Title != "间谍过家家" || got.TMDbID != 120089 {
- t.Fatalf("unexpected local scrape: status=%q title=%q tmdb=%d", got.ScrapeStatus, got.Title, got.TMDbID)
- }
- if got.SeasonNum != 2 || got.EpisodeNum != 12 || got.Overview != "本地剧情" {
- t.Fatalf("unexpected local episode data: s=%d e=%d overview=%q", got.SeasonNum, got.EpisodeNum, got.Overview)
- }
- if got.EpisodeTitle != "企鹅公园" {
- t.Fatalf("episode_title = %q, want local episode title", got.EpisodeTitle)
- }
-}
-
-func TestManualEnrichLibraryRetriesNoMatchAndCountsRealMatches(t *testing.T) {
- scraper, repos, closeServer := newTestScraper(t)
- defer closeServer()
-
- lib := model.Library{Name: "番剧", Path: t.TempDir(), Type: "tv", Enabled: true}
- if err := repos.DB.Create(&lib).Error; err != nil {
- t.Fatal(err)
- }
- mediaPath := filepath.Join(lib.Path, "间谍过家家 - S02E02.mkv")
- if err := repos.DB.Create(&model.Media{
- LibraryID: lib.ID,
- Title: "间谍过家家",
- Path: mediaPath,
- SeasonNum: 2,
- EpisodeNum: 2,
- ScrapeStatus: "no_match",
- }).Error; err != nil {
- t.Fatal(err)
- }
-
- if matched, err := scraper.EnrichLibrary(t.Context(), lib.ID); err != nil || matched != 0 {
- t.Fatalf("default EnrichLibrary matched=%d err=%v, want skipped no_match", matched, err)
- }
- if matched, err := scraper.EnrichLibrary(t.Context(), lib.ID, true); err != nil || matched != 1 {
- t.Fatalf("manual EnrichLibrary matched=%d err=%v, want one real match", matched, err)
- }
-}
-
-func TestManualEnrichLibraryCanRefreshAlreadyMatchedRows(t *testing.T) {
- scraper, repos, closeServer := newTestScraper(t)
- defer closeServer()
-
- lib := model.Library{Name: "番剧", Path: t.TempDir(), Type: "tv", Enabled: true}
- if err := repos.DB.Create(&lib).Error; err != nil {
- t.Fatal(err)
- }
- media := model.Media{
- LibraryID: lib.ID,
- Title: "间谍过家家",
- Path: filepath.Join(lib.Path, "间谍过家家 - S02E02.mkv"),
- SeasonNum: 2,
- EpisodeNum: 2,
- ScrapeStatus: "matched",
- }
- if err := repos.DB.Create(&media).Error; err != nil {
- t.Fatal(err)
- }
-
- defaultResult, err := scraper.EnrichLibraryDetailedWithOptions(t.Context(), lib.ID, ScrapeOptions{RetryNoMatch: true})
- if err != nil {
- t.Fatal(err)
- }
- if defaultResult.Processed != 0 || defaultResult.Candidates != 0 {
- t.Fatalf("default manual scrape result=%+v, want matched rows skipped without IncludeMatched", defaultResult)
- }
-
- refreshResult, err := scraper.EnrichLibraryDetailedWithOptions(t.Context(), lib.ID, ScrapeOptions{
- RetryNoMatch: true,
- IncludeMatched: true,
- })
- if err != nil {
- t.Fatal(err)
- }
- if refreshResult.Processed != 1 || refreshResult.Matched != 1 || refreshResult.Candidates != 1 {
- t.Fatalf("refresh result=%+v, want already matched row reprocessed", refreshResult)
- }
-}
-
-func TestScrapeCandidateRowsPrioritizeLibraryArtworkBeforeEpisodes(t *testing.T) {
- scraper, repos, closeServer := newTestScraper(t)
- defer closeServer()
-
- lib := model.Library{Name: "番剧", Path: t.TempDir(), Type: "tv", Enabled: true}
- if err := repos.DB.Create(&lib).Error; err != nil {
- t.Fatal(err)
- }
- rows := []model.Media{
- {
- Base: model.Base{ID: "001-episode"},
- LibraryID: lib.ID,
- Title: "间谍过家家 第 1 集",
- Path: filepath.Join(lib.Path, "间谍过家家 - S02E01.mkv"),
- SeasonNum: 2,
- EpisodeNum: 1,
- ScrapeStatus: "pending",
- },
- {
- Base: model.Base{ID: "999-series"},
- LibraryID: lib.ID,
- Title: "间谍过家家",
- Path: filepath.Join(lib.Path, "间谍过家家.mkv"),
- ScrapeStatus: "pending",
- },
- }
- if err := repos.DB.Create(&rows).Error; err != nil {
- t.Fatal(err)
- }
-
- got, err := scraper.scrapeCandidateRows(t.Context(), lib.ID, ScrapeOptions{})
- if err != nil {
- t.Fatal(err)
- }
- if len(got) != 2 {
- t.Fatalf("candidate rows = %d, want 2", len(got))
- }
- if got[0].ID != "999-series" || got[1].ID != "001-episode" {
- t.Fatalf("scrape order = [%s, %s], want series-level row before episode row", got[0].ID, got[1].ID)
- }
-}
-
-func TestEnrichLibraryIncludesMergedCloudLibraryMedia(t *testing.T) {
- scraper, repos, closeServer := newTestScraper(t)
- defer closeServer()
-
- local := model.Library{Name: "番剧", Path: t.TempDir(), Type: "tv", Enabled: true}
- cloud := model.Library{
- Name: "OpenList · 番剧",
- Path: BuildCloudLibraryPath("openlist", "/番剧", "/番剧"),
- Type: "tv",
- Enabled: true,
- }
- if err := repos.DB.Create(&local).Error; err != nil {
- t.Fatal(err)
- }
- if err := repos.DB.Create(&cloud).Error; err != nil {
- t.Fatal(err)
- }
- if err := repos.DB.Create(&model.Media{
- LibraryID: cloud.ID,
- Title: "间谍过家家",
- Path: "cloud://openlist/番剧/间谍过家家 - S02E02.mkv",
- SeasonNum: 2,
- EpisodeNum: 2,
- ScrapeStatus: "pending",
- }).Error; err != nil {
- t.Fatal(err)
- }
-
- result, err := scraper.EnrichLibraryDetailed(t.Context(), local.ID, true)
- if err != nil {
- t.Fatal(err)
- }
- if result.Matched != 1 || result.Processed != 1 || result.Candidates != 1 || result.Failed != 0 {
- t.Fatalf("result=%+v, want merged cloud media to be scraped once", result)
- }
- var got model.Media
- if err := repos.DB.First(&got, "library_id = ?", cloud.ID).Error; err != nil {
- t.Fatal(err)
- }
- if got.ScrapeStatus != "matched" || got.TMDbID != 12345 {
- t.Fatalf("merged cloud media was not enriched: status=%q tmdb=%d", got.ScrapeStatus, got.TMDbID)
- }
-}
-
-func newTestScraper(t *testing.T) (*ScraperService, *repository.Container, func()) {
- t.Helper()
-
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- switch {
- case strings.HasPrefix(r.URL.Path, "/search/tv"):
- _ = json.NewEncoder(w).Encode(map[string]any{
- "results": []map[string]any{{
- "id": 12345,
- "name": "间谍过家家",
- "original_name": "SPY×FAMILY",
- "overview": "测试简介",
- "poster_path": "/poster.jpg",
- "backdrop_path": "/backdrop.jpg",
- "first_air_date": "2022-04-09",
- "vote_average": 8.6,
- }},
- })
- case r.URL.Path == "/tv/12345/season/2/episode/1":
- _ = json.NewEncoder(w).Encode(map[string]any{
- "name": "任务代号: 猫",
- "overview": "单集剧情",
- "still_path": "/still.jpg",
- "air_date": "2023-10-07",
- "vote_average": 9.1,
- "runtime": 24,
- })
- case strings.HasPrefix(r.URL.Path, "/tv/12345"):
- _ = json.NewEncoder(w).Encode(map[string]any{
- "id": 12345,
- "name": "间谍过家家",
- "overview": "测试简介",
- "poster_path": "/poster.jpg",
- "backdrop_path": "/backdrop.jpg",
- "first_air_date": "2022-04-09",
- "vote_average": 8.6,
- "origin_country": []string{"JP"},
- "spoken_languages": []map[string]any{{
- "iso_639_1": "ja",
- }},
- "genres": []map[string]any{{
- "name": "Animation",
- }},
- })
- default:
- http.NotFound(w, r)
- }
- }))
-
- db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
- if err != nil {
- upstream.Close()
- t.Fatal(err)
- }
- if err := db.AutoMigrate(&model.Library{}, &model.Series{}, &model.Media{}); err != nil {
- upstream.Close()
- t.Fatal(err)
- }
- repos := repository.New(db)
- cfg := &config.Config{}
- cfg.Secrets.TMDbAPIKey = "test-key"
- cfg.Secrets.TMDbAPIProxy = upstream.URL
- cfg.Secrets.TMDbImageProxy = upstream.URL + "/images"
- log := zap.NewNop()
- tmdb := NewTMDbProvider(cfg, log, nil)
- scraper := NewScraperService(cfg, log, repos, tmdb, nil, nil, nil, NewHub(log))
-
- return scraper, repos, upstream.Close
-}
diff --git a/internal/service/scraper_test_helpers_test.go b/internal/service/scraper_test_helpers_test.go
index df044ac..673815a 100644
--- a/internal/service/scraper_test_helpers_test.go
+++ b/internal/service/scraper_test_helpers_test.go
@@ -1,5 +1,21 @@
package service
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/glebarez/sqlite"
+ "go.uber.org/zap"
+ "gorm.io/gorm"
+
+ "github.com/ShukeBta/MediaStationGo/internal/config"
+ "github.com/ShukeBta/MediaStationGo/internal/model"
+ "github.com/ShukeBta/MediaStationGo/internal/repository"
+)
+
func firstIndexFunc(values []string, match func(string) bool) int {
for i, value := range values {
if match(value) {
@@ -9,6 +25,77 @@ func firstIndexFunc(values []string, match func(string) bool) int {
return -1
}
+func newTestScraper(t *testing.T) (*ScraperService, *repository.Container, func()) {
+ t.Helper()
+
+ upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ switch {
+ case strings.HasPrefix(r.URL.Path, "/search/tv"):
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "results": []map[string]any{{
+ "id": 12345,
+ "name": "间谍过家家",
+ "original_name": "SPY×FAMILY",
+ "overview": "测试简介",
+ "poster_path": "/poster.jpg",
+ "backdrop_path": "/backdrop.jpg",
+ "first_air_date": "2022-04-09",
+ "vote_average": 8.6,
+ }},
+ })
+ case r.URL.Path == "/tv/12345/season/2/episode/1":
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "name": "任务代号: 猫",
+ "overview": "单集剧情",
+ "still_path": "/still.jpg",
+ "air_date": "2023-10-07",
+ "vote_average": 9.1,
+ "runtime": 24,
+ })
+ case strings.HasPrefix(r.URL.Path, "/tv/12345"):
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "id": 12345,
+ "name": "间谍过家家",
+ "overview": "测试简介",
+ "poster_path": "/poster.jpg",
+ "backdrop_path": "/backdrop.jpg",
+ "first_air_date": "2022-04-09",
+ "vote_average": 8.6,
+ "origin_country": []string{"JP"},
+ "spoken_languages": []map[string]any{{
+ "iso_639_1": "ja",
+ }},
+ "genres": []map[string]any{{
+ "name": "Animation",
+ }},
+ })
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+
+ db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
+ if err != nil {
+ upstream.Close()
+ t.Fatal(err)
+ }
+ if err := db.AutoMigrate(&model.Library{}, &model.Series{}, &model.Media{}); err != nil {
+ upstream.Close()
+ t.Fatal(err)
+ }
+ repos := repository.New(db)
+ cfg := &config.Config{}
+ cfg.Secrets.TMDbAPIKey = "test-key"
+ cfg.Secrets.TMDbAPIProxy = upstream.URL
+ cfg.Secrets.TMDbImageProxy = upstream.URL + "/images"
+ log := zap.NewNop()
+ tmdb := NewTMDbProvider(cfg, log, nil)
+ scraper := NewScraperService(cfg, log, repos, tmdb, nil, nil, nil, NewHub(log))
+
+ return scraper, repos, upstream.Close
+}
+
func firstQuery(values []string) string {
if len(values) == 0 {
return ""