mirror of
https://github.com/direct-img/direct-img.link.git
synced 2026-10-11 23:55:19 +00:00
- free.direct-img.link serves only unrestricted (public domain/CC0) images from Openverse (license=cc0,pdm) and Wikimedia Commons (haslicense:unrestricted), with optional ?src=openverse|wikimedia - ?i=1..20 picks the i-th working image on both sites; broken links are skipped so every index is a different image - Openverse OAuth token is cached in KV and refreshed automatically - Skip Flickr "photo unavailable" placeholders (served with HTTP 200) - Free/indexed cache keys use uppercase prefixes; plain main-site keys are unchanged so existing cache entries stay valid - Update README, homepage, privacy policy and terms Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
38 lines
1.7 KiB
JavaScript
38 lines
1.7 KiB
JavaScript
const API = "https://api.openverse.org/v1";
|
|
// Uppercase so it can't collide with query cache keys, which are always lowercased
|
|
const TOKEN_KEY = "OPENVERSE_TOKEN";
|
|
|
|
// Tokens expire (~12h); client ID/secret are permanent, so fetch a new token whenever the cached one is gone
|
|
async function getToken(env) {
|
|
if (!env.OPENVERSE_CLIENT_ID || !env.OPENVERSE_CLIENT_SECRET) return null;
|
|
const cached = await env.DIRECT_IMG_CACHE.get(TOKEN_KEY);
|
|
if (cached) return cached;
|
|
const res = await fetch(`${API}/auth_tokens/token/`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: new URLSearchParams({ grant_type: "client_credentials", client_id: env.OPENVERSE_CLIENT_ID, client_secret: env.OPENVERSE_CLIENT_SECRET }),
|
|
});
|
|
if (!res.ok) return null;
|
|
const { access_token, expires_in } = await res.json();
|
|
await env.DIRECT_IMG_CACHE.put(TOKEN_KEY, access_token, { expirationTtl: Math.max(60, expires_in - 600) });
|
|
return access_token;
|
|
}
|
|
|
|
export async function openverseImageSearch(query, env, ua) {
|
|
const url = `${API}/images/?${new URLSearchParams({ q: query, license: "cc0,pdm", extension: "jpg,png,gif,webp", page_size: 20 })}`;
|
|
const search = token => fetch(url, { headers: { "User-Agent": ua, ...(token && { Authorization: `Bearer ${token}` }) } });
|
|
try {
|
|
const token = await getToken(env).catch(() => null);
|
|
let res = await search(token);
|
|
if (res.status === 401 && token) {
|
|
await env.DIRECT_IMG_CACHE.delete(TOKEN_KEY);
|
|
res = await search();
|
|
}
|
|
if (!res.ok) return null;
|
|
const data = await res.json();
|
|
return data.results?.map(r => r.url).filter(Boolean) || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|