From 78b3ac85b00d56bfd775e296eca08ea3689532c8 Mon Sep 17 00:00:00 2001 From: type-two Date: Tue, 7 Jul 2026 19:00:38 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20tile-link=20diffing=20+=20grid=20census?= =?UTF-8?q?=20=E2=80=94=20video=20harvest=20stops=20flying=20blind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - snapshotAssets also keys on /project/…/edit/ tile anchors: stable https, survive reloads (video blob srcs don't) — the reliable new-asset signal - harvestFresh splits diff into direct media (downloaded) vs tile links (reported as assets when no media url — credits not lost, recoverable from the library) - gridCensus appended to every miss so failures describe the actual harvest surface Co-Authored-By: Claude Fable 5 --- content.js | 53 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/content.js b/content.js index d39d43d..e30fc3d 100644 --- a/content.js +++ b/content.js @@ -199,6 +199,9 @@ function snapshotAssets() { scope.querySelectorAll('a[href]').forEach(a => { if (a.hasAttribute('download') || /googleusercontent|media\.getMediaUrl|\/media\//i.test(a.href)) push(a.href); }); + // grid tiles are anchors to /project//edit/ — stable https keys that + // survive reloads (video blob: srcs don't). The most reliable "new asset" signal. + scope.querySelectorAll('a[href*="/edit/"]').forEach(a => push(a.href)); // images: match media hosts + the redirect API + blobs (diffed before/after, so only NEW ones count) scope.querySelectorAll('img[src]').forEach(i => { if (/googleusercontent|lh3|ggpht|storage|media\.getMediaUrl|^blob:/i.test(i.src)) push(i.src); @@ -277,6 +280,28 @@ async function waitForResultOrError(baseCount, maxMs, maxRetries = 3) { return null; } +// Split a fresh-URL diff into direct media vs tile links, download the media. +async function harvestFresh(id, kind, category, fresh) { + const tiles = fresh.filter(u => /\/edit\//.test(u)); + const media = fresh.filter(u => !/\/edit\//.test(u)); + const ext = kind === 'video' ? 'mp4' : 'png'; + const cat = (category || 'misc').replace(/[^a-z0-9_\-]/gi, '_'); + let saved = 0; + for (let i = 0; i < media.length; i++) { + const r = await bg({ type: 'download', url: media[i], filename: `flowrinse/${cat}/${id}_${i}.${ext}` }); + if (r && r.ok) saved++; + } + return { saved, media, tiles }; +} + +// One line describing the harvest surface — appended to failures so a miss tells +// us what the grid actually looked like instead of leaving us guessing. +function gridCensus() { + const q = s => document.querySelectorAll(s).length; + return `census: video=${q('video')} videoSrc=${q('video[src]')} poster=${q('video[poster]')} ` + + `img=${q('img[src]')} editTiles=${q('a[href*="/edit/"]')} dl=${q('a[download]')}`; +} + // ---- queue loop ---------------------------------------------------------- // Entry point (Start / reload). Kicks the first poll soon; the rest is paced. @@ -428,17 +453,9 @@ async function executeTask(task) { const after = snapshotAssets(); const fresh = [...after].filter(u => !before.has(u)); - log('success', `Harvested ${fresh.length} asset(s).`); - - // Browser download — uses your session, no CSP/CORS. Lands in Downloads/flowrinse//. - const ext = task.kind === 'video' ? 'mp4' : 'png'; - const cat = (task.category || 'misc').replace(/[^a-z0-9_\-]/gi, '_'); - let saved = 0; - for (let i = 0; i < fresh.length; i++) { - const r = await bg({ type: 'download', url: fresh[i], filename: `flowrinse/${cat}/${task.id}_${i}.${ext}` }); - if (r && r.ok) saved++; - } - if (fresh.length) log(saved === fresh.length ? 'success' : 'info', `Downloaded ${saved}/${fresh.length} → Downloads/flowrinse/${cat}/`); + const { saved, media, tiles } = await harvestFresh(task.id, task.kind, task.category, fresh); + if (media.length) log('success', `Downloaded ${saved}/${media.length} asset(s) → Downloads/flowrinse/.`); + else if (tiles.length) log('info', `New tile detected but no direct media URL — reporting the tile link (recover later). ${gridCensus()}`); await reportCompletion(task.id, 'success', null, fresh); chrome.storage.local.remove('requeue_' + task.id); successCount++; consecutiveRateLimits = 0; @@ -475,23 +492,19 @@ async function runPostReloadHarvest(ph) { if ((ph.tries || 0) < 2) { ph.tries = (ph.tries || 0) + 1; await new Promise(r => chrome.storage.local.set({ pendingHarvest: ph }, r)); - log('info', `Post-refresh grid shows nothing new yet — refresh ${ph.tries}/2 and re-check.`); + log('info', `Post-refresh grid shows nothing new yet — refresh ${ph.tries}/2 and re-check. ${gridCensus()}`); await sleep(10000); const r = await bg({ type: 'hardReload' }); if (!r || !r.ok) location.reload(); return; } chrome.storage.local.remove('pendingHarvest'); - await reportCompletion(ph.id, 'failed', 'video generated but never surfaced in the grid after 3 loads', []); + await reportCompletion(ph.id, 'failed', `video generated but never surfaced in the grid after 3 loads — ${gridCensus()}`, []); return; } - const cat = (ph.category || 'misc').replace(/[^a-z0-9_\-]/gi, '_'); - let saved = 0; - for (let i = 0; i < fresh.length; i++) { - const r = await bg({ type: 'download', url: fresh[i], filename: `flowrinse/${cat}/${ph.id}_${i}.${ph.kind === 'video' ? 'mp4' : 'png'}` }); - if (r && r.ok) saved++; - } - log('success', `Post-refresh harvest: ${fresh.length} asset(s), downloaded ${saved} → Downloads/flowrinse/${cat}/.`); + const { saved, media, tiles } = await harvestFresh(ph.id, ph.kind, ph.category, fresh); + if (media.length) log('success', `Post-refresh harvest: downloaded ${saved}/${media.length} → Downloads/flowrinse/.`); + else if (tiles.length) log('info', `Post-refresh: new tile found, no direct media URL — reporting tile link (recover later). ${gridCensus()}`); chrome.storage.local.remove('pendingHarvest'); chrome.storage.local.remove('requeue_' + ph.id); await reportCompletion(ph.id, 'success', null, fresh);