Files
truewhile b855e00345 优化
2026-09-10 16:58:41 +08:00

137 lines
3.2 KiB
Go

package service
import (
"sync"
"time"
"go.uber.org/zap"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
// ScraperService coordinates metadata enrichment across providers.
type ScraperService struct {
cfg *config.Config
log *zap.Logger
repo *repository.Container
tmdb *TMDbProvider
bangumi *BangumiProvider
thetvdb *TheTVDBProvider
douban *DoubanProvider
fanart *FanartProvider
adult *AdultProvider
hub *Hub
cache *RuntimeCacheService
images *ImageProxy
// Caches provider-chain results keyed by media kind/query/year. Per-instance
// so services with different provider config never share results.
lookupCache *scrapeLookupCache
// Serializes final sidecar replacement. Windows cannot rename over an
// existing file, and concurrent scrapes can target the same sidecar.
artworkWriteMu sync.Mutex
}
// NewScraperService is the constructor.
func NewScraperService(
cfg *config.Config,
log *zap.Logger,
repo *repository.Container,
tmdb *TMDbProvider,
bangumi *BangumiProvider,
thetvdb *TheTVDBProvider,
fanart *FanartProvider,
hub *Hub,
adult ...*AdultProvider,
) *ScraperService {
var adultProvider *AdultProvider
if len(adult) > 0 {
adultProvider = adult[0]
}
return &ScraperService{
cfg: cfg, log: log, repo: repo,
tmdb: tmdb, bangumi: bangumi, thetvdb: thetvdb, fanart: fanart, adult: adultProvider, hub: hub,
lookupCache: newScrapeLookupCache(),
}
}
func (s *ScraperService) SetDouban(douban *DoubanProvider) {
s.douban = douban
}
func (s *ScraperService) SetRuntimeCache(cache *RuntimeCacheService) *ScraperService {
if s != nil {
s.cache = cache
}
return s
}
func (s *ScraperService) SetImageProxy(images *ImageProxy) *ScraperService {
if s != nil {
s.images = images
}
return s
}
var tmdbDetailsTimeout = 8 * time.Second
const (
defaultScrapeDelayMinMS = 250
defaultScrapeDelayMaxMS = 500
maxScrapeDelayMS = 5 * 60 * 1000
)
// AnyEnabled reports whether at least one provider can run.
func (s *ScraperService) AnyEnabled() bool {
if s.tmdb != nil && s.tmdb.Enabled() {
return true
}
if s.bangumi != nil && s.bangumi.Enabled() {
return true
}
if s.thetvdb != nil && s.thetvdb.Enabled() {
return true
}
if s.adult != nil && s.adult.Enabled() {
return true
}
if s.douban != nil && s.douban.Enabled() {
return true
}
return false
}
// determineMediaType returns "tv" for TV shows and "movie" for movies.
// It uses the library type as the primary signal.
func (s *ScraperService) determineMediaType(lib *model.Library, match *Match) string {
return s.determineMediaTypeForMedia(lib, nil, match)
}
func (s *ScraperService) determineMediaTypeForMedia(lib *model.Library, media *model.Media, match *Match) string {
if match != nil {
switch normalizeOrganizeMediaType(match.MediaType) {
case "tv", "anime", "variety":
return "tv"
case "movie", "adult":
return "movie"
}
}
if media != nil && mediaIsEpisodic(media, lib) {
return "tv"
}
if lib != nil {
switch lib.Type {
case "tv", "anime", "variety", "show", "shows":
return "tv"
}
}
// Fallback: if Bangumi ID is present, treat as TV/anime.
if match != nil && match.BangumiID > 0 {
return "tv"
}
return "movie"
}