From 3627e18ed7de73389c3b4a1b6b38f03647a4ec58 Mon Sep 17 00:00:00 2001 From: "planetrenox@protonmail.com" Date: Fri, 16 Oct 2026 02:50:00 +0000 Subject: [PATCH] Feat: Serve bad.webp instead of an AI image when free.* finds nothing Co-Authored-By: Claude Opus 5.5 --- README.md | 12 ++++++------ functions/[[path]].js | 6 +++--- index.html | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index aca314e..8c99450 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ https://direct-img.link/orange+cat?i=2 - Leading zeros are ignored (`?i=02` = `?i=2`) - Only images that download count, so broken links and deleted photos are skipped and every `i` is a different image. `?i=2` is the 2nd working image - Higher `i` values take a little longer the first time, since the working images before it are downloaded to count them -- If there aren't `i` working images, an [AI image](#ai-images) is served instead (except on [free.](#free-images) with a `src`, which serves `bad.webp`) +- If there aren't `i` working images, an [AI image](#ai-images) is served instead (except on [free.](#free-images), which serves `bad.webp`) - Each `i` is cached separately. A search's results are kept for **1 hour**, so other `i` values of the same query within that hour don't search again and **don't count toward the limit**. One that was already counted downloads only its own image ## Query Normalization @@ -85,7 +85,7 @@ Literal slashes (`/`) and dots (`.`) in the URL path are **rejected** to prevent - **Fragments (`#...`)** are never sent to the server by browsers - **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, or on [free.](#free-images) with a `src` +- **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, or on [free.](#free-images) - **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 @@ -105,7 +105,7 @@ Need images you can publish? `free.direct-img.link` only serves images marked ** - Both are optional and can be used alone or together, in any order (`?i=2&src=wikimedia` = `?src=wikimedia&i=2`) - `i` above `20` serves `max-i.webp`, which lists each site's range. Other invalid values serve `bad.webp` -- With a `src`, only that source's images are served: if it has no image (or fewer than `i` working ones), `bad.webp` is served (remembered for 24h) instead of an AI image. Without one, an AI image is served like on the main site +- Only free images are served: if there's no image (or fewer than `i` working ones), `bad.webp` is served (remembered for 24h) instead of an AI image. With a `src`, only that source's images are searched - Each `src` + `i` + query combination is cached separately. Like the main site, other `i` values within an hour of a search reuse its results and don't count as searches **Sources:** @@ -154,7 +154,7 @@ Limits change over time, so they aren't listed here. **Check the Rate Limits sec - **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 (or there's none, on `free.` with a `src`), the failure is remembered for 24h (`bad.webp`) +- Searches that find nothing count toward the limit. If the AI image served instead fails too (or there's none, on `free.`), 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 ## Caching @@ -234,7 +234,7 @@ Create in your Cloudflare dashboard: | `NTFY_URL` | ntfy.sh topic URL for alerts | Optional | | `GOATCOUNTER_URL` | GoatCounter site URL for image hit analytics (e.g. `https://direct-img.goatcounter.com`) | Optional | | `GOATCOUNTER_TOKEN` | GoatCounter API token with "Record pageviews" permission | Optional | -| `AURORA_API_KEY` | Key for the [AuroraIMG-6M API](https://github.com/direct-img/AuroraIMG-6M) (AI images and the nothing-found fallback). Without it, `ai.` serves `bad.webp` and searches that find nothing serve `bad.webp` | Optional | +| `AURORA_API_KEY` | Key for the [AuroraIMG-6M API](https://github.com/direct-img/AuroraIMG-6M) (AI images and the nothing-found fallback). Without it, `ai.` serves `bad.webp` and searches that find nothing serve `bad.webp` (as they always do on `free.`) | Optional | ### 6. WAF Rules @@ -267,7 +267,7 @@ Every request looks up its key here first, and a cached image is served from tha | `FREE:::` (e.g. `FREE:wikimedia:2:orange cat`) | free images | | `AI:::` (e.g. `AI:auroraimg-6m:2:frog wizard`) | AI images | -An AI image served because a search found nothing is stored under the search's own key, like any other image. A search where that also fails, or a `free.` search with a `src` (which gets no AI image), stores an empty object with `err` custom metadata instead, which serves `bad.webp` for 24h. After that the query is searched again. +An AI image served because a search found nothing is stored under the search's own key, like any other image. A search where that also fails, or a `free.` search (which gets no AI image), stores an empty object with `err` custom metadata instead, which serves `bad.webp` for 24h. After that the query is searched again. Add an object lifecycle rule in **R2 → direct-img-store → Settings** that applies to all prefixes and deletes uploaded objects after **90 days**. It's required: nothing else expires cached images. diff --git a/functions/[[path]].js b/functions/[[path]].js index b3af2dd..32d2cda 100644 --- a/functions/[[path]].js +++ b/functions/[[path]].js @@ -184,10 +184,10 @@ COMMIT TRANSACTION;`; }; // Nothing found: serve an AI image of the query instead, cached like any other image. bad.webp (kept ERR_SECONDS) is - // only left for when that fails too, or for free.* with a ?src=, which asked for that source's images only. + // only left for when that fails too, or for free.*, which serves free images only. const fail = async (t, m, tag, p) => { - const fallback = (!free || src === "auto") && await aiImage(DEFAULT_AI_SOURCE, query, i, env); - note({ title: fallback.success ? `${t}, Served AI Image` : t, message: fallback.success ? m : `${m}\n${fallback ? `AI fallback failed: ${fallback.reason}` : `No AI fallback with src=${src}`}`, tags: tag, priority: p }); + const fallback = !free && await aiImage(DEFAULT_AI_SOURCE, query, i, env); + note({ title: fallback.success ? `${t}, Served AI Image` : t, message: fallback && !fallback.success ? `${m}\nAI fallback failed: ${fallback.reason}` : m, tags: tag, priority: p }); if (fallback.success) return await store(fallback); await env.R2_IMAGES.put(r2Key, "", { customMetadata: { err: "1" } }); release(); diff --git a/index.html b/index.html index 4f37d43..c360492 100644 --- a/index.html +++ b/index.html @@ -95,7 +95,7 @@ Use images to complement your responses, powered by Brave. i1–20 (which result)1 -

* Both params are optional and work in any order. With src, only that source's images are served, so if it has none you get bad.webp instead of an AI image. Licenses come from each source and can be wrong, so check the original before publishing. Images from Openverse (made using the Openverse API, not endorsed or certified by Openverse) and Wikimedia Commons.

+

* Both params are optional and work in any order. Only free images are served, so if there are none you get bad.webp instead of an AI image. With src, only that source is searched. Licenses come from each source and can be wrong, so check the original before publishing. Images from Openverse (made using the Openverse API, not endorsed or certified by Openverse) and Wikimedia Commons.

@@ -113,7 +113,7 @@ Use images to complement your responses, powered by Brave. i1–1000 (a different image each)1 -

* The same query always gives the same image. When a search on direct-img.link or free.direct-img.link (without src) finds nothing, an AI image is served instead. Made with AuroraIMG-6M.

+

* The same query always gives the same image. When a search on direct-img.link finds nothing, an AI image is served instead. Made with AuroraIMG-6M.

@@ -135,7 +135,7 @@ Use images to complement your responses, powered by Brave.

Caching

-

Images are cached for 90 days. Subsequent requests for the same query are served instantly. A search's results are kept for an hour, so trying other ?i= values right after is free and fast. Searches that find nothing serve an AI image instead, cached the same way.

+

Images are cached for 90 days. Subsequent requests for the same query are served instantly. A search's results are kept for an hour, so trying other ?i= values right after is free and fast. Searches on direct-img.link that find nothing serve an AI image instead, cached the same way.