feat: add content script that collects video ids from YouTube pages

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
IgorVolochay
2026-10-25 10:57:49 +03:00
co-authored by Claude Sonnet 5.5
parent c95da940eb
commit 15ac3ba570
2 changed files with 90 additions and 1 deletions
+14 -1
View File
@@ -7,5 +7,18 @@
"host_permissions": ["https://www.googleapis.com/youtube/v3/*"], "host_permissions": ["https://www.googleapis.com/youtube/v3/*"],
"background": { "service_worker": "src/background.js", "type": "module" }, "background": { "service_worker": "src/background.js", "type": "module" },
"action": { "default_title": "Outlier Saver", "default_popup": "src/popup/popup.html" }, "action": { "default_title": "Outlier Saver", "default_popup": "src/popup/popup.html" },
"options_ui": { "page": "src/options/options.html", "open_in_tab": true } "options_ui": { "page": "src/options/options.html", "open_in_tab": true },
"content_scripts": [
{
"matches": ["https://www.youtube.com/*"],
"js": ["src/content.js"],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
{
"resources": ["src/lib/video-id.js", "src/lib/vidiq.js"],
"matches": ["https://www.youtube.com/*"]
}
]
} }
+76
View File
@@ -0,0 +1,76 @@
(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 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;
// videoId -> true if already sent together with a VidIQ score, false if sent without one.
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);
};
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);
try {
chrome.runtime.sendMessage({ type: 'videos-seen', items }).catch(() => {});
} catch {
observer.disconnect(); // the extension was reloaded; this script is orphaned
}
}
function scheduleScan() {
if (scanTimer === null) scanTimer = setTimeout(scan, SCAN_INTERVAL_MS);
}
const observer = new MutationObserver(scheduleScan);
observer.observe(document.documentElement, { childList: true, subtree: true });
document.addEventListener('yt-navigate-finish', () => {
sent = new Map();
scheduleScan();
});
scheduleScan();
})();