mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
3c2bfb14ca
Co-authored-by: Cursor <cursoragent@cursor.com>
256 lines
7.4 KiB
Go
256 lines
7.4 KiB
Go
package service
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type embySeriesCacheEntry struct {
|
|
group embySeriesGroup
|
|
expiresAt time.Time
|
|
}
|
|
|
|
type embySeasonCacheEntry struct {
|
|
season embySeasonGroup
|
|
expiresAt time.Time
|
|
}
|
|
|
|
type embyArtworkCacheEntry struct {
|
|
primary string
|
|
backdrop string
|
|
expiresAt time.Time
|
|
}
|
|
|
|
func (e *EmbyService) rememberSeriesGroup(group embySeriesGroup) {
|
|
if e == nil || strings.TrimSpace(group.ID) == "" {
|
|
return
|
|
}
|
|
expiresAt := time.Now().Add(embyVirtualCacheTTL)
|
|
e.virtualMu.Lock()
|
|
defer e.virtualMu.Unlock()
|
|
if e.virtualSeries == nil {
|
|
e.virtualSeries = make(map[string]embySeriesCacheEntry)
|
|
}
|
|
if e.virtualSeasons == nil {
|
|
e.virtualSeasons = make(map[string]embySeasonCacheEntry)
|
|
}
|
|
if e.virtualArtwork == nil {
|
|
e.virtualArtwork = make(map[string]embyArtworkCacheEntry)
|
|
}
|
|
e.trimVirtualCachesLocked(time.Now())
|
|
e.virtualSeries[group.ID] = embySeriesCacheEntry{group: group, expiresAt: expiresAt}
|
|
e.virtualArtwork[group.ID] = embyArtworkCacheEntry{primary: group.PosterURL, backdrop: group.BackdropURL, expiresAt: expiresAt}
|
|
e.virtualArtwork[group.ID+"-bd"] = embyArtworkCacheEntry{primary: group.PosterURL, backdrop: group.BackdropURL, expiresAt: expiresAt}
|
|
for _, season := range e.seasonsForSeries(group) {
|
|
e.virtualSeasons[season.ID] = embySeasonCacheEntry{season: season, expiresAt: expiresAt}
|
|
e.virtualArtwork[season.ID] = embyArtworkCacheEntry{primary: season.Series.PosterURL, backdrop: season.Series.BackdropURL, expiresAt: expiresAt}
|
|
e.virtualArtwork[season.ID+"-bd"] = embyArtworkCacheEntry{primary: season.Series.PosterURL, backdrop: season.Series.BackdropURL, expiresAt: expiresAt}
|
|
}
|
|
}
|
|
|
|
func (e *EmbyService) rememberSeasonGroup(season embySeasonGroup) {
|
|
if e == nil || strings.TrimSpace(season.ID) == "" {
|
|
return
|
|
}
|
|
expiresAt := time.Now().Add(embyVirtualCacheTTL)
|
|
e.virtualMu.Lock()
|
|
defer e.virtualMu.Unlock()
|
|
if e.virtualSeasons == nil {
|
|
e.virtualSeasons = make(map[string]embySeasonCacheEntry)
|
|
}
|
|
if e.virtualArtwork == nil {
|
|
e.virtualArtwork = make(map[string]embyArtworkCacheEntry)
|
|
}
|
|
e.trimVirtualCachesLocked(time.Now())
|
|
e.virtualSeasons[season.ID] = embySeasonCacheEntry{season: season, expiresAt: expiresAt}
|
|
e.virtualArtwork[season.ID] = embyArtworkCacheEntry{primary: season.Series.PosterURL, backdrop: season.Series.BackdropURL, expiresAt: expiresAt}
|
|
e.virtualArtwork[season.ID+"-bd"] = embyArtworkCacheEntry{primary: season.Series.PosterURL, backdrop: season.Series.BackdropURL, expiresAt: expiresAt}
|
|
}
|
|
|
|
func (e *EmbyService) cachedSeriesGroup(id string) (embySeriesGroup, bool) {
|
|
if e == nil || strings.TrimSpace(id) == "" {
|
|
return embySeriesGroup{}, false
|
|
}
|
|
now := time.Now()
|
|
e.virtualMu.RLock()
|
|
entry, ok := e.virtualSeries[id]
|
|
e.virtualMu.RUnlock()
|
|
if !ok || now.After(entry.expiresAt) {
|
|
if ok {
|
|
e.virtualMu.Lock()
|
|
delete(e.virtualSeries, id)
|
|
e.virtualMu.Unlock()
|
|
}
|
|
return embySeriesGroup{}, false
|
|
}
|
|
return entry.group, true
|
|
}
|
|
|
|
func (e *EmbyService) cachedSeasonGroup(id string) (embySeasonGroup, bool) {
|
|
if e == nil || strings.TrimSpace(id) == "" {
|
|
return embySeasonGroup{}, false
|
|
}
|
|
now := time.Now()
|
|
e.virtualMu.RLock()
|
|
entry, ok := e.virtualSeasons[id]
|
|
e.virtualMu.RUnlock()
|
|
if !ok || now.After(entry.expiresAt) {
|
|
if ok {
|
|
e.virtualMu.Lock()
|
|
delete(e.virtualSeasons, id)
|
|
e.virtualMu.Unlock()
|
|
}
|
|
return embySeasonGroup{}, false
|
|
}
|
|
return entry.season, true
|
|
}
|
|
|
|
func (e *EmbyService) cachedArtworkURL(id, imageType string) (string, bool) {
|
|
if e == nil || strings.TrimSpace(id) == "" {
|
|
return "", false
|
|
}
|
|
now := time.Now()
|
|
e.virtualMu.RLock()
|
|
entry, ok := e.virtualArtwork[id]
|
|
e.virtualMu.RUnlock()
|
|
if !ok || now.After(entry.expiresAt) {
|
|
if ok {
|
|
e.virtualMu.Lock()
|
|
delete(e.virtualArtwork, id)
|
|
e.virtualMu.Unlock()
|
|
}
|
|
return "", false
|
|
}
|
|
switch strings.ToLower(imageType) {
|
|
case "backdrop", "art":
|
|
if entry.backdrop != "" {
|
|
return entry.backdrop, true
|
|
}
|
|
}
|
|
if entry.primary != "" {
|
|
return entry.primary, true
|
|
}
|
|
return entry.backdrop, entry.backdrop != ""
|
|
}
|
|
|
|
// embyArtworkRef is the poster/backdrop pair stored next to a Latest payload
|
|
// so a JSON-cache hit can refill the in-memory artwork map before the client
|
|
// asks for the image.
|
|
type embyArtworkRef struct {
|
|
Primary string `json:"primary,omitempty"`
|
|
Backdrop string `json:"backdrop,omitempty"`
|
|
}
|
|
|
|
func (e *EmbyService) artworkRefsForSeriesGroups(groups []embySeriesGroup) map[string]embyArtworkRef {
|
|
if e == nil || len(groups) == 0 {
|
|
return nil
|
|
}
|
|
refs := make(map[string]embyArtworkRef, len(groups)*2)
|
|
for _, group := range groups {
|
|
if group.PosterURL == "" && group.BackdropURL == "" {
|
|
continue
|
|
}
|
|
ref := embyArtworkRef{Primary: group.PosterURL, Backdrop: group.BackdropURL}
|
|
refs[group.ID] = ref
|
|
for _, season := range e.seasonsForSeries(group) {
|
|
refs[season.ID] = ref
|
|
}
|
|
}
|
|
if len(refs) == 0 {
|
|
return nil
|
|
}
|
|
return refs
|
|
}
|
|
|
|
func (e *EmbyService) rememberArtworkRefs(refs map[string]embyArtworkRef) {
|
|
if e == nil || len(refs) == 0 {
|
|
return
|
|
}
|
|
expiresAt := time.Now().Add(embyVirtualCacheTTL)
|
|
e.virtualMu.Lock()
|
|
defer e.virtualMu.Unlock()
|
|
if e.virtualArtwork == nil {
|
|
e.virtualArtwork = make(map[string]embyArtworkCacheEntry, len(refs))
|
|
}
|
|
e.trimVirtualArtworkLocked(time.Now())
|
|
for id, ref := range refs {
|
|
if strings.TrimSpace(id) == "" || (ref.Primary == "" && ref.Backdrop == "") {
|
|
continue
|
|
}
|
|
e.virtualArtwork[id] = embyArtworkCacheEntry{primary: ref.Primary, backdrop: ref.Backdrop, expiresAt: expiresAt}
|
|
}
|
|
}
|
|
|
|
func (e *EmbyService) trimVirtualCachesLocked(now time.Time) {
|
|
e.trimVirtualSeriesLocked(now)
|
|
e.trimVirtualSeasonsLocked(now)
|
|
e.trimVirtualArtworkLocked(now)
|
|
}
|
|
|
|
func (e *EmbyService) trimVirtualSeriesLocked(now time.Time) {
|
|
for id, entry := range e.virtualSeries {
|
|
if now.After(entry.expiresAt) {
|
|
delete(e.virtualSeries, id)
|
|
}
|
|
}
|
|
evictOldest(e.virtualSeries, embyVirtualSeriesCap, func(entry embySeriesCacheEntry) time.Time {
|
|
return entry.expiresAt
|
|
})
|
|
}
|
|
|
|
func (e *EmbyService) trimVirtualSeasonsLocked(now time.Time) {
|
|
for id, entry := range e.virtualSeasons {
|
|
if now.After(entry.expiresAt) {
|
|
delete(e.virtualSeasons, id)
|
|
}
|
|
}
|
|
evictOldest(e.virtualSeasons, embyVirtualSeasonCap, func(entry embySeasonCacheEntry) time.Time {
|
|
return entry.expiresAt
|
|
})
|
|
}
|
|
|
|
func (e *EmbyService) trimVirtualArtworkLocked(now time.Time) {
|
|
for id, entry := range e.virtualArtwork {
|
|
if now.After(entry.expiresAt) {
|
|
delete(e.virtualArtwork, id)
|
|
}
|
|
}
|
|
evictOldest(e.virtualArtwork, embyVirtualArtworkCap, func(entry embyArtworkCacheEntry) time.Time {
|
|
return entry.expiresAt
|
|
})
|
|
}
|
|
|
|
// evictOldest drops the soonest-expiring entries until the map is under cap.
|
|
// It must not replace the map: a homepage refresh remembers many series at
|
|
// once, and wiping the whole cache made the just-advertised backdrops 404
|
|
// into a 1x1 placeholder.
|
|
func evictOldest[T any](items map[string]T, cap int, expiresAt func(T) time.Time) {
|
|
excess := len(items) - cap
|
|
if cap <= 0 || excess <= 0 {
|
|
return
|
|
}
|
|
type pair struct {
|
|
id string
|
|
at time.Time
|
|
}
|
|
ordered := make([]pair, 0, len(items))
|
|
for id, entry := range items {
|
|
ordered = append(ordered, pair{id: id, at: expiresAt(entry)})
|
|
}
|
|
sort.Slice(ordered, func(i, j int) bool { return ordered[i].at.Before(ordered[j].at) })
|
|
if excess > len(ordered) {
|
|
excess = len(ordered)
|
|
}
|
|
for i := 0; i < excess; i++ {
|
|
delete(items, ordered[i].id)
|
|
}
|
|
}
|
|
|
|
func embyVirtualImageTag(id, suffix string) string {
|
|
if strings.HasPrefix(id, embyVirtualSeriesPrefix) || strings.HasPrefix(id, embyVirtualSeasonPrefix) {
|
|
return id + suffix
|
|
}
|
|
return id
|
|
}
|