-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathYoutubeAdblock.user.js
More file actions
10528 lines (9903 loc) · 486 KB
/
Copy pathYoutubeAdblock.user.js
File metadata and controls
10528 lines (9903 loc) · 486 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ==UserScript==
// @name YoutubeAdblock
// @namespace https://github.com/SysAdminDoc
// @version 0.8.2
// @description Local YouTube ad blocking with signed rules, plus an in-page Control Center that explains what is running.
// @author SysAdminDoc
// @license MIT
// @icon https://raw.githubusercontent.com/SysAdminDoc/YoutubeAdblock/main/icon.png
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @match https://music.youtube.com/*
// @match https://tv.youtube.com/*
// @match https://www.youtube-nocookie.com/*
// @match https://youtubekids.com/*
// @match https://www.youtubekids.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @inject-into content
// @run-at document-start
// @connect raw.githubusercontent.com
// @connect github.com
// @connect githubusercontent.com
// @connect cdn.jsdelivr.net
// @connect sponsor.ajay.app
// @connect dearrow-thumb.ajay.app
// @connect returnyoutubedislikeapi.com
// @connect *
// @homepageURL https://github.com/SysAdminDoc/YoutubeAdblock
// @supportURL https://github.com/SysAdminDoc/YoutubeAdblock/issues
// @downloadURL https://raw.githubusercontent.com/SysAdminDoc/YoutubeAdblock/main/YoutubeAdblock.user.js
// @updateURL https://raw.githubusercontent.com/SysAdminDoc/YoutubeAdblock/main/YoutubeAdblock.user.js
// ==/UserScript==
(function() {
'use strict';
/* =========================================================================
* CONSTANTS & CONFIG
* ===================================================================== */
const SCRIPT_NAME = 'YoutubeAdblock';
const SCRIPT_VERSION = '0.8.2';
const PROJECT_URL = 'https://github.com/SysAdminDoc/YoutubeAdblock';
const ISSUES_URL = `${PROJECT_URL}/issues`;
const FILTER_URL_DEFAULT = 'https://raw.githubusercontent.com/SysAdminDoc/YoutubeAdblock/refs/heads/main/youtube-adblock-filters.txt';
const FILTER_MANIFEST_URL_DEFAULT = 'https://raw.githubusercontent.com/SysAdminDoc/YoutubeAdblock/refs/heads/main/youtube-adblock-filters.manifest.json';
const FILTER_SIGNATURE_URL_DEFAULT = 'https://raw.githubusercontent.com/SysAdminDoc/YoutubeAdblock/refs/heads/main/youtube-adblock-filters.txt.sig';
const WEBPACK_SIGNATURE_URL_DEFAULT = 'https://raw.githubusercontent.com/SysAdminDoc/YoutubeAdblock/refs/heads/main/webpack-ad-signatures.json';
const WEBPACK_SIGNATURE_MANIFEST_URL_DEFAULT = 'https://raw.githubusercontent.com/SysAdminDoc/YoutubeAdblock/refs/heads/main/webpack-ad-signatures.manifest.json';
const WEBPACK_SIGNATURE_SIG_URL_DEFAULT = 'https://raw.githubusercontent.com/SysAdminDoc/YoutubeAdblock/refs/heads/main/webpack-ad-signatures.json.sig';
const FILTER_PUBLIC_KEY_BASE64 = 'MCowBQYDK2VwAyEAdkjPuIDzXFI9UPn5w4t4selqoqbT4WCinGI58a2/a6E=';
const FILTER_URL_MIRRORS = [
'https://cdn.jsdelivr.net/gh/SysAdminDoc/YoutubeAdblock@main/youtube-adblock-filters.txt',
];
const FILTER_MANIFEST_URL_MIRRORS = [
'https://cdn.jsdelivr.net/gh/SysAdminDoc/YoutubeAdblock@main/youtube-adblock-filters.manifest.json',
];
const FILTER_SIGNATURE_URL_MIRRORS = [
'https://cdn.jsdelivr.net/gh/SysAdminDoc/YoutubeAdblock@main/youtube-adblock-filters.txt.sig',
];
const FILTER_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours
const FILTER_MAX_BYTES = 5 * 1024 * 1024; // 5MB safety cap on remote lists
const FILTER_FETCH_TIMEOUT_MS = 15000;
const SSAP_POLL_INTERVAL_MS = 1000;
const STATS_PERSIST_INTERVAL_MS = 2000;
const STATS_UI_REFRESH_MS = 5000;
const CSS_PREFIX = 'ytab';
const IS_EXTENSION_BUILD = typeof __YTAB_STORAGE_KEY !== 'undefined';
const SCRIPT_EVAL_READY_STATE = (typeof document !== 'undefined' && document.readyState) ? document.readyState : 'unknown';
const SCRIPT_EVAL_ELAPSED_MS = (typeof performance !== 'undefined' && typeof performance.now === 'function')
? Math.round(performance.now())
: null;
const LATE_INJECTION_THRESHOLD_MS = 1500;
const DNR_DIAGNOSTICS_REQUEST_EVENT = 'ytab:dnr-diagnostics-request';
const DNR_DIAGNOSTICS_RESPONSE_EVENT = 'ytab:dnr-diagnostics-response';
const DNR_DIAGNOSTICS_WINDOW_MINUTES = 5;
const DNR_DIAGNOSTICS_WINDOW_MS = DNR_DIAGNOSTICS_WINDOW_MINUTES * 60 * 1000;
const DNR_DIAGNOSTICS_MAX_MATCHES = 128;
const DNR_DIAGNOSTICS_MAX_COUNT = 1000000;
const DNR_DIAGNOSTICS_REASONS = new Set([
'api-unavailable',
'permission-required',
'quota-exceeded',
'cooldown',
'invalid-context',
'query-failed',
'timeout'
]);
const DEFAULT_STATS = {
blocked: 0,
pruned: 0,
ssapSkipped: 0,
sponsorSkipped: 0,
dearrowReplaced: 0,
feedFiltered: 0,
ssaiDetected: 0,
complianceDialogs: 0,
sabrOnlyResponses: 0,
domBypassBlocked: 0
};
const SPONSORBLOCK_API = 'https://sponsor.ajay.app/api/skipSegments';
const SPONSORBLOCK_CATEGORIES = [
'sponsor', 'selfpromo', 'interaction',
'intro', 'outro', 'preview',
'music_offtopic', 'filler'
];
const SPONSORBLOCK_TIMEOUT_MS = 10000;
const DEARROW_API = 'https://sponsor.ajay.app/api/branding';
const DEARROW_TIMEOUT_MS = 10000;
const DEARROW_CACHE_TTL = 6 * 60 * 60 * 1000; // 6 hours
const DEARROW_CACHE_MAX = 400;
const RYD_API = 'https://returnyoutubedislikeapi.com/votes';
const RYD_TIMEOUT_MS = 8000;
const RYD_CACHE_TTL = 30 * 60 * 1000; // 30 minutes
const RYD_CACHE_MAX = 200;
const API_COOLDOWN_DEFAULT_MS = 60 * 1000;
const API_COOLDOWN_MAX_MS = 15 * 60 * 1000;
const VOLUME_BOOST_MAX = 5; // hard cap — beyond this audio clips badly
const SECTION_IDS = {
overview: `${CSS_PREFIX}-section-overview`,
rules: `${CSS_PREFIX}-section-rules`,
core: `${CSS_PREFIX}-section-core`,
anti: `${CSS_PREFIX}-section-anti`,
cleanup: `${CSS_PREFIX}-section-cleanup`,
sponsor: `${CSS_PREFIX}-section-sponsor`,
enhance: `${CSS_PREFIX}-section-enhance`,
clutter: `${CSS_PREFIX}-section-clutter`,
blocklist: `${CSS_PREFIX}-section-blocklist`,
diagnostics: `${CSS_PREFIX}-section-diagnostics`
};
const STRINGS = {
common: {
none: 'none',
unknown: 'unknown',
never: 'never',
notInstalled: 'not installed',
notSyncedYet: 'Not synced yet',
unknownShort: '?'
},
sites: {
youtube: 'YouTube',
music: 'YouTube Music',
tv: 'YouTube TV',
mobile: 'YouTube Mobile',
noCookie: 'YouTube No-Cookie',
kids: 'YouTube Kids'
},
surfaces: {
watch: 'Watch Page',
shorts: 'Shorts Feed',
search: 'Search Results',
playlist: 'Playlist',
subscriptions: 'Subscriptions',
history: 'History',
library: 'Library',
channel: 'Channel',
live: 'Live Stream',
browse: 'Browse',
home: 'Home',
current: 'Current Page'
},
access: {
toolbarButton: 'Toolbar Button',
userscriptMenu: 'Userscript Menu',
extensionHint: 'Click the toolbar button from any YouTube tab. Optional shortcuts can be bound in browser extension settings.',
userscriptHint: 'Open the Control Center from your userscript manager menu any time.'
},
filters: {
sourceLabels: {
remote: 'Remote list',
cached: 'Cached list',
stale: 'Cached list (stale)',
'built-in': 'Built-in fallback',
custom: 'Custom source'
},
integrityLabels: {
verified: 'Verified',
'unsigned-custom': 'Unsigned Custom',
failed: 'Verification Failed',
cached: 'Cached',
'built-in': 'Built-In',
unknown: 'Unknown'
},
cachedIntegrityMessage: 'Cached rules are active; refresh to re-check signature status.',
builtInIntegrityMessage: 'Built-in fallback rules are bundled with the script.',
signedCompanionsUnavailable: 'Signed filter companion URLs are unavailable.',
signedManifestInvalid: 'Signed filter manifest is invalid.',
signedManifestRejected: (reason) => {
if (reason === 'rollback') return 'Signed filter manifest is older than one already accepted; the last known good rules stayed active.';
if (reason === 'expired') return 'Signed filter manifest has expired; the last known good rules stayed active.';
if (reason === 'unsigned-manifest') return 'Signed filter manifest signature did not verify.';
return 'Signed filter manifest is invalid.';
},
webpackSignatureStale: 'Webpack signature manifest was replayed or expired; the last known good database stayed active.',
signedByteMismatch: 'Signed filter byte count does not match the downloaded list.',
signedHashMismatch: 'Signed filter hash does not match the downloaded list.',
signedVerificationFailed: 'Signed filter verification failed.',
webCryptoShaUnavailable: 'WebCrypto SHA-256 is unavailable.',
webCryptoEd25519Unavailable: 'WebCrypto Ed25519 verification is unavailable.',
defaultVerified: updated => `Default Rule Library verified with Ed25519${updated ? ` (${updated})` : ''}.`,
customSourceLoadedWithoutSignature: 'Custom Rule Library source loaded without signature verification.',
allSourcesUnreachable: 'All filter sources (primary + mirrors) were unreachable. Your current rules stayed active.',
remoteUnreachable: 'The remote list was unreachable, so YoutubeAdblock stayed on the last known rule set.',
remoteTooLarge: maxMb => `Remote filter list exceeds ${maxMb}MB limit.`,
invalidJsonSchema: 'Invalid JSON filter schema.',
noUsableRules: 'The remote list produced no usable rules.',
couldNotVerifyOrParse: 'Remote rules could not be verified or parsed.',
remoteParseFailed: 'The remote list could not be parsed. Your current rules stayed active.',
remoteVerificationFailed: 'Remote rules could not be verified.',
remoteRequestFailed: 'Remote filter request failed.',
remoteRequestTimedOut: 'Remote filter request timed out.',
ruleLibraryProblem: detail => `Rule library problem: ${detail} Your current rules stayed active.`,
webpackSignatureTooLarge: maxKb => `Remote webpack signature database exceeds ${maxKb}KB limit.`,
webpackSignatureInvalid: 'Remote webpack signature database did not contain usable tokens.',
webpackSignatureFetchFailed: 'Webpack signature refresh failed.',
webpackSignatureTampered: 'Remote webpack signature database failed integrity verification. Using cached/built-in signatures.',
webpackSignatureVerified: 'Webpack signature database verified.',
refreshComplete: (count, version, integrity) => {
const suffix = integrity === 'unsigned-custom'
? ' Unsigned custom source.'
: ' Signature verified.';
return `Rule refresh complete. ${formatNumber(count)} rules active (${version || STRINGS.common.unknownShort}).${suffix}`;
}
},
protectionSummary: {
paused: {
label: 'Paused',
tone: 'warn',
description: 'Every blocking engine is paused until you turn protection back on.'
},
refreshing: {
label: 'Refreshing…',
tone: 'info',
description: 'Pulling the latest rule set while keeping your current protection active.'
},
protected: 'Protected',
remoteDescription: 'Remote rules are live and the fallback remains ready if the source goes away.',
cachedDescription: 'Cached rules are active while YoutubeAdblock waits for a fresher remote copy.',
staleDescription: 'Previously saved rules are active while YoutubeAdblock refreshes in the background.',
builtInDescription: 'Built-in rules are active, so protection still works even without a remote list.'
},
injectionTiming: {
confirmedTitle: 'Document-start confirmed',
lateTitle: 'Late injection suspected',
confirmedDescription: (readyState, elapsedText) => `YoutubeAdblock evaluated while the document was ${readyState} (${elapsedText}), so manager setup looks correct. If ads still appear, refresh rules or check engine health instead of reinstalling.`,
extensionProduct: 'extension content script',
userscriptProduct: 'userscript manager',
lateDescription: (readyState, elapsedText, product) => `YoutubeAdblock evaluated after document-start (${readyState}, ${elapsedText}). This usually points to ${product} setup, such as Chrome's "Allow User Scripts" toggle, a disabled manager, or a manager that missed @run-at document-start. Reload YouTube after fixing setup; rule refreshes cannot recover player responses that loaded before the script.`
},
toastTitles: {
info: 'Heads Up',
success: 'Updated',
error: 'Needs Attention',
warn: 'Check This'
},
sponsorBlock: {
highlightTitle: timeText => `Jump to highlight (${timeText})`,
highlightSymbol: '★'
},
ryd: {
dislikeLabel: label => `Dislike (${label})`
},
volumeBoost: {
tag: 'Boost',
title: 'YoutubeAdblock volume boost'
},
ui: {
controlCenter: 'Control Center',
headerDescription: 'Pause protection, refresh the rule library, and adjust modules without leaving YouTube.',
findSetting: 'Find a setting',
findSettingPlaceholder: 'Find a setting…',
closeControlCenter: 'Close the YoutubeAdblock Control Center',
notifications: 'Control Center notifications',
footerHint: 'Tab moves between controls. Press Esc to close.',
settingsNavigation: 'Settings sections',
workspace: 'Workspace',
productMark: 'YA',
localSettings: 'Changes stay on this device unless extension sync is available.',
savedStatus: 'Saved · Changes save automatically.',
savedWithRefreshProblem: 'Saved locally · Rule refresh needs attention.',
syncingStatus: 'Refreshing the Rule Library while your current protection stays active.',
footerSync: timestamp => `Last synced: ${formatTimestamp(timestamp)}`,
navigation: {
overview: 'Overview',
rules: 'Rule Library',
core: 'Core Blocking',
anti: 'Anti-Interference',
cleanup: 'Ad & Overlay Cleanup',
sponsor: 'SponsorBlock',
enhance: 'Enhancements',
clutter: 'Interface Cleanup',
blocklist: 'Focus & Filters',
diagnostics: 'Diagnostics'
},
quickActions: 'Quick actions',
protectionOn: 'Protection On',
protectionPaused: 'Protection Paused',
masterSwitch: 'Master Switch',
masterSwitchPause: 'Pause every blocking engine without uninstalling the script.',
masterSwitchResume: 'Resume blocking instantly with your saved settings intact.',
toggleProtection: 'Toggle YoutubeAdblock protection',
ruleLibrary: 'Rule Library',
diagnostics: 'Diagnostics',
currentPage: 'Current Page',
currentPageDetail: 'Context for this tab.',
recommendedSourceActive: 'Recommended source active.',
customSourceActive: 'Custom source active.',
lastSync: 'Last Sync',
activeRulesDetail: count => `${formatNumber(count)} active rules.`,
metrics: {
blocked: 'Ads Blocked',
pruned: 'Responses Pruned',
ssapSkipped: 'SSAP Skips',
sponsorSkipped: 'Sponsor Skips',
dearrowReplaced: 'DeArrow Replaced',
feedFiltered: 'Feed Filtered',
ssaiDetected: 'SSAI Signals'
},
managerSetupWarning: 'Manager Setup Warning',
protectionDegraded: 'Protection Degraded',
coexistenceDetected: 'Coexistence Detected',
ssaiDetectedTitle: 'Server-Side Ad Detected',
ssaiDetectedBody: (lastSeen, url) => {
const locationText = url ? ` Last URL: ${url}.` : '';
return `A PlayerResponse reported serverStitchedAd at ${formatTimestamp(lastSeen)}. JSON pruning cannot remove ads already stitched into the media stream; manifest scrub, DNR, SSAP auto-skip, and video fast-forward remain active as fallback layers.${locationText}`;
},
degradedBody: (engineList, lockedList, preProxied) => {
let body = `Some engines could not fully install: ${engineList}.`;
if (lockedList) body += ` Locked natives: ${lockedList}.`;
if (preProxied.length) body += ` Pre-proxied by another extension: ${preProxied.join(', ')}.`;
return body + ' Another extension or YouTube may have claimed these first. Remaining engines are still active; reloading the page usually wins the race back.';
},
coexistenceBody: preProxied => `Another extension already hooked: ${preProxied.join(', ')}. YoutubeAdblock replaced them with its own proxies. If you see unexpected behavior, try disabling the other blocker.`,
refreshing: 'Refreshing…',
refreshRules: 'Refresh Rules',
ruleLibraryDescription: 'Choose the source that feeds cosmetic selectors and remote rule updates. YoutubeAdblock keeps your last working rules or the built-in fallback ready if a refresh fails.',
sourceUrl: 'Source URL',
filterHelpExtension: 'Point this at a raw EasyList or uBO-style source. Extension installs work best with hosts that allow direct browser fetches from YouTube pages.',
filterHelpUserscript: 'Point this at a raw EasyList or uBO-style source. Refreshing applies new rules without dropping your current protection.',
filterPlaceholder: 'https://example.com/youtube-filters.txt…',
invalidFilterUrl: 'Enter a valid http or https URL before refreshing the Rule Library.',
useRecommendedSource: 'Use Recommended Source',
recommendedSourceToast: 'The recommended Rule Library is active again.',
ruleVersionPill: version => `Version ${version || STRINGS.common.unknownShort}`,
syncedPill: timestamp => `Synced ${formatTimestamp(timestamp)}`,
integrityPill: label => `Integrity ${label}`,
rulesPill: count => `${formatNumber(count)} Rules`,
selectorsPill: count => `${formatNumber(count)} Selectors`,
prunePathsPill: count => `${formatNumber(count)} Prune Paths`,
networkOnlyPill: count => `${formatNumber(count)} Network-Only`,
unsupportedScriptletsPill: count => `${formatNumber(count)} Unsupported Scriptlets`,
rejectedDangerousPill: count => `${formatNumber(count)} Rejected Dangerous`,
refreshProblem: 'Refresh Problem',
signatureVerified: 'Signature Verified',
verifiedFilterNote: 'The recommended remote list was verified before it replaced your active rules.',
unsignedCustomSource: 'Unsigned Custom Source',
unsignedCustomNote: 'Custom Rule Library sources are allowed, but they are not verified by the bundled Ed25519 key.',
customSourceTitle: 'Custom Source Active',
customSourceExtensionNote: 'Keep the source raw text, refresh after edits, and use a host that allows direct browser fetches from YouTube pages.',
customSourceUserscriptNote: 'Keep the source raw text and refresh after edits so the new rules load.',
recommendedSourceTitle: 'Recommended Source Active',
recommendedSourceNote: 'The recommended remote list is live, and the built-in fallback stays ready if the source ever goes offline.',
fallbackReady: 'Fallback Ready',
fallbackReadyNote: 'Protection is still running with cached or built-in rules. Refresh when you want a newer remote copy.',
onPill: (enabled, total) => `${enabled}/${total} On`,
unavailableDearrowExtension: 'Unavailable in the extension build: the DeArrow API requires explicit permission for browser extensions. Use the userscript build for this feature.',
sponsorAttribution: 'Segment data from SponsorBlock, licensed CC BY-NC-SA 4.0.',
sponsorAttributionLink: 'sponsor.ajay.app',
enhanceAttribution: 'Title/thumbnail data from DeArrow (CC BY-NC-SA 4.0); dislike counts from Return YouTube Dislike.',
dearrowAttributionLink: 'dearrow.ajay.app',
rydAttributionLink: 'returnyoutubedislike.com',
pause: {
heading: 'Temporary pause',
help: 'Suspend every engine for this tab without changing your saved settings. It restores itself automatically and never syncs to your other devices.',
start5m: 'Pause 5 min',
start30m: 'Pause 30 min',
startSession: 'Pause for this tab',
resume: 'Resume now',
scopeLabel: (id) => id === 'session' ? 'this tab' : (id === '30m' ? '30 minutes' : '5 minutes'),
startedToast: (scope) => `Protection paused for ${scope}. It resumes automatically.`,
resumedToast: 'Protection resumed.',
activeTimed: (remaining) => `Paused - resumes in ${remaining}.`,
activeSession: 'Paused for this tab - resumes when the tab closes.'
},
consent: {
heading: 'Community data consent',
intro: 'These optional services contact third-party APIs. Nothing is sent for a service until you allow it here, and you can turn any of them off again at any time.',
statusGranted: 'Allowed',
statusDenied: 'Off',
statusUnset: 'Not yet allowed',
allow: 'Allow',
revoke: 'Turn off',
clearCache: 'Clear cache',
cacheEmpty: 'Nothing cached this session.',
cacheSummary: (entries, age) => age
? `${entries} cached ${entries === 1 ? 'entry' : 'entries'} this session, oldest ${age}.`
: `${entries} cached ${entries === 1 ? 'entry' : 'entries'} this session.`,
cacheCleared: 'Cached data for that service was cleared.',
requiresSegments: 'Requires SponsorBlock segments to be allowed first.',
sponsorBlockTitle: 'SponsorBlock segments',
sponsorBlockDetail: 'Sends the first 4 hex characters of sha256(video ID), never the full video ID, to sponsor.ajay.app to fetch skippable segments. Purpose: skipping sponsors and similar segments. Responses are cached in memory for this tab only. Segment data is licensed CC BY-NC-SA 4.0.',
sponsorBlockViewTitle: 'SponsorBlock skip reports',
sponsorBlockViewDetail: 'After an auto-skip, reports that segment’s UUID to sponsor.ajay.app so community view counts stay accurate. The UUID identifies the segment, not you, but it is derived from what you watch.',
dearrowTitle: 'DeArrow titles & thumbnails',
dearrowDetail: 'Sends the first 4 hex characters of sha256(video ID) to sponsor.ajay.app for alternative titles, and fetches thumbnails from dearrow-thumb.ajay.app (thumbnail URLs contain the full video ID). Cached in memory for about 6 hours. Data is licensed CC BY-NC-SA 4.0.',
rydTitle: 'Return YouTube Dislike counts',
rydDetail: 'Sends the full video ID of videos you watch to returnyoutubedislikeapi.com to fetch dislike counts. Cached in memory for about 30 minutes.',
grantedToast: 'Community service allowed.',
revokedToast: 'Community service turned off and its cache cleared.',
migrationToast: 'Community services (SponsorBlock and friends) now require one-time consent. Open the Control Center to allow them.'
},
blocklist: {
invalidLinesNote: (count) => count === 1 ? '1 line rejected:' : `${count} lines rejected:`,
invalidLine: (line, reason) => `Line ${line}: ${reason}`,
truncatedNote: (max) => `List truncated: only the first ${max} entries are active.`,
regexReasons: {
tooLong: 'regex too long (max 256 characters)',
backreference: 'regex backreferences are not supported',
lookaround: 'regex lookarounds are not supported',
nestedQuantifier: 'a quantified regex group may not contain another quantifier or alternation',
adjacentQuantifier: 'two unbounded regex quantifiers (* + {n,}) may not run together; separate them with required text',
syntax: 'invalid regex syntax'
},
blockedChannels: 'Blocked Channels',
blockedChannelsWhitelistHelp: 'Whitelist mode active: only videos from these channels will be shown. Supports names, UC IDs, @handles, channel URLs, and regex.',
blockedChannelsHelp: 'One channel per line. Supports names, UC IDs, @handles, channel URLs, and regex, e.g. /^Exact Channel$/.',
blockedKeywords: 'Blocked Keywords',
blockedKeywordsHelp: 'One keyword per line. Substring match (case-insensitive). Wrap in /slashes/ for regex, e.g. /sponsor|promo/i.',
adAllowedChannels: 'Ad-Allowed Channels',
adAllowedChannelsHelp: 'Ads will play on videos from these channels. Supports names, UC IDs, @handles, channel URLs, and regex.',
importExport: 'Import / Export',
importExportHelp: 'Move blocklists and local settings between installs without changing your cached rule library.',
importPlaceholder: 'Paste YoutubeAdblock JSON, BlockTube/FilterTube-style JSON, or plain channel names / @handles / UC IDs. Use keyword: or title: prefixes for keyword text imports.',
copyJson: 'Copy JSON',
settingsJsonCopied: 'Settings JSON copied.',
settingsJsonClipboardFallback: 'Clipboard unavailable. JSON is in the import box.',
copyChannelText: 'Copy Channel Text',
channelBlocklistCopied: 'Channel blocklist copied.',
channelClipboardFallback: 'Clipboard unavailable. Channel text is in the import box.',
importJson: 'Import JSON',
importedSettings: count => `Imported ${count} settings.`,
importChannelText: 'Import Channel Text',
channelBlocklistImported: 'Channel blocklist imported.',
importMigration: 'Import Migration',
rejectedEntries: items => `Rejected entries:\n${items.join('\n')}`,
migrationImported: (channels, keywords, rejectedCount) => `Migration imported ${channels} channel and ${keywords} keyword ${channels + keywords === 1 ? 'entry' : 'entries'}${rejectedCount ? `; ${rejectedCount} rejected.` : '.'}`,
migrationNoSupportedEntries: 'Migration import did not find supported channel or keyword entries.',
importJsonParseError: 'Import JSON could not be parsed.',
importJsonNoSupportedSettings: 'Import JSON did not contain supported YoutubeAdblock settings.',
importWrongApp: 'That export came from a different application.',
importFutureSchema: (found, supported) => `That export uses settings schema v${found}; this version supports up to v${supported}. Update YoutubeAdblock first.`,
importInvalidField: (key, reason) => `${key}: ${reason}`,
importInvalidUrl: 'must be a valid http(s) URL',
importInvalidType: 'has an unsupported value type',
importTooLarge: 'is larger than the supported limit',
importUnknownKeys: (keys) => `Ignored unknown keys: ${keys.join(', ')}`,
importPreview: 'Preview',
importPreviewHeading: 'This import will:',
importPreviewAdd: (key) => `add ${key}`,
importPreviewChange: (key) => `change ${key}`,
importPreviewRemove: (key) => `clear ${key}`,
importPreviewNoChanges: 'Nothing would change. The import matches your current settings.',
importConfirm: 'Apply import',
importCancel: 'Cancel',
importRolledBack: 'Import failed part-way and every change was rolled back.',
importUndo: 'Undo import',
importUndone: 'Import undone. Previous settings restored.',
durationTitle: 'Duration Filter (seconds)',
durationHelp: 'Hide videos shorter than min or longer than max. Leave blank to skip.',
minPlaceholder: 'Min (sec)',
maxPlaceholder: 'Max (sec)'
},
diagnosticsSection: {
title: 'Diagnostics & Recovery',
description: 'Copy a clean snapshot for bug reports or reset local state without reinstalling the script.',
installTiming: 'Install Timing',
installTimingHelp: 'Separate userscript-manager setup problems from YouTube rule breakage before changing settings.',
browserNetworkLayer: 'Browser Network Layer',
browserNetworkLayerHelp: 'Confirm whether the extension\'s packaged browser rules matched recently. This summary contains only rule IDs, counts, and timestamps; it never includes request URLs.',
networkPendingTitle: 'Checking packaged rules',
networkPendingBody: 'Waiting for the browser\'s privacy-bounded matched-rule summary.',
networkAvailableTitle: count => `${formatNumber(count)} recent network ${count === 1 ? 'match' : 'matches'}`,
networkAvailableBody: (ruleCount, minutes, lastMatched) => `${formatNumber(ruleCount)} packaged ${ruleCount === 1 ? 'rule' : 'rules'} matched in the last ${minutes} minutes. Last match: ${lastMatched}.`,
networkQuietTitle: 'No recent network matches',
networkQuietBody: minutes => `No packaged blocking rule matches were reported in the last ${minutes} minutes. This can be normal on a clean or already-loaded page.`,
networkUnavailableTitle: 'Matched-rule evidence unavailable',
networkUnavailableBody: {
'api-unavailable': 'This browser does not expose matched-rule feedback. Network blocking stays active; only this diagnostic counter is unavailable.',
'permission-required': 'The browser withheld matched-rule feedback permission. Network blocking stays active; only this diagnostic counter is unavailable.',
'quota-exceeded': 'The browser diagnostics quota is temporarily exhausted. Wait a few minutes, then refresh the evidence.',
cooldown: 'Another tab refreshed matched-rule evidence moments ago. Wait about 30 seconds, then try again.',
'invalid-context': 'Open Diagnostics from a supported YouTube tab to read tab-scoped match evidence.',
timeout: 'The extension service worker did not answer in time. Reopen the Control Center and try again.',
'query-failed': 'Matched-rule feedback could not be read. Network blocking stays active; only this diagnostic counter is unavailable.'
},
networkUserscriptTitle: 'Extension-only evidence',
networkUserscriptBody: 'Browser matched-rule counters are available in the extension build. Userscript diagnostics continue to report page-world blocking and pruning.',
refreshNetworkEvidence: 'Refresh Evidence',
refreshingNetworkEvidence: 'Checking Evidence…',
shareSnapshot: 'Share a Snapshot',
shareSnapshotHelp: 'Copy the active Rule Library, module states, counters, and environment details, then open the repo issue tracker with clean context.',
copyDiagnostics: 'Copy Diagnostics',
openIssues: 'Open Issues',
resetLocalState: 'Reset Local State',
resetLocalStateHelp: 'Reset counters or restore the recommended defaults without reinstalling. Your cached rule library stays ready.',
localOnly: 'Local Only',
localOnlyHelp: 'These actions change only local settings and counters. They do not remove the script or erase your current cached rules.',
resetCounters: 'Reset Counters',
confirmReset: 'Confirm Reset',
countersReset: 'Session counters reset.',
restoreDefaults: 'Restore Defaults',
confirmRestore: 'Confirm Restore',
defaultsRestored: 'Recommended defaults restored. Your current rules stayed in place.'
},
searchEmptyTitle: 'No Matching Settings',
searchEmptyBody: query => `Nothing matches "${query}". Try terms like "rule", "shorts", "sponsor", or "reset".`,
armedAction: label => `${label} is armed. Click again to confirm.`,
featureToggle: (label, enabled) => `${label} ${enabled ? 'enabled' : 'disabled'}.`,
protectionResumed: 'Protection resumed across every engine.',
protectionPausedToast: 'Protection paused. YoutubeAdblock stays installed and ready to resume.',
diagnosticsCopied: 'Diagnostics copied. You can paste them into a bug report or note.',
diagnosticsClipboardFailed: 'Clipboard access was unavailable, so diagnostics could not be copied.',
stillLoading: 'Control Center is still loading. Try again in a moment.',
loadedLate: hint => `YoutubeAdblock loaded late. Open Diagnostics for setup steps. ${hint}`,
activeToast: hint => `YoutubeAdblock is active. ${hint}`,
youtubeChanged: 'YouTube may have changed its ad delivery. Try refreshing rules from the Control Center.'
},
featureGroups: {
core: {
title: 'Core Blocking',
description: 'Intercept the network and data paths that carry ad payloads before YouTube can render them.',
features: {
jsonParsePrune: {
label: 'JSON response pruning',
desc: 'Removes ad payloads from parsed player responses before they are consumed.'
},
fetchIntercept: {
label: 'fetch() interception',
desc: 'Applies pruning to player and browse requests handled through fetch().'
},
xhrIntercept: {
label: 'XMLHttpRequest interception',
desc: 'Catches older request paths that still deliver ad-related responses.'
},
setUndefinedTraps: {
label: 'Initial property traps',
desc: 'Keeps early ad-related player properties undefined during first-page hydration.'
}
}
},
anti: {
title: 'Anti-Detection',
description: 'Reduce the odds of YouTube detecting, rehydrating, or bypassing the protections already in place.',
features: {
abnormalityBypass: {
label: 'Abnormality callback bypass',
desc: 'Neutralizes callbacks that flag ad blocking as abnormal behavior.'
},
domBypassPrevention: {
label: 'Iframe bypass prevention',
desc: 'Stops clean iframe contexts from restoring unmodified browser APIs.'
},
requestBodyModify: {
label: 'No-ad request signal',
desc: 'Marks outbound player requests with the inline-playback no-ad flag so YouTube serves no ad payload and no fake-buffering delay. Works on cold loads and in-app navigation.'
},
ssapAutoSkip: {
label: 'SSAP auto-skip',
desc: 'Fast-forwards through stitched server-side ads whenever they are detected.'
},
timerNeutralization: {
label: 'Timer neutralization',
desc: 'Disarms the long timers YouTube uses to validate ad playback.'
},
aggressiveAntiStall: {
label: 'Aggressive anti-stall',
desc: 'Fast-forwards the 17-second bound timers YouTube uses to stall playback when a blocker is suspected.'
},
videoAdFastForward: {
label: 'Video ad fast-forward',
desc: 'If an unskippable ad still plays, mutes it and accelerates playback as a fallback safety net.'
},
nativeToStringMask: {
label: 'Hide proxies from toString',
desc: 'Patches Function.prototype.toString so YouTube cannot detect our hooked natives by source inspection.'
},
serviceWorkerBlock: {
label: 'Block service worker injection',
desc: 'Prevents YouTube from registering a service worker that could bypass our request proxies.'
},
webpackChunkHook: {
label: 'Webpack chunk prune',
desc: 'Rewrites YouTube webpack chunks before execution to strip modules that render ad placements.'
}
}
},
cleanup: {
title: 'Interface Cleanup',
description: 'Remove the visible clutter that remains after payload blocking has already done the heavy lifting.',
features: {
cosmeticHiding: {
label: 'Cosmetic cleanup',
desc: 'Hides promoted shelves, banners, overlays, and remaining ad containers.'
},
upsellBlock: {
label: 'Premium upsell blocking',
desc: 'Suppresses Premium upgrade popups and related prompts.'
},
shortsAdBlock: {
label: 'Shorts ad removal',
desc: 'Removes sponsored entries from Shorts feeds before they appear.'
}
}
},
sponsor: {
title: 'Community Sponsor Segments',
description: 'Silently jump past sponsor reads, self-promotion, intros, outros, and other crowd-marked segments.',
features: {
sponsorBlock: {
label: 'SponsorBlock auto-skip',
desc: 'Uses the SponsorBlock community database to silently skip sponsor, self-promo, intro, outro, interaction, preview, music-off-topic, and filler segments. No notifications.'
}
}
},
enhance: {
title: 'Experience Enhancements',
description: 'Player, metadata, and audio tweaks that make watching nicer once the ads are gone.',
features: {
dearrow: {
label: 'DeArrow titles & thumbnails',
desc: 'Replaces clickbait titles and thumbnails with crowd-submitted alternatives via the privacy-preserving DeArrow hash-prefix API.'
},
returnYoutubeDislike: {
label: 'Return YouTube Dislike',
desc: 'Restores the public dislike count under the like button using the Return YouTube Dislike archive.'
},
forceOriginalAudio: {
label: 'Force original audio',
desc: 'Switches back to the original-language audio track when YouTube defaults to an auto-dubbed or translated track.'
},
volumeBoost: {
label: 'Volume boost (up to 5x)',
desc: 'Adds a gain slider under the player so you can amplify quiet videos past the browser\u2019s 100% ceiling.'
}
}
},
clutter: {
title: 'Clutter-Free Mode',
description: 'Hide the parts of YouTube you never want to see. This uses selectors only, so the engine stays in charge of ads.',
features: {
hideHomeFeed: {
label: 'Hide home feed',
desc: 'Clears the infinite scroll on the YouTube homepage and shows the empty-state layout instead.'
},
hideShortsShelf: {
label: 'Hide Shorts shelves',
desc: 'Removes Shorts carousels from the home, subscriptions, search, and channel surfaces.'
},
hideShortsTab: {
label: 'Hide Shorts nav entries',
desc: 'Removes the Shorts sidebar entry, chip, and navigation destination.'
},
hideRelated: {
label: 'Hide related videos',
desc: 'Clears the up-next/suggested rail on the watch page.'
},
hideComments: {
label: 'Hide comments',
desc: 'Collapses the comment section on watch pages.'
},
hideEndScreen: {
label: 'Hide end-screen cards',
desc: 'Suppresses the card and "more videos" overlay that appears at the end of a video.'
},
hideLiveChat: {
label: 'Hide live chat',
desc: 'Removes the live-stream chat panel from watch pages.'
},
hideMerch: {
label: 'Hide merch shelves',
desc: 'Hides merchandise, ticket, and shopping shelves below videos.'
},
hideMembersOnly: {
label: 'Hide members-only videos',
desc: 'Removes videos with a Members badge from feeds so free-tier users never see paywalled content.'
},
hideSponsoredComments: {
label: 'Hide sponsored comments & affiliate links',
desc: 'Hides sponsor-badged comments and common affiliate redirect links in video descriptions.'
}
}
},
blocklist: {
title: 'Channels & Keywords',
description: 'Quietly remove videos from the feed when the channel or title matches your rules. Lists live locally only.',
features: {
shortsRedirect: {
label: 'Redirect Shorts to /watch',
desc: 'Rewrites any /shorts/VIDEO_ID URL into the regular watch page so the full player is always used.'
},
channelBlocker: {
label: 'Channel blocklist',
desc: 'Drops videos from your blocked-channel list out of every feed. Manage the list via the text area below.'
},
keywordBlocker: {
label: 'Keyword blocklist',
desc: 'Drops videos whose title matches one of your blocked keywords (one per line, case-insensitive).'
},
whitelistMode: {
label: 'Whitelist mode',
desc: 'Inverts the channel list: only show videos from listed channels, hide everything else.'
},
durationFilter: {
label: 'Duration filter',
desc: 'Hides videos shorter or longer than your thresholds. Set via the fields below.'
},
adAllowlist: {
label: 'Per-channel ad allowlist',
desc: 'Skips ad pruning for listed channels so their ads play normally. Supports creator sponsorship.'
}
}
}
},
diagnosticsReport: {
captured: 'Captured',
site: 'Site',
surface: 'Surface',
build: 'Build',
extension: 'extension',
userscript: 'userscript',
ua: 'UA',
injectionStatus: 'Injection status',
injectionReadyState: 'Injection readyState',
injectionElapsed: 'Injection elapsed',
injectionGuidance: 'Injection guidance',
protectionEnabled: 'Protection enabled',
filterSource: 'Filter source',
filterIntegrity: 'Filter integrity',
filterIntegrityDetail: 'Filter integrity detail',
filterUrl: 'Filter URL',
filterVersion: 'Filter version',
lastSync: 'Last sync',
lastError: 'Last error',
rulesActive: 'Rules active',
pruneKeys: 'Prune keys',
cosmeticSelectors: 'Cosmetic selectors',
interceptPatterns: 'Intercept patterns',
appliedSelectors: 'Applied selector rules',
appliedPrunePaths: 'Applied prune paths',
networkOnlyRules: 'Network-only filter rules',
droppedUnsafeSelectors: 'Dropped unsafe selectors',
supportedScriptlets: 'Supported scriptlets',
unsupportedScriptlets: 'Unsupported scriptlets',
rejectedDangerousScriptlets: 'Rejected dangerous scriptlets',
ssaiSignals: 'SSAI signals',
complianceDialogs: 'Compliance dialogs',
sabrOnly: 'SABR-only responses',
dnrMatchedRules: 'DNR matched rules',
communityApiPermission: 'Community API permission',
communityConsent: 'Community data consent',
communityCache: 'Community cache entries',
communityApiCooldown: 'Community API cooldown',
webpackSignatureSource: 'Webpack signature source',
webpackSignatureVersion: 'Webpack signature version',
webpackSignatureTokens: 'Webpack signature tokens',
webpackSignatureIntegrity: 'Webpack signature integrity',
webpackSignatureError: 'Webpack signature error',
channelBlockEntries: 'Channel block entries',
keywordBlockEntries: 'Keyword block entries',
adAllowEntries: 'Ad-allow entries',
trappedRoots: 'Trapped roots',
engineHealth: 'Engine health',
lockedNatives: 'Locked natives',
preProxied: 'Pre-proxied (another extension)',
stats: 'Stats',
enabledFeatures: 'Enabled features',
disabledFeatures: 'Disabled features'
},
menu: {
openControlCenter: 'Open Control Center',
pauseProtection: 'Pause Protection',
resumeProtection: 'Resume Protection',
refreshRules: 'Refresh Rules',
copyDiagnostics: 'Copy Diagnostics'
}
};
/* =========================================================================
* DEFAULT FILTERS (fallback when remote unavailable)
* ===================================================================== */
const DEFAULT_FILTERS = {
version: '0.0.2',
updated: '2026-04-17',
pruneKeys: [
'adPlacements', 'adSlots', 'playerAds',
'playerResponse.adPlacements', 'playerResponse.adSlots', 'playerResponse.playerAds',
// Anti-adblock enforcement popup payloads. YT 2026 delivery
// surface: these arrive via /browse, /guide, and /next rather
// than /player, so widening pruneKeys catches them before the
// engagement-message renderer builds the popup.
'adBreakHeartbeatParams',
'frameworkUpdates',
'responseContext.adSignalsInfo',
'playerResponse.adBreakHeartbeatParams',
'playerResponse.auxiliaryUi.messageRenderers.upsellDialogRenderer',
'auxiliaryUi.messageRenderers.upsellDialogRenderer',
// v0.4.0: wider renderer coverage. YT ships promoted content
// through a dozen distinct renderer names; pruning each of
// them at the payload layer is cheaper and more reliable
// than racing cosmetic filters.
'promotedSparklesWebRenderer',
'promotedVideoRenderer',
'compactPromotedVideoRenderer',
'compactPromotedItemRenderer',
'backgroundPromoRenderer',
'statementBannerRenderer',
'brandVideoShelfRenderer',
'brandVideoSingletonRenderer',
'inlineAdLayoutRenderer',
'adSlotRenderer',
'linkedInstreamAdRenderer',
'shoppingCarouselRenderer',
'merchandiseShelfRenderer'
],
setUndefined: [
'ytInitialPlayerResponse.playerAds',
'ytInitialPlayerResponse.adPlacements',
'ytInitialPlayerResponse.adSlots',
'ytInitialPlayerResponse.adBreakHeartbeatParams',
'ytInitialPlayerResponse.auxiliaryUi.messageRenderers.upsellDialogRenderer',
'ytInitialData.frameworkUpdates',
'playerResponse.adPlacements'
],
replaceKeys: { adPlacements: 'no_ads', adSlots: 'no_ads', playerAds: 'no_ads' },
interceptPatterns: [
'/youtubei/v1/player', '/youtubei/v1/get_watch',
'/youtubei/v1/browse', '/youtubei/v1/search', '/youtubei/v1/next',
'/youtubei/v1/guide',
// v0.4.0: cover newer InnerTube surfaces. `log_event` is the
// primary adblock-detection beacon; `att/*` are attestation
// challenges; `reel_watch_sequence` delivers Shorts ads;
// `get_survey` delivers survey ads.
'/youtubei/v1/log_event',
'/youtubei/v1/att/get', '/youtubei/v1/att/log',
'/youtubei/v1/reel_watch_sequence',
'/youtubei/v1/get_survey',
'/youtubei/v1/player/ad_break',
'/youtubei/v1/tenx_player',
'/watch?', '/playlist?list=', '/reel_watch_sequence'
],
cosmeticSelectors: [
'#masthead-ad', '#promotion-shelf', '#shopping-timely-shelf',
'.masthead-ad-control', '.ad-div', '.pyv-afc-ads-container',
'.ytp-ad-progress', '.ytp-suggested-action-badge',
'ytd-ad-slot-renderer', 'ytd-video-masthead-ad-advertiser-info-renderer',
'ytm-promoted-sparkles-web-renderer', 'ytd-search-pyv-renderer',
'ytd-merch-shelf-renderer', 'ad-slot-renderer', 'ytm-companion-ad-renderer',
'ytd-statement-banner-renderer',
// Anti-adblock enforcement modal. Hiding it cosmetically is a
// defense-in-depth layer — the primary kill is pruning the
// frameworkUpdates payload that builds it.
'ytd-enforcement-message-view-model',
'tp-yt-paper-dialog:has(ytd-enforcement-message-view-model)',
'ytd-rich-item-renderer:has(> #content > ytd-ad-slot-renderer)',
'#shorts-inner-container > .ytd-shorts:has(> .ytd-reel-video-renderer > ytd-ad-slot-renderer)',
'.ytd-watch-flexy > .ytd-watch-next-secondary-results-renderer > ytd-ad-slot-renderer',
'.ytd-two-column-browse-results-renderer > ytd-rich-grid-renderer > #masthead-ad',
// v0.4.0: broaden cosmetic coverage for renderer variants
// pruning may not catch during the first frame.
'ytd-in-feed-ad-layout-renderer',
'ytd-banner-promo-renderer',
'ytd-promoted-video-renderer',
'ytd-compact-promoted-video-renderer',
'ytd-action-companion-ad-renderer',
'ytd-brand-video-shelf-renderer',
'ytd-brand-video-singleton-renderer',
// YouTube TV uses the ytu-* component family rather than ytd-*.
// The ad title tray is present even before playback and becomes
// visible when first-party TV ad media is active.
'ytu-ads-title-tray'
],
upsellSelectors: [
'ytd-popup-container > .ytd-popup-container > #contentWrapper > .ytd-popup-container[position-type="OPEN_POPUP_POSITION_BOTTOMLEFT"]'
],
features: {
jsonParsePrune: true, fetchIntercept: true, xhrIntercept: true,
setUndefinedTraps: true, ssapAutoSkip: true, abnormalityBypass: true,
domBypassPrevention: true, shortsAdBlock: true,
cosmeticHiding: true, upsellBlock: true, requestBodyModify: true,
timerNeutralization: true,
// New in 0.2.1 — opt-in by default because they trade off
// slightly more aggressive behavior for stronger protection.
aggressiveAntiStall: true,
videoAdFastForward: true,
sponsorBlock: true,
// v0.4.0 anti-detect hardening
nativeToStringMask: true,
serviceWorkerBlock: true,
webpackChunkHook: true,
// v0.4.0 UX — all off by default so the engine-first posture
// is preserved; users opt in from the Control Center.
dearrow: false,
returnYoutubeDislike: false,
forceOriginalAudio: false,
volumeBoost: false,
shortsRedirect: false,
channelBlocker: false,
keywordBlocker: false,
whitelistMode: false,
durationFilter: false,
adAllowlist: false,
// v0.4.0 interface cleanup (Unhook-style)
hideHomeFeed: false,
hideShortsShelf: false,
hideShortsTab: false,
hideRelated: false,
hideComments: false,
hideEndScreen: false,
hideLiveChat: false,
hideMerch: false,
hideMembersOnly: false,
hideSponsoredComments: false
}
};
const FEATURE_COPY = STRINGS.featureGroups;
function featureCopy(groupKey, featureKey) {
return FEATURE_COPY[groupKey].features[featureKey];
}
const FEATURE_GROUPS = [
{
sectionId: SECTION_IDS.core,
title: FEATURE_COPY.core.title,
description: FEATURE_COPY.core.description,
features: [
{ key: 'jsonParsePrune', ...featureCopy('core', 'jsonParsePrune') },
{ key: 'fetchIntercept', ...featureCopy('core', 'fetchIntercept') },
{ key: 'xhrIntercept', ...featureCopy('core', 'xhrIntercept') },
{ key: 'setUndefinedTraps', ...featureCopy('core', 'setUndefinedTraps') },
]
},
{
sectionId: SECTION_IDS.anti,
title: FEATURE_COPY.anti.title,
description: FEATURE_COPY.anti.description,
features: [
{ key: 'abnormalityBypass', ...featureCopy('anti', 'abnormalityBypass') },
{ key: 'domBypassPrevention', ...featureCopy('anti', 'domBypassPrevention') },
{ key: 'requestBodyModify', ...featureCopy('anti', 'requestBodyModify') },
{ key: 'ssapAutoSkip', ...featureCopy('anti', 'ssapAutoSkip') },
{ key: 'timerNeutralization', ...featureCopy('anti', 'timerNeutralization') },
{ key: 'aggressiveAntiStall', ...featureCopy('anti', 'aggressiveAntiStall') },
{ key: 'videoAdFastForward', ...featureCopy('anti', 'videoAdFastForward') },
{ key: 'nativeToStringMask', ...featureCopy('anti', 'nativeToStringMask') },
{ key: 'serviceWorkerBlock', ...featureCopy('anti', 'serviceWorkerBlock') },
{ key: 'webpackChunkHook', ...featureCopy('anti', 'webpackChunkHook') }
]
},
{
sectionId: SECTION_IDS.cleanup,
title: FEATURE_COPY.cleanup.title,
description: FEATURE_COPY.cleanup.description,
features: [
{ key: 'cosmeticHiding', ...featureCopy('cleanup', 'cosmeticHiding') },
{ key: 'upsellBlock', ...featureCopy('cleanup', 'upsellBlock') },
{ key: 'shortsAdBlock', ...featureCopy('cleanup', 'shortsAdBlock') }
]
},
{
sectionId: SECTION_IDS.sponsor,
title: FEATURE_COPY.sponsor.title,
description: FEATURE_COPY.sponsor.description,
features: [
{ key: 'sponsorBlock', ...featureCopy('sponsor', 'sponsorBlock') }
]
},
{
sectionId: SECTION_IDS.enhance,
title: FEATURE_COPY.enhance.title,
description: FEATURE_COPY.enhance.description,
features: [
{ key: 'dearrow', ...featureCopy('enhance', 'dearrow') },
{ key: 'returnYoutubeDislike', ...featureCopy('enhance', 'returnYoutubeDislike') },
{ key: 'forceOriginalAudio', ...featureCopy('enhance', 'forceOriginalAudio') },
{ key: 'volumeBoost', ...featureCopy('enhance', 'volumeBoost') }
]
},
{
sectionId: SECTION_IDS.clutter,
title: FEATURE_COPY.clutter.title,
description: FEATURE_COPY.clutter.description,
features: [
{ key: 'hideHomeFeed', ...featureCopy('clutter', 'hideHomeFeed') },
{ key: 'hideShortsShelf', ...featureCopy('clutter', 'hideShortsShelf') },
{ key: 'hideShortsTab', ...featureCopy('clutter', 'hideShortsTab') },