mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 22:06:38 +08:00
优化
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user