From 8225b4e319770e0381a253349a75c9111da80a39 Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:05:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/service/adult_scraper.go | 47 +++++++++++++ .../service/adult_scraper_routing_test.go | 22 ++++++- internal/service/manual_scrape.go | 16 ++++- internal/service/manual_scrape_test.go | 66 +++++++++++++++++++ 4 files changed, 149 insertions(+), 2 deletions(-) diff --git a/internal/service/adult_scraper.go b/internal/service/adult_scraper.go index 134921a..f65fe42 100644 --- a/internal/service/adult_scraper.go +++ b/internal/service/adult_scraper.go @@ -9,6 +9,7 @@ import ( "net/url" "regexp" "strings" + "sync" "time" "go.uber.org/zap" @@ -180,6 +181,7 @@ func (p *AdultProvider) SearchCandidates(ctx context.Context, query string) ([]* if err != nil { return nil, err } + matches = p.enrichMetaTubeCandidates(ctx, mtCfg, code, matches) for _, m := range matches { m.OriginalName = code m.Title = FormatAdultTitle(code, m.Title) @@ -194,6 +196,7 @@ func (p *AdultProvider) SearchCandidates(ctx context.Context, query string) ([]* if mtCfg.ServerURL != "" { matches, err := p.metatube.Search(ctx, mtCfg, code) if err == nil && len(matches) > 0 { + matches = p.enrichMetaTubeCandidates(ctx, mtCfg, code, matches) for _, m := range matches { m.OriginalName = code m.Title = FormatAdultTitle(code, m.Title) @@ -217,6 +220,50 @@ func (p *AdultProvider) SearchCandidates(ctx context.Context, query string) ([]* } } +// GetMetaTubeCandidate fetches the selected provider result instead of +// re-running a search that may choose a different provider. +func (p *AdultProvider) GetMetaTubeCandidate(ctx context.Context, provider, id string) (*Match, error) { + if p == nil || p.metatube == nil { + return nil, nil + } + provider = strings.TrimSpace(provider) + id = strings.TrimSpace(id) + if provider == "" || id == "" { + return nil, nil + } + engine := strings.ToLower(p.getSetting(ctx, "adult.scraper.engine", "builtin")) + if engine != "metatube" && engine != "auto" { + return nil, nil + } + cfg := p.ResolveMetaTubeConfig(ctx) + if cfg.ServerURL == "" { + return nil, nil + } + return p.metatube.GetMovie(ctx, cfg, provider, id) +} + +func (p *AdultProvider) enrichMetaTubeCandidates(ctx context.Context, cfg MetaTubeConfig, code string, matches []*Match) []*Match { + var wg sync.WaitGroup + for i, candidate := range matches { + if candidate == nil || + normalizeAdultCode(candidate.OriginalName) != code || + strings.TrimSpace(candidate.DoubanID) == "" || + strings.TrimSpace(candidate.TheTVDBID) == "" { + continue + } + wg.Add(1) + go func(index int, current *Match) { + defer wg.Done() + detailed, err := p.metatube.GetMovie(ctx, cfg, current.TheTVDBID, current.DoubanID) + if err == nil && detailed != nil { + matches[index] = detailed + } + }(i, candidate) + } + wg.Wait() + return matches +} + func (p *AdultProvider) searchBuiltin(ctx context.Context, code string) (*Match, error) { bases := p.resolveBases(ctx) if len(bases) == 0 { diff --git a/internal/service/adult_scraper_routing_test.go b/internal/service/adult_scraper_routing_test.go index 6b3caee..0750116 100644 --- a/internal/service/adult_scraper_routing_test.go +++ b/internal/service/adult_scraper_routing_test.go @@ -25,7 +25,8 @@ func TestAdultProviderRouting(t *testing.T) { repos := repository.New(db) mtServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/v1/movies/search" { + switch r.URL.Path { + case "/v1/movies/search": q := r.URL.Query().Get("q") if q == "SSIS-001" { results := struct { @@ -46,6 +47,21 @@ func TestAdultProviderRouting(t *testing.T) { _ = json.NewEncoder(w).Encode(results) return } + case "/v1/movies/javdb/999": + _ = json.NewEncoder(w).Encode(struct { + Data MetaTubeMovieInfo `json:"data"` + }{ + Data: MetaTubeMovieInfo{ + ID: "999", + Number: "SSIS-001", + Title: "河北彩花 専属デビュー", + Provider: "javdb", + CoverURL: "https://example.com/poster.jpg", + PreviewImages: []string{"https://example.com/backdrop.jpg"}, + ReleaseDate: "2021-06-19", + }, + }) + return } http.NotFound(w, r) })) @@ -80,6 +96,10 @@ func TestAdultProviderRouting(t *testing.T) { if len(candidates) != 1 { t.Fatalf("expected 1 candidate, got %d", len(candidates)) } + if candidates[0].PosterURL != "https://example.com/poster.jpg" || + candidates[0].BackdropURL != "https://example.com/backdrop.jpg" { + t.Fatalf("candidate artwork was not enriched: %#v", candidates[0]) + } // 2. Test auto mode with failing metatube query _ = repos.Setting.Set(context.Background(), "adult.scraper.engine", "auto") diff --git a/internal/service/manual_scrape.go b/internal/service/manual_scrape.go index 6eb9077..d068d5e 100644 --- a/internal/service/manual_scrape.go +++ b/internal/service/manual_scrape.go @@ -127,7 +127,21 @@ func (s *ScraperService) manualRequestMatch(ctx context.Context, req ManualScrap } } case source == "adult": - if match := s.manualAdultMatch(ctx, firstText(req.OriginalName, req.Title)); match != nil { + var match *Match + if s.adult != nil { + match, _ = s.adult.GetMetaTubeCandidate(ctx, req.TheTVDBID, req.DoubanID) + } + if match == nil { + match = s.manualAdultMatch(ctx, firstText(req.OriginalName, req.Title)) + } + if match != nil { + // Older search responses used the portrait poster as a backdrop + // fallback. Do not let that placeholder replace a real preview + // image fetched from the selected MetaTube provider. + if strings.TrimSpace(match.BackdropURL) != "" && + strings.TrimSpace(req.BackdropURL) == strings.TrimSpace(req.PosterURL) { + req.BackdropURL = "" + } return mergeManualRequestIntoMatch(match, req), nil } } diff --git a/internal/service/manual_scrape_test.go b/internal/service/manual_scrape_test.go index 1da72f5..767c7c2 100644 --- a/internal/service/manual_scrape_test.go +++ b/internal/service/manual_scrape_test.go @@ -35,6 +35,72 @@ func TestManualRequestMatchFallsBackToCandidatePayload(t *testing.T) { } } +func TestManualAdultMatchUsesSelectedMetaTubeDetailsAndRealBackdrop(t *testing.T) { + var detailCalls atomic.Int32 + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/movies/AVE/94600" { + http.NotFound(w, r) + return + } + detailCalls.Add(1) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(struct { + Data MetaTubeMovieInfo `json:"data"` + }{ + Data: MetaTubeMovieInfo{ + ID: "94600", + Number: "CWPBD-138", + Title: "Selected title", + Provider: "AVE", + CoverURL: "https://example.com/poster.jpg", + PreviewImages: []string{"https://example.com/backdrop.jpg"}, + }, + }) + })) + defer upstream.Close() + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.Setting{}, &model.APIConfig{}); err != nil { + t.Fatal(err) + } + repos := repository.New(db) + if err := repos.Setting.Set(t.Context(), "adult.scraper.engine", "metatube"); err != nil { + t.Fatal(err) + } + if err := repos.Setting.Set(t.Context(), "adult.scraper.metatube_server", upstream.URL); err != nil { + t.Fatal(err) + } + adult := NewAdultProvider(zap.NewNop(), nil, repos) + scraper := &ScraperService{adult: adult} + + match, err := scraper.manualRequestMatch(t.Context(), ManualScrapeRequest{ + Source: "adult", + MediaType: "adult", + Title: "CWPBD-138 Selected title", + OriginalName: "CWPBD-138", + PosterURL: "https://example.com/search-cover.jpg", + BackdropURL: "https://example.com/search-cover.jpg", + DoubanID: "94600", + TheTVDBID: "AVE", + NSFW: true, + }) + if err != nil { + t.Fatal(err) + } + if detailCalls.Load() != 1 { + t.Fatalf("selected MetaTube detail calls = %d, want 1", detailCalls.Load()) + } + if match.PosterURL != "https://example.com/search-cover.jpg" { + t.Fatalf("selected poster = %q", match.PosterURL) + } + if match.BackdropURL != "https://example.com/backdrop.jpg" { + t.Fatalf("backdrop = %q, want detail preview", match.BackdropURL) + } +} + func TestParsePositiveIDStringAcceptsProviderPrefixes(t *testing.T) { cases := map[string]string{ "12345": "12345",