Real-image posters of library titles: use the 500 px copy

Library titles fetched Radarr's/Sonarr's 250×375 poster copy, which
real images show at up to 640 pixels, so movie posters looked soft next
to series posters that came from the provider's full-size original
(Sonarr behind a login). With real images the 500×750 copy is used;
block-character posters keep the small one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
This commit is contained in:
2026-10-21 15:31:56 +02:00
co-authored by Claude Opus 5.5
parent b259e22458
commit 2798977867
7 changed files with 71 additions and 35 deletions
+23 -15
View File
@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
@@ -17,10 +18,11 @@ const maxImageBytes = 8 << 20
// function that downloads it, or ok=false when there is none.
//
// Library items have a copy on the Radarr/Sonarr server, which is fetched
// with the API key (preferring the small -250 variant). Titles that are not
// in the library yet only have the remote URL of the metadata provider; that
// is fetched without the API key.
func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(ctx context.Context) ([]byte, error), ok bool) {
// with the API key, preferring a scaled variant: -250 (250×375), or -500
// (500×750) when fetch is asked for a large poster. Titles that are not in
// the library yet only have the remote URL of the metadata provider; that is
// fetched without the API key.
func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(ctx context.Context, large bool) ([]byte, error), ok bool) {
for _, img := range images {
if img.CoverType != "poster" {
continue
@@ -28,8 +30,8 @@ func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(
if inLibrary && img.URL != "" {
local, remote := img.URL, img.RemoteURL
base, _ := c.settings()
return base + "|" + local, func(ctx context.Context) ([]byte, error) {
data, err := c.localPoster(ctx, local)
return base + "|" + local, func(ctx context.Context, large bool) ([]byte, error) {
data, err := c.localPoster(ctx, local, large)
if err != nil && remote != "" {
// The instance has no usable copy (not cached, or behind a login):
// use the metadata provider's, as for titles not in the library.
@@ -42,17 +44,17 @@ func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(
}
if img.RemoteURL != "" {
remote := img.RemoteURL
return remote, func(ctx context.Context) ([]byte, error) { return FetchURL(ctx, remote) }, true
return remote, func(ctx context.Context, _ bool) ([]byte, error) { return FetchURL(ctx, remote) }, true
}
}
return "", nil, false
}
// localPoster fetches a poster from the instance: the small variant, then the
// original. When the instance answers with a page instead of an image (it
// localPoster fetches a poster from the instance: the scaled variant (-500
// when large, else -250), then the original. When the instance answers with a page instead of an image (it
// wants a login for /MediaCover) the API key is also tried as a query
// parameter, which is the other way Radarr and Sonarr accept it.
func (c *Client) localPoster(ctx context.Context, local string) ([]byte, error) {
func (c *Client) localPoster(ctx context.Context, local string, large bool) ([]byte, error) {
try := func(ref string) ([]byte, error) {
data, err := c.GetBytes(ctx, ref)
var page *notImageError
@@ -63,8 +65,12 @@ func (c *Client) localPoster(ctx context.Context, local string) ([]byte, error)
}
return data, err
}
if small := smallVariant(local); small != local {
data, err := try(small)
width := 250
if large {
width = 500
}
if scaled := scaledVariant(local, width); scaled != local {
data, err := try(scaled)
if err == nil {
return data, nil
}
@@ -76,13 +82,15 @@ func (c *Client) localPoster(ctx context.Context, local string) ([]byte, error)
return try(local)
}
// smallVariant turns /MediaCover/1/poster.jpg?x into /MediaCover/1/poster-250.jpg?x.
func smallVariant(p string) string {
// scaledVariant turns /MediaCover/1/poster.jpg?x into
// /MediaCover/1/poster-250.jpg?x (for width 250; Radarr and Sonarr keep 250
// and 500).
func scaledVariant(p string, width int) string {
path, query, hasQuery := strings.Cut(p, "?")
if !strings.HasSuffix(path, "/poster.jpg") {
return p
}
path = strings.TrimSuffix(path, ".jpg") + "-250.jpg"
path = strings.TrimSuffix(path, ".jpg") + "-" + strconv.Itoa(width) + ".jpg"
if hasQuery {
return path + "?" + query
}