mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 19:36:36 +08:00
144 lines
4.0 KiB
Go
144 lines
4.0 KiB
Go
// Package service — Douban (豆瓣) metadata provider.
|
|
//
|
|
// Douban is the dominant Chinese movie/TV rating and metadata site. Its
|
|
// unofficial API returns rich Chinese-language titles, overviews, ratings
|
|
// and poster URLs. A valid Douban cookie is required to avoid IP bans.
|
|
//
|
|
// We use the search endpoint at:
|
|
//
|
|
// https://movie.douban.com/j/subject_suggest?q=...
|
|
//
|
|
// And the detail endpoint at:
|
|
//
|
|
// https://movie.douban.com/j/subject_abstract?subject_id=...
|
|
//
|
|
// The provider is used as a supplemental source: after TMDb matches we
|
|
// attempt a Douban lookup to grab a localized Chinese title + overview.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/config"
|
|
)
|
|
|
|
// DoubanProvider talks to the unofficial Douban movie API.
|
|
type DoubanProvider struct {
|
|
cfg *config.Config
|
|
log *zap.Logger
|
|
client *http.Client
|
|
}
|
|
|
|
// NewDoubanProvider is the constructor.
|
|
func NewDoubanProvider(cfg *config.Config, log *zap.Logger) *DoubanProvider {
|
|
return &DoubanProvider{
|
|
cfg: cfg,
|
|
log: log,
|
|
client: NewExternalHTTPClient(15 * time.Second),
|
|
}
|
|
}
|
|
|
|
// Enabled reports whether Douban lookup is available. Public movie.douban.com
|
|
// suggest endpoints work without an API key; a cookie is optional and only
|
|
// helps when Douban applies stricter anti-scraping rules.
|
|
func (d *DoubanProvider) Enabled() bool {
|
|
return true
|
|
}
|
|
|
|
// userAgents for anti-scraping randomization.
|
|
var userAgents = []string{
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
|
}
|
|
|
|
// DoubanMatch is the result of a Douban search hit.
|
|
type DoubanMatch struct {
|
|
DoubanID string `json:"douban_id"`
|
|
Title string `json:"title"`
|
|
Year string `json:"year"`
|
|
Img string `json:"img"`
|
|
Rating float32 `json:"rating"`
|
|
Type string `json:"type,omitempty"`
|
|
}
|
|
|
|
// Search runs a Douban subject_suggest query and returns the top match.
|
|
func (d *DoubanProvider) Search(ctx context.Context, query string) (*DoubanMatch, error) {
|
|
if !d.Enabled() || query == "" {
|
|
return nil, nil
|
|
}
|
|
u := "https://movie.douban.com/j/subject_suggest?q=" + url.QueryEscape(query)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.setHeaders(req)
|
|
|
|
resp, err := d.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode >= 400 {
|
|
return nil, fmt.Errorf("douban search: %d", resp.StatusCode)
|
|
}
|
|
|
|
type suggestion struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Year string `json:"year"`
|
|
Img string `json:"img"`
|
|
Type string `json:"type"`
|
|
}
|
|
var results []suggestion
|
|
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(results) == 0 {
|
|
return nil, nil
|
|
}
|
|
r := results[0]
|
|
return &DoubanMatch{
|
|
DoubanID: r.ID,
|
|
Title: r.Title,
|
|
Year: r.Year,
|
|
Img: r.Img,
|
|
Type: r.Type,
|
|
}, nil
|
|
}
|
|
|
|
func (d *DoubanProvider) SearchMatch(ctx context.Context, query string) (*Match, error) {
|
|
got, err := d.Search(ctx, query)
|
|
if err != nil || got == nil {
|
|
return nil, err
|
|
}
|
|
match := &Match{
|
|
DoubanID: got.DoubanID,
|
|
Title: got.Title,
|
|
PosterURL: got.Img,
|
|
Rating: got.Rating,
|
|
}
|
|
if len(got.Year) >= 4 {
|
|
_, _ = fmt.Sscanf(got.Year[:4], "%d", &match.Year)
|
|
}
|
|
return match, nil
|
|
}
|
|
|
|
func (d *DoubanProvider) setHeaders(req *http.Request) {
|
|
req.Header.Set("User-Agent", userAgents[secureRandomIntn(len(userAgents))])
|
|
req.Header.Set("Referer", "https://movie.douban.com/")
|
|
req.Header.Set("Accept", "application/json, text/plain, */*")
|
|
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
|
if cookie := strings.TrimSpace(d.cfg.Secrets.DoubanCookie); cookie != "" {
|
|
req.Header.Set("Cookie", cookie)
|
|
}
|
|
}
|