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:
co-authored by
Claude Sonnet 5.5
parent
cdf8c08c7f
commit
fe65aee032
+33
-44
@@ -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,
|
||||
// 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,
|
||||
};
|
||||
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() {
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user