From 20941c007c04f8aef3c4cb95bb39e331deb5505d Mon Sep 17 00:00:00 2001 From: type-two Date: Wed, 8 Jul 2026 09:42:56 +1000 Subject: [PATCH] =?UTF-8?q?fix(harvest):=20credit-safe=20size=20picker=20?= =?UTF-8?q?=E2=80=94=20original=20size=20for=20images=20AND=20videos,=20ne?= =?UTF-8?q?ver=20the=20paid=20upscales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Video download flyout differs from images: 270p GIF / 720p Original Size / 1080p Upscaled / 4K Upscaled·50 credits. Old finder matched '1k' (image-only) and would have missed video originals. New findSizeOption() keys on 'original size' (common to both 1K and 720p rows) and hard-rejects any 'upscal'/'credit' row — the 4K video upscale costs 50 credits, so a stray click there spends real money. Proven against all flyout string variants (spaced + newline-collapsed). Co-Authored-By: Claude Fable 5 --- content.js | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/content.js b/content.js index bb9101b..f535aa0 100644 --- a/content.js +++ b/content.js @@ -551,6 +551,23 @@ function findMenuItem(re) { return leaf.length ? (leaf[0].closest('[role="menuitem"], li, button') || leaf[0]) : null; } +// The FREE "original size" download option. Images: "1K Original size". Videos: +// "720p Original Size". Both contain "original size" — that's the safe key. +// SAFETY: never return an "Upscaled" / "…credits" row. The 4K video upscale costs +// 50 credits; the 1080p upscale isn't free either. A stray click there spends money. +function findSizeOption() { + const cands = [...document.querySelectorAll('[role="menuitem"], [role="menu"] button, div, span, li')] + .filter(visible) + .filter(e => { + const t = (e.textContent || '').trim(); + return t.length < 40 && /original\s*size/i.test(t) && !/upscal|credit/i.test(t); + }); + if (!cands.length) return null; + // smallest matching node = the option leaf, not a container wrapping the whole flyout + cands.sort((a, b) => (a.textContent || '').length - (b.textContent || '').length); + return cands[0].closest('[role="menuitem"], li, button') || cands[0]; +} + let harvestAbort = false; // DL Mode from the popup: harvest what's on screen right now, no queue involved. @@ -599,21 +616,22 @@ async function executeHarvest(task) { 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. + // Stage 3/4: HOVER Download — the size flyout unfurls beside it (side depends on + // where the tile sits; 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. We pick "original size" (1K image / 720p video) and + // NEVER the paid upscales — findSizeOption enforces that. 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); + oneK = await waitFor(findSizeOption, 1600, 250); } 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); + oneK = await waitFor(findSizeOption, 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; } + if (!oneK) { missed++; seen.add(href); log('info', 'harvest: no "original size" option 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.