feat: scan video cards and send VidIQ-scored ones to the service worker

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
IgorVolochay
2026-10-25 12:08:49 +03:00
co-authored by Claude Sonnet 5.5
parent cdf8c08c7f
commit fe65aee032
+33 -44
View File
@@ -1,65 +1,54 @@
(async () => { (async () => {
const { parseVideoLink, pageFromPath } = await import(chrome.runtime.getURL('src/lib/video-id.js')); const { pageFromPath } = await import(chrome.runtime.getURL('src/lib/video-id.js'));
const { readVidiqScore } = await import(chrome.runtime.getURL('src/lib/vidiq.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 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 sent = new Map();
let scanTimer = null; let scanTimer = null;
function collectItems(page) { // Cards on a channel page do not name the channel, so take it from the page itself.
const items = new Map(); function channelContext() {
const consider = (parsed, card) => { const [kind, name] = location.pathname.split('/').filter(Boolean);
const state = sent.get(parsed.videoId); const path = kind?.startsWith('@') ? kind : ['channel', 'c', 'user'].includes(kind) && name ? `${kind}/${name}` : null;
if (state === true) return; return {
const vidiqScore = readVidiqScore(card); channelUrl: path ? `https://www.youtube.com/${path}` : null,
if (state === false && vidiqScore === null) return; channelTitle: document.title.replace(/^\(\d+\)\s*/, '').replace(/\s*-\s*YouTube$/, '') || null,
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);
};
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() { function scan() {
scanTimer = null; scanTimer = null;
const page = pageFromPath(location.pathname); const page = pageFromPath(location.pathname);
if (!page) return; if (!page) return;
const items = collectItems(page); const context = page === 'channel' ? channelContext() : null;
if (items.length === 0) return;
for (const item of items) sent.set(item.videoId, item.vidiqScore !== 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 { try {
chrome.runtime.sendMessage({ type: 'videos-seen', items }).catch(() => {}); chrome.runtime.sendMessage({ type: 'cards', items: batch.map(({ item }) => item) }).catch(() => {});
} catch { } catch {
observer.disconnect(); // the extension was reloaded; this script is orphaned observer.disconnect(); // the extension was reloaded; this script is orphaned
return;
} }
if (found.size > batch.length) scheduleScan();
} }
function scheduleScan() { function scheduleScan() {