FilterTube must be able to:
- Identify channel identity for a piece of content (preferably a stable UC channel ID, and also capture @handle when available).
- Persist blocked/allowed channels in extension storage with dual filtering modes.
- Hide (and optionally keyword-filter) all content attributable to those blocked channels in Blocklist mode.
- Show only content from allowed channels in Whitelist mode (v3.2.5).
- Work reliably across YouTube surfaces (Home, Search, Shorts, Watch, Kids), including SPA navigation and DOM recycling.
- Provide accurate channel names in 3-dot menus, upgrading from UC IDs/handles to human-readable names.
- Recover full collaborator rosters on watch-like surfaces when YouTube hides them behind a dialog/sheet instead of exposing them directly in the byline.
- Support mode switching with list migration (blocklist ↔ whitelist).
- This document does not implement new behavior beyond the scope of channel blocking/allowing.
- Watch-page playlist specifics are documented elsewhere and will be handled as a separate workstream.
FilterTube runs in multiple JavaScript “worlds”:
-
Background (
js/background.js)- Owns persistence (Chrome storage) and network fetches that shouldn’t depend on page lifecycle.
-
Isolated World (
js/content/*+js/content_bridge.js)- Runs as content scripts.
- Can access the DOM.
- Cannot directly access
window.ytInitialData(Main World objects) reliably. js/content/block_channel.jsdetects 3-dot dropdown openings and resolves the clicked card.js/content/dom_fallback.jsimplementsapplyDOMFallback(...)(hide/restore logic).js/content_bridge.jsrenders menu entries and orchestrates block/persist/hide flows (and schedules DOM fallback reprocessing).
-
Main World (
js/seed.js,js/filter_logic.js,js/injector.js)- Runs in the page context.
- Can access
window.ytInitialData/window.ytInitialPlayerResponse. - Intercepts JSON responses early to avoid “flash of blocked content”.
This separation is the reason we have explicit cross-world message passing (via window.postMessage) between content_bridge.js and injector.js.
-
UC ID (e.g.
UCM6nZ84qXYFWPWzlO_zpkWw)- Most stable identifier.
- Many YouTube surfaces expose this in JSON (
browseEndpoint.browseId).
-
@handle (e.g.
@Santasmusicroom.Official)- Human-friendly alias.
- May be percent-encoded in URLs.
-
customUrl (e.g.
c/VídeoseMensagens,user/LegacyName)- Legacy custom URLs (pre-2022).
- Often found in
browseEndpoint.canonicalBaseUrl. - Stored as
c/slugoruser/slug.
In storage (background-managed filterChannels) channel entries can contain:
id: UC IDhandle: normalized handle used for matchingcustomUrl: normalized legacy URL slughandleDisplay: UI/display handlename: channel nameoriginalInput: what the user actually typed or clickedfilterAll: boolean- collaboration metadata
The system maintains two bidirectional lookup maps in local storage:
channelMap:(handle | customUrl) <-> UC ID.- Critical for converting aliases into stable UC IDs.
videoChannelMap:videoId -> UC ID.- Used for Shorts and watch-page playlist panels (and any surface where DOM metadata is incomplete).
- Since many cards lack identity, we store the mapping after the first successful resolution so it works offline/instantly on next load.
- All worlds call into the helpers in
js/shared/identity.jsto normalize canonicalBaseUrl strings into predictable keys (c/<slug>oruser/<slug>, percent-decoding as needed). background.js:fetchChannelInfo()now fetches/c/<slug>or/user/<slug>directly and records the resulting UC ID back intochannelMap.content_bridge.jsandfilter_logic.jsboth consultchannelMapbefore falling back to network, so DOM-only custom URL cards still hide immediately once a mapping is learned.- Prefetch (section 5.4) persists any newly learned mapping into
videoChannelMap, so future encounters are zero-network even on poor connections.
FilterTube uses a proactive, XHR-first strategy to extract channel identity before rendering, minimizing network calls and ensuring instant blocking. The waterfall priority is:
- XHR JSON interception (Main World)
ytInitial*snapshots (Main World)- DOM extraction (Isolated World)
- Network fetch (Background, rare fallback)
seed.js now stashes network snapshots for proactive identity extraction:
/youtubei/v1/next→window.filterTube.lastYtNextResponse/youtubei/v1/browse→window.filterTube.lastYtBrowseResponse/youtubei/v1/player→window.filterTube.lastYtPlayerResponse
filter_logic.js harvests from these snapshots:
- UC ID from
browseEndpoint.browseId - @handle and customUrl from
canonicalBaseUrl - Channel name and logo from metadata/byline
- Collaborators from
avatarStackViewModelandshowDialogCommand
Cross-world messages broadcast identity:
FilterTube_UpdateChannelMapFilterTube_UpdateVideoChannelMapFilterTube_CacheCollaboratorInfo
When XHR snapshots are unavailable, we fall back to:
window.ytInitialDatawindow.ytInitialPlayerResponsewindow.filterTube.lastYtInitialDatawindow.filterTube.lastYtInitialPlayerResponse
injector.js searches these for:
videoId→browseEndpoint.browseId(UC ID)videoId→canonicalBaseUrl(handle/customUrl)- Collaboration lists via
avatarStackViewModel/showDialogCommand
content_bridge.js extracts from DOM when JSON isn't available:
- Search:
#channel-info ytd-channel-name afor name;hreffor handle/UC - Home: lockup metadata, avatar alt text, or channel links
- Shorts:
data-filtertube-channel-*attributes or fallback links - Kids: native UI patterns (
ytk-compact-video-renderer)
Only used when:
- No identity found in XHR/ytInitial*/DOM
- Manual channel addition via popup
- Post-block enrichment for missing fields
Fetch strategies:
- Watch page:
/watch?v=<videoId>(parse ytInitialData/meta) - Shorts page:
/shorts/<videoId>(parse ytInitialData/meta) - Channel about:
/@handle/about(404-aware) - Custom URLs:
/c/<slug>or/user/<slug>(legacy support)
Kids safety: All network fetches are skipped on Kids surfaces (skipNetwork: true).
videoChannelMap:videoId -> UC IDmappings for persistence (Shorts/Watch)channelMap:(handle | customUrl) <-> UC IDbidirectional lookups- Session caches: In-memory caches for active browsing session
- Race-safe updates: Debounced writes to prevent storage conflicts
- XHR snapshot stashing:
window.filterTube.lastYt*Responsefor proactive lookups
After a channel is blocked, background.js may schedule post-block enrichment to fill missing fields:
- Trigger: Only from
handleAddFilteredChannelafter successful persist - Rate limiting: 6 hours per channel (
postBlockEnrichmentAttemptedMap) - Conditions: Runs only if missing handle/customUrl/logo/name
- Debounce: 1.5s delay + random 750ms to avoid burst traffic
- Profile-aware: Separate keys for
mainvskids - Skip: If
source === 'postBlockEnrichment'(prevents loops)
This ensures manual adds and 3-dot blocks eventually get full metadata without spamming the network.
HTML GET → XHR JSON (/youtubei/v1/next, /browse, /player)
↓
FilterLogic (main world)
- Extract UC IDs, handles, customUrls, names, logos
- Harvest collaborators from avatarStack/showDialog
- Broadcasts:
• FilterTube_UpdateChannelMap
• FilterTube_UpdateVideoChannelMap
• FilterTube_CacheCollaboratorInfo
↓
Content Script (isolated world)
- Receives messages
- Stamps cards with data-filtertube-*
- Updates 3-dot menus instantly
↓
DOM (visible UI)
- Cards appear pre-stamped
- No network calls needed for blocking
↓
Network (rare fallback)
- Only if JSON lacked identity
- Uses Shorts/Watch/About fetches
- Kids surfaces avoid this entirely
Main → Isolated:
FilterTube_UpdateChannelMap:{handle|customUrl: ucId}FilterTube_UpdateVideoChannelMap:{videoId: ucId}FilterTube_CacheCollaboratorInfo:{videoId, collaborators[]}
Isolated → Main:
FilterTube_RequestChannelInfo:{videoId, expectedHandle?, expectedName?}FilterTube_RequestCollaborators:{videoId}
When FilterTube_UpdateVideoChannelMap arrives:
content_bridge.jsstamps all matching cards:const cards = document.querySelectorAll(`[data-filtertube-video-id="${videoId}"]`); for (const card of cards) { stampChannelIdentity(card, { id: channelId }); }
- 3-dot menus show correct names immediately
- No "Fetching…" delay (proactive XHR interception provides instant identity)
Two paths, both proactive:
1) XHR JSON (filter_logic.js):
avatarStackViewModel.avatars[]→ extract UC/handle/customUrl/nameshowDialogCommand/showSheetCommand→ full collaborator listshowSheetCommand.panelLoadingStrategy.inlineContent.sheetViewModel.header.panelHeaderViewModel.title.content == "Collaborators"is the authoritative roster discriminator- Broadcast via
FilterTube_CacheCollaboratorInfo
2) DOM fallback (content_bridge.js):
- Detect avatar stack elements
- Query
data-filtertube-collaboratorsattributes - Fall back to main-world
searchYtInitialDataForVideoChannel
Result: Multi-channel menus appear instantly on watch/home/search.
Authoritative roster precedence:
Collaborators sheet JSON
> dialog/sheet roster variants with collaborator header
> avatar-stack / direct-list fallback with stable identities
> DOM byline and collapsed text warm-up
The fallback paths are still important for early UI warm-up, but they must not override a header-backed Collaborators sheet for the same videoId.
The current collaboration path intentionally avoids treating plain separator text as proof of multiple channels.
Evidence now needs to come from one or more of:
- avatar-stack metadata
showDialogCommand/showSheetCommandcollaborator rosters- collapsed
and N morestyle markup - multiple distinct channel links on the same card
This prevents false positives such as single channel names containing & / and.
Additional roster guards added on 2026-04-28:
- fallback collaborator candidates are sanitized before scoring, caching, and menu rendering
- placeholder rows such as
and 2 moreare dropped - weak name-only composite rows are dropped when their normalized label is fully covered by two other collaborator labels
- example:
Daddy Yankee Bizarrapis removed whenDaddy YankeeandBizarrapare already in the roster - if a pruned composite row inflated
expectedCollaboratorCount, the expected count is collapsed to the pruned roster length
On watch-like surfaces, the 3-dot menu can now also:
- open with provisional single-channel context
- request the authoritative roster from Main World
- refresh the active menu in place once the collaborator list arrives
This feature gives whitelist mode a second acquisition path besides manual add/import files.
- main YouTube only
- main profile whitelist only
- driven from Tab View channel management
- sourced from the active YouTube account in the selected tab
graph TD
A["Tab View: Import Subscribed Channels"] --> B["Move selected YouTube tab to /feed/channels"]
B --> C["Wait for bridge + MAIN-world injector"]
C --> D["Collect subscription rows"]
D --> E["Normalize channel identity"]
E --> F["Background batch merge into whitelistChannels"]
F --> G{"Turn on whitelist?"}
G -->|No| H["Whitelist stored only"]
G -->|Yes| I["Existing blocklist merged into whitelist and cleared"]
- FilterTube currently has two ways to build whitelist:
- direct whitelist population, such as subscribed-channels import or other whitelist-specific adds
- blocklist-to-whitelist migration when whitelist mode is activated
Import Onlyappends subscriptions tomain.whitelistChannels- it does not change the current blocklist
Import + Turn On Whitelistcalls the existing mode-switch path- that current path merges the profile's blocklist channels and keywords into whitelist and clears the blocklist
Imported subscription rows are normalized like other channel entries:
- prefer stable
UC...IDs - keep
@handlewhen present - keep
customUrlwhen present - keep best available name/logo
The background batch import:
- dedupes against existing whitelist entries
- updates weak existing rows with stronger imported metadata
- mirrors the result into legacy whitelist storage where needed
- updates
channelMapwhen new handle/custom URL mappings are learned
This keeps subscriptions import compatible with the rest of the blocking/allowing system instead of creating a second storage model.
js/content/block_channel.jsdetects overflow dropdown opening and resolves associated card.- It calls
content_bridge.js:injectFilterTubeMenuItem(dropdown, videoCard)to render "Block channel" menu entry. - On click,
content_bridge.js:handleBlockChannelClick(channelInfo, ...)runs.
FilterTube v3.2.5 extends the channel blocking system to support Whitelist mode, where the filtering logic is inverted:
// In content_bridge.js - mode-aware menu injection
function injectFilterTubeMenuItem(menuList, videoCard, channelInfo) {
const state = StateManager.getState();
const mode = state?.mode === 'whitelist' ? 'whitelist' : 'blocklist';
// Update menu text based on mode
const menuItemText = mode === 'whitelist' ? 'Allow channel' : 'Block channel';
// Send to appropriate list based on mode
const action = mode === 'whitelist' ? 'addWhitelistChannelPersistent' : 'addChannelPersistent';
}The Profiles V4 schema now includes whitelist-specific fields:
// In background.js - compiled settings with whitelist support
const compiledSettings = {
listMode: 'blocklist' | 'whitelist',
filterChannels: [...], // Blocklist channels
whitelistChannels: [...], // Whitelist channels (v3.2.5)
filterKeywords: [...], // Blocklist keywords
whitelistKeywords: [...] // Whitelist keywords (v3.2.5)
};FilterTube v3.2.1 includes a sophisticated post-block enrichment system that asynchronously fills missing channel metadata after successful blocking operations.
// In background.js - intelligent enrichment scheduling
function schedulePostBlockEnrichment(channel, profile = 'main', metadata = {}) {
// Avoid duplicate enrichment requests
const source = metadata?.source || '';
if (source === 'postBlockEnrichment') return;
const id = channel?.id || '';
if (!id || !id.toUpperCase().startsWith('UC')) return;
// Rate limiting: 6-hour cooldown per channel
const key = `${profile === 'kids' ? 'kids' : 'main'}:${id.toLowerCase()}`;
const now = Date.now();
const lastAttempt = postBlockEnrichmentAttempted.get(key) || 0;
if (now - lastAttempt < 6 * 60 * 60 * 1000) return;
// Check if enrichment is needed
const needsEnrichment = (
(!channel.handle && !channel.customUrl) ||
!channel.logo ||
!channel.name
);
if (!needsEnrichment) return;
// Schedule with random delay (3.5-4s) to avoid patterns
const delayMs = 3500 + Math.floor(Math.random() * 750);
setTimeout(async () => {
await handleAddFilteredChannel(
id,
false,
null,
null,
{ source: 'postBlockEnrichment' },
profile,
''
);
}, delayMs);
}Enrichment features:
- Smart detection - only enriches channels missing key metadata
- Rate limited - 6-hour cooldown prevents excessive requests
- Background processing - doesn't block UI operations
- Random delays - avoids detectable request patterns
- Profile-aware - separate tracking for Main and Kids profiles
// In block_channel.js - improved Kids context capture
function captureKidsMenuContext(menuButton) {
const context = {
ts: now,
videoId: '',
channelId: '',
channelHandle: '',
customUrl: '',
channelName: '',
source: 'kidsMenu'
};
// Extract handle from href
if (href) {
const extractedHandle = window.FilterTubeIdentity?.extractRawHandle?.(href) || '';
if (extractedHandle && extractedHandle.startsWith('@')) {
context.channelHandle = extractedHandle;
}
// Extract customUrl from /c/ and /user/ paths
const decoded = (() => {
try { return decodeURIComponent(href); } catch (e) { return href; }
})();
if (decoded.startsWith('/c/')) {
const slug = decoded.split('/')[2] || '';
if (slug) context.customUrl = `c/${slug}`;
} else if (decoded.startsWith('/user/')) {
const slug = decoded.split('/')[2] || '';
if (slug) context.customUrl = `user/${slug}`;
}
}
return context;
}
// Enhanced Kids blocking with better validation
async function handleKidsNativeBlock(blockType = 'video', options = {}) {
let ctx = lastKidsMenuContext;
// Refresh stale context to reduce errors
if (!ctx || (!ctx.channelId && !ctx.channelName)) {
const fresh = captureKidsMenuContext(lastClickedMenuButton);
if (fresh) {
lastKidsMenuContext = fresh;
ctx = fresh;
}
}
// Validate and sanitize channel name
let channelName = ctx?.channelName || '';
if (/^[a-zA-Z0-9_-]{11}$/.test(channelName) || /^UC[\w-]{22}$/i.test(channelName)) {
channelName = '';
}
const safeHandle = (ctx?.channelHandle || '').trim();
const safeCustomUrl = (ctx?.customUrl || '').trim();
// Send to background with proper identifiers
chrome.runtime?.sendMessage({
action: 'FilterTube_KidsBlockChannel',
videoId: ctx?.videoId || null,
channel: {
name: channelName || null,
id: ctx?.channelId || '',
handle: safeHandle || null,
customUrl: safeCustomUrl || null,
originalInput: (ctx?.channelId && ctx?.channelId.startsWith('UC'))
? ctx.channelId
: (safeHandle || safeCustomUrl || ''),
source: blockType === 'channel' ? 'kidsNativeChannel' : 'kidsNativeVideo'
}
});
}Kids blocking improvements:
- Handle extraction from channel links
- CustomUrl support for /c/ and /user/ channels
- Context refresh to reduce stale data
- Name validation to avoid persisting IDs as names
- Proper identifier prioritization
// Background enrichment - rarely needed thanks to proactive XHR
if (needsEnrichment && enrichedInfo?.videoId) {
// Route to appropriate fetch handler (last resort)
if (isKidsUrl) {
// Kids: skip network, rely on intercepted JSON only
enrichedInfo = await performKidsWatchIdentityFetch(videoId);
} else if (enrichedInfo.fetchStrategy === 'shorts') {
enrichedInfo = await fetchChannelFromShortsUrl(videoId, null, { allowDirectFetch: false });
} else {
enrichedInfo = await fetchChannelFromWatchUrl(videoId);
}
return enrichedInfo;
}Current behavior note: enrichment is now rare thanks to proactive XHR interception.
- If
fetchStrategy === "mainworld", we searchytInitialDatasnapshots. - Network fetches are avoided on Kids (
allowDirectFetch: false). - Handle → UC ID resolution uses the persisted
channelMapfirst.
// Update menu label when enrichment completes
fetchPromise.then(finalChannelInfo => {
if (!finalChannelInfo) return;
// Upgrade UC IDs, Mix titles, metadata strings to real names
updateInjectedMenuChannelName(dropdown, finalChannelInfo);
});- Main profile: Stores in
filterChannelsarray - Kids profile: Stores in
ftProfilesV3.kids.blockedChannels background.js:handleAddFilteredChannel()routes based on sender URL:
const isKids = isKidsUrl(sender.tab?.url);
const targetProfile = isKids ? 'kids' : 'main';
if (targetProfile === 'kids') {
// Store in kids profile
await addToKidsProfile(channelData);
} else {
// Store in main profile
await addToMainProfile(channelData);
}After persisting, schedulePostBlockEnrichment may run to fill missing metadata (see Section 3.6).
seed.jsintercepts YouTube JSON before render viafetchandXMLHttpRequesthooks.filter_logic.jsapplies blocking rules based on current mode:- Blocklist Mode: Remove items matching blocked channels/keywords
- Whitelist Mode (v3.2.5): Remove items NOT matching whitelisted channels/keywords
- XHR endpoints monitored:
/youtubei/v1/search- Search results/youtubei/v1/browse- Home feed, channel pages/youtubei/v1/next- Infinite scroll pagination/youtubei/v1/guide- Sidebar recommendations/youtubei/v1/player- Video player data
Important nuance: For Search + Channel pages, engine filtering is sometimes skipped to allow DOM restore behavior, but the engine should still learn mappings ("harvest only").
- DOM fallback exists because YouTube can:
- hydrate client-side after initial render
- recycle DOM nodes during SPA navigation
- render elements that bypass data interception.
DOM fallback must be careful about:
- identifying the correct container to hide (e.g., Shorts inside
ytd-rich-item-renderer) - not poisoning future matches with stale
data-filtertube-channel-*attributes - handling Mix/playlist cards where video titles might be confused with channel names
- Standard filtering engine applies
- 3-dot menu uses full enrichment pipeline
- All surface types supported (Home, Search, Watch, Shorts, Posts)
- Native UI integration via passive event listeners
- Limited CORS handling for network requests
- Separate storage namespace (
ftProfilesV3.kids) - DOM fallback uses videoChannelMap mappings from Kids browse/search
The 3-dot menu now intelligently upgrades placeholder labels to real channel names using proactive XHR data:
// Detect values that should be upgraded
const isUcIdLike = (value) => /^UC[a-zA-Z0-9_-]{22}$/.test(value.trim());
const isProbablyNotChannelName = (value) => {
if (!value || typeof value !== 'string') return true;
const trimmed = value.trim();
if (!trimmed) return true;
if (isUcIdLike(trimmed)) return true;
if (trimmed.includes('•')) return true; // Metadata separator
if (/\bviews?\b/i.test(trimmed)) return true; // View count
if (/\bago\b/i.test(trimmed)) return true; // Time ago
if (/\bwatching\b/i.test(trimmed)) return true; // Watching count
const lower = trimmed.toLowerCase();
if (lower.startsWith('mix')) return true;
if (lower.includes('mix') && trimmed.includes('–')) return true;
return false;
};Shorts cards:
- Initial: Often only
@handleorvideoId - Enrichment: Fetch from
/shorts/<videoId>page - Result: Human-readable channel name replaces handle
Mix/Playlist cards:
- Detection:
isMixCardElement()identifies by URL patterns (list=RDMM) or badge text - Extraction: Never use video title; extract from actual channel links
- Result: Real channel name, not "Mix - Artist Name"
Watch page right pane:
- Challenge: Playlist items show metadata like "Title • 1.2M views • 2 days ago"
- Solution: Extract from dedicated channel links, ignore metadata text
- Result: Channel name only, clean display
function updateInjectedMenuChannelName(dropdown, channelInfo) {
const current = nameEl.textContent.trim();
const next = pickMenuChannelDisplayName(channelInfo, {});
// Only replace placeholders with better names
if (isUcIdLike(current) || isProbablyNotChannelName(current)) {
nameEl.textContent = next;
}
}Example: @Santasmusicroom.Official appears in search page UI, but opening /@Santasmusicroom.Official/about returns 404.
Impact:
- handle → UC resolution fails.
- blocking may succeed only after refresh if UC mapping is learned later via other channels.
- mitigation (Dec 2025):
- Mapping Sync:
background.jslistens for changes tochannelMapand immediately re-compiles settings for all tabs. New mappings are broadcast instantly. - videoChannelMap: Shorts mappings are cached per video ID to bypass network resolution on repeat encounters.
- Truth in Extraction:
identity.jsprovidesextractCustomUrlFromPathto ensure/c/and/user/paths are parsed identically in all worlds.
- Mapping Sync:
Example: @CorridosdeOroNorte%C3%B1os.
Risk areas:
- Regex patterns like
/@([\w.-]+)/are ASCII/underscore biased and can truncate or fail. - If handle normalization drops unicode glyphs, stored handle won’t match DOM/JSON handle.
Mitigation (current):
- Handle parsing/normalization is centralized in
js/shared/identity.jsand is percent-decoding + unicode-aware.
- A single video can belong to multiple collaborators.
- If resolution selects the wrong collaborator, we can store a wrong UC ID.
- This is why
expectedName/expectedHandlehints exist.
- Cards may be recycled by YouTube SPA.
- If
data-filtertube-channel-id/handle/namesurvives recycling, future matching can be wrong.
Mitigation (Dec 2025):
resetCardIdentityIfStale()detects mismatcheddata-filtertube-video-idand clears all FilterTube attrs before the card is queued for prefetch.- Collaboration cards additionally call
getValidatedCachedCollaborators()to wipe stale collaborator rosters before requesting dialog data.
- Legacy channels often expose only
canonicalBaseUrl(e.g.,/c/VídeoseMensagens). - Historically inconsistencies between background/content extraction caused missing UC mappings.
Mitigation:
filter_logic.js,content_bridge.js, andbackground.jsnow normalize custom URLs intoc/<slug>oruser/<slug>via shared helpers (identity.js) and push them intochannelMap.- When a card surfaces only a custom URL, prefetch resolves via
channelMapbefore any network fetch and persists the UC ID invideoChannelMapso future encounters hide immediately (even offline).
Remaining gap:
- If a brand-new
/c/slug is encountered and no UC mapping exists anywhere, we still need to fetch the channel page (background.js:fetchChannelInfo). This flow is unchanged; just be aware of potential latency.
content_bridge.jshas unicode/percent decode logic (extractRawHandle,normalizeHandleValue).injector.jsnow has a similar-but-separate unicode/percent decode helper (extractRawHandle).filter_logic.jshas its ownnormalizeChannelHandle()logic.
This duplication is a likely root cause of “works in one surface, fails in another”.
- Create a single shared “identity utilities” module (conceptually) with:
- handle extraction (unicode + percent decode)
- normalization rules (comparison vs display)
- UC ID extraction
- Then port:
injector.jshandle parsingfilter_logic.jshandle parsingcontent_bridge.jshandle parsing to the same semantics.
- Should we treat UC ID as the only canonical matching key, and treat handles purely as aliases?
- When
ytInitialDataprovidesbrowseId, should we always trust it over handle URLs? - For handle-only situations, do we prefer:
ytInitialDatalookup byvideoId- over
/@handle/aboutfetch - over Shorts-page fetch ?
- What is the expected behavior when a channel can only be identified by handle (no UC ID available anywhere)?
- Unify unicode-safe handle parsing in
injector.jsand any regex use. - Relax/adjust
expectedHandle/expectedNamematching rules so they prevent wrong-collab matches without rejecting valid 404/unicode cases. - Ensure “block succeeded” always results in immediate hide without requiring hard refresh:
- guarantee that
applyDOMFallback(forceReprocess)sees the new filter entry - guarantee that the engine/harvester learns the mapping where available
- guarantee that
This section answers:
- Which element types exist on the page?
- Where do we read
@handle/ UC ID / name from? - Which file implements that logic?
- How does whitelist mode affect filtering? (v3.2.5)
-
Primary card containers
ytd-rich-item-rendererytd-rich-grid-media- Modern UI variants:
yt-lockup-view-model,yt-lockup-metadata-view-model
-
Extraction path
js/content/dom_fallback.js:applyDOMFallback()enumeratesVIDEO_CARD_SELECTORSand calls:extractChannelMetadataFromElement(...)(best-effort id/handle)extractCollaboratorMetadataFromElement(...)for collaborations
- The engine (
seed.js+filter_logic.js) may pre-stampdata-filtertube-channel-handle/idonto DOM nodes.
-
Whitelist mode behavior (v3.2.5)
- In whitelist mode, only cards from whitelisted channels remain visible
- All other cards are hidden via DOM fallback or data interception
-
Common handle/ID sources
hreflike"/@Handle"or"/channel/UC..."browseEndpointdata (from JSON interception → data attributes)
-
Primary card container
ytd-video-renderer
-
Extraction pitfall
- YouTube often places
data-filtertube-channel-handleon a thumbnail link, which may contain overlay text (duration, “Now playing”).
- YouTube often places
-
Extraction priority (what the code actually does)
content_bridge.js:extractChannelFromCard():- If
data-filtertube-channel-handle/idexist, it still prefers the real channel name element:#channel-info ytd-channel-name a(or equivalent)
- Handle is parsed from
hrefusingextractRawHandle().
- If
-
Primary containers
ytd-watch-metadata/ytd-video-owner-rendererfor the active video- Right-rail
yt-lockup-view-model,ytd-compact-video-renderer, and newwatchCard*renderers - Playlist queue rows (
ytd-playlist-panel-video-renderer) - Embedded Shorts tiles rendered inside the watch column
-
3-dot menu / collaboration status (v3.2.1)
- The watch page reuses the same collaborator roster cache as Home/Search, so any card with ≥2 collaborators immediately renders per-channel menu rows (plus "Block All") with accurate names/handles.
- Shorts tiles opened inside the watch shell may mark
fetchStrategy: 'shorts'; when identity is not already known via proactive XHR interception orvideoChannelMap, we can fall back to/shorts/<id>fetch (and thenfetchChannelFromWatchUrl) to guarantee a canonical UC ID. - In many cases, the UC ID is already known by the time the menu opens because FilterTube harvests ownership from
ytInitialPlayerResponseand/youtubei/v1/playerpayloads and persistsvideoId -> UC...intovideoChannelMap. - Non-collaboration rows still show the generic “Block Channel” label because the synchronous DOM scrape rarely includes the channel name. Follow-up work is tracked to probe
ytd-watch-metadata/ytd-video-owner-renderersynchronously so we can display names everywhere.
-
Playlist/mix gap (still open)
- After a hard refresh the playlist/mix queue can leak hidden videos when SPA navigation rehydrates stale rows; the hidden track may briefly play (~1–1.5 s) or reappear after pressing Next/Prev.
- Root causes and reproduction steps remain documented in
docs/WATCH_PLAYLIST_BREAKDOWN.md; that file still tracks the refilter crash/restore bugs to resolve post-3.1.0.
-
Containers
ytd-shorts-lockup-view-modelytd-reel-item-rendererytd-reel-video-rendererytm-shorts-lockup-view-model/ytm-shorts-lockup-view-model-v2.shortsLockupViewModelHost/.ytGridShelfViewModelGridShelfItem- Sometimes Shorts appear as a full
ytd-video-renderermarked by FilterTube usingdata-filtertube-short="true".
-
Why Shorts are special
- Many Shorts cards do not reliably expose UC ID in DOM.
- So FilterTube uses a three-phase approach:
- Immediate hide (DOM fallback using
videoChannelMapif known). - Asynchronous enrichment: as Shorts are browsed, FilterTube learns
videoId -> UC...from intercepted YouTube JSON (notablyytInitialPlayerResponseand/youtubei/v1/player) and persists those mappings. - Identity resolution (fallback):
https://www.youtube.com/shorts/<videoId>fetch is used only when the UC ID is not available via DOM extraction,videoChannelMap, or main-world lookups.
- Immediate hide (DOM fallback using
-
Key functions
content_bridge.js:extractChannelFromCard()→ may return{id: 'UC...', videoId}immediately when the DOM exposes/channel/UC....- If the card only exposes
{videoId, needsFetch: true},content_bridge.jsmarks the card asfetchStrategy: 'shorts'/source: 'shortsCard'so the block flow usesshorts:<videoId>instead of the watch resolver. - Quick Cross does the same through
block_channel.js:buildQuickBlockContext()andgetQuickBlockInput(), so tablet Shorts and mobile watch-page Shorts do not fall back to weakwatch:<videoId>placeholders. content_bridge.js:fetchChannelFromShortsUrl(videoId, requestedHandle)parses Shorts HTML as a last resort.
The current regression target was tablet Shorts cards failing on most pages and mobile watch-page Shorts failing via 3-dot / Quick Cross. The fix keeps Shorts on a Shorts-specific identity path:
flowchart TB
A["Shorts card or watch-page Shorts tile"] --> B["isShortsContentElement()"]
B --> C["extractShortsVideoIdFromElement()"]
C --> D{"Stable UC / handle already known?"}
D -- yes --> E["Block with UC / handle"]
D -- no --> F["Use shorts:VIDEO_ID placeholder"]
F --> G["background performShortsIdentityFetch() first"]
G --> H{"Resolved UC / handle?"}
H -- yes --> I["Persist channel + videoChannelMap"]
H -- no --> J["Fallback watch identity fetch / visible failure"]
I --> K["Hide Shorts card container"]
Rules:
shorts:<videoId>is only a resolver hint. It is never stored as a channel identity.- The background resolver tries
/shorts/<videoId>before/watch?v=<videoId>for this path. - The Android runtime bridge recognizes both
watch:<videoId>andshorts:<videoId>as resolver placeholders, matching the extension source. - 3-dot rows now show a pending style while the resolver runs, then close the injected menu after a successful block so orphaned menu rows do not remain after the target card disappears.
-
Container
ytd-post-renderer
-
Identity source
#author-text/#author-thumbnail alinks.- Handle extracted from
hrefviaextractRawHandle().
-
Detection
#attributed-channel-nameis used as a collaboration signal.
-
Identity model
- A collaboration card produces:
- a primary channel (first collaborator)
allCollaborators[]with best-effort{name, handle, id}for each collaborator
- A collaboration card produces:
-
3-dot menu behavior (2+ collaborators)
- For collaboration cards, FilterTube injects one menu row per collaborator.
- It also injects a final row:
- 2 collaborators: “Both Channels”
- 3–6 collaborators: “All N Collaborators”
-
Multi-select behavior (3–6 collaborators)
- When there are 3+ collaborators, FilterTube uses a multi-step selection UI:
- Clicking an individual collaborator row selects it (does not immediately persist or hide).
- The bottom row label becomes “Done • Block X Selected”.
- Clicking Done persists only the selected collaborators.
- For 2 collaborators, multi-select is not used (each row blocks immediately; “Both Channels” blocks both).
- When there are 3+ collaborators, FilterTube uses a multi-step selection UI:
-
N collaborator limit
- The collaboration menu is currently capped to 6 total channels (YouTube typically shows up to 5 collaborators + 1 uploader).
- This is enforced in
content_bridge.jsso the menu stays usable and matches YouTube’s UI expectations.
-
Identity integrity (important)
- When blocking a collaborator, FilterTube treats the following as identity keys:
id(UC ID)handle(@handle)customUrl(c/<slug>oruser/<slug>)
- FilterTube avoids persisting “mixed identity” entries (example: UC ID from collaborator A but @handle from collaborator B).
- If a collaborator is only known via legacy
/c/or/user/URLs, the system can still persist and later resolve that identity viachannelMap.
- When blocking a collaborator, FilterTube treats the following as identity keys:
-
Enrichment path
- Isolated world gathers best-effort collaborator list from DOM.
- Main world (
injector.js) can extract collaborator list fromytInitialDataand/or DOM hydration.
-
Surface differences: Home vs Search vs Shorts
- Home (lockup cards)
- Collaboration display may only show names.
- We attempt:
- lockup renderer data (
showDialogCommandlist items), - avatar stacks,
- metadata rows,
- and finally Main World
ytInitialDatalookup byvideoId.
- lockup renderer data (
- Search (ytd-video-renderer)
- Collaboration display is often under
#attributed-channel-name. - The first collaborator often has a direct
/@handlelink; others require dialog/ytInitialData.
- Collaboration display is often under
- Shorts
- Shorts cards can be handle-only; collaboration extraction is more limited.
- If needed, we resolve identity via
/shorts/<id>or/watch?v=<id>fallbacks and updatevideoChannelMap.
- Home (lockup cards)
This section answers “where is content hidden and by what mechanism?”
- Where: Main world
seed.jsintercepts JSON payloads and runsfilter_logic.jsbefore YouTube renders.
- What it does: removes/filters items in JSON so they never render.
- Where: Isolated world (
js/content/dom_fallback.js+content_bridge.js) - Entry point:
applyDOMFallback(settings, {forceReprocess})- Enumerates
VIDEO_CARD_SELECTORS - Extracts:
- title text
- channel metadata (
extractChannelMetadataFromElement) - collaborator metadata (
extractCollaboratorMetadataFromElement)
- Calls
shouldHideContent(...) - Applies hiding via
toggleVisibility(target, shouldHide, reason)
- Enumerates
- Where:
content_bridge.js:handleBlockChannelClick(...) - Behavior: once a channel is successfully added to storage, it hides:
- the clicked card
- and all visible duplicates of the same video in the current surface
applyDOMFallback()runs repeatedly (SPA navigation + storage updates).- If items no longer match filters (or we reprocess),
toggleVisibility(..., false)restores them. - The “Hide/Restore Summary” is debug accounting from
filteringTracker.
This is not accidental duplication; it’s forced by:
-
World separation
content_bridge.jscannot reliably readwindow.ytInitialData.- So it asks
injector.jsviapostMessage.
-
YouTube renderer diversity
- Search ≠ Home ≠ Shorts ≠ Posts.
- Each has different DOM shape and different JSON structures.
The real problem today is not that these paths exist, but that:
- we had different handle parsers
- and some paths did network resolution at the wrong time.
This section maps directly to the logs you shared.
What you observed:
- The UI shows
@Santasmusicroom.Official. - Fetching
/@Santasmusicroom.Official/aboutreturns 404 (YouTube bug).
Previously:
- Background refused to store the channel if
fetchChannelInfo()failed.
Now:
- Background refuses to persist unresolved handle-only rows.
- A block can recover through
channelMap,watch:<videoId>, watch-page identity fetch, or a known UC ID. - If none of those produce a stable
UC...ID, the menu shows a failure state instead of writing a weak handle-only channel row.
- A block can recover through
The Shorts HTML contains lots of ytInitialData objects.
Our generic deep scan (extractChannelFromInitialData) can pick up unrelated byline runs and return e.g. @TronLegacyScore.
Now:
- When we have an expected handle, we skip that deep scan and rely on:
- engagement panel
- overlay header
- canonical/owner links
There was a caching bug in content_bridge.js:fetchIdForHandle():
- If a handle was being resolved and got marked
PENDING, later calls returned early and did not consultchannelMap.
Now:
- Even if a handle is pending, we still consult
channelMapfirst. - If
/aboutreturns non-OK (including 404), we clear the pending marker.
The menu injection background enrichment should not do network fetches.
Now:
- Menu “background fetch” uses
fetchIdForHandle(handle, { skipNetwork: true }).