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 ""