Compare commits
4 Commits
a4f2f5c60b
...
8e9e0e0b76
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e9e0e0b76 | |||
| 7ccc73bf03 | |||
| 5bbeac5e89 | |||
| db81232f0f |
243
content.js
243
content.js
@ -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(
|
||||||
sharedIndex.clear();
|
{ [STORAGE_KEY]: [], [STORAGE_KEY_SEEN]: [], [STORAGE_KEY_MEMBER]: [] },
|
||||||
data[STORAGE_KEY].forEach(item => sharedIndex.set(item.id, item));
|
data => {
|
||||||
updateHudDot();
|
sharedIndex.clear();
|
||||||
});
|
data[STORAGE_KEY].forEach(item => sharedIndex.set(item.id, item));
|
||||||
|
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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user