// Current settings snapshot. let whitelist = []; let whitelistHandles = []; let debugEnabled = false; // In-memory index of known member-only videos. const memberOnlyIndex = new Map(); // id -> { channelKey, hidden } const sharedIndex = new Map(); // stableId -> meta for cross-tab HUD // Data-* attribute prefix for DOM tags. const DATA_PREFIX = "chipperfluff-nobs"; // nobs = "no-bs" (member-only videos) const HUD_DOT_ID = `${DATA_PREFIX}-hud-dot`; const HUD_PANEL_ID = `${DATA_PREFIX}-hud-panel`; let generatedIdCounter = 0; let hudDirty = false; let lastHudSignature = ""; const STORAGE_KEY = "memberOnlyHidden"; const MAX_GLOBAL_ITEMS = 200; const MAX_PER_CHANNEL = 20; let persistTimer = null; /* ---------------- CSS injection ---------------- */ // Inject a single style tag for hiding matched videos. 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; } #${HUD_DOT_ID} { position: fixed; right: 18px; bottom: 18px; width: 12px; height: 12px; border-radius: 999px; background: #9b9b9b; border: 2px solid #6f6f6f; box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.12); z-index: 2147483647; cursor: pointer; } #${HUD_DOT_ID}[data-${DATA_PREFIX}-active="true"] { background: #ff9c1a; border-color: #b04a00; box-shadow: 0 0 0 3px rgba(255, 140, 0, 0.4); animation: ${DATA_PREFIX}-pulse 1.8s ease-in-out infinite; } @keyframes ${DATA_PREFIX}-pulse { 0% { transform: scale(0.9); box-shadow: 0 0 0 0 rgba(255, 140, 0, 0.6); } 70% { transform: scale(1.1); box-shadow: 0 0 0 6px rgba(255, 140, 0, 0); } 100% { transform: scale(0.9); box-shadow: 0 0 0 0 rgba(255, 140, 0, 0); } } #${HUD_PANEL_ID} { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 520px; height: 520px; overflow: auto; padding: 10px; border-radius: 10px; border: 1px solid #b04a00; background: #fff6e8; color: #2b2014; font-size: 12px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); z-index: 2147483647; } #${HUD_PANEL_ID}[data-${DATA_PREFIX}-hidden="true"] { display: none; } .${DATA_PREFIX}-hud-title { font-weight: 700; margin: 0 0 8px; text-transform: uppercase; font-size: 11px; letter-spacing: 0.4px; } .${DATA_PREFIX}-channel { margin: 6px 0; padding: 6px 8px; border-radius: 8px; background: #fffaf0; border: 1px solid #e2d7be; } .${DATA_PREFIX}-channel-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; cursor: pointer; font-weight: 700; } .${DATA_PREFIX}-channel-header::before { content: "▸"; margin-right: 6px; color: #8b5a2a; font-weight: 700; } details[open] > .${DATA_PREFIX}-channel-header::before { content: "▾"; } .${DATA_PREFIX}-video-list { margin: 6px 0 0; padding-left: 18px; border-left: 2px solid #f0d8b4; } .${DATA_PREFIX}-hud-item { display: grid; grid-template-columns: 64px 1fr; gap: 8px; align-items: center; margin: 6px 0; padding: 6px; border-radius: 8px; background: #fffaf0; border: 1px solid #e2d7be; } .${DATA_PREFIX}-hud-thumb { width: 64px; height: 36px; object-fit: cover; border-radius: 4px; background: #eee2cc; } .${DATA_PREFIX}-hud-link { color: #5a2b00; text-decoration: none; font-weight: 700; } .${DATA_PREFIX}-hud-link:hover { text-decoration: underline; } `; document.head.appendChild(style); } injectStyleOnce(); /* ---------------- load whitelist ---------------- */ // Debug helpers (no output unless debug is enabled). 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(); } // Normalize strings so matching is consistent. function normalizeKey(value) { return value .toLowerCase() .replace(/\u00a0/g, " ") .replace(/\s+/g, " ") .replace(/\u2022/g, "") .trim(); } // Support legacy strings and new { name, handle } entries. 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 }; } // Read settings and re-apply visibility immediately. 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(); updateHudDot(); }); } loadSettings(); loadStoredIndex(); // Reload settings or shared HUD data if storage changes. chrome.storage.onChanged.addListener(changes => { if (changes[STORAGE_KEY]) { const items = changes[STORAGE_KEY].newValue || []; sharedIndex.clear(); items.forEach(item => sharedIndex.set(item.id, item)); updateHudDot(); } if (changes.whitelist || changes.debug) { loadSettings(); } }); /* ---------------- detection logic ---------------- */ // Detect member-only videos and tag them for future updates. 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 title = titleEl?.textContent?.trim() ?? "(no title)"; const url = titleEl?.href ?? ""; const thumbImg = video.querySelector("ytd-thumbnail img"); const thumb = thumbImg?.getAttribute("src") || thumbImg?.getAttribute("data-thumb") || thumbImg?.getAttribute("data-src") || ""; const channelInfo = getChannelInfo(video); const channel = channelInfo.label || "(no channel)"; const channelUrl = channelInfo.url || ""; const channelKey = channelInfo.key; const id = getOrCreateVideoId(video, url); const stableId = getStableId(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) { const existing = memberOnlyIndex.get(id); if (!existing) { memberOnlyIndex.set(id, { channelKey, channelLabel: channel, channelUrl, stableId, hidden: false, title, url, thumb }); hudDirty = true; } } 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)) { const meta = memberOnlyIndex.get(id); if (!meta.hidden) hudDirty = true; meta.hidden = true; updateSharedIndex(meta, true); } } else { video.removeAttribute(`data-${DATA_PREFIX}-hidden`); if (id && memberOnlyIndex.has(id)) { const meta = memberOnlyIndex.get(id); if (meta.hidden) hudDirty = true; meta.hidden = false; updateSharedIndex(meta, false); } } }); updateHudDot(); } // Apply whitelist to already-tagged member-only videos. 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`); if (meta.hidden) hudDirty = true; meta.hidden = false; updateSharedIndex(meta, false); } else { video.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); if (!meta.hidden) hudDirty = true; meta.hidden = true; updateSharedIndex(meta, true); } } updateHudDot(); } // Allow full visibility on whitelisted channel pages. function isWhitelistedChannelPage() { const handle = getChannelHandleFromUrl(location.href); if (!handle) return false; return whitelistHandles.includes(normalizeKey(handle)); } // Extract @handle from a channel URL. function getChannelHandleFromUrl(url) { const match = url.match(/youtube\.com\/@([^/]+)/i); return match ? match[1] : ""; } // Remove hidden flags from any tagged videos. function revealAll() { const videos = document.querySelectorAll( `[data-${DATA_PREFIX}-hidden="true"]` ); videos.forEach(video => video.removeAttribute(`data-${DATA_PREFIX}-hidden`)); } // Choose a stable identifier for indexing. function getOrCreateVideoId(video, url) { const dataId = video.getAttribute("data-video-id"); if (dataId) return dataId; if (url) return url; const fallbackId = video.id; if (fallbackId) return fallbackId; const generated = `mf-${generatedIdCounter++}`; video.setAttribute(`data-${DATA_PREFIX}-id`, generated); return generated; } function getStableId(video, url) { const dataId = video.getAttribute("data-video-id"); if (dataId) return dataId; if (url) return url; return ""; } // Read the channel key from DOM or fallback to text. function getChannelKey(video) { const stored = video.getAttribute(`data-${DATA_PREFIX}-channel`); if (stored) return stored; return getChannelInfo(video).key; } function getChannelKeyFromData(channelText, channelUrl) { const handle = getChannelHandleFromUrl(channelUrl); if (handle) return normalizeKey(handle); return channelText ? normalizeKey(channelText) : ""; } function getChannelInfo(video) { const channelBlock = video.querySelector("ytd-channel-name") || video.querySelector(".ytd-channel-name"); const linkEl = channelBlock?.querySelector("a") || video.querySelector('a[href^="/@"]') || video.querySelector('a[href^="/channel/"]') || video.querySelector('a[href^="/c/"]'); const labelEl = channelBlock?.querySelector("#text") || channelBlock?.querySelector("yt-formatted-string") || channelBlock; const label = labelEl?.textContent?.trim() ?? ""; const url = linkEl?.href ?? ""; const handleFromUrl = getChannelHandleFromUrl(url); const handleFromPage = getChannelHandleFromUrl(location.href); const fallbackLabel = handleFromUrl || handleFromPage || ""; const finalLabel = label || fallbackLabel; const key = getChannelKeyFromData(finalLabel, url || handleFromPage); return { label: finalLabel, url: url || handleFromPage, key }; } // Find a video element by stored id. function findVideoById(id) { const escaped = cssEscape(id); return document.querySelector(`[data-${DATA_PREFIX}-id="${escaped}"]`); } // Safe attribute selector escaping. function cssEscape(value) { if (window.CSS && CSS.escape) return CSS.escape(value); return String(value).replace(/["\\]/g, "\\$&"); } function ensureHudDot() { let dot = document.getElementById(HUD_DOT_ID); if (!dot) { dot = document.createElement("div"); dot.id = HUD_DOT_ID; dot.setAttribute(`data-${DATA_PREFIX}-active`, "false"); dot.title = "Member-only videos: 0 (0 hidden)"; document.body.appendChild(dot); } let panel = document.getElementById(HUD_PANEL_ID); if (!panel) { panel = document.createElement("div"); panel.id = HUD_PANEL_ID; panel.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); document.body.appendChild(panel); } if (!dot.dataset.bound) { dot.dataset.bound = "true"; dot.addEventListener("click", event => { event.stopPropagation(); const isHidden = panel.getAttribute(`data-${DATA_PREFIX}-hidden`) === "true"; panel.setAttribute( `data-${DATA_PREFIX}-hidden`, isHidden ? "false" : "true" ); if (isHidden) renderHudPanel(panel); }); panel.addEventListener("click", event => event.stopPropagation()); document.addEventListener("click", event => { if (panel.getAttribute(`data-${DATA_PREFIX}-hidden`) !== "false") return; if (panel.contains(event.target) || dot.contains(event.target)) return; panel.setAttribute(`data-${DATA_PREFIX}-hidden`, "true"); }); } return dot; } function updateHudDot() { const dot = ensureHudDot(); const panel = document.getElementById(HUD_PANEL_ID); const total = sharedIndex.size; const hidden = total; const signature = `${total}:${hidden}`; dot.title = `Member-only videos: ${total} (${hidden} hidden)`; dot.setAttribute( `data-${DATA_PREFIX}-active`, total > 0 ? "true" : "false" ); if ( panel && panel.getAttribute(`data-${DATA_PREFIX}-hidden`) === "false" && (hudDirty || signature !== lastHudSignature) ) { renderHudPanel(panel); hudDirty = false; lastHudSignature = signature; } } function updateSharedIndex(meta, hidden) { if (!meta.stableId) return; if (hidden) { sharedIndex.set(meta.stableId, { id: meta.stableId, channelKey: meta.channelKey, channelLabel: meta.channelLabel, channelUrl: meta.channelUrl, title: meta.title, url: meta.url, thumb: meta.thumb, hidden: true }); } else { sharedIndex.delete(meta.stableId); } schedulePersistSharedIndex(); } function schedulePersistSharedIndex() { if (persistTimer) return; persistTimer = setTimeout(() => { persistTimer = null; persistSharedIndex(); }, 200); } function persistSharedIndex() { const items = Array.from(sharedIndex.values()); items.sort((a, b) => a.id.localeCompare(b.id)); const perChannelCounts = new Map(); const pruned = []; for (const item of items) { const key = item.channelKey || item.channelLabel || "(unknown)"; const count = perChannelCounts.get(key) || 0; if (count >= MAX_PER_CHANNEL) continue; if (pruned.length >= MAX_GLOBAL_ITEMS) break; perChannelCounts.set(key, count + 1); pruned.push(item); } chrome.storage.local.set({ [STORAGE_KEY]: pruned }); } function loadStoredIndex() { chrome.storage.local.get({ [STORAGE_KEY]: [] }, data => { sharedIndex.clear(); data[STORAGE_KEY].forEach(item => sharedIndex.set(item.id, item)); updateHudDot(); }); } function renderHudPanel(panel) { panel.innerHTML = ""; const title = document.createElement("div"); title.className = `${DATA_PREFIX}-hud-title`; title.textContent = "Hidden member-only videos"; panel.appendChild(title); const groups = new Map(); for (const meta of sharedIndex.values()) { if (!meta.hidden) continue; const key = meta.channelKey || getChannelKeyFromData(meta.channelLabel, meta.channelUrl) || "(unknown)"; const list = groups.get(key) || []; list.push(meta); groups.set(key, list); } if (groups.size === 0) { const empty = document.createElement("div"); empty.textContent = "No hidden member-only videos."; panel.appendChild(empty); return; } for (const [creator, items] of groups.entries()) { const details = document.createElement("details"); details.className = `${DATA_PREFIX}-channel`; details.open = false; const summary = document.createElement("summary"); summary.className = `${DATA_PREFIX}-channel-header`; const label = items[0]?.channelLabel && items[0].channelLabel !== "(no channel)" ? items[0].channelLabel : creator; summary.textContent = `${label} (${items.length})`; details.appendChild(summary); const list = document.createElement("div"); list.className = `${DATA_PREFIX}-video-list`; items.forEach(meta => { const item = document.createElement("div"); item.className = `${DATA_PREFIX}-hud-item`; const img = document.createElement("img"); img.className = `${DATA_PREFIX}-hud-thumb`; img.alt = meta.title || "thumbnail"; if (meta.thumb) img.src = meta.thumb; item.appendChild(img); const link = document.createElement("a"); link.className = `${DATA_PREFIX}-hud-link`; link.textContent = meta.title || meta.url || "(no title)"; link.href = meta.url || "#"; link.target = "_blank"; link.rel = "noopener noreferrer"; item.appendChild(link); list.appendChild(item); }); panel.appendChild(details); details.appendChild(list); } } /* ---------------- initial + observer ---------------- */ process(); // Watch for new items so member-only tags are applied as they load. 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 });