mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
127 lines
3.6 KiB
Go
127 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// A TV library scrapes one media row per episode, and every row in the same
|
|
// show computes the same query candidate (the series folder title). Without a
|
|
// cache each episode re-issues the identical TMDb/Douban/Bangumi/TheTVDB
|
|
// search, so a 100-episode season costs 100x the network calls it needs.
|
|
//
|
|
// The cache lives on the ScraperService instance (not a package global) so that
|
|
// two services with different provider configuration never share results. TTL
|
|
// bounds staleness for out-of-band edits; explicit invalidation is unnecessary
|
|
// because the key includes the effective media kind, theatrical flag, query
|
|
// and year. Negative entries use a shorter TTL so a failed query is retried
|
|
// sooner without manual intervention.
|
|
const (
|
|
scrapeLookupCacheTTL = 10 * time.Minute
|
|
scrapeLookupNegativeCacheTTL = 2 * time.Minute
|
|
scrapeLookupCacheMaxItems = 1024
|
|
)
|
|
|
|
type scrapeLookupCache struct {
|
|
mu sync.Mutex
|
|
entries map[string]scrapeLookupCacheEntry
|
|
}
|
|
|
|
type scrapeLookupCacheEntry struct {
|
|
match *Match
|
|
expiresAt time.Time
|
|
}
|
|
|
|
func newScrapeLookupCache() *scrapeLookupCache {
|
|
return &scrapeLookupCache{entries: map[string]scrapeLookupCacheEntry{}}
|
|
}
|
|
|
|
func scrapeLookupCacheKey(kind, query string, year int, isTheatrical bool) string {
|
|
theatrical := "0"
|
|
if isTheatrical {
|
|
theatrical = "1"
|
|
}
|
|
return strings.ToLower(strings.TrimSpace(kind)) + "|" +
|
|
theatrical + "|" +
|
|
strconv.Itoa(year) + "|" +
|
|
strings.ToLower(strings.TrimSpace(query))
|
|
}
|
|
|
|
// get reports whether the key is cached. A hit may carry a nil match, which
|
|
// means the provider chain already ran and found nothing (negative cache).
|
|
// Use clearNegative before a user-triggered retry so stale negative entries
|
|
// do not suppress the fresh provider round-trip.
|
|
func (c *scrapeLookupCache) get(key string) (*Match, bool) {
|
|
if c == nil || key == "" {
|
|
return nil, false
|
|
}
|
|
now := time.Now()
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
item, ok := c.entries[key]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
if now.After(item.expiresAt) {
|
|
delete(c.entries, key)
|
|
return nil, false
|
|
}
|
|
return cloneMatch(item.match), true
|
|
}
|
|
|
|
func (c *scrapeLookupCache) set(key string, match *Match) {
|
|
if c == nil || key == "" {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(c.entries) >= scrapeLookupCacheMaxItems {
|
|
for k, item := range c.entries {
|
|
if now.After(item.expiresAt) || len(c.entries) >= scrapeLookupCacheMaxItems {
|
|
delete(c.entries, k)
|
|
}
|
|
if len(c.entries) < scrapeLookupCacheMaxItems {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
ttl := scrapeLookupCacheTTL
|
|
if match == nil {
|
|
ttl = scrapeLookupNegativeCacheTTL
|
|
}
|
|
c.entries[key] = scrapeLookupCacheEntry{match: cloneMatch(match), expiresAt: now.Add(ttl)}
|
|
}
|
|
|
|
// clearNegatives drops cached misses so a user-triggered retry gets a fresh
|
|
// provider round-trip instead of reusing a stale negative entry.
|
|
func (c *scrapeLookupCache) clearNegatives() {
|
|
if c == nil {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
for k, item := range c.entries {
|
|
if item.match == nil {
|
|
delete(c.entries, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// cloneMatch returns an independent copy so callers that mutate the result
|
|
// (localized title preference, local metadata merge, fanart artwork) cannot
|
|
// corrupt the cached entry or leak state between media rows.
|
|
func cloneMatch(m *Match) *Match {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
out := *m
|
|
out.Languages = append([]string(nil), m.Languages...)
|
|
out.Countries = append([]string(nil), m.Countries...)
|
|
out.Genres = append([]string(nil), m.Genres...)
|
|
out.Aliases = append([]string(nil), m.Aliases...)
|
|
return &out
|
|
}
|