feat: tile-link diffing + grid census — video harvest stops flying blind

- snapshotAssets also keys on /project/…/edit/<uuid> 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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-07 19:00:38 +10:00
parent 39d133ddfb
commit 78b3ac85b0

View File

@ -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/<id>/edit/<uuid> — 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/<category>/.
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);