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).
- Channel add/block actions must preserve the requested list target (
blocklistvswhitelist) across UI, content script, and background paths. - Whitelist Shorts creator-page fallback may use trusted page context to keep allowed channel Shorts visible while card identity hydrates.
- Watch autoplay/end-screen endpoint filtering participates in channel/content hiding when end-screen settings are enabled.
- Runtime DOM state should avoid broad externally visible FilterTube-specific attributes unless a feature needs that DOM-visible state.
- 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 IDalternateIds: additional UC IDs that YouTube explicitly associated with the same visible creator identity on one video (for example a current creator channel plus a legacy VEVO channel)handle: normalized handle used for matchingcustomUrl: normalized legacy URL slughandleDisplay: UI/display handlename: channel nameoriginalInput: what the user actually typed or clickedfilterAll: boolean- collaboration metadata
alternateIds does not create more channels in the UI. A creator with one
primary id and one verified alternate remains one stored rule and one menu or
settings row. Matching treats every ID in that rule as canonical for the rule,
so content attributed to either linked ID receives the same block/allow
decision.
The link is evidence-gated. A second ID is retained only when the exact video-scoped JSON supplies both IDs and their normalized name, handle, or custom URL identifies exactly one creator. A collaborator sheet can provide one ID while that renderer's channel thumbnail supplies another; a solo renderer can likewise pair card, owner, and player signals. Loose name similarity, an ambiguous roster label, and MIX/radio participant prose are not sufficient.
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 can often resolve from the local map before any fallback resolver is considered.
FilterTube uses a JSON-first identity strategy: intercepted YouTubei/ytInitial*
payloads are preferred when they expose stable channel identity. This can reduce
resolver latency and avoid some background fetches, but it is not a guarantee
that identity exists before DOM render, that every card is complete at first
paint, or that every route can avoid a fallback resolver.
The practical waterfall priority is:
- XHR JSON interception (Main World)
ytInitial*snapshots (Main World)- Harvested learned maps (
channelMap,videoChannelMap,videoMetaMap) - DOM extraction (Isolated World, visible-card fallback/enrichment)
- Network fetch / resolver enrichment (Background, last-resort resolver and post-action enrichment layer)
The important distinction is that XHR/JSON is the preferred source tier, not
the only authority and not automatic permission to hide, fetch, mutate, stamp,
or persist. JSON/player payloads are often converted into learned maps first,
then later DOM and menu paths use videoId as a join key. On watch, Shorts,
playlist, Kids, and some YouTube Music surfaces, the DOM can expose only a video
id or weak display text, so a channel-accurate decision must join that id to
JSON/player/map data or use the background resolver/enrichment layer when the
current target still lacks usable identity. That layer is not proof of exact
clicked-target scope by itself: current successful channel blocks can also
trigger visible Shorts and playlist-row enrichment for rows missing
videoChannelMap identity. Any
documentation or implementation change that claims "instant," "before render," or "zero network" must first prove that claim for the specific profile, mode, route, renderer family, source tier, and allowed side effect. The same proof
requirement applies to "no work" claims, because an empty visible list can
still leave harvesting, observers, fetch hooks, map writes, or fallback scans
active today.
Current behavior still lacks a shared modeSurfaceEffectAuthority, so every
optimization must keep source confidence separate from effect permission:
source tier says: where identity came from
mode/surface says: whether that identity may hide, allow, restore, fetch,
write learned maps, count stats, or do no work
For the route-by-route version of this boundary, use
docs/audit/FILTERTUBE_ROUTE_IDENTITY_DECISION_INDEX_2026-05-20.md. That index maps
Main home/search, watch, Shorts, watch rail/end screen, playlists/Mix, Kids,
YTM, collaborator surfaces, posts, and comments to separate decision classes
instead of treating this waterfall as one global behavior rule.
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
When one exact video exposes two corroborated UC IDs for the same visible
creator, injector.js preserves the second as alternateIds. For a header-backed
collaborator roster, this enrichment happens on the matching roster member and
does not append another collaborator. For solo content, candidate merging keeps
both IDs only when the candidate labels corroborate; otherwise the identities
remain separate.
Current camelCase YouTube Music DOM bylines such as A and B are only lookup
triggers. They provide the videoId and expected names used to locate a retained
XHR response. The header-backed JSON sheet and its browse endpoints remain the
identity authority; the DOM phrase is never split and persisted as truth.
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)
Used when:
- No identity found in XHR/ytInitial*/learned maps/DOM for a target that still needs a channel decision
- Manual channel addition via popup
- Post-block enrichment for missing fields
- Current successful-block follow-up enrichment for visible Shorts and playlist
rows that do not yet have
videoChannelMapidentity
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 boundary: DOM/page-context handle resolution can request skipNetwork: true, but that does not mean every Kids identity path is network-free. The background has a Kids watch identity fallback after stored/session/pending checks when a Kids watch surface only provides a video id. Treat Kids network behavior as a last-resort resolver that must be governed by a future identityFetchAuthority, not as a blanket zero-network guarantee.
Work-budget boundary: this layer is not only about correctness. Current
source can also do follow-up work after a successful block, including visible
Shorts and playlist-row enrichment beyond the clicked target. Future
optimization must separate exact-target identity repair from page-wide
post-action fanout with an explicit identityWorkBudget.
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 when enough identity is already known
↓
DOM (visible UI)
- Cards appear pre-stamped
- No network calls needed for blocking when JSON/maps/DOM already prove identity
↓
Network (last-resort fallback)
- Only if JSON/player/maps/DOM lacked enough identity
- Uses Shorts/Watch/About fetches
- Kids watch uses stored/session/pending identity first and may fall back to a Kids watch resolver
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 can use the stamped identity without a background fetch when the UC ID, handle, or trusted learned map is already known.
- Weak targets can still show a loading/fetching state, route through a watch/Shorts/Kids resolver, or fail if the current JSON/maps/DOM/fallback path cannot prove stable channel identity.
Two paths, both source-confidence inputs. Neither path by itself proves that every visible card has complete identity before render:
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 can appear quickly on watch/home/search when the collaborator roster is already present in JSON, cached state, or trusted DOM attributes. Collapsed or delayed rosters still require the collaborator dialog or sheet recovery path.
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 - fallback after JSON/maps/DOM checks
if (needsEnrichment && enrichedInfo?.videoId) {
// Route to appropriate fetch handler (last resort)
if (isKidsUrl) {
// Kids watch: use the guarded Kids watch resolver after cache/map checks
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 a fallback after JSON snapshots, learned maps, and DOM context fail to prove enough identity, but it is not globally budgeted by one authority yet.
- If
fetchStrategy === "mainworld", we searchytInitialDatasnapshots. - Page-context direct network fetches can be avoided on Kids, but the background Kids watch resolver can still run after cache/map checks.
- 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 supported YouTube JSON before render viafetchandXMLHttpRequesthooks when the endpoint is in scope and the payload exposes enough data.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 can upgrade placeholder labels to real channel names when JSON snapshots, learned maps, or stamped DOM already prove enough identity:
// 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 JSON snapshots orvideoChannelMap, we can fall back to/shorts/<id>fetch (and thenfetchChannelFromWatchUrl) to attempt canonical UC ID resolution. - 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 }).