224 lines
5.9 KiB
JavaScript
224 lines
5.9 KiB
JavaScript
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 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);
|
|
|
|
if (debugEnabled) {
|
|
console.group("[MemberFilter]");
|
|
console.log("Title :", title);
|
|
console.log("Channel:", channel);
|
|
console.log("URL :", url);
|
|
console.log("ID :", id || "(no id)");
|
|
console.log("Whitelisted:", whitelisted);
|
|
console.groupEnd();
|
|
}
|
|
|
|
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
|
|
});
|