mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 04:26:38 +08:00
修复 Emby 大海报在剧集图缓存被淘汰后显示占位图
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,17 +32,14 @@ func (e *EmbyService) ImageURL(ctx context.Context, id, imageType string) (strin
|
|||||||
}
|
}
|
||||||
return backdrop
|
return backdrop
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(id, embyVirtualSeasonPrefix) {
|
if strings.HasPrefix(id, embyVirtualSeasonPrefix) || strings.HasPrefix(id, embyVirtualSeriesPrefix) {
|
||||||
if raw, ok := e.cachedArtworkURL(id, imageType); ok {
|
if raw, ok := e.cachedArtworkURL(id, imageType); ok {
|
||||||
return raw, nil
|
return raw, nil
|
||||||
}
|
}
|
||||||
return "", nil
|
// Latest JSON can outlive (or be served after) the in-memory artwork
|
||||||
}
|
// map. Rebuild from the library instead of handing the client a 1x1
|
||||||
if strings.HasPrefix(id, embyVirtualSeriesPrefix) {
|
// placeholder that it then caches as a successful image.
|
||||||
if raw, ok := e.cachedArtworkURL(id, imageType); ok {
|
return e.resolveVirtualArtwork(ctx, id, imageType, pick)
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
}
|
||||||
m, err := e.repo.Media.FindByID(ctx, id)
|
m, err := e.repo.Media.FindByID(ctx, id)
|
||||||
if err == nil && m != nil {
|
if err == nil && m != nil {
|
||||||
@@ -71,6 +68,21 @@ func (e *EmbyService) ImageURL(ctx context.Context, id, imageType string) (strin
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *EmbyService) resolveVirtualArtwork(ctx context.Context, id, imageType string, pick func(primary, backdrop string) string) (string, error) {
|
||||||
|
if strings.HasPrefix(id, embyVirtualSeasonPrefix) {
|
||||||
|
season, ok, err := e.findSeasonGroup(ctx, id, "")
|
||||||
|
if err != nil || !ok {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return pick(season.Series.PosterURL, season.Series.BackdropURL), nil
|
||||||
|
}
|
||||||
|
series, ok, err := e.findSeriesGroup(ctx, id, "")
|
||||||
|
if err != nil || !ok {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return pick(series.PosterURL, series.BackdropURL), nil
|
||||||
|
}
|
||||||
|
|
||||||
// imageInfoTypes 是 GET /Items/{Id}/Images 会报告的图片类型。只列 MeBox
|
// imageInfoTypes 是 GET /Items/{Id}/Images 会报告的图片类型。只列 MeBox
|
||||||
// 真正存储的两类:ImageURL 对 Thumb / Logo / Banner 等其余类型会回退到
|
// 真正存储的两类:ImageURL 对 Thumb / Logo / Banner 等其余类型会回退到
|
||||||
// 主图,若一并列出会让客户端以为存在这些图并去请求,实际拿到的却是主图。
|
// 主图,若一并列出会让客户端以为存在这些图并去请求,实际拿到的却是主图。
|
||||||
|
|||||||
@@ -117,6 +117,18 @@ const (
|
|||||||
embyVirtualCacheTTL = 10 * time.Minute
|
embyVirtualCacheTTL = 10 * time.Minute
|
||||||
embyVisibilityCacheTTL = 30 * time.Second
|
embyVisibilityCacheTTL = 30 * time.Second
|
||||||
embySeriesGroupingLimit = maxMediaSearchLimit
|
embySeriesGroupingLimit = maxMediaSearchLimit
|
||||||
|
// Virtual artwork used to be wiped entirely once the in-memory map crossed
|
||||||
|
// a few thousand entries. A homepage refresh asks Latest for every library
|
||||||
|
// at once, so that wipe dropped the series the client was about to paint.
|
||||||
|
// Caps are sized for that fan-out; overflow evicts the oldest entries only.
|
||||||
|
embyVirtualSeriesCap = 8000
|
||||||
|
embyVirtualSeasonCap = 16000
|
||||||
|
embyVirtualArtworkCap = 24000
|
||||||
|
// Clients that already cached the 1x1 placeholder treat a stable tag as
|
||||||
|
// immutable. Virtual ids are the ones that served that placeholder, so
|
||||||
|
// only those tags get a suffix that forces a refetch.
|
||||||
|
embyVirtualPrimaryTagSuffix = "-p2"
|
||||||
|
embyVirtualBackdropTagSuffix = "-bd2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ type embyItemsCacheValue struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type embyLatestCacheValue struct {
|
type embyLatestCacheValue struct {
|
||||||
Items []map[string]any `json:"items"`
|
Items []map[string]any `json:"items"`
|
||||||
|
Artwork map[string]embyArtworkRef `json:"artwork,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EmbyService) embyItemsCacheKey(kind string, p ItemsParams) string {
|
func (e *EmbyService) embyItemsCacheKey(kind string, p ItemsParams) string {
|
||||||
@@ -43,7 +44,8 @@ func (e *EmbyService) embyItemsCacheKey(kind string, p ItemsParams) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *EmbyService) embyLatestCacheKey(userID, parentID string, limit int) string {
|
func (e *EmbyService) embyLatestCacheKey(userID, parentID string, limit int) string {
|
||||||
sum := sha256.Sum256([]byte(strings.Join([]string{"latest", userID, parentID, strconv.Itoa(limit)}, "|")))
|
// v2: payload tags for virtual artwork changed so clients drop cached placeholders.
|
||||||
|
sum := sha256.Sum256([]byte(strings.Join([]string{"latest-v2", userID, parentID, strconv.Itoa(limit)}, "|")))
|
||||||
return "media:emby:" + hex.EncodeToString(sum[:])
|
return "media:emby:" + hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,15 +132,16 @@ func (e *EmbyService) LatestItems(ctx context.Context, userID, parentID string,
|
|||||||
cacheKey := e.embyLatestCacheKey(userID, parentID, limit)
|
cacheKey := e.embyLatestCacheKey(userID, parentID, limit)
|
||||||
var cached embyLatestCacheValue
|
var cached embyLatestCacheValue
|
||||||
if e.cache != nil && e.cache.GetJSON(ctx, cacheKey, &cached) {
|
if e.cache != nil && e.cache.GetJSON(ctx, cacheKey, &cached) {
|
||||||
|
e.rememberArtworkRefs(cached.Artwork)
|
||||||
return cached.Items, nil
|
return cached.Items, nil
|
||||||
}
|
}
|
||||||
q := e.repo.DB.WithContext(ctx).Model(&model.Media{}).Where("deleted_at IS NULL")
|
q := e.repo.DB.WithContext(ctx).Model(&model.Media{}).Where("deleted_at IS NULL")
|
||||||
q = e.applyUserMediaVisibility(ctx, q, userID)
|
q = e.applyUserMediaVisibility(ctx, q, userID)
|
||||||
if parentID != "" {
|
if parentID != "" {
|
||||||
if episodic, err := e.libraryIsEpisodic(ctx, parentID); err == nil && episodic {
|
if episodic, err := e.libraryIsEpisodic(ctx, parentID); err == nil && episodic {
|
||||||
out, err := e.latestSeriesItemsForLibrary(ctx, userID, parentID, limit)
|
out, artwork, err := e.latestSeriesItemsForLibrary(ctx, userID, parentID, limit)
|
||||||
if err == nil && e.cache != nil {
|
if err == nil && e.cache != nil {
|
||||||
e.cache.SetJSON(ctx, cacheKey, embyLatestCacheValue{Items: out}, time.Duration(e.embyLatestCacheTTLSeconds())*time.Second)
|
e.cache.SetJSON(ctx, cacheKey, embyLatestCacheValue{Items: out, Artwork: artwork}, time.Duration(e.embyLatestCacheTTLSeconds())*time.Second)
|
||||||
}
|
}
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
@@ -171,7 +172,7 @@ func (e *EmbyService) LatestItems(ctx context.Context, userID, parentID string,
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EmbyService) latestSeriesItemsForLibrary(ctx context.Context, userID, libraryID string, limit int) ([]map[string]any, error) {
|
func (e *EmbyService) latestSeriesItemsForLibrary(ctx context.Context, userID, libraryID string, limit int) ([]map[string]any, map[string]embyArtworkRef, error) {
|
||||||
if limit <= 0 || limit > 100 {
|
if limit <= 0 || limit > 100 {
|
||||||
limit = 20
|
limit = 20
|
||||||
}
|
}
|
||||||
@@ -180,7 +181,7 @@ func (e *EmbyService) latestSeriesItemsForLibrary(ctx context.Context, userID, l
|
|||||||
q = e.applyUserMediaVisibility(ctx, q, userID)
|
q = e.applyUserMediaVisibility(ctx, q, userID)
|
||||||
var rows []model.Media
|
var rows []model.Media
|
||||||
if err := q.Order(mediaReleaseOrderSQL(true)).Limit(embySeriesGroupingLimit).Find(&rows).Error; err != nil {
|
if err := q.Order(mediaReleaseOrderSQL(true)).Limit(embySeriesGroupingLimit).Find(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
groups := e.seriesGroupsFromMedia(ctx, rows)
|
groups := e.seriesGroupsFromMedia(ctx, rows)
|
||||||
sortSeriesGroups(groups, ItemsParams{SortBy: "premieredate", SortOrder: "Descending"})
|
sortSeriesGroups(groups, ItemsParams{SortBy: "premieredate", SortOrder: "Descending"})
|
||||||
@@ -191,7 +192,7 @@ func (e *EmbyService) latestSeriesItemsForLibrary(ctx context.Context, userID, l
|
|||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
items = append(items, e.seriesPayload(group))
|
items = append(items, e.seriesPayload(group))
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, e.artworkRefsForSeriesGroups(groups), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResumeItems 列出有未完成播放进度的媒体。
|
// ResumeItems 列出有未完成播放进度的媒体。
|
||||||
@@ -519,11 +520,11 @@ func (e *EmbyService) itemPayload(ctx context.Context, m *model.Media, fav bool,
|
|||||||
if seriesID != "" {
|
if seriesID != "" {
|
||||||
if sEntry, ok, _ := e.payloadSeriesEntry(ctx, seriesID); ok {
|
if sEntry, ok, _ := e.payloadSeriesEntry(ctx, seriesID); ok {
|
||||||
if sEntry.posterURL != "" {
|
if sEntry.posterURL != "" {
|
||||||
item["SeriesPrimaryImageTag"] = seriesID
|
item["SeriesPrimaryImageTag"] = embyVirtualImageTag(seriesID, embyVirtualPrimaryTagSuffix)
|
||||||
}
|
}
|
||||||
if len(backdropTags) == 0 && sEntry.backdropURL != "" {
|
if len(backdropTags) == 0 && (sEntry.backdropURL != "" || sEntry.posterURL != "") {
|
||||||
item["ParentBackdropItemId"] = seriesID
|
item["ParentBackdropItemId"] = seriesID
|
||||||
item["ParentBackdropImageTags"] = []string{seriesID + "-bd"}
|
item["ParentBackdropImageTags"] = []string{embyVirtualImageTag(seriesID, embyVirtualBackdropTagSuffix)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -37,11 +38,7 @@ func (e *EmbyService) rememberSeriesGroup(group embySeriesGroup) {
|
|||||||
if e.virtualArtwork == nil {
|
if e.virtualArtwork == nil {
|
||||||
e.virtualArtwork = make(map[string]embyArtworkCacheEntry)
|
e.virtualArtwork = make(map[string]embyArtworkCacheEntry)
|
||||||
}
|
}
|
||||||
if len(e.virtualSeries) > 2000 || len(e.virtualSeasons) > 5000 || len(e.virtualArtwork) > 7000 {
|
e.trimVirtualCachesLocked(time.Now())
|
||||||
e.virtualSeries = make(map[string]embySeriesCacheEntry)
|
|
||||||
e.virtualSeasons = make(map[string]embySeasonCacheEntry)
|
|
||||||
e.virtualArtwork = make(map[string]embyArtworkCacheEntry)
|
|
||||||
}
|
|
||||||
e.virtualSeries[group.ID] = embySeriesCacheEntry{group: group, expiresAt: expiresAt}
|
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] = embyArtworkCacheEntry{primary: group.PosterURL, backdrop: group.BackdropURL, expiresAt: expiresAt}
|
||||||
e.virtualArtwork[group.ID+"-bd"] = embyArtworkCacheEntry{primary: group.PosterURL, backdrop: group.BackdropURL, expiresAt: expiresAt}
|
e.virtualArtwork[group.ID+"-bd"] = embyArtworkCacheEntry{primary: group.PosterURL, backdrop: group.BackdropURL, expiresAt: expiresAt}
|
||||||
@@ -65,6 +62,7 @@ func (e *EmbyService) rememberSeasonGroup(season embySeasonGroup) {
|
|||||||
if e.virtualArtwork == nil {
|
if e.virtualArtwork == nil {
|
||||||
e.virtualArtwork = make(map[string]embyArtworkCacheEntry)
|
e.virtualArtwork = make(map[string]embyArtworkCacheEntry)
|
||||||
}
|
}
|
||||||
|
e.trimVirtualCachesLocked(time.Now())
|
||||||
e.virtualSeasons[season.ID] = embySeasonCacheEntry{season: season, expiresAt: expiresAt}
|
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] = 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}
|
e.virtualArtwork[season.ID+"-bd"] = embyArtworkCacheEntry{primary: season.Series.PosterURL, backdrop: season.Series.BackdropURL, expiresAt: expiresAt}
|
||||||
@@ -135,3 +133,123 @@ func (e *EmbyService) cachedArtworkURL(id, imageType string) (string, bool) {
|
|||||||
}
|
}
|
||||||
return entry.backdrop, entry.backdrop != ""
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -296,6 +296,71 @@ func TestEmbyVirtualSeriesArtworkUsesListCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEmbyVirtualSeriesArtworkRebuildsAfterMemoryDrop(t *testing.T) {
|
||||||
|
svc := newTestEmbyService(t)
|
||||||
|
svc.cache = NewRuntimeCacheService(nil, nil)
|
||||||
|
lib := model.Library{Name: "番剧", Path: `/media/anime`, Type: "anime", Enabled: true}
|
||||||
|
if err := svc.repo.Library.Create(t.Context(), &lib); err != nil {
|
||||||
|
t.Fatalf("create library: %v", err)
|
||||||
|
}
|
||||||
|
media := model.Media{
|
||||||
|
Base: model.Base{ID: "ep-hero"},
|
||||||
|
LibraryID: lib.ID,
|
||||||
|
Title: "树海之魔",
|
||||||
|
Path: `/media/anime/树海之魔/Season 01/树海之魔 - S01E01.mkv`,
|
||||||
|
PosterURL: `/poster.jpg`,
|
||||||
|
BackdropURL: `/backdrop.jpg`,
|
||||||
|
SeasonNum: 1,
|
||||||
|
EpisodeNum: 1,
|
||||||
|
}
|
||||||
|
if err := svc.repo.DB.Create(&media).Error; err != nil {
|
||||||
|
t.Fatalf("create media: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := svc.LatestItems(t.Context(), "", lib.ID, 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("latest items: %v", err)
|
||||||
|
}
|
||||||
|
if len(items) != 1 {
|
||||||
|
t.Fatalf("latest len = %d, want 1", len(items))
|
||||||
|
}
|
||||||
|
seriesID, _ := items[0]["Id"].(string)
|
||||||
|
tags, _ := items[0]["BackdropImageTags"].([]string)
|
||||||
|
if seriesID == "" || len(tags) != 1 || tags[0] != seriesID+embyVirtualBackdropTagSuffix {
|
||||||
|
t.Fatalf("hero item should advertise a cache-busted backdrop tag, got id=%q tags=%#v", seriesID, items[0]["BackdropImageTags"])
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.virtualMu.Lock()
|
||||||
|
svc.virtualArtwork = nil
|
||||||
|
svc.virtualSeries = nil
|
||||||
|
svc.virtualSeasons = nil
|
||||||
|
svc.virtualMu.Unlock()
|
||||||
|
|
||||||
|
backdrop, err := svc.ImageURL(t.Context(), seriesID, "Backdrop")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("backdrop after memory drop: %v", err)
|
||||||
|
}
|
||||||
|
if backdrop != "/backdrop.jpg" {
|
||||||
|
t.Fatalf("backdrop = %q, want rebuilt backdrop", backdrop)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.virtualMu.Lock()
|
||||||
|
svc.virtualArtwork = nil
|
||||||
|
svc.virtualMu.Unlock()
|
||||||
|
if _, err := svc.LatestItems(t.Context(), "", lib.ID, 5); err != nil {
|
||||||
|
t.Fatalf("cached latest: %v", err)
|
||||||
|
}
|
||||||
|
cancelled, cancel := context.WithCancel(t.Context())
|
||||||
|
cancel()
|
||||||
|
backdrop, err = svc.ImageURL(cancelled, seriesID, "Backdrop")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("backdrop from rewarmed cache: %v", err)
|
||||||
|
}
|
||||||
|
if backdrop != "/backdrop.jpg" {
|
||||||
|
t.Fatalf("rewarmed backdrop = %q, want cached backdrop", backdrop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEmbyCloudAnimeUsesSeriesNameFromChineseSeasonFolder(t *testing.T) {
|
func TestEmbyCloudAnimeUsesSeriesNameFromChineseSeasonFolder(t *testing.T) {
|
||||||
svc := newTestEmbyService(t)
|
svc := newTestEmbyService(t)
|
||||||
lib := model.Library{Name: "OpenList · 国漫", Path: `cloud://openlist/国漫`, Type: "anime", Enabled: true}
|
lib := model.Library{Name: "OpenList · 国漫", Path: `cloud://openlist/国漫`, Type: "anime", Enabled: true}
|
||||||
@@ -429,13 +494,13 @@ func TestInferSeriesNameFromPath(t *testing.T) {
|
|||||||
want: "间谍过家家",
|
want: "间谍过家家",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
got := inferSeriesNameFromPath(tc.path)
|
got := inferSeriesNameFromPath(tc.path)
|
||||||
if got != tc.want {
|
if got != tc.want {
|
||||||
t.Errorf("inferSeriesNameFromPath(%q) = %q, want %q", tc.path, got, tc.want)
|
t.Errorf("inferSeriesNameFromPath(%q) = %q, want %q", tc.path, got, tc.want)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEmbySeriesSortByDateLastMediaAdded(t *testing.T) {
|
func TestEmbySeriesSortByDateLastMediaAdded(t *testing.T) {
|
||||||
svc := newTestEmbyService(t)
|
svc := newTestEmbyService(t)
|
||||||
|
|||||||
@@ -5,28 +5,28 @@ func (e *EmbyService) seriesPayload(group embySeriesGroup) map[string]any {
|
|||||||
imageTags := map[string]string{}
|
imageTags := map[string]string{}
|
||||||
backdropTags := []string{}
|
backdropTags := []string{}
|
||||||
if group.PosterURL != "" {
|
if group.PosterURL != "" {
|
||||||
imageTags["Primary"] = group.ID
|
imageTags["Primary"] = embyVirtualImageTag(group.ID, embyVirtualPrimaryTagSuffix)
|
||||||
}
|
}
|
||||||
if group.BackdropURL != "" {
|
if group.BackdropURL != "" || group.PosterURL != "" {
|
||||||
backdropTags = append(backdropTags, group.ID+"-bd")
|
backdropTags = append(backdropTags, embyVirtualImageTag(group.ID, embyVirtualBackdropTagSuffix))
|
||||||
}
|
}
|
||||||
lastMediaAdded := group.DateLastMediaAdded
|
lastMediaAdded := group.DateLastMediaAdded
|
||||||
if lastMediaAdded.IsZero() {
|
if lastMediaAdded.IsZero() {
|
||||||
lastMediaAdded = group.CreatedAt
|
lastMediaAdded = group.CreatedAt
|
||||||
}
|
}
|
||||||
item := map[string]any{
|
item := map[string]any{
|
||||||
"Id": group.ID,
|
"Id": group.ID,
|
||||||
"Name": group.Name,
|
"Name": group.Name,
|
||||||
"ServerId": embyServerID,
|
"ServerId": embyServerID,
|
||||||
"Type": "Series",
|
"Type": "Series",
|
||||||
"MediaType": "Video",
|
"MediaType": "Video",
|
||||||
"IsFolder": true,
|
"IsFolder": true,
|
||||||
"ParentId": group.LibraryID,
|
"ParentId": group.LibraryID,
|
||||||
"ProductionYear": group.Year,
|
"ProductionYear": group.Year,
|
||||||
"Overview": group.Overview,
|
"Overview": group.Overview,
|
||||||
"CommunityRating": group.Rating,
|
"CommunityRating": group.Rating,
|
||||||
"RecursiveItemCount": len(group.Episodes),
|
"RecursiveItemCount": len(group.Episodes),
|
||||||
"ChildCount": len(e.seasonsForSeries(group)),
|
"ChildCount": len(e.seasonsForSeries(group)),
|
||||||
"DateCreated": group.CreatedAt,
|
"DateCreated": group.CreatedAt,
|
||||||
"DateLastMediaAdded": lastMediaAdded,
|
"DateLastMediaAdded": lastMediaAdded,
|
||||||
"ImageTags": imageTags,
|
"ImageTags": imageTags,
|
||||||
@@ -39,7 +39,7 @@ func (e *EmbyService) seriesPayload(group embySeriesGroup) map[string]any {
|
|||||||
"UserData": emptyUserData(),
|
"UserData": emptyUserData(),
|
||||||
}
|
}
|
||||||
if group.PosterURL != "" {
|
if group.PosterURL != "" {
|
||||||
item["PrimaryImageTag"] = group.ID
|
item["PrimaryImageTag"] = embyVirtualImageTag(group.ID, embyVirtualPrimaryTagSuffix)
|
||||||
}
|
}
|
||||||
if premiered, ok := embyPremiereDate(group.ReleaseDate); ok {
|
if premiered, ok := embyPremiereDate(group.ReleaseDate); ok {
|
||||||
item["PremiereDate"] = premiered
|
item["PremiereDate"] = premiered
|
||||||
@@ -52,10 +52,10 @@ func (e *EmbyService) seasonPayload(season embySeasonGroup) map[string]any {
|
|||||||
imageTags := map[string]string{}
|
imageTags := map[string]string{}
|
||||||
backdropTags := []string{}
|
backdropTags := []string{}
|
||||||
if season.Series.PosterURL != "" {
|
if season.Series.PosterURL != "" {
|
||||||
imageTags["Primary"] = season.ID
|
imageTags["Primary"] = embyVirtualImageTag(season.ID, embyVirtualPrimaryTagSuffix)
|
||||||
}
|
}
|
||||||
if season.Series.BackdropURL != "" {
|
if season.Series.BackdropURL != "" || season.Series.PosterURL != "" {
|
||||||
backdropTags = append(backdropTags, season.ID+"-bd")
|
backdropTags = append(backdropTags, embyVirtualImageTag(season.ID, embyVirtualBackdropTagSuffix))
|
||||||
}
|
}
|
||||||
item := map[string]any{
|
item := map[string]any{
|
||||||
"Id": season.ID,
|
"Id": season.ID,
|
||||||
@@ -75,12 +75,12 @@ func (e *EmbyService) seasonPayload(season embySeasonGroup) map[string]any {
|
|||||||
"UserData": emptyUserData(),
|
"UserData": emptyUserData(),
|
||||||
}
|
}
|
||||||
if season.Series.PosterURL != "" {
|
if season.Series.PosterURL != "" {
|
||||||
item["PrimaryImageTag"] = season.ID
|
item["PrimaryImageTag"] = embyVirtualImageTag(season.ID, embyVirtualPrimaryTagSuffix)
|
||||||
item["SeriesPrimaryImageTag"] = season.Series.ID
|
item["SeriesPrimaryImageTag"] = embyVirtualImageTag(season.Series.ID, embyVirtualPrimaryTagSuffix)
|
||||||
}
|
}
|
||||||
if season.Series.BackdropURL != "" {
|
if season.Series.BackdropURL != "" || season.Series.PosterURL != "" {
|
||||||
item["ParentBackdropItemId"] = season.Series.ID
|
item["ParentBackdropItemId"] = season.Series.ID
|
||||||
item["ParentBackdropImageTags"] = []string{season.Series.ID + "-bd"}
|
item["ParentBackdropImageTags"] = []string{embyVirtualImageTag(season.Series.ID, embyVirtualBackdropTagSuffix)}
|
||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user