From fe65aee032c101de792ff6bf6820719d26498d67 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sun, 25 Oct 2026 12:08:49 +0300 Subject: [PATCH] feat: scan video cards and send VidIQ-scored ones to the service worker Co-Authored-By: Claude Sonnet 5.5 --- src/content.js | 77 ++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/src/content.js b/src/content.js index f4a1ea6..794041b 100644 --- a/src/content.js +++ b/src/content.js @@ -1,65 +1,54 @@ (async () => { - const { parseVideoLink, pageFromPath } = await import(chrome.runtime.getURL('src/lib/video-id.js')); - const { readVidiqScore } = await import(chrome.runtime.getURL('src/lib/vidiq.js')); + const { pageFromPath } = await import(chrome.runtime.getURL('src/lib/video-id.js')); + const { CARD_SELECTOR, LINK_SELECTOR, scrapeCard } = await import(chrome.runtime.getURL('src/lib/card.js')); - const LINK_SELECTOR = 'a[href*="/watch?v="], a[href*="/shorts/"]'; - // Only used to find the element that contains a VidIQ badge; ID collection does not depend on it. - const CARD_SELECTOR = [ - 'ytd-rich-item-renderer', - 'ytd-video-renderer', - 'ytd-compact-video-renderer', - 'ytd-grid-video-renderer', - 'yt-lockup-view-model', - 'ytm-shorts-lockup-view-model', - 'ytd-reel-item-renderer', - ].join(','); const SCAN_INTERVAL_MS = 500; + const MAX_ITEMS_PER_MESSAGE = 200; - // videoId -> true if already sent together with a VidIQ score, false if sent without one. + // videoId -> "outlierScore|views|subscribers" of the last message that carried the video. + // VidIQ may draw or update its numbers after the card appears, so a changed signature is sent again. let sent = new Map(); let scanTimer = null; - function collectItems(page) { - const items = new Map(); - const consider = (parsed, card) => { - const state = sent.get(parsed.videoId); - if (state === true) return; - const vidiqScore = readVidiqScore(card); - if (state === false && vidiqScore === null) return; - const item = items.get(parsed.videoId) ?? { - videoId: parsed.videoId, - vidiqScore: null, - viaShortsUrl: false, - page, - }; - item.vidiqScore ??= vidiqScore; - item.viaShortsUrl ||= parsed.isShort; - items.set(parsed.videoId, item); + // Cards on a channel page do not name the channel, so take it from the page itself. + function channelContext() { + const [kind, name] = location.pathname.split('/').filter(Boolean); + const path = kind?.startsWith('@') ? kind : ['channel', 'c', 'user'].includes(kind) && name ? `${kind}/${name}` : null; + return { + channelUrl: path ? `https://www.youtube.com/${path}` : null, + channelTitle: document.title.replace(/^\(\d+\)\s*/, '').replace(/\s*-\s*YouTube$/, '') || null, }; - - for (const link of document.querySelectorAll(LINK_SELECTOR)) { - const parsed = parseVideoLink(link.getAttribute('href')); - if (parsed) consider(parsed, link.closest(CARD_SELECTOR) ?? link.parentElement); - } - if (page === 'watch') { - const current = parseVideoLink(location.href); - if (current) consider(current, null); - } - return [...items.values()]; } function scan() { scanTimer = null; const page = pageFromPath(location.pathname); if (!page) return; - const items = collectItems(page); - if (items.length === 0) return; - for (const item of items) sent.set(item.videoId, item.vidiqScore !== null); + const context = page === 'channel' ? channelContext() : null; + + const found = new Map(); + const visited = new Set(); + for (const link of document.querySelectorAll(LINK_SELECTOR)) { + const card = link.closest(CARD_SELECTOR); + if (!card || visited.has(card)) continue; + visited.add(card); + const item = scrapeCard(card, link, context); + if (!item || item.outlierScore === null || item.views === null) continue; + const signature = `${item.outlierScore}|${item.views}|${item.subscribers}`; + if (sent.get(item.videoId) === signature) continue; + found.set(item.videoId, { item: { ...item, page }, signature }); + } + if (found.size === 0) return; + + const batch = [...found.values()].slice(0, MAX_ITEMS_PER_MESSAGE); + for (const { item, signature } of batch) sent.set(item.videoId, signature); try { - chrome.runtime.sendMessage({ type: 'videos-seen', items }).catch(() => {}); + chrome.runtime.sendMessage({ type: 'cards', items: batch.map(({ item }) => item) }).catch(() => {}); } catch { observer.disconnect(); // the extension was reloaded; this script is orphaned + return; } + if (found.size > batch.length) scheduleScan(); } function scheduleScan() {