From 5a5555d28a8cc7999ec7d37e0cd0f43c9836bb0d Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:26:57 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=9C=E7=A8=8B=20Emby=20=E6=97=A0?= =?UTF-8?q?=E5=9B=BE=E6=9D=A1=E7=9B=AE=E4=B8=8D=E5=86=8D=E4=B8=8B=E5=8F=91?= =?UTF-8?q?=E6=97=A0=E6=95=88=E5=9B=BE=E7=89=87=20URL=EF=BC=88=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=E7=A0=B4=E5=9B=BE=E4=BF=AE=E5=A4=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 远程条目仅当 ImageTags 含 Primary/Backdrop 时才生成图片 URL; 无图条目 poster_url 为空,前端走 No Poster 优雅占位而非裂图 - 剧集海报回退使用 Emby 的 SeriesPrimaryImageTag(Fields=SeriesPrimaryImage), 仅当系列真实有图时才回退,避免连环无效请求 - 列表/剧集接口带 SeriesPrimaryImage 字段 --- internal/service/emby_remote_web.go | 58 +++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/internal/service/emby_remote_web.go b/internal/service/emby_remote_web.go index 1f6a55a..2101e3c 100644 --- a/internal/service/emby_remote_web.go +++ b/internal/service/emby_remote_web.go @@ -109,7 +109,8 @@ func (r *EmbyRemoteService) mapRemoteViewToLibrary(acct *model.StrmAccount, cfg } // MapRemoteItemToMedia 把远程 Emby item JSON 映射为本地 Media 结构。 -// poster/backdrop 填远程图片绝对地址(前端经 /api/img 同源代理拉取)。 +// poster/backdrop 只有在远程确实存在图片标签时才填 URL(避免对无图条目 +// 发出必失败的图片请求导致前端破图);剧集回退到系列海报(SeriesPrimaryImage)。 func (r *EmbyRemoteService) MapRemoteItemToMedia(ctx context.Context, acct *model.StrmAccount, cfg *EmbyRemoteConfig, item map[string]any) model.Media { remoteID := remoteItemString(item, "Id") // 条目可能已被 RewriteEmbyRemoteIDs 伪装(图片/嵌套 ID 需要原始远程 ID)。 @@ -128,11 +129,16 @@ func (r *EmbyRemoteService) MapRemoteItemToMedia(ctx context.Context, acct *mode Year: remoteItemInt(item, "ProductionYear"), Rating: float32(remoteItemFloat(item, "CommunityRating")), Path: remoteItemString(item, "Path"), - PosterURL: r.remoteItemImageURL(cfg, remoteID, "Primary"), - BackdropURL: r.remoteItemImageURL(cfg, remoteID, "Backdrop"), Genres: remoteItemGenres(item), ScrapeStatus: "done", } + // 只有远程明确存在图片标签才下发图片 URL。 + if remoteItemHasImageTag(item, "Primary") { + media.PosterURL = r.remoteItemImageURL(cfg, remoteID, "Primary") + } + if remoteItemHasImageTag(item, "Backdrop") || len(remoteBackdropTags(item)) > 0 { + media.BackdropURL = r.remoteItemImageURL(cfg, remoteID, "Backdrop") + } if ticks := remoteItemInt64(item, "RunTimeTicks"); ticks > 0 { media.DurationSec = int(ticks / 10_000_000) } @@ -158,8 +164,10 @@ func (r *EmbyRemoteService) MapRemoteItemToMedia(ctx context.Context, acct *mode if seriesName := remoteItemString(item, "SeriesName"); seriesName != "" { media.Title = seriesName } - // Emby 单集通常无独立海报:回退到系列海报,避免网页端空图。 - if media.PosterURL == "" && seriesID != "" { + // 单集通常无独立海报:若远程返回 SeriesPrimaryImageTag(需要 + // Fields=SeriesPrimaryImage)且系列有图,则回退到系列海报。 + if media.PosterURL == "" && seriesID != "" && + strings.TrimSpace(remoteItemString(item, "SeriesPrimaryImageTag")) != "" { media.PosterURL = r.remoteItemImageURL(cfg, seriesID, "Primary") } default: // Movie / Series / Season / Folder @@ -184,7 +192,7 @@ func (r *EmbyRemoteService) RemoteLibraryMedia(ctx context.Context, acct *model. q.Set("Recursive", "false") q.Set("StartIndex", strconv.Itoa(offset)) q.Set("Limit", strconv.Itoa(limit)) - q.Set("Fields", "Overview,Genres,ProviderIds,Path") + q.Set("Fields", "Overview,Genres,ProviderIds,Path,SeriesPrimaryImage") var body struct { Items []map[string]any `json:"Items"` TotalRecordCount int64 `json:"TotalRecordCount"` @@ -268,7 +276,7 @@ func (r *EmbyRemoteService) remoteEpisodesOf(ctx context.Context, acct *model.St q.Set("Recursive", "true") q.Set("StartIndex", "0") q.Set("Limit", "500") - q.Set("Fields", "Overview,Genres,ProviderIds,Path") + q.Set("Fields", "Overview,Genres,ProviderIds,Path,SeriesPrimaryImage") var body struct { Items []map[string]any `json:"Items"` TotalRecordCount int64 `json:"TotalRecordCount"` @@ -297,7 +305,7 @@ func (r *EmbyRemoteService) RemoteSeriesCards(ctx context.Context, acct *model.S q.Set("Recursive", "false") q.Set("StartIndex", "0") q.Set("Limit", "1000") - q.Set("Fields", "Overview,Genres,ProviderIds,Path,RecursiveItemCount") + q.Set("Fields", "Overview,Genres,ProviderIds,Path,RecursiveItemCount,SeriesPrimaryImage") var body struct { Items []map[string]any `json:"Items"` } @@ -485,6 +493,40 @@ func (r *EmbyRemoteService) remoteItemImageURL(cfg *EmbyRemoteConfig, remoteID, "?api_key=" + url.QueryEscape(cfg.Token) } +// remoteItemHasImageTag 远程 item 是否带某类型图片标签(Emby 的 ImageTags map)。 +func remoteItemHasImageTag(item map[string]any, typ string) bool { + if item == nil { + return false + } + switch tags := item["ImageTags"].(type) { + case map[string]any: + _, ok := tags[typ] + return ok + case map[string]string: + _, ok := tags[typ] + return ok + } + return false +} + +// remoteBackdropTags 远程 item 的 BackdropImageTags 数组。 +func remoteBackdropTags(item map[string]any) []any { + if item == nil { + return nil + } + switch tags := item["BackdropImageTags"].(type) { + case []any: + return tags + case []string: + out := make([]any, 0, len(tags)) + for _, s := range tags { + out = append(out, s) + } + return out + } + return nil +} + // remoteItemTypeOf 从映射后的 Media 推断远程类型(无详情载荷时兜底)。 func remoteItemTypeOf(m *model.Media) string { if m == nil {