132 lines
3.9 KiB
Go
132 lines
3.9 KiB
Go
package arr
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const maxImageBytes = 8 << 20
|
|
|
|
// Poster returns a key identifying the poster among cached images and a
|
|
// 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) {
|
|
for _, img := range images {
|
|
if img.CoverType != "poster" {
|
|
continue
|
|
}
|
|
if inLibrary && img.URL != "" {
|
|
local, remote := img.URL, img.RemoteURL
|
|
base, _ := c.settings()
|
|
return base + "|" + local, func(ctx context.Context) ([]byte, error) {
|
|
// MediaCover has poster-250.jpg next to poster.jpg; fall back to the original.
|
|
if small := smallVariant(local); small != local {
|
|
if data, err := c.GetBytes(ctx, small); err == nil {
|
|
return data, nil
|
|
}
|
|
}
|
|
data, err := c.GetBytes(ctx, local)
|
|
if err != nil && remote != "" {
|
|
// The instance has not cached (or has lost) its copy: use the
|
|
// metadata provider's, as for titles that are not in the library.
|
|
if remoteData, remoteErr := FetchURL(ctx, remote); remoteErr == nil {
|
|
return remoteData, nil
|
|
}
|
|
}
|
|
return data, err
|
|
}, true
|
|
}
|
|
if img.RemoteURL != "" {
|
|
remote := img.RemoteURL
|
|
return remote, func(ctx context.Context) ([]byte, error) { return FetchURL(ctx, remote) }, true
|
|
}
|
|
}
|
|
return "", nil, false
|
|
}
|
|
|
|
// smallVariant turns /MediaCover/1/poster.jpg?x into /MediaCover/1/poster-250.jpg?x.
|
|
func smallVariant(p string) string {
|
|
path, query, hasQuery := strings.Cut(p, "?")
|
|
if !strings.HasSuffix(path, "/poster.jpg") {
|
|
return p
|
|
}
|
|
path = strings.TrimSuffix(path, ".jpg") + "-250.jpg"
|
|
if hasQuery {
|
|
return path + "?" + query
|
|
}
|
|
return path
|
|
}
|
|
|
|
// GetBytes downloads a server-relative path such as /MediaCover/1/poster.jpg
|
|
// from the instance, authenticated with the API key.
|
|
func (c *Client) GetBytes(ctx context.Context, ref string) ([]byte, error) {
|
|
u, err := c.resolve(ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, apiKey := c.settings()
|
|
req.Header.Set("X-Api-Key", apiKey)
|
|
return doImage(c.http, req)
|
|
}
|
|
|
|
// resolve joins a server-relative path to the base URL. Radarr/Sonarr include
|
|
// their URL base in image paths, so it must not be added twice.
|
|
func (c *Client) resolve(ref string) (string, error) {
|
|
baseURL, _ := c.settings()
|
|
base, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if base.Path != "" && base.Path != "/" && strings.HasPrefix(ref, strings.TrimRight(base.Path, "/")+"/") {
|
|
return base.Scheme + "://" + base.Host + ref, nil
|
|
}
|
|
return baseURL + ref, nil
|
|
}
|
|
|
|
var imageClient = &http.Client{Timeout: 20 * time.Second}
|
|
|
|
// FetchURL downloads a public image. No credentials are sent.
|
|
func FetchURL(ctx context.Context, rawURL string) ([]byte, error) {
|
|
u, err := url.Parse(rawURL)
|
|
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
|
|
return nil, fmt.Errorf("unsupported image URL")
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return doImage(imageClient, req)
|
|
}
|
|
|
|
func doImage(client *http.Client, req *http.Request) ([]byte, error) {
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, stripURL(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("image: HTTP %d", resp.StatusCode)
|
|
}
|
|
data, err := io.ReadAll(io.LimitReader(resp.Body, maxImageBytes+1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(data) > maxImageBytes {
|
|
return nil, fmt.Errorf("image larger than %d MB", maxImageBytes>>20)
|
|
}
|
|
return data, nil
|
|
}
|