Posters: fetch TheMovieDB images at the shown size, not the original

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 16:00:34 +02:00
co-authored by Claude Opus 5.5
parent ca8c71fa52
commit a70daaa0a3
3 changed files with 42 additions and 2 deletions
+3
View File
@@ -2,6 +2,9 @@
## Unreleased ## Unreleased
- **Posters of titles not in the library load faster:** they are fetched from TheMovieDB at the
size shown (780 pixels wide for real images) instead of the original Radarr names, which is often
2000×3000 and over 2 MB: about a tenth of the download and a sixth of the decoding time.
- **Real-image posters fill their box in every terminal:** they are scaled to the exact pixel size - **Real-image posters fill their box in every terminal:** they are scaled to the exact pixel size
of their cells before they are sent. Ghostty shows an image that is smaller than its place at its of their cells before they are sent. Ghostty shows an image that is smaller than its place at its
own size, so library posters (Radarr's copy is only 333×500, not 500×750 as assumed in 0.4.0) own size, so library posters (Radarr's copy is only 333×500, not 500×750 as assumed in 0.4.0)
+22 -2
View File
@@ -35,7 +35,7 @@ func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(
if err != nil && remote != "" { if err != nil && remote != "" {
// The instance has no usable copy (not cached, or behind a login): // The instance has no usable copy (not cached, or behind a login):
// use the metadata provider's, as for titles not in the library. // use the metadata provider's, as for titles not in the library.
if remoteData, remoteErr := FetchURL(ctx, remote); remoteErr == nil { if remoteData, remoteErr := FetchURL(ctx, tmdbSized(remote, large)); remoteErr == nil {
return remoteData, nil return remoteData, nil
} }
} }
@@ -44,7 +44,9 @@ func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(
} }
if img.RemoteURL != "" { if img.RemoteURL != "" {
remote := img.RemoteURL remote := img.RemoteURL
return remote, func(ctx context.Context, _ bool) ([]byte, error) { return FetchURL(ctx, remote) }, true return remote, func(ctx context.Context, large bool) ([]byte, error) {
return FetchURL(ctx, tmdbSized(remote, large))
}, true
} }
} }
return "", nil, false return "", nil, false
@@ -84,6 +86,24 @@ func (c *Client) localPoster(ctx context.Context, local string, large bool) ([]b
return try(local) return try(local)
} }
// tmdbSized asks TheMovieDB for a poster of the size that is shown instead of
// the original Radarr hands out (often 2000×3000 and over 2 MB, which takes
// longer to download and to decode than everything else together): 780
// pixels wide for real images, 342 for block characters. Other URLs are
// returned as they are.
func tmdbSized(rawURL string, large bool) string {
u, err := url.Parse(rawURL)
if err != nil || u.Host != "image.tmdb.org" || !strings.HasPrefix(u.Path, "/t/p/original/") {
return rawURL
}
size := "w342"
if large {
size = "w780"
}
u.Path = "/t/p/" + size + strings.TrimPrefix(u.Path, "/t/p/original")
return u.String()
}
// scaledVariant turns /MediaCover/1/poster.jpg?x into // scaledVariant turns /MediaCover/1/poster.jpg?x into
// /MediaCover/1/poster-250.jpg?x (for width 250; Radarr and Sonarr keep 250 // /MediaCover/1/poster-250.jpg?x (for width 250; Radarr and Sonarr keep 250
// and 500). // and 500).
+17
View File
@@ -8,6 +8,23 @@ import (
"testing" "testing"
) )
func TestTMDBSized(t *testing.T) {
for _, tc := range []struct {
in string
large bool
want string
}{
{"https://image.tmdb.org/t/p/original/abc.jpg", true, "https://image.tmdb.org/t/p/w780/abc.jpg"},
{"https://image.tmdb.org/t/p/original/abc.jpg", false, "https://image.tmdb.org/t/p/w342/abc.jpg"},
{"https://image.tmdb.org/t/p/w500/abc.jpg", true, "https://image.tmdb.org/t/p/w500/abc.jpg"},
{"https://artworks.thetvdb.com/banners/posters/1.jpg", true, "https://artworks.thetvdb.com/banners/posters/1.jpg"},
} {
if got := tmdbSized(tc.in, tc.large); got != tc.want {
t.Errorf("tmdbSized(%q, %v) = %q, want %q", tc.in, tc.large, got, tc.want)
}
}
}
func TestScaledVariant(t *testing.T) { func TestScaledVariant(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
in string in string