fix: 使 TMDbProvider 支持从数据库读取 API Key

- 修改 TMDbProvider 添加 apiConfig *APIConfigService 字段
- NewTMDbProvider 接受 apiConfig 参数(可为 nil)
- 添加 resolveAPIKey(ctx) 方法:优先从 config 读取,回退到数据库
- 添加 resolveBaseURL(ctx) 方法:解析基础 URL
- 修改 SearchMovie 使用 resolveAPIKey / resolveBaseURL
- 修改 DiscoverService.Fetch 使用 resolveAPIKey / resolveBaseURL
- 调整 service.go 初始化顺序,确保 tmdb 在 scraper 之前初始化
- 构建验证通过
This commit is contained in:
ShukeBta
2026-05-18 01:57:48 +08:00
parent cdd28b8b88
commit 7fa99a5669
3 changed files with 83 additions and 23 deletions
+11 -3
View File
@@ -53,14 +53,22 @@ func (d *DiscoverService) fetch(ctx context.Context, path string) ([]Match, erro
// It paginates page=1 only — that's all the home page needs and it
// keeps us under TMDb's 50 rps limit.
func (d *DiscoverService) Fetch(ctx context.Context, path string) ([]Match, error) {
if d.tmdb == nil || !d.tmdb.Enabled() {
if d.tmdb == nil {
return nil, nil
}
// Resolve API key from config or database
apiKey := d.tmdb.resolveAPIKey(ctx)
if apiKey == "" {
return nil, nil
}
base := d.tmdb.resolveBaseURL(ctx)
q := url.Values{}
q.Set("api_key", d.tmdb.cfg.Secrets.TMDbAPIKey)
q.Set("api_key", apiKey)
q.Set("language", "zh-CN")
q.Set("page", "1")
u := d.tmdb.base + path + "?" + q.Encode()
u := base + path + "?" + q.Encode()
type result struct {
ID int `json:"id"`
+3 -3
View File
@@ -81,7 +81,9 @@ func New(cfg *config.Config, log *zap.Logger, repos *repository.Container) *Cont
go sseHub.Run()
probe := NewFFprobeService(cfg, log)
tmdb := NewTMDbProvider(cfg, log)
crypto := NewCryptoService(cfg.Secrets.JWTSecret, log)
apiConfig := NewAPIConfigService(log, repos, crypto)
tmdb := NewTMDbProvider(cfg, log, apiConfig)
bangumi := NewBangumiProvider(cfg, log)
thetvdb := NewTheTVDBProvider(cfg, log)
fanart := NewFanartProvider(cfg, log)
@@ -94,8 +96,6 @@ func New(cfg *config.Config, log *zap.Logger, repos *repository.Container) *Cont
watcher := NewWatcherService(log, repos, scanner)
nfo := NewNFOService(log, repos)
ai := NewAIService(cfg, log)
crypto := NewCryptoService(cfg.Secrets.JWTSecret, log)
apiConfig := NewAPIConfigService(log, repos, crypto)
duplicate := NewDuplicateService(log, repos, hub)
filemanager := NewFileManagerService(cfg, log, repos)
dlna := NewDLNAService(log)
+69 -17
View File
@@ -31,16 +31,19 @@ import (
// TMDbProvider talks to https://api.themoviedb.org/3.
type TMDbProvider struct {
cfg *config.Config
log *zap.Logger
client *http.Client
base string
imgCDN string
cfg *config.Config
log *zap.Logger
client *http.Client
base string
imgCDN string
apiConfig *APIConfigService
}
// NewTMDbProvider is the constructor. APIBase / image CDN can be overridden
// via secrets.tmdb_api_proxy + tmdb_image_proxy for users behind GFW.
func NewTMDbProvider(cfg *config.Config, log *zap.Logger) *TMDbProvider {
// apiConfig is optional; when non-nil, the provider will also check the
// api_configs table for TMDB API key.
func NewTMDbProvider(cfg *config.Config, log *zap.Logger, apiConfig *APIConfigService) *TMDbProvider {
base := cfg.Secrets.TMDbAPIProxy
if base == "" {
base = "https://api.themoviedb.org/3"
@@ -50,16 +53,61 @@ func NewTMDbProvider(cfg *config.Config, log *zap.Logger) *TMDbProvider {
img = "https://image.tmdb.org/t/p"
}
return &TMDbProvider{
cfg: cfg,
log: log,
base: base,
imgCDN: img,
client: &http.Client{Timeout: 15 * time.Second},
cfg: cfg,
log: log,
apiConfig: apiConfig,
base: base,
imgCDN: img,
client: &http.Client{Timeout: 15 * time.Second},
}
}
// Enabled reports whether the operator has supplied an API key.
func (t *TMDbProvider) Enabled() bool { return t.cfg.Secrets.TMDbAPIKey != "" }
// It checks both the config file and the database (via apiConfig).
func (t *TMDbProvider) Enabled() bool {
// Fast path: check config
if t.cfg.Secrets.TMDbAPIKey != "" {
return true
}
// Secondary check: if we have apiConfig, the key might be in the database
// We can't query the database here (no ctx), so we rely on the caller
// to check properly before making API calls.
// The actual key resolution happens in resolveAPIKey(ctx).
return t.apiConfig != nil
}
// resolveAPIKey returns the TMDb API key, checking config first, then database.
func (t *TMDbProvider) resolveAPIKey(ctx context.Context) string {
// Check config first (fast path)
if t.cfg.Secrets.TMDbAPIKey != "" {
return t.cfg.Secrets.TMDbAPIKey
}
// Fall back to database
if t.apiConfig != nil {
resolved, err := t.apiConfig.Resolve(ctx, "tmdb")
if err == nil && resolved.APIKey != "" {
return resolved.APIKey
}
}
return ""
}
// resolveBaseURL returns the TMDb base URL, checking config first, then database.
func (t *TMDbProvider) resolveBaseURL(ctx context.Context) string {
// Check config first
base := t.cfg.Secrets.TMDbAPIProxy
if base == "" {
base = "https://api.themoviedb.org/3"
}
// Override from database if available
if t.apiConfig != nil {
resolved, err := t.apiConfig.Resolve(ctx, "tmdb")
if err == nil && resolved.BaseURL != "" {
base = resolved.BaseURL
}
}
return base
}
// Match describes a successful metadata match. The same struct is reused
// across providers; provider-specific IDs sit side-by-side so the scraper
@@ -78,22 +126,26 @@ type Match struct {
// SearchMovie issues `/search/movie` and returns the best match, or nil
// when no result is found. The `year` argument is optional (0 = any).
func (t *TMDbProvider) SearchMovie(ctx context.Context, query string, year int) (*Match, error) {
if !t.Enabled() {
return nil, nil
}
if query == "" {
return nil, errors.New("empty query")
}
// Resolve API key from config or database
apiKey := t.resolveAPIKey(ctx)
if apiKey == "" {
return nil, nil
}
base := t.resolveBaseURL(ctx)
q := url.Values{}
q.Set("api_key", t.cfg.Secrets.TMDbAPIKey)
q.Set("api_key", apiKey)
q.Set("query", query)
q.Set("language", "zh-CN")
q.Set("include_adult", "false")
if year > 0 {
q.Set("year", fmt.Sprintf("%d", year))
}
u := t.base + "/search/movie?" + q.Encode()
u := base + "/search/movie?" + q.Encode()
type result struct {
ID int `json:"id"`