Posters: decode WebP and GIF; say what unreadable poster data looks like

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 00:20:14 +02:00
co-authored by Claude Sonnet 5.5
parent 706a094b20
commit 4d602b2acb
6 changed files with 105 additions and 10 deletions
+48 -3
View File
@@ -6,8 +6,11 @@ import (
"fmt"
"image"
"image/color"
_ "image/jpeg" // poster formats
_ "image/png"
_ "image/gif" // poster formats
_ "image/jpeg" // (Radarr and Sonarr store the provider's bytes under a .jpg name,
_ "image/png" // whatever format they are in)
_ "golang.org/x/image/webp"
"net/url"
"os/exec"
"strings"
@@ -33,7 +36,7 @@ const (
func decodeCover(data []byte, maxHeight int) (*image.RGBA, error) {
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("not a supported image: %w", err)
return nil, fmt.Errorf("not a supported image (%s): %w", describeBytes(data), err)
}
if cfg.Width <= 0 || cfg.Height <= 0 || cfg.Width*cfg.Height > maxCoverPixels {
return nil, fmt.Errorf("image dimensions %dx%d not supported", cfg.Width, cfg.Height)
@@ -534,3 +537,45 @@ func posterSource(key string) string {
path, _, _ := strings.Cut(key, "?")
return path
}
// describeBytes says what a blob of unknown data looks like, for error
// messages: a file Radarr/Sonarr stored as poster.jpg can be any format, or an
// error page.
func describeBytes(data []byte) string {
if len(data) == 0 {
return "empty"
}
head := data[:min(len(data), 16)]
switch {
case len(data) >= 12 && string(data[:4]) == "RIFF" && string(data[8:12]) == "WEBP":
return "WebP"
case bytes.HasPrefix(data, []byte("GIF8")):
return "GIF"
case bytes.HasPrefix(data, []byte("\x89PNG")):
return "PNG"
case bytes.HasPrefix(data, []byte("\xff\xd8")):
return "JPEG"
case len(data) >= 12 && string(data[4:8]) == "ftyp":
return "AVIF/HEIC, " + strings.TrimSpace(string(data[8:12]))
case bytes.HasPrefix(data, []byte("BM")):
return "BMP"
case bytes.HasPrefix(data, []byte("II*\x00")) || bytes.HasPrefix(data, []byte("MM\x00*")):
return "TIFF"
case bytes.HasPrefix(bytes.TrimSpace(data), []byte("<")):
return "text, probably an HTML or SVG page: " + printable(bytes.TrimSpace(data), 40)
}
return fmt.Sprintf("%d bytes starting % x", len(data), head)
}
// printable returns up to n bytes of data with control characters replaced.
func printable(data []byte, n int) string {
if len(data) > n {
data = data[:n]
}
return strings.Map(func(r rune) rune {
if r < 32 || r == 127 {
return ' '
}
return r
}, string(data))
}