From 78b5b31ac438d9d5b767e49e36dcc898fe34fc64 Mon Sep 17 00:00:00 2001 From: Marek Date: Sun, 5 Apr 2026 09:07:52 +0200 Subject: [PATCH] update --- browser_extension/background.js | 11 ++- browser_extension/content.js | 146 +++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/browser_extension/background.js b/browser_extension/background.js index 3160420..3e6d21b 100644 --- a/browser_extension/background.js +++ b/browser_extension/background.js @@ -1,2 +1,9 @@ -// TODO: Nachrichten vom Content Script empfangen und an Server senden -console.log("Background Script geladen"); +const SERVER_URL = "http://localhost:8000/videos"; + +browser.runtime.onMessage.addListener((video) => { + fetch(SERVER_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(video), + }).catch(() => {}); +}); diff --git a/browser_extension/content.js b/browser_extension/content.js index 59b1a62..f744a2b 100644 --- a/browser_extension/content.js +++ b/browser_extension/content.js @@ -1,2 +1,144 @@ -// TODO: YouTube-DOM auslesen und Videodaten extrahieren -console.log("YouTube Video Erfasser geladen"); +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();