let whitelist = []; let whitelistHandles = []; let debugEnabled = false; const memberOnlyIndex = new Map(); const DATA_PREFIX = "chipperfluff-nobs"; /* ---------------- CSS injection ---------------- */ function injectStyleOnce() { if (document.getElementById("member-filter-style")) return; const style = document.createElement("style"); style.id = "member-filter-style"; style.textContent = ` [data-${DATA_PREFIX}-hidden="true"] { display: none !important; } `; document.head.appendChild(style); } injectStyleOnce(); /* ---------------- load whitelist ---------------- */ function debugLog(...args) { if (!debugEnabled) return; console.log("[MemberFilter]", ...args); } function debugGroup(...args) { if (!debugEnabled) return; console.group("[MemberFilter]", ...args); } function debugGroupEnd() { if (!debugEnabled) return; console.groupEnd(); } function normalizeKey(value) { return value .toLowerCase() .replace(/\u00a0/g, " ") .replace(/\s+/g, " ") .replace(/\u2022/g, "") .trim(); } function normalizeWhitelist(list) { const names = []; const handles = []; (list || []).forEach(item => { if (typeof item === "string") { if (item) names.push(normalizeKey(item)); return; } const name = normalizeKey(item.name || ""); const handle = normalizeKey(item.handle || ""); if (name) names.push(name); if (handle) handles.push(handle); }); return { names, handles }; } function loadSettings() { chrome.storage.local.get({ whitelist: [], debug: false }, data => { const { names, handles } = normalizeWhitelist(data.whitelist); whitelist = names; whitelistHandles = handles; debugEnabled = Boolean(data.debug); debugLog("settings loaded:", { whitelist, whitelistHandles, debugEnabled }); if (isWhitelistedChannelPage()) { revealAll(); return; } updateKnownVisibility(); process(); }); } loadSettings(); // Reload settings if popup changes them chrome.storage.onChanged.addListener(loadSettings); /* ---------------- detection logic ---------------- */ function process(root = document) { if (isWhitelistedChannelPage()) return; const badges = root.querySelectorAll("badge-shape"); badges.forEach(badge => { if (!badge.textContent?.includes("Nur für Kanalmitglieder")) return; const video = badge.closest( "ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer" ); if (!video) return; const titleEl = video.querySelector("#video-title"); const channelEl = video.querySelector("ytd-channel-name a") || video.querySelector(".ytd-channel-name a"); const title = titleEl?.textContent?.trim() ?? "(no title)"; const url = titleEl?.href ?? ""; const channel = channelEl?.textContent?.trim() ?? "(no channel)"; const channelKey = normalizeKey(channel); const id = getVideoId(video, url); video.setAttribute(`data-${DATA_PREFIX}-member-only`, "true"); if (channelKey) video.setAttribute(`data-${DATA_PREFIX}-channel`, channelKey); if (id) video.setAttribute(`data-${DATA_PREFIX}-id`, id); if (id) { memberOnlyIndex.set(id, { channelKey, hidden: false }); } const whitelisted = whitelist.includes(channelKey); debugGroup(); debugLog("Title :", title); debugLog("Channel:", channel); debugLog("URL :", url); debugLog("ID :", id || "(no id)"); debugLog("Whitelisted:", whitelisted); debugGroupEnd(); if (!whitelisted) { video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); if (id && memberOnlyIndex.has(id)) { memberOnlyIndex.get(id).hidden = true; } } else { video.removeAttribute(`data-${DATA_PREFIX}-hidden`); if (id && memberOnlyIndex.has(id)) { memberOnlyIndex.get(id).hidden = false; } } }); } function updateKnownVisibility() { if (isWhitelistedChannelPage()) { revealAll(); return; } for (const [id, meta] of memberOnlyIndex.entries()) { if (!meta.channelKey) continue; const video = findVideoById(id); if (!video) { memberOnlyIndex.delete(id); continue; } if (whitelist.includes(meta.channelKey)) { video.removeAttribute(`data-${DATA_PREFIX}-hidden`); meta.hidden = false; } else { video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); meta.hidden = true; } } } function isWhitelistedChannelPage() { const handle = getChannelHandleFromUrl(location.href); if (!handle) return false; return whitelistHandles.includes(normalizeKey(handle)); } function getChannelHandleFromUrl(url) { const match = url.match(/youtube\.com\/@([^/]+)/i); return match ? match[1] : ""; } function revealAll() { const videos = document.querySelectorAll( `[data-${DATA_PREFIX}-hidden="true"]` ); videos.forEach(video => video.removeAttribute(`data-${DATA_PREFIX}-hidden`)); } function getVideoId(video, url) { const dataId = video.getAttribute("data-video-id"); if (dataId) return dataId; if (url) return url; const fallbackId = video.id; return fallbackId || ""; } function getChannelKey(video) { const stored = video.getAttribute(`data-${DATA_PREFIX}-channel`); if (stored) return stored; const channelEl = video.querySelector("ytd-channel-name a") || video.querySelector(".ytd-channel-name a"); const channel = channelEl?.textContent?.trim() ?? ""; return channel ? normalizeKey(channel) : ""; } function findVideoById(id) { const escaped = cssEscape(id); return document.querySelector(`[data-${DATA_PREFIX}-id="${escaped}"]`); } function cssEscape(value) { if (window.CSS && CSS.escape) return CSS.escape(value); return String(value).replace(/["\\]/g, "\\$&"); } /* ---------------- initial + observer ---------------- */ process(); const observer = new MutationObserver(mutations => { for (const m of mutations) { for (const node of m.addedNodes) { if (node instanceof HTMLElement) { process(node); } } } }); observer.observe(document.body, { childList: true, subtree: true });