console.log("[YT-Erfasser] Content Script geladen"); const sentUrls = new Set(); function getVideoId(url) { const match = url.match(/[?&]v=([^&]+)/); return match ? match[1] : null; } function getThumbnailUrl(videoId) { return `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`; } // --- DOM Extractors --- function extractFromLockupViewModel(element) { const div = element.querySelector(".yt-lockup-view-model"); const classStr = div?.className || element.className || ""; const idMatch = classStr.match(/content-id-([\w-]+)/); let videoId = idMatch ? idMatch[1] : null; if (!videoId) { const link = element.querySelector('a[href*="/watch?v="]'); if (link) videoId = getVideoId(link.href); } if (!videoId) return null; const title = element.querySelector("a.yt-lockup-metadata-view-model__title > span") ?.textContent?.trim() || ""; if (!title) return null; const youtuber = element.querySelector('a.yt-core-attributed-string__link[href^="/@"]') ?.textContent?.trim() || "Unbekannt"; return { title, youtuber, thumbnail_url: getThumbnailUrl(videoId), youtube_url: `https://www.youtube.com/watch?v=${videoId}`, }; } function extractFromVideoRenderer(element) { const titleLink = element.querySelector("a#video-title"); if (!titleLink) return null; const videoId = getVideoId(titleLink.href); if (!videoId) return null; const title = titleLink.textContent?.trim() || element.querySelector("#video-title yt-formatted-string") ?.textContent?.trim() || ""; if (!title) return null; const youtuber = element.querySelector("ytd-channel-name a")?.textContent?.trim() || element.querySelector(".ytd-channel-name a")?.textContent?.trim() || "Unbekannt"; return { title, youtuber, thumbnail_url: getThumbnailUrl(videoId), youtube_url: `https://www.youtube.com/watch?v=${videoId}`, }; } function extractVideoFromCard(element) { const lockup = element.matches?.("yt-lockup-view-model") ? element : element.querySelector("yt-lockup-view-model"); if (lockup) return extractFromLockupViewModel(lockup); const renderer = element.matches?.("ytd-video-renderer") ? element : element.querySelector("ytd-video-renderer"); if (renderer) return extractFromVideoRenderer(renderer); return null; } // --- Processing --- function processCard(element) { const video = extractVideoFromCard(element); if (!video) return; if (sentUrls.has(video.youtube_url)) return; sentUrls.add(video.youtube_url); console.log("[YT-Erfasser]", video.title, "-", video.youtuber); browser.runtime.sendMessage(video); } function scanExistingCards() { document .querySelectorAll("yt-lockup-view-model, ytd-video-renderer") .forEach((el) => processCard(el)); } // --- MutationObserver (debounced) --- let pendingElements = []; let debounceTimer = null; function processPendingElements() { const elements = pendingElements; pendingElements = []; debounceTimer = null; for (const el of elements) { if (el.matches?.("yt-lockup-view-model, ytd-video-renderer")) { processCard(el); } el.querySelectorAll?.("yt-lockup-view-model, ytd-video-renderer").forEach( (card) => processCard(card) ); } } const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; pendingElements.push(node); } } if (pendingElements.length > 0 && !debounceTimer) { debounceTimer = setTimeout(processPendingElements, 250); } }); observer.observe(document.body, { childList: true, subtree: true }); // --- SPA Navigation --- document.addEventListener("yt-navigate-finish", () => { setTimeout(scanExistingCards, 500); }); // --- Init --- scanExistingCards();