1
mirror of https://github.com/jakejarvis/hoot.git synced 2025-10-18 22:34:25 -04:00
Files
hoot/server/services/favicon.ts

93 lines
2.9 KiB
TypeScript

import { getOrCreateCachedAsset } from "@/lib/cache";
import { FAVICON_TTL_SECONDS, USER_AGENT } from "@/lib/constants";
import { fetchWithTimeout } from "@/lib/fetch";
import { convertBufferToImageCover } from "@/lib/image";
import { ns } from "@/lib/redis";
import { uploadImage } from "@/lib/storage";
const DEFAULT_SIZE = 32;
const REQUEST_TIMEOUT_MS = 1500; // per each method
function buildSources(domain: string): string[] {
const enc = encodeURIComponent(domain);
return [
`https://icons.duckduckgo.com/ip3/${enc}.ico`,
`https://www.google.com/s2/favicons?domain=${enc}&sz=${DEFAULT_SIZE}`,
`https://${domain}/favicon.ico`,
`http://${domain}/favicon.ico`,
];
}
export async function getOrCreateFaviconBlobUrl(
domain: string,
): Promise<{ url: string | null }> {
const indexKey = ns("favicon", "url", domain, String(DEFAULT_SIZE));
const lockKey = ns("lock", "favicon", domain, String(DEFAULT_SIZE));
const ttl = FAVICON_TTL_SECONDS;
return await getOrCreateCachedAsset({
indexKey,
lockKey,
ttlSeconds: ttl,
eventName: "favicon_fetch",
baseMetrics: { domain, size: DEFAULT_SIZE },
purgeQueue: "favicon",
produceAndUpload: async () => {
const sources = buildSources(domain);
for (const src of sources) {
try {
const res = await fetchWithTimeout(
src,
{
redirect: "follow",
headers: {
Accept:
"image/avif,image/webp,image/png,image/*;q=0.9,*/*;q=0.8",
"User-Agent": USER_AGENT,
},
},
{ timeoutMs: REQUEST_TIMEOUT_MS },
);
if (!res.ok) continue;
const contentType = res.headers.get("content-type");
const ab = await res.arrayBuffer();
const buf = Buffer.from(ab);
const webp = await convertBufferToImageCover(
buf,
DEFAULT_SIZE,
DEFAULT_SIZE,
contentType,
);
if (!webp) continue;
const { url, key } = await uploadImage({
kind: "favicon",
domain,
width: DEFAULT_SIZE,
height: DEFAULT_SIZE,
buffer: webp,
});
const source = (() => {
if (src.includes("icons.duckduckgo.com")) return "duckduckgo";
if (src.includes("www.google.com/s2/favicons")) return "google";
if (src.startsWith("https://")) return "direct_https";
if (src.startsWith("http://")) return "direct_http";
return "unknown";
})();
return {
url,
key,
metrics: {
source,
upstream_status: res.status,
upstream_content_type: contentType ?? null,
},
};
} catch {
// try next source
}
}
return { url: null };
},
});
}