fix(catalog): clamp still/poster/logo backdrops to largest cached variant
Episode stills used as backdrops only exist at w500/w300 in the cache, so requesting a w1280/w1920 backdrop width 404s. Add catalog.BackdropVariantPath + imageTypeFromCachedPath and route featured (w1920) and Continue Watching / Next Up (w1280) backdrops through it; still/poster/logo paths clamp to their type's largest cached variant while real backdrops keep the requested width. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -996,16 +996,11 @@ func featuredPosterPath(path string) string {
|
||||
}
|
||||
|
||||
// featuredBackdropPath converts an S3 backdrop path from original to w1920 for
|
||||
// featured/hero contexts (displayed at full viewport width).
|
||||
// Full URLs (TMDB/TVDB) and plugin-prefixed paths are returned as-is.
|
||||
// featured/hero contexts (displayed at full viewport width). Episode stills
|
||||
// used as backdrops lack a w1920 variant and clamp to their largest cached
|
||||
// size. Full URLs (TMDB/TVDB) and plugin-prefixed paths are returned as-is.
|
||||
func featuredBackdropPath(path string) string {
|
||||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||
return path
|
||||
}
|
||||
if strings.Contains(path, "://") {
|
||||
return path
|
||||
}
|
||||
return strings.Replace(path, "/original.", "/w1920.", 1)
|
||||
return catalog.BackdropVariantPath(path, "w1920")
|
||||
}
|
||||
|
||||
// presignURL resolves an image path to a usable URL, delegating to the
|
||||
|
||||
@@ -1359,15 +1359,7 @@ func (h *SectionHandler) toSectionItemResponse(sectionType sections.SectionType,
|
||||
// uses the cached w1280 backdrop for Continue Watching / Next Up rows.
|
||||
func sectionBackdropPath(sectionType sections.SectionType, path string) string {
|
||||
if sectionType == sections.SectionContinueWatching || sectionType == sections.SectionNextUp {
|
||||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||
return path
|
||||
}
|
||||
// Plugin-prefixed paths pass through; the resolver still receives the
|
||||
// existing "featured" semantic variant.
|
||||
if strings.Contains(path, "://") {
|
||||
return path
|
||||
}
|
||||
return strings.Replace(path, "/original.", "/w1280.", 1)
|
||||
return catalog.BackdropVariantPath(path, "w1280")
|
||||
}
|
||||
return featuredBackdropPath(path)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,18 @@ func TestSectionBackdropPathUsesExpectedVariants(t *testing.T) {
|
||||
path: "/tmdb/shows/1399/backdrop/original.jpg",
|
||||
want: "/tmdb/shows/1399/backdrop/w1920.jpg",
|
||||
},
|
||||
{
|
||||
name: "continue watching episode still clamps to w500",
|
||||
sectionType: sections.SectionContinueWatching,
|
||||
path: "tvdb/series/73141/seasons/22/episodes/9/still/original.webp",
|
||||
want: "tvdb/series/73141/seasons/22/episodes/9/still/w500.webp",
|
||||
},
|
||||
{
|
||||
name: "featured section episode still clamps to w500",
|
||||
sectionType: sections.SectionRecentlyAdded,
|
||||
path: "tvdb/series/73141/seasons/22/episodes/9/still/original.webp",
|
||||
want: "tvdb/series/73141/seasons/22/episodes/9/still/w500.webp",
|
||||
},
|
||||
{
|
||||
name: "http paths pass through",
|
||||
sectionType: sections.SectionContinueWatching,
|
||||
|
||||
@@ -2189,6 +2189,43 @@ func cachedImageVariantPath(path, imageType, size string) string {
|
||||
return path
|
||||
}
|
||||
|
||||
// imageTypeFromCachedPath returns the image type segment ("poster", "backdrop",
|
||||
// "logo", "still") encoded in a cached S3 image path of the form
|
||||
// ".../{imageType}/{variant}.{ext}". It returns "" for full URLs,
|
||||
// plugin-prefixed paths, or paths with no directory segment.
|
||||
func imageTypeFromCachedPath(path string) string {
|
||||
if path == "" || strings.Contains(path, "://") {
|
||||
return ""
|
||||
}
|
||||
lastSlash := strings.LastIndex(path, "/")
|
||||
if lastSlash <= 0 {
|
||||
return ""
|
||||
}
|
||||
dir := path[:lastSlash]
|
||||
return dir[strings.LastIndex(dir, "/")+1:]
|
||||
}
|
||||
|
||||
// BackdropVariantPath rewrites a cached "/original." image path to the
|
||||
// requested backdrop variant (e.g. "w1280" or "w1920"). Episode "backdrops"
|
||||
// are frequently the episode still, which the cache only generates at
|
||||
// w500/w300 — so requesting a backdrop width 404s. For still/poster/logo
|
||||
// paths this clamps to that type's largest cached variant instead. Full URLs,
|
||||
// plugin-prefixed paths, and non-"/original." paths pass through unchanged.
|
||||
func BackdropVariantPath(path, desiredVariant string) string {
|
||||
if path == "" || strings.Contains(path, "://") || !strings.Contains(path, "/original.") {
|
||||
return path
|
||||
}
|
||||
variant := desiredVariant
|
||||
switch imageType := imageTypeFromCachedPath(path); imageType {
|
||||
case "still", "poster", "logo":
|
||||
variant = cachedImageVariantKey(imageType, "")
|
||||
}
|
||||
if variant == "" {
|
||||
return path
|
||||
}
|
||||
return strings.Replace(path, "/original.", "/"+variant+".", 1)
|
||||
}
|
||||
|
||||
func cachedImageVariantKey(imageType, size string) string {
|
||||
if size == "original" {
|
||||
return "original"
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package catalog
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestImageTypeFromCachedPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"episode still", "tvdb/series/73141/seasons/22/episodes/9/still/original.webp", "still"},
|
||||
{"movie backdrop", "tmdb/movies/550/backdrop/original.webp", "backdrop"},
|
||||
{"leading slash poster", "/tmdb/movies/550/poster/original.jpg", "poster"},
|
||||
{"logo", "tmdb/movies/550/logo/original.png", "logo"},
|
||||
{"http url", "https://images.example.com/backdrop/original.jpg", ""},
|
||||
{"plugin path", "plugin://tmdb/backdrop/original.jpg", ""},
|
||||
{"no slash", "original.webp", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := imageTypeFromCachedPath(tt.path); got != tt.want {
|
||||
t.Fatalf("imageTypeFromCachedPath(%q) = %q, want %q", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackdropVariantPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
desired string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "real backdrop keeps requested w1280",
|
||||
path: "tmdb/movies/550/backdrop/original.webp",
|
||||
desired: "w1280",
|
||||
want: "tmdb/movies/550/backdrop/w1280.webp",
|
||||
},
|
||||
{
|
||||
name: "real backdrop keeps requested w1920",
|
||||
path: "/tmdb/shows/1399/backdrop/original.jpg",
|
||||
desired: "w1920",
|
||||
want: "/tmdb/shows/1399/backdrop/w1920.jpg",
|
||||
},
|
||||
{
|
||||
name: "episode still clamps to w500 (no large variant exists)",
|
||||
path: "tvdb/series/73141/seasons/22/episodes/9/still/original.webp",
|
||||
desired: "w1280",
|
||||
want: "tvdb/series/73141/seasons/22/episodes/9/still/w500.webp",
|
||||
},
|
||||
{
|
||||
name: "episode still clamps w1920 to w500",
|
||||
path: "tvdb/series/73141/seasons/22/episodes/9/still/original.webp",
|
||||
desired: "w1920",
|
||||
want: "tvdb/series/73141/seasons/22/episodes/9/still/w500.webp",
|
||||
},
|
||||
{
|
||||
name: "http url passes through",
|
||||
path: "https://images.example.com/backdrop/original.jpg",
|
||||
desired: "w1280",
|
||||
want: "https://images.example.com/backdrop/original.jpg",
|
||||
},
|
||||
{
|
||||
name: "plugin path passes through",
|
||||
path: "plugin://tmdb/backdrop/original.jpg",
|
||||
desired: "w1280",
|
||||
want: "plugin://tmdb/backdrop/original.jpg",
|
||||
},
|
||||
{
|
||||
name: "path without original segment passes through",
|
||||
path: "tmdb/movies/550/backdrop/w300.webp",
|
||||
desired: "w1280",
|
||||
want: "tmdb/movies/550/backdrop/w300.webp",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := BackdropVariantPath(tt.path, tt.desired); got != tt.want {
|
||||
t.Fatalf("BackdropVariantPath(%q, %q) = %q, want %q", tt.path, tt.desired, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user