1
0

Compare commits

...

4 Commits

View File

@ -4,7 +4,9 @@ let whitelistHandles = [];
let debugEnabled = false; let debugEnabled = false;
// In-memory index of known member-only videos. // In-memory index of known member-only videos.
const memberOnlyIndex = new Map(); // id -> { channelKey, hidden } const memberOnlyIndex = new Map(); // id -> { channelKey, hidden }
const sharedIndex = new Map(); // stableId -> meta for cross-tab HUD const sharedIndex = new Map(); // stableId -> meta for cross-tab HUD (hidden only)
const sharedSeenIndex = new Map(); // stableId -> meta for all seen videos
const sharedMemberIndex = new Map(); // stableId -> meta for member-only videos
// Data-* attribute prefix for DOM tags. // Data-* attribute prefix for DOM tags.
const DATA_PREFIX = "chipperfluff-nobs"; // nobs = "no-bs" (member-only videos) const DATA_PREFIX = "chipperfluff-nobs"; // nobs = "no-bs" (member-only videos)
const HUD_DOT_ID = `${DATA_PREFIX}-hud-dot`; const HUD_DOT_ID = `${DATA_PREFIX}-hud-dot`;
@ -13,9 +15,13 @@ let generatedIdCounter = 0;
let hudDirty = false; let hudDirty = false;
let lastHudSignature = ""; let lastHudSignature = "";
const STORAGE_KEY = "memberOnlyHidden"; const STORAGE_KEY = "memberOnlyHidden";
const DEFAULT_MAX_ARCHIVE = 500; const STORAGE_KEY_SEEN = "memberOnlySeen";
const STORAGE_KEY_MEMBER = "memberOnlyDetected";
const DEFAULT_MAX_ARCHIVE = 2000;
const MAX_SNAPSHOT_CHARS = 20000; const MAX_SNAPSHOT_CHARS = 20000;
let persistTimer = null; let persistHiddenTimer = null;
let persistSeenTimer = null;
let persistMemberTimer = null;
let contextInvalidated = false; let contextInvalidated = false;
let hudFlashTimer = null; let hudFlashTimer = null;
@ -312,6 +318,18 @@ chrome.storage.onChanged.addListener(changes => {
items.forEach(item => sharedIndex.set(item.id, item)); items.forEach(item => sharedIndex.set(item.id, item));
updateHudDot(); updateHudDot();
} }
if (changes[STORAGE_KEY_SEEN]) {
const items = changes[STORAGE_KEY_SEEN].newValue || [];
sharedSeenIndex.clear();
items.forEach(item => sharedSeenIndex.set(item.id, item));
updateHudDot();
}
if (changes[STORAGE_KEY_MEMBER]) {
const items = changes[STORAGE_KEY_MEMBER].newValue || [];
sharedMemberIndex.clear();
items.forEach(item => sharedMemberIndex.set(item.id, item));
updateHudDot();
}
if (changes.whitelist || changes.debug) { if (changes.whitelist || changes.debug) {
loadSettings(); loadSettings();
} }
@ -321,7 +339,8 @@ chrome.storage.onChanged.addListener(changes => {
// Detect member-only videos and tag them for future updates. // Detect member-only videos and tag them for future updates.
function process(root = document) { function process(root = document) {
if (isWhitelistedChannelPage()) return; const bypassHide = isWhitelistedChannelPage();
scanAllVideos(root);
const badges = root.querySelectorAll("badge-shape"); const badges = root.querySelectorAll("badge-shape");
badges.forEach(badge => { badges.forEach(badge => {
@ -362,6 +381,18 @@ function process(root = document) {
thumb thumb
}); });
hudDirty = true; hudDirty = true;
updateMemberIndex({
stableId,
channelKey,
channelLabel: channel,
channelUrl
});
updateSeenIndex({
stableId,
channelKey,
channelLabel: channel,
channelUrl
});
} else { } else {
// Fill missing metadata as it becomes available. // Fill missing metadata as it becomes available.
if (!existing.title && title) existing.title = title; if (!existing.title && title) existing.title = title;
@ -373,6 +404,18 @@ function process(root = document) {
if (!existing.snapshot) existing.snapshot = safeSnapshot(video); if (!existing.snapshot) existing.snapshot = safeSnapshot(video);
updateSharedIndex(existing, true); updateSharedIndex(existing, true);
} }
updateMemberIndex({
stableId: existing.stableId,
channelKey: existing.channelKey,
channelLabel: existing.channelLabel,
channelUrl: existing.channelUrl
});
updateSeenIndex({
stableId: existing.stableId,
channelKey: existing.channelKey,
channelLabel: existing.channelLabel,
channelUrl: existing.channelUrl
});
} }
} }
@ -386,7 +429,7 @@ function process(root = document) {
debugLog("Whitelisted:", whitelisted); debugLog("Whitelisted:", whitelisted);
debugGroupEnd(); debugGroupEnd();
if (!whitelisted) { if (!whitelisted && !bypassHide) {
if (id && memberOnlyIndex.has(id)) { if (id && memberOnlyIndex.has(id)) {
const meta = memberOnlyIndex.get(id); const meta = memberOnlyIndex.get(id);
if (!meta.hidden) flashHudDot(); if (!meta.hidden) flashHudDot();
@ -410,6 +453,27 @@ function process(root = document) {
updateHudDot(); updateHudDot();
} }
function scanAllVideos(root) {
const selector =
"ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer";
const videos = root.querySelectorAll(selector);
const rootIsVideo =
root instanceof HTMLElement && root.matches && root.matches(selector);
const allVideos = rootIsVideo ? [root, ...videos] : Array.from(videos);
allVideos.forEach(video => {
const videoMeta = getVideoMeta(video);
if (!videoMeta.videoId) return;
const channelInfo = getChannelInfo(video);
updateSeenIndex({
stableId: videoMeta.videoId,
channelKey: channelInfo.key,
channelLabel: channelInfo.label,
channelUrl: channelInfo.url
});
});
}
// Apply whitelist to already-tagged member-only videos. // Apply whitelist to already-tagged member-only videos.
function updateKnownVisibility() { function updateKnownVisibility() {
if (isWhitelistedChannelPage()) { if (isWhitelistedChannelPage()) {
@ -662,6 +726,8 @@ function ensureHudDot() {
} }
function updateHudDot() { function updateHudDot() {
if (contextInvalidated || !document?.body) return;
try {
const dot = ensureHudDot(); const dot = ensureHudDot();
const panel = document.getElementById(HUD_PANEL_ID); const panel = document.getElementById(HUD_PANEL_ID);
const total = sharedIndex.size; const total = sharedIndex.size;
@ -681,6 +747,9 @@ function updateHudDot() {
hudDirty = false; hudDirty = false;
lastHudSignature = signature; lastHudSignature = signature;
} }
} catch (error) {
contextInvalidated = true;
}
} }
function flashHudDot() { function flashHudDot() {
@ -717,10 +786,52 @@ function updateSharedIndex(meta, hidden) {
schedulePersistSharedIndex(); schedulePersistSharedIndex();
} }
function updateSeenIndex(meta) {
if (!meta.stableId) return;
const existing = sharedSeenIndex.get(meta.stableId);
if (!existing) {
sharedSeenIndex.set(meta.stableId, {
id: meta.stableId,
channelKey: meta.channelKey,
channelLabel: meta.channelLabel,
channelUrl: meta.channelUrl,
lastSeen: Date.now()
});
schedulePersistSeenIndex();
return;
}
existing.channelKey = meta.channelKey || existing.channelKey;
existing.channelLabel = meta.channelLabel || existing.channelLabel;
existing.channelUrl = meta.channelUrl || existing.channelUrl;
existing.lastSeen = Date.now();
schedulePersistSeenIndex();
}
function updateMemberIndex(meta) {
if (!meta.stableId) return;
const existing = sharedMemberIndex.get(meta.stableId);
if (!existing) {
sharedMemberIndex.set(meta.stableId, {
id: meta.stableId,
channelKey: meta.channelKey,
channelLabel: meta.channelLabel,
channelUrl: meta.channelUrl,
lastSeen: Date.now()
});
schedulePersistMemberIndex();
return;
}
existing.channelKey = meta.channelKey || existing.channelKey;
existing.channelLabel = meta.channelLabel || existing.channelLabel;
existing.channelUrl = meta.channelUrl || existing.channelUrl;
existing.lastSeen = Date.now();
schedulePersistMemberIndex();
}
function schedulePersistSharedIndex() { function schedulePersistSharedIndex() {
if (persistTimer) return; if (persistHiddenTimer) return;
persistTimer = setTimeout(() => { persistHiddenTimer = setTimeout(() => {
persistTimer = null; persistHiddenTimer = null;
persistSharedIndex(); persistSharedIndex();
}, 200); }, 200);
} }
@ -741,13 +852,80 @@ function persistSharedIndex() {
}); });
} }
function schedulePersistSeenIndex() {
if (persistSeenTimer) return;
persistSeenTimer = setTimeout(() => {
persistSeenTimer = null;
persistSeenIndex();
}, 200);
}
function persistSeenIndex() {
if (!chrome?.storage?.local || contextInvalidated) return;
const items = Array.from(sharedSeenIndex.values());
items.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
chrome.storage.local.get({ maxArchive: DEFAULT_MAX_ARCHIVE }, data => {
const maxArchive = Number(data.maxArchive);
const max = Number.isFinite(maxArchive) ? maxArchive : DEFAULT_MAX_ARCHIVE;
if (max < 0) {
safeStorageSet({ [STORAGE_KEY_SEEN]: items });
return;
}
safeStorageSet({ [STORAGE_KEY_SEEN]: items.slice(0, max) });
});
}
function schedulePersistMemberIndex() {
if (persistMemberTimer) return;
persistMemberTimer = setTimeout(() => {
persistMemberTimer = null;
persistMemberIndex();
}, 200);
}
function persistMemberIndex() {
if (!chrome?.storage?.local || contextInvalidated) return;
const items = Array.from(sharedMemberIndex.values());
items.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
chrome.storage.local.get({ maxArchive: DEFAULT_MAX_ARCHIVE }, data => {
const maxArchive = Number(data.maxArchive);
const max = Number.isFinite(maxArchive) ? maxArchive : DEFAULT_MAX_ARCHIVE;
if (max < 0) {
safeStorageSet({ [STORAGE_KEY_MEMBER]: items });
return;
}
safeStorageSet({ [STORAGE_KEY_MEMBER]: items.slice(0, max) });
});
}
function loadStoredIndex() { function loadStoredIndex() {
if (!chrome?.storage?.local) return; if (!chrome?.storage?.local) return;
chrome.storage.local.get({ [STORAGE_KEY]: [] }, data => { chrome.storage.local.get(
{ [STORAGE_KEY]: [], [STORAGE_KEY_SEEN]: [], [STORAGE_KEY_MEMBER]: [] },
data => {
sharedIndex.clear(); sharedIndex.clear();
data[STORAGE_KEY].forEach(item => sharedIndex.set(item.id, item)); data[STORAGE_KEY].forEach(item => sharedIndex.set(item.id, item));
updateHudDot(); sharedSeenIndex.clear();
data[STORAGE_KEY_SEEN].forEach(item => sharedSeenIndex.set(item.id, item));
sharedMemberIndex.clear();
data[STORAGE_KEY_MEMBER].forEach(item =>
sharedMemberIndex.set(item.id, item)
);
if (sharedMemberIndex.size === 0 && sharedIndex.size > 0) {
for (const item of sharedIndex.values()) {
sharedMemberIndex.set(item.id, {
id: item.id,
channelKey: item.channelKey,
channelLabel: item.channelLabel,
channelUrl: item.channelUrl,
lastSeen: item.lastSeen || Date.now()
}); });
}
schedulePersistMemberIndex();
}
updateHudDot();
}
);
} }
function renderHudPanel(panel) { function renderHudPanel(panel) {
@ -789,6 +967,9 @@ function renderHudPanel(panel) {
return; return;
} }
const totals = getSeenTotalsByCreator();
const memberTotals = getMemberTotalsByCreator();
for (const [creator, items] of groups.entries()) { for (const [creator, items] of groups.entries()) {
const details = document.createElement("details"); const details = document.createElement("details");
details.className = `${DATA_PREFIX}-channel`; details.className = `${DATA_PREFIX}-channel`;
@ -799,7 +980,10 @@ function renderHudPanel(panel) {
const label = items[0]?.channelLabel && items[0].channelLabel !== "(no channel)" const label = items[0]?.channelLabel && items[0].channelLabel !== "(no channel)"
? items[0].channelLabel ? items[0].channelLabel
: creator; : creator;
summary.textContent = `${label} (${items.length})`; const total = totals.get(creator) || items.length;
const memberCount = memberTotals.get(creator) || 0;
const percent = total > 0 ? Math.round((memberCount / total) * 100) : 0;
summary.textContent = `${label} (${memberCount}/${total} known, ${percent}%)`;
details.appendChild(summary); details.appendChild(summary);
const list = document.createElement("div"); const list = document.createElement("div");
@ -840,9 +1024,38 @@ function renderHudPanel(panel) {
} }
} }
function getSeenTotalsByCreator() {
const totals = new Map();
for (const meta of sharedSeenIndex.values()) {
const key =
meta.channelKey ||
getChannelKeyFromData(meta.channelLabel, meta.channelUrl) ||
"(unknown)";
totals.set(key, (totals.get(key) || 0) + 1);
}
return totals;
}
function getMemberTotalsByCreator() {
const totals = new Map();
for (const meta of sharedMemberIndex.values()) {
const key =
meta.channelKey ||
getChannelKeyFromData(meta.channelLabel, meta.channelUrl) ||
"(unknown)";
totals.set(key, (totals.get(key) || 0) + 1);
}
return totals;
}
function clearArchive() { function clearArchive() {
sharedIndex.clear(); sharedIndex.clear();
safeStorageSet({ [STORAGE_KEY]: [] }, () => updateHudDot()); sharedSeenIndex.clear();
sharedMemberIndex.clear();
safeStorageSet(
{ [STORAGE_KEY]: [], [STORAGE_KEY_SEEN]: [], [STORAGE_KEY_MEMBER]: [] },
() => updateHudDot()
);
} }
function safeStorageSet(payload, callback) { function safeStorageSet(payload, callback) {