fix(harvest): credit-safe size picker — original size for images AND videos, never the paid upscales

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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-08 09:42:56 +10:00
parent fc3e668504
commit 20941c007c

View File

@ -551,6 +551,23 @@ function findMenuItem(re) {
return leaf.length ? (leaf[0].closest('[role="menuitem"], li, button') || leaf[0]) : null; 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; let harvestAbort = false;
// DL Mode from the popup: harvest what's on screen right now, no queue involved. // 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); 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; } 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 // Stage 3/4: HOVER Download — the size flyout unfurls beside it (side depends on
// depends on where the tile sits on screen; irrelevant, coords are re-queried // where the tile sits; irrelevant, coords are re-queried fresh). Jiggle the
// fresh). Jiggle the pointer a few px and retry — one mouseMoved sometimes // pointer a few px and retry — one mouseMoved sometimes isn't enough for the
// isn't enough for the hover intent to register. // hover intent to register. We pick "original size" (1K image / 720p video) and
// NEVER the paid upscales — findSizeOption enforces that.
let oneK = null; let oneK = null;
for (let j = 0; j < 3 && !oneK; j++) { for (let j = 0; j < 3 && !oneK; j++) {
const c = center(dl); const c = center(dl);
await bg({ type: 'hoverAt', x: c.x - 4 + j * 4, y: c.y }); 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 if (!oneK) { // last resort: some builds expand on click
await bg({ type: 'clickAt', ...center(dl) }); 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, // 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. // then VERIFY a download event actually fired before counting it.