diff --git a/background.js b/background.js index 57d3e9a..c806044 100644 --- a/background.js +++ b/background.js @@ -29,10 +29,13 @@ async function ensure(tabId) { const ENTER = { key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 }; const ESC = { key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27 }; -// While a harvest pass runs, route every browser download into flowrinse/harvest/. +// While a harvest pass runs, route every browser download into flowrinse/harvest/ +// and count them — the content script uses the count to VERIFY each 1K click fired. let harvestRouting = false; +let harvestCount = 0; chrome.downloads.onDeterminingFilename.addListener((item, suggest) => { if (!harvestRouting) return; + harvestCount++; suggest({ filename: `flowrinse/harvest/${(item.filename || 'asset').split(/[\\/]/).pop()}`, conflictAction: 'uniquify' }); }); @@ -75,6 +78,8 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } else if (msg.type === 'harvestRouting') { harvestRouting = !!msg.on; sendResponse({ ok: true }); + } else if (msg.type === 'harvestStats') { + sendResponse({ ok: true, count: harvestCount }); } else if (msg.type === 'hardReload') { // Cache-bypass reload — the by-hand fix for a wedged Flow session. if (attached.has(tabId)) { chrome.debugger.detach({ tabId }, () => {}); attached.delete(tabId); } diff --git a/content.js b/content.js index a4b2c17..0ce71cb 100644 --- a/content.js +++ b/content.js @@ -569,27 +569,50 @@ async function executeHarvest(task) { } idle = 0; const href = tile.href.split('#')[0]; - tile.scrollIntoView({ block: 'center' }); await sleep(600); - await bg({ type: 'hoverAt', ...center(tile) }); await sleep(600); - const dots = findTileMenuButton(tile); - if (!dots) { missed++; seen.add(href); log('info', `harvest: no ⋮ on a tile — ${gridCensus()}`); continue; } - await bg({ type: 'clickAt', ...center(dots) }); await sleep(800); - const dl = findMenuItem(/^\s*download\b/i); - if (!dl) { missed++; seen.add(href); log('info', `harvest: menu open but no Download item — ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; } - // "Download" is a submenu parent: HOVER unfurls the nested 1K/2K/4K flyout - // (clicking it closes the menu — the bug the first pass hit). - await bg({ type: 'hoverAt', ...center(dl) }); await sleep(800); - let oneK = findMenuItem(/\b1k\b|original/i); - if (!oneK) { // some builds expand on click instead — try once - await bg({ type: 'clickAt', ...center(dl) }); await sleep(800); - oneK = findMenuItem(/\b1k\b|original/i); + tile.scrollIntoView({ block: 'center' }); await sleep(700); + + // Stage 1/4: hover the tile until the ♡ ↻ ⋮ pill renders + await bg({ type: 'hoverAt', ...center(tile) }); + const dots = await waitFor(() => findTileMenuButton(tile), 3000, 300); + if (!dots) { missed++; seen.add(href); log('info', `harvest: control pill never appeared — ${gridCensus()}`); continue; } + + // Stage 2/4: click ⋮ ("More"), wait for the menu with Download in it + await bg({ type: 'clickAt', ...center(dots) }); + const dl = await waitFor(() => findMenuItem(/^\s*download\b/i), 3000, 300); + if (!dl) { missed++; seen.add(href); log('info', `harvest: ⋮ clicked but no Download item — ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + + // Stage 3/4: HOVER Download — the 1K/2K/4K flyout unfurls beside it (which side + // depends on where the tile sits on screen; irrelevant, coords are re-queried + // fresh). Jiggle the pointer a few px and retry — one mouseMoved sometimes + // isn't enough for the hover intent to register. + let oneK = null; + for (let j = 0; j < 3 && !oneK; j++) { + const c = center(dl); + await bg({ type: 'hoverAt', x: c.x - 4 + j * 4, y: c.y }); + oneK = await waitFor(() => findMenuItem(/\b1k\b|original size/i), 1600, 250); } - if (!oneK) { missed++; seen.add(href); log('info', 'harvest: Download hovered but no 1K flyout appeared.'); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + if (!oneK) { // last resort: some builds expand on click + await bg({ type: 'clickAt', ...center(dl) }); + oneK = await waitFor(() => findMenuItem(/\b1k\b|original size/i), 1600, 250); + } + if (!oneK) { missed++; seen.add(href); log('info', 'harvest: no 1K flyout after hover + click-expand.'); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + + // Stage 4/4: walk the pointer INTO the flyout (keeps it open), then click 1K, + // then VERIFY a download event actually fired before counting it. + const dlBefore = ((await bg({ type: 'harvestStats' })) || {}).count || 0; + await bg({ type: 'hoverAt', ...center(oneK) }); await sleep(350); await bg({ type: 'clickAt', ...center(oneK) }); - got++; seen.add(href); + let fired = false; + for (let j = 0; j < 12 && !fired; j++) { + await sleep(500); + fired = (((await bg({ type: 'harvestStats' })) || {}).count || 0) > dlBefore; + } + if (fired) { got++; if (got % 10 === 0) log('info', `harvest: ${got} downloaded so far…`); } + else { missed++; log('info', 'harvest: clicked 1K but no download event fired.'); } + seen.add(href); chrome.storage.local.set({ harvested_urls_v2: [...seen].slice(-3000) }); - if (got % 10 === 0) log('info', `harvest: ${got} downloaded so far…`); - await sleep(1800 + Math.random() * 1500); // pace — don't machine-gun the account + await bg({ type: 'pressEscape' }); await sleep(300); // ensure menus are closed before the next tile + await sleep(1200 + Math.random() * 1200); // pace — don't machine-gun the account } log('success', `Harvest pass done: ${got} downloaded, ${missed} missed, ${skipped} skipped → Downloads/flowrinse/harvest/`); await reportCompletion(task.id, 'success', null, [`downloaded:${got}`, `missed:${missed}`]);