chore: restore tmdb project api key fallback and move requests nav
- tmdb client falls back to bundled project API key when none configured - move Requests link next to Recommendations in the sidebar
This commit is contained in:
@@ -17,16 +17,12 @@ import (
|
||||
|
||||
const (
|
||||
defaultBaseURL = "https://api.themoviedb.org/3"
|
||||
projectAPIKey = "4ef0d7355d9ffb5151e987764708ce96"
|
||||
maxRetries = 3
|
||||
maxResponseBody = 1 << 20 // 1 MB
|
||||
maxCollectionPresetResults = 500
|
||||
)
|
||||
|
||||
// ErrMissingAPIKey is returned by client methods when the client was
|
||||
// constructed without an API key. Callers should source the key from
|
||||
// configuration rather than relying on a built-in default.
|
||||
var ErrMissingAPIKey = fmt.Errorf("tmdb: api key is required")
|
||||
|
||||
// Client is an HTTP client for the TMDB collection preset API surface.
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
@@ -36,14 +32,16 @@ type Client struct {
|
||||
}
|
||||
|
||||
// NewClient creates a TMDB API client with the given API key and rate limit
|
||||
// (requests per second). The apiKey must be sourced from configuration; a
|
||||
// client constructed with an empty key returns ErrMissingAPIKey from every
|
||||
// API call so misconfiguration surfaces immediately rather than silently
|
||||
// using a shared built-in key.
|
||||
// (requests per second). If apiKey is empty, Silo's public project API key is
|
||||
// used.
|
||||
func NewClient(apiKey string, rateLimit int) *Client {
|
||||
apiKey = strings.TrimSpace(apiKey)
|
||||
if apiKey == "" {
|
||||
apiKey = projectAPIKey
|
||||
}
|
||||
return &Client{
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
apiKey: strings.TrimSpace(apiKey),
|
||||
apiKey: apiKey,
|
||||
baseURL: defaultBaseURL,
|
||||
limiter: rate.NewLimiter(rate.Limit(rateLimit), rateLimit),
|
||||
}
|
||||
@@ -57,9 +55,6 @@ func (c *Client) SetBaseURL(url string) {
|
||||
// doGet executes a GET request against the TMDB API with rate limiting,
|
||||
// exponential backoff on 5xx/429, and JSON decoding into dest.
|
||||
func (c *Client) doGet(ctx context.Context, path string, dest any) error {
|
||||
if c.apiKey == "" {
|
||||
return ErrMissingAPIKey
|
||||
}
|
||||
if err := c.limiter.Wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,17 +2,26 @@ package tmdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewClientRequiresAPIKey(t *testing.T) {
|
||||
func TestNewClientUsesProjectAPIKeyWhenEmpty(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.URL.Query().Get("api_key"); got != projectAPIKey {
|
||||
t.Fatalf("api_key query = %q, want project API key", got)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"page":1,"total_pages":1,"total_results":0,"results":[]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient("", 1000)
|
||||
_, err := client.GetCollectionPreset(context.Background(), "trending", "all", "day", 10)
|
||||
if !errors.Is(err, ErrMissingAPIKey) {
|
||||
t.Fatalf("err = %v, want ErrMissingAPIKey", err)
|
||||
client.SetBaseURL(server.URL)
|
||||
|
||||
if _, err := client.GetCollectionPreset(context.Background(), "trending", "all", "day", 10); err != nil {
|
||||
t.Fatalf("GetCollectionPreset returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -495,6 +495,23 @@ export default function AppSidebar({ onNavigate, collapsed = false }: AppSidebar
|
||||
<SidebarLabel show={showLabels}>Recommendations</SidebarLabel>
|
||||
</ViewTransitionLink>
|
||||
</li>
|
||||
<li>
|
||||
<ViewTransitionLink
|
||||
to="/requests"
|
||||
onClick={onNavigate}
|
||||
className={navLinkClass("/requests")}
|
||||
aria-current={isActive("/requests") ? "page" : undefined}
|
||||
>
|
||||
{isActive("/requests") && (
|
||||
<span
|
||||
className="absolute top-1/2 left-0 h-[18px] w-[3px] -translate-y-1/2 rounded-r-sm"
|
||||
style={{ background: "var(--primary)" }}
|
||||
/>
|
||||
)}
|
||||
<Send className="h-[18px] w-[18px] shrink-0" />
|
||||
<SidebarLabel show={showLabels}>Requests</SidebarLabel>
|
||||
</ViewTransitionLink>
|
||||
</li>
|
||||
<li>
|
||||
<ViewTransitionLink
|
||||
to="/calendar"
|
||||
@@ -587,23 +604,6 @@ export default function AppSidebar({ onNavigate, collapsed = false }: AppSidebar
|
||||
<SidebarLabel show={showLabels}>Collections</SidebarLabel>
|
||||
</ViewTransitionLink>
|
||||
</li>
|
||||
<li>
|
||||
<ViewTransitionLink
|
||||
to="/requests"
|
||||
onClick={onNavigate}
|
||||
className={navLinkClass("/requests")}
|
||||
aria-current={isActive("/requests") ? "page" : undefined}
|
||||
>
|
||||
{isActive("/requests") && (
|
||||
<span
|
||||
className="absolute top-1/2 left-0 h-[18px] w-[3px] -translate-y-1/2 rounded-r-sm"
|
||||
style={{ background: "var(--primary)" }}
|
||||
/>
|
||||
)}
|
||||
<Send className="h-[18px] w-[18px] shrink-0" />
|
||||
<SidebarLabel show={showLabels}>Requests</SidebarLabel>
|
||||
</ViewTransitionLink>
|
||||
</li>
|
||||
<li>
|
||||
<ViewTransitionLink
|
||||
to={buildPersonalCatalogHref("history")}
|
||||
|
||||
Reference in New Issue
Block a user