From 38f76a9e041d235c12da4afec8d3d64e0f284c95 Mon Sep 17 00:00:00 2001 From: "planetrenox@protonmail.com" Date: Mon, 12 Oct 2026 14:41:36 +0000 Subject: [PATCH] Feat: Fall back to Bing when Brave refuses, and serve donate.webp when Bing blocks too Brave errors (e.g. its spending limit being hit) now fall back to Bing like empty results do, with the error in a Brave Unavailable alert. If Bing fails too (HTTP error or a page without results, which for Bing means it's blocking us), a new papercraft donate.webp is served instead of an AI image, and nothing is cached so the query works once either search is back. Co-Authored-By: Claude Opus 5.5 --- PRIVACY.md | 2 +- README.md | 5 ++++- assets/donate.webp | Bin 0 -> 30034 bytes functions/[[path]].js | 21 ++++++++++++++++----- functions/_utils/bing.js | 24 ++++++++---------------- functions/_utils/brave.js | 17 +++++++++++------ 6 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 assets/donate.webp diff --git a/PRIVACY.md b/PRIVACY.md index 93c5023..1ce5a0b 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -12,7 +12,7 @@ direct-img.link has no accounts and doesn't sell data. This page explains what i ## Search queries and images -- When a query isn't already cached, it's sent to Brave Search, and to Bing if Brave returns nothing. On free.direct-img.link, it's sent to Openverse and/or Wikimedia Commons instead. Searches and image downloads are made from our servers, so your IP address isn't shared with these search providers or the sites images come from. +- When a query isn't already cached, it's sent to Brave Search, and to Bing if Brave returns nothing or is unavailable. On free.direct-img.link, it's sent to Openverse and/or Wikimedia Commons instead. Searches and image downloads are made from our servers, so your IP address isn't shared with these search providers or the sites images come from. - Each query (with its `i` and `src` options) and the image it returned are cached for up to 90 days. Failed searches are remembered for 24 hours. - Cached images are shared: anyone requesting the same query and options gets the same image. diff --git a/README.md b/README.md index dc18a70..67edf69 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ Literal slashes (`/`) and dots (`.`) in the URL path are **rejected** to prevent - **Double-encoded values** are decoded once — `%2520` becomes `%20` (literal), not a space - Two queries that normalize to the same string share the same cached image - **Nothing found?** If a search finds no image (or fewer than `i` working ones), an [AI image](#ai-images) of the query is served instead of `bad.webp`, and cached like any other image. `bad.webp` is only served if the AI image fails too +- **Out of searches?** If Brave refuses (e.g. its monthly spending limit is reached), Bing is searched instead. If Bing is blocking us too, `donate.webp` is served instead of an AI image, and nothing is cached, so the query works again once either is back ## Free Images @@ -151,6 +152,7 @@ Limits change over time, so they aren't listed here. **Check the Rate Limits sec - **New searches (cache misses)** are limited per IP per day, then `limit.webp` is served. `direct-img.link`, `free.direct-img.link` and `ai.direct-img.link` have separate counters, all reset at midnight UTC - **Cache hits are unlimited** (within the burst limit) - Other `?i=` values of a query searched in the last hour reuse its results, so they don't count (except on `ai.`, where each `i` is a new image) +- If Brave's spending limit is hit, searches use Bing; if Bing also blocks us, `donate.webp` is served (not cached) - Searches that find nothing count toward the limit. If the AI image served instead fails too, the failure is remembered for 24h (`bad.webp`) - A Cloudflare WAF burst limit applies to all requests (HTTP 429). Its counters are approximate (not shared perfectly across Cloudflare servers), so bursts may slip slightly past it @@ -182,6 +184,7 @@ Free community service. Donations help cover API and infrastructure costs. 4. Subscribe — you get **$5 in free monthly credits** (covers 1,000 queries/month) 5. Go to your [API dashboard](https://api.search.brave.com/app/#/subscriptions) 6. Copy your **API key** (starts with `BSA...`) +7. Optional: set a monthly spending limit there so you never pay past the credit. Once it's reached, searches fall back to Bing ### 2. Openverse API Credentials (Optional) @@ -292,7 +295,7 @@ After deploying, visit `https:///_setup` once. It creates the `dire - **Cloudflare KV** — generic lookups - **SurrealDB (v2.6.5)** — atomic rate limiting, search locks + cached result lists - **Cloudflare WAF** — layer 7 mitigation -- **Brave Image Search API** — image sourcing +- **Brave Image Search API** — image sourcing, with Bing as the fallback - **Openverse API + Wikimedia Commons API** — free image sourcing - **[AuroraIMG-6M](https://github.com/direct-img/AuroraIMG-6M) on Dokploy** — AI images and the nothing-found fallback diff --git a/assets/donate.webp b/assets/donate.webp new file mode 100644 index 0000000000000000000000000000000000000000..3328c01ba4ed30b2b495388bd394311c56c7beda GIT binary patch literal 30034 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%0A7}eVj=mw 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 i; let at = known ? pos[i - 1] : next, found = known ? i - 1 : pos.length, imgResult = null, timedOut = false; @@ -279,12 +284,18 @@ function parseIndex(searchParams) { return /^\d+$/.test(i) && +i >= 1 && +i <= MAX_INDEX ? +i : null; } -// Brave results, or Bing's if Brave has none +// Brave results, or Bing's if Brave has none or refuses (e.g. once its spending limit is hit). null if Bing fails too, +// which means we're out of searches rather than that nothing exists. async function webImageUrls(query, env, note) { - const urls = await braveImageSearch(query, env.BRAVE_API_KEY); - if (urls?.length) return urls; - note({ title: "Brave Search Empty", message: `No results for: ${query}. Trying Bing Fallback.`, tags: "warning,mag", priority: 3 }); - return (await bingImageSearchFallback(query)) || []; + const brave = await braveImageSearch(query, env.BRAVE_API_KEY); + if (brave.urls?.length) return brave.urls; + note(brave.error + ? { title: "Brave Unavailable", message: `Brave failed for: ${query}. Trying Bing Fallback.\nBrave: ${brave.error}`, tags: "warning,mag", priority: 3 } + : { title: "Brave Search Empty", message: `No results for: ${query}. Trying Bing Fallback.`, tags: "warning,mag", priority: 3 }); + const bing = await bingImageSearchFallback(query); + if (bing.urls) return bing.urls; + note({ title: "Bing Blocked, Served donate.webp", message: `Bing failed for: ${query}\nBing: ${bing.error}\nBrave: ${brave.error || "no results"}`, tags: "rotating_light,x", priority: 4 }); + return null; } async function setupSurreal(env) { diff --git a/functions/_utils/bing.js b/functions/_utils/bing.js index 3466aa4..9499cd7 100644 --- a/functions/_utils/bing.js +++ b/functions/_utils/bing.js @@ -1,3 +1,5 @@ +// { urls }, or { error } if Bing failed. Bing finds something for nearly any query (even gibberish), so a page without +// results almost always means it's blocking us (e.g. with a CAPTCHA) or changed its page format. export async function bingImageSearchFallback(query) { try { // SafeSearch off, like Brave. The cookie is what actually sets it; adlt= alone is ignored. @@ -8,24 +10,14 @@ export async function bingImageSearchFallback(query) { "Cookie": "SRCHHPGUSR=ADLT=OFF" } }); - if (!res.ok) return null; + if (!res.ok) return { error: `HTTP ${res.status}` }; const html = await res.text(); - const urls = []; - + // Bing encodes the image payload inside HTML entity attributes and script tags - const regex1 = /"murl":"(https:[^&"]+)"/g; - const regex2 = /"murl":"(https:[^"]+)"/g; - - let match; - while ((match = regex1.exec(html)) !== null) { - urls.push(match[1].replace(/\\\//g, '/')); - } - while ((match = regex2.exec(html)) !== null) { - urls.push(match[1].replace(/\\\//g, '/')); - } - - return urls.length > 0 ? [...new Set(urls)] : null; + const urls = [...html.matchAll(/"murl":"(https:[^&"]+)"/g), ...html.matchAll(/"murl":"(https:[^"]+)"/g)] + .map(m => m[1].replace(/\\\//g, '/')); + return urls.length ? { urls: [...new Set(urls)] } : { error: `No results in page (${html.length} bytes, ${res.url})` }; } catch (err) { - return null; + return { error: err.message }; } } diff --git a/functions/_utils/brave.js b/functions/_utils/brave.js index b35d9e6..0ee2292 100644 --- a/functions/_utils/brave.js +++ b/functions/_utils/brave.js @@ -1,8 +1,13 @@ +// { urls } ([] if none found), or { error } if Brave refused or failed, e.g. once our rate or spending limit is hit export async function braveImageSearch(query, apiKey) { - const res = await fetch(`https://api.search.brave.com/res/v1/images/search?q=${encodeURIComponent(query)}&count=50&safesearch=off`, { - headers: { "Accept": "application/json", "X-Subscription-Token": apiKey }, - }); - if (!res.ok) return null; - const data = await res.json(); - return data.results?.map(r => r.properties?.url || r.thumbnail?.src).filter(url => !!url) || null; + try { + const res = await fetch(`https://api.search.brave.com/res/v1/images/search?q=${encodeURIComponent(query)}&count=50&safesearch=off`, { + headers: { "Accept": "application/json", "X-Subscription-Token": apiKey }, + }); + if (!res.ok) return { error: `HTTP ${res.status}: ${(await res.text()).slice(0, 300)}` }; + const data = await res.json(); + return { urls: data.results?.map(r => r.properties?.url || r.thumbnail?.src).filter(url => !!url) || [] }; + } catch (err) { + return { error: err.message }; + } }