This commit is contained in:
Marek
2026-04-05 14:54:10 +02:00
parent 7d66746969
commit 6bbadb69c7
11 changed files with 129 additions and 147 deletions

View File

@@ -2,143 +2,96 @@ 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 link = element.querySelector('a[href*="/watch?v="]');
if (!link) return null;
const renderer = element.matches?.("ytd-video-renderer")
? element
: element.querySelector("ytd-video-renderer");
if (renderer) return extractFromVideoRenderer(renderer);
const match = link.href.match(/[?&]v=([^&]+)/);
if (!match) return null;
return null;
const title = element.querySelector("h3 a")?.textContent?.trim();
if (!title) return null;
const thumbnail = element.querySelector('a[href*="/watch?v="] img')?.src;
const youtuber = element.querySelector('a[href^="/@"]')?.textContent?.trim() || "Unbekannt";
return {
title,
youtuber,
thumbnail_url: thumbnail || `https://img.youtube.com/vi/${match[1]}/hqdefault.jpg`,
youtube_url: `https://www.youtube.com/watch?v=${match[1]}`,
};
}
// --- 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;
function collectVideos(elements) {
const videos = [];
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 video = extractVideoFromCard(el);
if (!video) continue;
if (sentUrls.has(video.youtube_url)) continue;
sentUrls.add(video.youtube_url);
videos.push(video);
}
return videos;
}
// --- Debounced Batch-Versand ---
let pendingVideos = [];
let sendTimer = null;
function queueVideos(videos) {
pendingVideos.push(...videos);
if (!sendTimer) {
sendTimer = setTimeout(() => {
if (pendingVideos.length > 0) {
console.log(`[YT-Erfasser] ${pendingVideos.length} Videos senden`);
browser.runtime.sendMessage(pendingVideos);
}
pendingVideos = [];
sendTimer = null;
}, 250);
}
}
const observer = new MutationObserver((mutations) => {
// --- IntersectionObserver: nur sichtbare Cards erfassen ---
const visibilityObserver = new IntersectionObserver((entries) => {
const cards = entries.filter((e) => e.isIntersecting).map((e) => e.target);
if (cards.length > 0) {
queueVideos(collectVideos(cards));
}
}, { threshold: 0.5 });
function observeCard(el) {
visibilityObserver.observe(el);
}
// --- MutationObserver: neue Cards registrieren ---
const mutationObserver = 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 (node.matches?.("ytd-rich-item-renderer, ytd-video-renderer")) {
observeCard(node);
}
node.querySelectorAll?.("ytd-rich-item-renderer, ytd-video-renderer").forEach(observeCard);
}
}
if (pendingElements.length > 0 && !debounceTimer) {
debounceTimer = setTimeout(processPendingElements, 250);
}
});
observer.observe(document.body, { childList: true, subtree: true });
mutationObserver.observe(document.body, { childList: true, subtree: true });
// --- SPA Navigation ---
document.addEventListener("yt-navigate-finish", () => {
setTimeout(scanExistingCards, 500);
sentUrls.clear();
setTimeout(() => {
document.querySelectorAll("ytd-rich-item-renderer, ytd-video-renderer").forEach(observeCard);
}, 500);
});
// --- Init ---
scanExistingCards();
document.querySelectorAll("ytd-rich-item-renderer, ytd-video-renderer").forEach(observeCard);