This guide helps developers understand and extend FilterTube's proactive channel identity system and whitelist mode functionality. It covers how to add support for new YouTube endpoints, renderer types, collaboration patterns, and dual filtering modes.
-
Main World (
js/seed.js,js/filter_logic.js,js/injector.js): Page context, can accesswindow.ytInitialData -
Isolated World (
js/content/*,js/content_bridge.js): Content scripts, can access DOM -
Background (
js/background.js): Extension service worker, handles persistence and network
-
Blocklist Mode: Traditional filtering - hide matching content
-
Whitelist Mode: Inverted filtering - show only matching content
-
XHR interception → JSON snapshots
-
Channel extraction from snapshots
-
Mode-aware filtering logic
-
Cross-world messaging
-
DOM stamping
-
Instant UI updates
When YouTube introduces new API endpoints, you'll want to add them to the snapshot stashing system.
function stashNetworkSnapshot(data, dataName) {
try {
if (!window.filterTube) return;
if (!data || typeof data !== 'object') return;
const name = typeof dataName === 'string' ? dataName : '';
if (!name) return;
const ts = Date.now();
// Add new endpoint here
if (name.includes('/youtubei/v1/reel')) {
window.filterTube.lastYtReelResponse = data;
window.filterTube.lastYtReelResponseName = name;
window.filterTube.lastYtReelResponseTs = ts;
return;
}
// Existing endpoints
if (name.includes('/youtubei/v1/next')) {
window.filterTube.lastYtNextResponse = data;
window.filterTube.lastYtNextResponseName = name;
window.filterTube.lastYtNextResponseTs = ts;
return;
}
// ... other endpoints
} catch (e) {
// Ignore errors
}
}function searchCollaboratorsInData(videoId) {
const roots = [];
// Add new snapshot to search targets
if (window.filterTube?.lastYtReelResponse) {
roots.push({ root: window.filterTube.lastYtReelResponse, label: 'filterTube.lastYtReelResponse' });
}
// Continue with existing logic...
}- Open YouTube and trigger the new endpoint
- Check console for "Stashing network snapshot" messages
- Verify the snapshot is stored in
window.filterTube - Test channel extraction from the snapshot
YouTube frequently introduces new renderer types. Here's how to add support:
Use the browser dev tools to inspect the JSON structure:
// In console on a YouTube page
console.log(window.filterTube.lastYtNextResponse);Look for patterns like:
newRendererTypevideoIdorcontentIdbrowseEndpoint.browseId(UC ID)canonicalBaseUrl(handle/customUrl)
_extractChannelInfo(item, rules) {
const channelInfo = { name: '', id: '', handle: '', customUrl: '' };
// Add new renderer type
if (item.newRendererType) {
const renderer = item.newRendererType;
// Extract video ID
if (renderer.videoId) {
// Extract channel info
const endpoint = renderer.channelEndpoint?.browseEndpoint;
if (endpoint?.browseId) {
channelInfo.id = endpoint.browseId;
}
if (endpoint?.canonicalBaseUrl) {
channelInfo.handle = normalizeChannelHandle(endpoint.canonicalBaseUrl);
channelInfo.customUrl = extractCustomUrlFromCanonicalBaseUrl(endpoint.canonicalBaseUrl);
}
channelInfo.name = renderer.channelTitle?.simpleText || '';
// Register mappings
if (channelInfo.id && channelInfo.handle) {
this._registerMapping(channelInfo.id, channelInfo.handle);
}
if (channelInfo.id && channelInfo.customUrl) {
this._registerCustomUrlMapping(channelInfo.id, channelInfo.customUrl);
}
return channelInfo;
}
}
// Continue with existing logic...
}// In the same function, register video → channel mapping
if (item.newRendererType?.videoId && channelInfo.id) {
this._registerVideoChannelMapping(item.newRendererType.videoId, channelInfo.id);
}- Find a page that uses the new renderer
- Open FilterTube dev tools
- Look for extraction logs
- Verify the channel appears correctly in the 3-dot menu
YouTube may introduce new ways to represent collaborations:
// In filter_logic.js or injector.js
function extractCollaboratorsFromDataObject(obj) {
// Add new avatar stack pattern
if (obj.newAvatarStackType) {
const collaborators = [];
for (const avatar of obj.newAvatarStackType.avatars) {
const collab = {
id: avatar.channelEndpoint?.browseId,
handle: extractHandle(avatar.channelEndpoint?.canonicalBaseUrl),
name: avatar.title
};
collaborators.push(collab);
}
return collaborators.length > 1 ? collaborators : null;
}
// Continue with existing logic...
}// Add new dialog pattern
if (obj.newShowDialogCommand) {
const listItems = obj.newShowDialogCommand.panel?.listItems;
if (listItems) {
// Extract collaborators from new structure
return extractCollaboratorsFromListItems(listItems);
}
}- Find a collaboration video using the new pattern
- Open the 3-dot menu
- Verify all collaborators appear
- Test blocking individual collaborators
When XHR interception doesn't provide enough data, you may need to improve DOM extraction:
// In content_bridge.js
function extractChannelFromCard(card) {
// Add new card type
if (card.tagName.toLowerCase() === 'ytd-new-video-renderer') {
const link = card.querySelector('a[href*="/channel/"], a[href*="/@"]');
if (link) {
const href = link.getAttribute('href');
return {
id: extractChannelIdFromPath(href),
handle: extractRawHandle(href),
name: card.querySelector('.channel-name')?.textContent?.trim()
};
}
}
// Continue with existing logic...
}// Add better name extraction for specific surfaces
if (card.matches('.special-surface')) {
// Use specific selector for this surface
const nameEl = card.querySelector('.specific-name-selector');
if (nameEl) {
return { name: nameEl.textContent.trim() };
}
}The subscribed-channels importer is a separate pipeline from normal filtering. Maintain it as a bulk acquisition flow, not as another quick-add entry point.
/Users/devanshvarshney/FilterTube/js/tab-view.js/Users/devanshvarshney/FilterTube/js/state_manager.js/Users/devanshvarshney/FilterTube/js/content/bridge_settings.js/Users/devanshvarshney/FilterTube/js/injector.js/Users/devanshvarshney/FilterTube/js/background.js
- Tab View finds or opens a main YouTube tab.
- That tab is routed to
/feed/channels. waitForYoutubeTabReady()waits for:- page load
- subscriptions-import receiver
- MAIN-world injector bridge
- Only after that does
FilterTube_ImportSubscribedChannelsrun.
This means most early failures are startup/bridge issues, not subscription-data issues.
injector.js currently does:
- page-seed collection from
/feed/channels FEchannelsbrowse requests- normalization + dedupe
- progress events back to the isolated bridge
Do not move this into popup-only logic. It depends on a live signed-in YouTube page context.
- Keep a normal signed-in YouTube tab open.
- Start import from Tab View.
- Confirm the selected tab moves to
/feed/channels. - Confirm progress appears inline.
- Confirm whitelist rows were persisted.
- Use a browser profile with no YouTube sign-in.
- Start import.
- Confirm the sign-in page is surfaced and the UI shows a retryable sign-in warning.
- Keep a YouTube tab open.
- install or reload the extension
- retry import before refreshing the old tab
- verify whether a one-time hard refresh is still required for bridge startup
signed_out- the selected tab really is in sign-in/account-routing flow, or the request profile looked logged out
subscriptions_import_unavailable- isolated bridge or MAIN-world bridge never came up
timeout- page or
youtubeirequest stalled too long
- page or
persist_failed- page import succeeded, background merge/persist did not
Safe places to extend later:
- add stronger
FEchannelsparsing ininjector.js - improve progress metadata returned from
injector.js - refine count fidelity separately from startup reliability
Do not mix startup fixes and source-fidelity changes in the same patch unless the regression surface is tiny and fully validated.
// Check available snapshots
console.log('Available snapshots:', {
next: window.filterTube?.lastYtNextResponse,
browse: window.filterTube?.lastYtBrowseResponse,
player: window.filterTube?.lastYtPlayerResponse
});
// Search for a video in snapshots
function findVideo(videoId) {
const snapshots = [
window.filterTube?.lastYtNextResponse,
window.filterTube?.lastYtBrowseResponse,
window.filterTube?.lastYtPlayerResponse
];
for (const snapshot of snapshots) {
if (snapshot && JSON.stringify(snapshot).includes(videoId)) {
console.log('Found in snapshot:', snapshot);
return snapshot;
}
}
}Enable debug logging by setting in console:
window.filterTubeDebug = true;This enables verbose logging in:
- Channel extraction
- Collaboration detection
- Cross-world messaging
- Channel Extraction: Test with various JSON structures
- Handle Normalization: Test unicode and percent-encoded handles
- Collaboration Detection: Test different collaboration patterns
- Cross-world Messaging: Test message passing between worlds
- Surface Coverage: Test all YouTube surfaces (Home, Search, Watch, Shorts)
- Network Conditions: Test with slow/failed network
- Kids Mode: Test zero-network behavior
- Edge Cases: Test empty data, malformed JSON
- Existing Functionality: Ensure old surfaces still work
- Performance: Verify no performance regressions
- Memory Usage: Check for memory leaks in snapshot storage
Always register mappings when you extract channel info:
// Don't forget this!
if (channelInfo.id && channelInfo.handle) {
this._registerMapping(channelInfo.id, channelInfo.handle);
}Remember to skip network fetches on Kids:
if (isKidsHost) {
// Use only XHR data, no network fetches
return extractFromSnapshots(videoId);
}Always validate data structure:
if (!item?.newRendererType?.videoId) {
return null; // or continue with next pattern
}Ensure handle extraction supports unicode:
function normalizeHandle(handle) {
if (!handle) return '';
// Decode percent encoding
return decodeURIComponent(handle);
}- Snapshots are stored in memory, consider size limits
- Clear old snapshots periodically if needed
- Use WeakMap for temporary storage
- Batch multiple updates in a single message
- Avoid sending duplicate data
- Use request IDs for request/response patterns
- Use document fragments for bulk DOM changes
- Debounce rapid DOM updates
- Avoid layout thrashing
Always validate external data:
function validateChannelInfo(info) {
return {
id: typeof info.id === 'string' && info.id.startsWith('UC') ? info.id : '',
handle: typeof info.handle === 'string' ? info.handle.slice(0, 100) : '',
name: typeof info.name === 'string' ? info.name.slice(0, 200) : ''
};
}Sanitize cross-world messages:
function sanitizeMessage(message) {
// Validate message structure
if (!message.type || typeof message.type !== 'string') return null;
// Validate payload based on type
switch (message.type) {
case 'FilterTube_UpdateChannelMap':
return validateChannelMap(message.payload);
// ... other cases
}
}- Follow existing code style
- Add JSDoc comments for new functions
- Use TypeScript-style JSDoc for better IDE support
- Update this guide when adding new patterns
- Add examples for new renderer types
- Document any breaking changes
- Add unit tests for new extraction logic
- Test on multiple YouTube surfaces
- Verify Kids mode compatibility
- YouTube Data API documentation
- Chrome Extension API docs
- JavaScript extension best practices