Posters: recognise a login page instead of an image; try the key in the query, then the provider
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- **Posters in WebP and GIF:** Sonarr/Radarr can store WebP data under a `.jpg` name; such posters
|
- **Posters in WebP and GIF:** Sonarr/Radarr can store WebP data under a `.jpg` name; such posters
|
||||||
(and GIFs) now show instead of failing with "unknown format". When a poster cannot be read, the Log
|
(and GIFs) now show instead of failing with "unknown format". When a poster cannot be read, the Log
|
||||||
says what it looks like. If an instance has no copy of a poster, the provider's is used.
|
says what it looks like. If an instance has no copy of a poster, or only answers with its login page, the API key is also tried as a query parameter and then the provider's poster is used.
|
||||||
- **Config (F6):** edit the settings of the config file inside the program (addresses, API keys,
|
- **Config (F6):** edit the settings of the config file inside the program (addresses, API keys,
|
||||||
add-form defaults, refresh interval, posters, notifications), test the connections, save, and
|
add-form defaults, refresh interval, posters, notifications), test the connections, save, and
|
||||||
have everything take effect immediately. Environment-provided values are never written to the file.
|
have everything take effect immediately. Environment-provided values are never written to the file.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Notes:
|
|||||||
- **Posters:** the detail panes show the poster at the top right; the head (title, facts, path) and the plot are one text that flows along the poster's left side and continues at full width below it (text is wrapped by arr-tui itself, with a narrower width for the first lines); on panes too small for the poster, the text simply uses the full width. Two ways to draw the poster, chosen by `ui.cover_style`:
|
- **Posters:** the detail panes show the poster at the top right; the head (title, facts, path) and the plot are one text that flows along the poster's left side and continues at full width below it (text is wrapped by arr-tui itself, with a narrower width for the first lines); on panes too small for the poster, the text simply uses the full width. Two ways to draw the poster, chosen by `ui.cover_style`:
|
||||||
- **Real images** (`kitty`, and `auto` in kitty/Ghostty): the Kitty graphics protocol with Unicode placeholders. The poster (kept up to 640 px tall) is uploaded once as PNG (`a=t,f=100,q=2`, 4096-byte base64 chunks), placed virtually with the cell footprint (`a=p,U=1,c=,r=`; a new size deletes and recreates the placement), and shown by writing U+10EEEE placeholder characters whose combining marks give row and column (the 297-entry table from the protocol) and whose foreground color is the image id. Since it is ordinary text, popups, dialogs and view changes cover it correctly and tcell's diffing keeps it in place. The footprint uses the real cell size in pixels (the window's pixel size divided by its cell count, rounded down to whole pixels so padding around the grid does not skew it; the placements are made again whenever the window or cell size changes) and a box whose shape is within 3% of the poster's; the remaining sliver is cropped from the poster's edges before upload (one upload per size, the previous one is deleted), because the terminal letterboxes an image that does not fit its box exactly. As a second safeguard the placeholder cells use the panes' background color, so anything the picture does not cover blends in (without it the terminal's own, slightly different, default background showed as grey bars beside the poster). Writes to the terminal go through the screen's tty on the UI goroutine, so they cannot interleave with drawing. Requires 24-bit color; otherwise, and inside tmux/screen, the block fallback is used. Terminals are recognised from `TERM`/`TERM_PROGRAM`/`KITTY_WINDOW_ID`/`GHOSTTY_RESOURCES_DIR`. The go-termimg library was looked at and not used: its placeholder support is marked experimental, its diacritic table does not match the protocol's, and it writes images straight to the terminal instead of through the screen.
|
- **Real images** (`kitty`, and `auto` in kitty/Ghostty): the Kitty graphics protocol with Unicode placeholders. The poster (kept up to 640 px tall) is uploaded once as PNG (`a=t,f=100,q=2`, 4096-byte base64 chunks), placed virtually with the cell footprint (`a=p,U=1,c=,r=`; a new size deletes and recreates the placement), and shown by writing U+10EEEE placeholder characters whose combining marks give row and column (the 297-entry table from the protocol) and whose foreground color is the image id. Since it is ordinary text, popups, dialogs and view changes cover it correctly and tcell's diffing keeps it in place. The footprint uses the real cell size in pixels (the window's pixel size divided by its cell count, rounded down to whole pixels so padding around the grid does not skew it; the placements are made again whenever the window or cell size changes) and a box whose shape is within 3% of the poster's; the remaining sliver is cropped from the poster's edges before upload (one upload per size, the previous one is deleted), because the terminal letterboxes an image that does not fit its box exactly. As a second safeguard the placeholder cells use the panes' background color, so anything the picture does not cover blends in (without it the terminal's own, slightly different, default background showed as grey bars beside the poster). Writes to the terminal go through the screen's tty on the UI goroutine, so they cannot interleave with drawing. Requires 24-bit color; otherwise, and inside tmux/screen, the block fallback is used. Terminals are recognised from `TERM`/`TERM_PROGRAM`/`KITTY_WINDOW_ID`/`GHOSTTY_RESOURCES_DIR`. The go-termimg library was looked at and not used: its placeholder support is marked experimental, its diacritic table does not match the protocol's, and it writes images straight to the terminal instead of through the screen.
|
||||||
- **Block characters** (`quadrant`, `sextant`, and the fallback): every cell shows several pixels in the best two-color split (`quadrant`: 2×2 pixels, glyphs from the Block Elements range every font has; `sextant`: 2×3 pixels, sharper, glyphs from Symbols for Legacy Computing). `auto` picks sextants on kitty, WezTerm, foot and Ghostty, and on other terminals when fontconfig lists a font with the sextant characters (`fc-list :charset=1fb00`, about 20 ms at startup; terminals draw glyphs their font lacks from fallback fonts; skipped over SSH because the installed fonts are the remote machine's), and quadrants otherwise.
|
- **Block characters** (`quadrant`, `sextant`, and the fallback): every cell shows several pixels in the best two-color split (`quadrant`: 2×2 pixels, glyphs from the Block Elements range every font has; `sextant`: 2×3 pixels, sharper, glyphs from Symbols for Legacy Computing). `auto` picks sextants on kitty, WezTerm, foot and Ghostty, and on other terminals when fontconfig lists a font with the sextant characters (`fc-list :charset=1fb00`, about 20 ms at startup; terminals draw glyphs their font lacks from fallback fonts; skipped over SSH because the installed fonts are the remote machine's), and quadrants otherwise.
|
||||||
Posters are JPEG, PNG, GIF or WebP (Radarr and Sonarr keep whatever bytes the provider sent under a `.jpg` name, which can be WebP), decoded and downscaled in the process; for any other format the Log says what the data looked like (e.g. AVIF, an HTML error page, or its first bytes). Library items fetch `/MediaCover/<id>/poster-250.jpg` from their Radarr/Sonarr instance with the API key (then the original `poster.jpg`, and if the instance has no copy, the provider's `remoteUrl`, fetched without credentials); search results fall back to the provider's `remoteUrl` (fetched without credentials). Posters are cached in memory (the terminal's copies are deleted when the cache is dropped and on exit), failed downloads are not retried for two minutes and are written to the Log (F7) with the reason, and the poster shrinks or disappears when the pane is too short. `ui.covers: false` disables it.
|
Posters are JPEG, PNG, GIF or WebP (Radarr and Sonarr keep whatever bytes the provider sent under a `.jpg` name, which can be WebP), decoded and downscaled in the process; for any other format the Log says what the data looked like (e.g. AVIF, an HTML error page, or its first bytes). Library items fetch `/MediaCover/<id>/poster-250.jpg` from their Radarr/Sonarr instance with the API key (then the original `poster.jpg`; an instance that answers 200 with an HTML page, i.e. wants a login for `/MediaCover`, is also tried with the key as the `apikey` query parameter; and if the instance has no usable copy, the provider's `remoteUrl`, fetched without credentials); search results fall back to the provider's `remoteUrl` (fetched without credentials). Posters are cached in memory (the terminal's copies are deleted when the cache is dropped and on exit), failed downloads are not retried for two minutes and are written to the Log (F7) with the reason, and the poster shrinks or disappears when the pane is too short. `ui.covers: false` disables it.
|
||||||
- **Calendar view (F5):** a two-week window (starting today, moved a week at a time with `[`/`]`, `t` returns to today) with the library's movie releases and airing episodes, grouped by day. Radarr `GET /calendar` supplies the cinema, digital and physical release dates (taken as written: midnight UTC means that calendar day) and Sonarr `GET /calendar?includeSeries=true` the episodes with their air time (shown in local time). Unmonitored items are included with `u`. A movie appears once per release date in the window. Each entry shows type, title, episode or release kind and status (downloaded / missing / upcoming); the detail pane shows poster and plot; `Enter`/`i` starts the release search and `s` an automatic search.
|
- **Calendar view (F5):** a two-week window (starting today, moved a week at a time with `[`/`]`, `t` returns to today) with the library's movie releases and airing episodes, grouped by day. Radarr `GET /calendar` supplies the cinema, digital and physical release dates (taken as written: midnight UTC means that calendar day) and Sonarr `GET /calendar?includeSeries=true` the episodes with their air time (shown in local time). Unmonitored items are included with `u`. A movie appears once per release date in the window. Each entry shows type, title, episode or release kind and status (downloaded / missing / upcoming); the detail pane shows poster and plot; `Enter`/`i` starts the release search and `s` an automatic search.
|
||||||
- **Download notifications:** a background poller (independent of the visible view) reads SABnzbd's history every 15 s and announces downloads that reach `Completed` or `Failed` as a popup and log entry; the first poll only records the current state. `ui.notify_downloads` (default true) and `ui.bell` (default false, rings the terminal bell) control it. Connection errors are not reported here, the indicators and the log already do that.
|
- **Download notifications:** a background poller (independent of the visible view) reads SABnzbd's history every 15 s and announces downloads that reach `Completed` or `Failed` as a popup and log entry; the first poll only records the current state. `ui.notify_downloads` (default true) and `ui.bell` (default false, rings the terminal bell) control it. Connection errors are not reported here, the indicators and the log already do that.
|
||||||
- **Config view (F6):** one list with a field for every setting of the config file, one below the other under the headings Radarr, Sonarr, SABnzbd and Interface (a single scrolling form; ↑↓ passes over the headings and ↓ after the last field reaches the Save / Revert buttons, so no Tab is needed; Tab jumps between the fields and the buttons). The `Test` button is part of the list, right below the SABnzbd settings; `Save` and `Revert` are centered in a bar below the list. The fields start from the settings in use; API keys are masked (a checkbox shows them). `Test` builds temporary clients from the typed values and reports each service. `Save` validates like startup does (URLs, non-empty keys, refresh 1–3600 s), then writes the file from the file's own contents plus the edits (`config.LoadFile`), so values that only come from `ARR_TUI_*` environment variables are left as the file has them and are shown greyed out; the write is atomic (temp file + rename) and keeps the file's permissions (`0600` when new); defaults such as `covers: true` stay out of the file unless it already named them. Everything then applies without a restart: the Radarr/Sonarr/SABnzbd clients are re-pointed (`Reconfigure`, guarded by a mutex), the connection indicators, add-form defaults and options are refreshed, the library and calendar reload, the Downloads interval, notifications, bell and poster style are switched live. `q` is typed text in this view (`F9` opens the help).
|
- **Config view (F6):** one list with a field for every setting of the config file, one below the other under the headings Radarr, Sonarr, SABnzbd and Interface (a single scrolling form; ↑↓ passes over the headings and ↓ after the last field reaches the Save / Revert buttons, so no Tab is needed; Tab jumps between the fields and the buttons). The `Test` button is part of the list, right below the SABnzbd settings; `Save` and `Revert` are centered in a bar below the list. The fields start from the settings in use; API keys are masked (a checkbox shows them). `Test` builds temporary clients from the typed values and reports each service. `Save` validates like startup does (URLs, non-empty keys, refresh 1–3600 s), then writes the file from the file's own contents plus the edits (`config.LoadFile`), so values that only come from `ARR_TUI_*` environment variables are left as the file has them and are shown greyed out; the write is atomic (temp file + rename) and keeps the file's permissions (`0600` when new); defaults such as `covers: true` stay out of the file unless it already named them. Everything then applies without a restart: the Radarr/Sonarr/SABnzbd clients are re-pointed (`Reconfigure`, guarded by a mutex), the connection indicators, add-form defaults and options are refreshed, the library and calendar reload, the Downloads interval, notifications, bell and poster style are switched live. `q` is typed text in this view (`F9` opens the help).
|
||||||
|
|||||||
+65
-11
@@ -2,6 +2,7 @@ package arr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -28,16 +29,10 @@ func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(
|
|||||||
local, remote := img.URL, img.RemoteURL
|
local, remote := img.URL, img.RemoteURL
|
||||||
base, _ := c.settings()
|
base, _ := c.settings()
|
||||||
return base + "|" + local, func(ctx context.Context) ([]byte, error) {
|
return base + "|" + local, func(ctx context.Context) ([]byte, error) {
|
||||||
// MediaCover has poster-250.jpg next to poster.jpg; fall back to the original.
|
data, err := c.localPoster(ctx, local)
|
||||||
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 != "" {
|
if err != nil && remote != "" {
|
||||||
// The instance has not cached (or has lost) its copy: use the
|
// The instance has no usable copy (not cached, or behind a login):
|
||||||
// metadata provider's, as for titles that are 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, remote); remoteErr == nil {
|
||||||
return remoteData, nil
|
return remoteData, nil
|
||||||
}
|
}
|
||||||
@@ -53,6 +48,34 @@ func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(
|
|||||||
return "", nil, false
|
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
|
||||||
|
// 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) {
|
||||||
|
try := func(ref string) ([]byte, error) {
|
||||||
|
data, err := c.GetBytes(ctx, ref)
|
||||||
|
var page *notImageError
|
||||||
|
if errors.As(err, &page) {
|
||||||
|
if data, err2 := c.getBytes(ctx, ref, true); err2 == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
if small := smallVariant(local); small != local {
|
||||||
|
data, err := try(small)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
var page *notImageError
|
||||||
|
if errors.As(err, &page) {
|
||||||
|
return nil, err // the original would be a page as well
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return try(local)
|
||||||
|
}
|
||||||
|
|
||||||
// smallVariant turns /MediaCover/1/poster.jpg?x into /MediaCover/1/poster-250.jpg?x.
|
// smallVariant turns /MediaCover/1/poster.jpg?x into /MediaCover/1/poster-250.jpg?x.
|
||||||
func smallVariant(p string) string {
|
func smallVariant(p string) string {
|
||||||
path, query, hasQuery := strings.Cut(p, "?")
|
path, query, hasQuery := strings.Cut(p, "?")
|
||||||
@@ -69,19 +92,45 @@ func smallVariant(p string) string {
|
|||||||
// GetBytes downloads a server-relative path such as /MediaCover/1/poster.jpg
|
// GetBytes downloads a server-relative path such as /MediaCover/1/poster.jpg
|
||||||
// from the instance, authenticated with the API key.
|
// from the instance, authenticated with the API key.
|
||||||
func (c *Client) GetBytes(ctx context.Context, ref string) ([]byte, error) {
|
func (c *Client) GetBytes(ctx context.Context, ref string) ([]byte, error) {
|
||||||
|
return c.getBytes(ctx, ref, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBytes sends the API key as a header, or with keyInQuery as the apikey
|
||||||
|
// query parameter.
|
||||||
|
func (c *Client) getBytes(ctx context.Context, ref string, keyInQuery bool) ([]byte, error) {
|
||||||
u, err := c.resolve(ref)
|
u, err := c.resolve(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
_, apiKey := c.settings()
|
||||||
|
if keyInQuery {
|
||||||
|
parsed, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
q := parsed.Query()
|
||||||
|
q.Set("apikey", apiKey)
|
||||||
|
parsed.RawQuery = q.Encode()
|
||||||
|
u = parsed.String()
|
||||||
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_, apiKey := c.settings()
|
if !keyInQuery {
|
||||||
req.Header.Set("X-Api-Key", apiKey)
|
req.Header.Set("X-Api-Key", apiKey)
|
||||||
|
}
|
||||||
return doImage(c.http, req)
|
return doImage(c.http, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// notImageError means the server answered 200 with something that is not an
|
||||||
|
// image, typically the login page of an instance that requires authentication.
|
||||||
|
type notImageError struct{ contentType string }
|
||||||
|
|
||||||
|
func (e *notImageError) Error() string {
|
||||||
|
return fmt.Sprintf("image: the server sent %q instead of an image (a login page?)", e.contentType)
|
||||||
|
}
|
||||||
|
|
||||||
// resolve joins a server-relative path to the base URL. Radarr/Sonarr include
|
// 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.
|
// their URL base in image paths, so it must not be added twice.
|
||||||
func (c *Client) resolve(ref string) (string, error) {
|
func (c *Client) resolve(ref string) (string, error) {
|
||||||
@@ -120,6 +169,11 @@ func doImage(client *http.Client, req *http.Request) ([]byte, error) {
|
|||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("image: HTTP %d", resp.StatusCode)
|
return nil, fmt.Errorf("image: HTTP %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
// Pages and error documents. (Not all of text/*: a plain server may label
|
||||||
|
// image bytes it cannot identify as text/plain.)
|
||||||
|
if ct := resp.Header.Get("Content-Type"); strings.HasPrefix(ct, "text/html") || strings.HasPrefix(ct, "application/xhtml") || strings.HasPrefix(ct, "application/json") {
|
||||||
|
return nil, ¬ImageError{contentType: ct}
|
||||||
|
}
|
||||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxImageBytes+1))
|
data, err := io.ReadAll(io.LimitReader(resp.Body, maxImageBytes+1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,3 +125,69 @@ func TestPosterFallsBackToTheProviderWhenTheInstanceHasNoCopy(t *testing.T) {
|
|||||||
t.Error("expected the local error")
|
t.Error("expected the local error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loginWall answers like an instance that requires a login for /MediaCover:
|
||||||
|
// a 200 with an HTML page, unless the request carries the key as a query parameter.
|
||||||
|
func loginWall(acceptQuery bool, hits *[]string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
*hits = append(*hits, r.URL.RequestURI()+" key-header="+r.Header.Get("X-Api-Key"))
|
||||||
|
if acceptQuery && r.URL.Query().Get("apikey") == "secret" {
|
||||||
|
w.Header().Set("Content-Type", "image/jpeg")
|
||||||
|
w.Write([]byte("IMAGE"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Write([]byte("<!DOCTYPE html><html><head><title>Login</title>"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPosterTriesTheKeyInTheQueryWhenGivenALoginPage(t *testing.T) {
|
||||||
|
var hits []string
|
||||||
|
srv := httptest.NewServer(loginWall(true, &hits))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := NewClient(srv.URL, "secret")
|
||||||
|
_, fetch, _ := c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/89/poster.jpg"}}, true)
|
||||||
|
data, err := fetch(context.Background())
|
||||||
|
if err != nil || string(data) != "IMAGE" {
|
||||||
|
t.Fatalf("data=%q err=%v, hits=%v", data, err, hits)
|
||||||
|
}
|
||||||
|
// Small variant with the header (a page), then the same file with the key in the query.
|
||||||
|
if len(hits) != 2 || !strings.Contains(hits[0], "poster-250.jpg") || !strings.Contains(hits[0], "key-header=secret") ||
|
||||||
|
!strings.Contains(hits[1], "poster-250.jpg?apikey=secret") {
|
||||||
|
t.Errorf("requests = %v", hits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPosterUsesTheProviderWhenTheInstanceOnlyShowsALogin(t *testing.T) {
|
||||||
|
var hits []string
|
||||||
|
instance := httptest.NewServer(loginWall(false, &hits))
|
||||||
|
defer instance.Close()
|
||||||
|
var remoteKey, remoteQuery string
|
||||||
|
remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
remoteKey, remoteQuery = r.Header.Get("X-Api-Key"), r.URL.RawQuery
|
||||||
|
w.Header().Set("Content-Type", "image/webp")
|
||||||
|
w.Write([]byte("WEBP"))
|
||||||
|
}))
|
||||||
|
defer remote.Close()
|
||||||
|
|
||||||
|
c := NewClient(instance.URL, "secret")
|
||||||
|
_, fetch, _ := c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/89/poster.jpg", RemoteURL: remote.URL + "/p.jpg"}}, true)
|
||||||
|
data, err := fetch(context.Background())
|
||||||
|
if err != nil || string(data) != "WEBP" {
|
||||||
|
t.Fatalf("data=%q err=%v", data, err)
|
||||||
|
}
|
||||||
|
if remoteKey != "" || remoteQuery != "" {
|
||||||
|
t.Errorf("credentials reached the provider: header %q query %q", remoteKey, remoteQuery)
|
||||||
|
}
|
||||||
|
if len(hits) != 2 { // the small variant with the header and with the query; the original is skipped
|
||||||
|
t.Errorf("instance requests = %v", hits)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without a provider address the failure says what came back.
|
||||||
|
_, fetch, _ = c.Poster([]Image{{CoverType: "poster", URL: "/MediaCover/89/poster.jpg"}}, true)
|
||||||
|
_, err = fetch(context.Background())
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "text/html") || !strings.Contains(err.Error(), "login") {
|
||||||
|
t.Errorf("error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user