Real-image posters: scale to the box's pixel size

Ghostty shows an image smaller than its placement at its own size, so
library posters (Radarr's poster-500 is 333x500, not 500x750) came out
smaller than the 426x640 remote ones. Images are now scaled (bilinear)
to exactly cols*cellWidth x rows*cellHeight pixels before upload, and
library titles use the original poster for real images. The disk cache
stores posters as decoded (JPEG) instead of the possibly large originals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 11:25:45 +02:00
co-authored by Claude Opus 5.5
parent 786e783951
commit 15e97f4582
8 changed files with 100 additions and 25 deletions
+11 -9
View File
@@ -18,8 +18,8 @@ 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 a scaled variant: -250 (250×375), or -500
// (500×750) when fetch is asked for a large poster. Titles that are not in
// with the API key: the copy 250 pixels tall (-250), or the original 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) {
@@ -50,10 +50,11 @@ func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(
return "", nil, false
}
// 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.
// localPoster fetches a poster from the instance: the small variant (-250),
// then the original; when large, only 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, large bool) ([]byte, error) {
try := func(ref string) ([]byte, error) {
data, err := c.GetBytes(ctx, ref)
@@ -65,11 +66,12 @@ func (c *Client) localPoster(ctx context.Context, local string, large bool) ([]b
}
return data, err
}
width := 250
// Radarr/Sonarr keep copies 250 and 500 pixels tall; even the larger one
// is smaller than a real image is shown, so those take the original.
if large {
width = 500
return try(local)
}
if scaled := scaledVariant(local, width); scaled != local {
if scaled := scaledVariant(local, 250); scaled != local {
data, err := try(scaled)
if err == nil {
return data, nil
+2 -2
View File
@@ -24,7 +24,7 @@ func TestScaledVariant(t *testing.T) {
}
}
func TestLargePosterUsesThe500Variant(t *testing.T) {
func TestLargePosterUsesTheOriginal(t *testing.T) {
var paths []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paths = append(paths, r.URL.Path)
@@ -39,7 +39,7 @@ func TestLargePosterUsesThe500Variant(t *testing.T) {
if _, err := fetch(context.Background(), false); err != nil {
t.Fatal(err)
}
if strings.Join(paths, " ") != "/MediaCover/1/poster-500.jpg /MediaCover/1/poster-250.jpg" {
if strings.Join(paths, " ") != "/MediaCover/1/poster.jpg /MediaCover/1/poster-250.jpg" {
t.Errorf("requests = %v", paths)
}
}