Resolve thumbnail-less embeds, including Discord CDN unfurls
This commit is contained in:
@@ -392,7 +392,8 @@ export class BackfillQueue {
|
||||
job.processed += progressed + result.ratelimited;
|
||||
|
||||
const elapsed = Math.max(1, this.now() - (job.startedAt ?? this.now()));
|
||||
job.perMinute = job.processed / (elapsed / 60_000);
|
||||
// Early in a run the measured rate is noise; report null until it settles.
|
||||
job.perMinute = elapsed >= 30_000 ? job.processed / (elapsed / 60_000) : null;
|
||||
|
||||
const after = this.engine.rateLimitStatus();
|
||||
if (after.active) this.limitedUntil = after.until ?? this.now() + after.remainingMs;
|
||||
|
||||
@@ -34,6 +34,11 @@ async function getText(url: string, options: DownloadOptions): Promise<string |
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS)
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
|
||||
// Never pull media bytes into memory just to look for an og:image.
|
||||
const type = response.headers.get("content-type") ?? "";
|
||||
if (type && !/text\/html|application\/xhtml|text\/plain/i.test(type)) return null;
|
||||
|
||||
return (await response.text()).slice(0, MAX_HTML_BYTES);
|
||||
} catch {
|
||||
return null;
|
||||
@@ -74,6 +79,16 @@ export async function resolveEmbedPreview(
|
||||
|
||||
const host = parsed.hostname.replace(/^www\./, "").toLowerCase();
|
||||
|
||||
// Discord unfurls a pasted attachment link into an embed. The URL is still a CDN file, so
|
||||
// the proxy can render a still (and a video frame) exactly like a plain attachment.
|
||||
if ((host === "cdn.discordapp.com" || host === "media.discordapp.net") && parsed.pathname.startsWith("/attachments/")) {
|
||||
parsed.hostname = "media.discordapp.net";
|
||||
parsed.searchParams.set("width", "320");
|
||||
parsed.searchParams.set("height", "320");
|
||||
parsed.searchParams.set("format", "webp");
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
if (host === "youtube.com" || host === "m.youtube.com" || host === "youtu.be") {
|
||||
const data = await getJson(
|
||||
`https://www.youtube.com/oembed?url=${encodeURIComponent(media.url)}&format=json`,
|
||||
|
||||
@@ -356,9 +356,9 @@ export class ScrapeStore {
|
||||
pendingPreviews(limit = 5000, includeEmbeds = false): MediaRow[] {
|
||||
const local =
|
||||
"((source IS NULL OR source != 'embed') AND kind IN ('image', 'gif', 'video', 'audio'))";
|
||||
const condition = includeEmbeds
|
||||
? `(${local} OR (source = 'embed' AND preview_url IS NOT NULL))`
|
||||
: local;
|
||||
// With embeds on, a thumbnail-less embed still needs a pass: the engine resolves one
|
||||
// from the provider or the page (or the Discord proxy for CDN attachment embeds).
|
||||
const condition = includeEmbeds ? `(${local} OR source = 'embed')` : local;
|
||||
|
||||
return this.db
|
||||
.prepare(
|
||||
|
||||
@@ -612,6 +612,68 @@ void (async () => {
|
||||
assert.equal(calls.some(url => url.includes("youtube.com/watch")), false);
|
||||
});
|
||||
|
||||
await test("pendingPreviews includes embeds without a stored thumbnail", () => {
|
||||
const dir = join(root, "pending-embeds");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const store = new ScrapeStore(dir);
|
||||
store.insertPage([
|
||||
message("670", {
|
||||
embeds: [
|
||||
{
|
||||
type: "video",
|
||||
url: "https://cdn.discordapp.com/attachments/1/2/clip.mp4",
|
||||
title: null,
|
||||
provider: "cdn.discordapp.com",
|
||||
videoUrl: "https://cdn.discordapp.com/attachments/1/2/clip.mp4",
|
||||
imageUrl: null,
|
||||
thumbnailUrl: null
|
||||
}
|
||||
]
|
||||
})
|
||||
]);
|
||||
|
||||
assert.equal(store.pendingPreviews(10, false).length, 0);
|
||||
assert.equal(store.pendingPreviews(10, true).length, 1);
|
||||
store.close();
|
||||
});
|
||||
|
||||
await test("CDN attachment embeds resolve through the Discord proxy", async () => {
|
||||
const resolved = await resolveEmbedPreview({
|
||||
url: "https://cdn.discordapp.com/attachments/1/2/clip.mp4",
|
||||
source: "embed",
|
||||
preview_url: null
|
||||
});
|
||||
|
||||
assert.ok(resolved);
|
||||
const parsed = new URL(String(resolved));
|
||||
assert.equal(parsed.hostname, "media.discordapp.net");
|
||||
assert.equal(parsed.searchParams.get("width"), "320");
|
||||
assert.equal(parsed.searchParams.get("format"), "webp");
|
||||
});
|
||||
|
||||
await test("the og:image fallback never reads media bytes", async () => {
|
||||
const seen: string[] = [];
|
||||
const resolved = await resolveEmbedPreview(
|
||||
{
|
||||
url: "https://example.com/clip.mp4",
|
||||
source: "embed",
|
||||
preview_url: null
|
||||
},
|
||||
{
|
||||
fetchImpl: async (input: any) => {
|
||||
seen.push(String(input));
|
||||
return new Response("binary", {
|
||||
status: 200,
|
||||
headers: { "content-type": "video/mp4" }
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(resolved, null);
|
||||
assert.equal(seen.length, 1);
|
||||
});
|
||||
|
||||
await test("engine caches previews in links mode, including embed thumbnails", async () => {
|
||||
const backend = new FakeBackend([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user