Block Youtube Users

Hide videos of blacklisted users/channels and comments

  1. // ==UserScript==
  2. // @name Block Youtube Users
  3. // @namespace https://codeberg.org/schegge
  4. // @description Hide videos of blacklisted users/channels and comments
  5. // @version 2.6.1
  6. // @author Schegge
  7. // @compatible firefox
  8. // @license CC-BY-NC-SA-4.0
  9. // @match https://www.youtube.com/*
  10. // @exclude *://*.youtube.com/embed/*
  11. // @exclude *://*.youtube.com/live_chat*
  12. // @run-at document-end
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_notification
  16. // @grant GM.getValue
  17. // @grant GM.setValue
  18. // @grant GM.notification
  19. // ==/UserScript==
  20.  
  21. // fix trusted-types https://github.com/Tampermonkey/tampermonkey/issues/1334#issuecomment-2556498540
  22. window.testTrusted = function() {
  23. if (typeof window != "undefined" &&
  24. ('trustedTypes' in window) &&
  25. ('createPolicy' in window.trustedTypes) &&
  26. (typeof window.trustedTypes.createPolicy == "function")) {
  27. window.trustedTypes.createPolicy('default', {createScriptURL: s => s, createScript: s => s, createHTML: s => s})
  28. } else {
  29. setTimeout(window.testTrusted, 1000);
  30. }
  31. }
  32. window.testTrusted();
  33.  
  34. // gm4 polyfill https://github.com/greasemonkey/gm4-polyfill
  35. if (typeof GM == 'undefined') {
  36. this.GM = {};
  37. Object.entries({
  38. 'GM_getValue': 'getValue',
  39. 'GM_setValue': 'setValue',
  40. 'GM_notification': 'notification'
  41. }).forEach(([oldKey, newKey]) => {
  42. let old = this[oldKey];
  43. if (old && (typeof GM[newKey] == 'undefined')) {
  44. GM[newKey] = function(...args) {
  45. return new Promise((resolve, reject) => { try { resolve(old.apply(this, args)); } catch (e) { reject(e); } });
  46. };
  47. }
  48. });
  49. }
  50.  
  51. (async function() {
  52.  
  53. /* VALUES */
  54.  
  55. const Values = {
  56. storageVer: '1',
  57. storageSep: ',',
  58. storageTimer: 1000,
  59. storageComment: '',
  60. storageVideo: '',
  61. storageAdd: '',
  62. storageHideShorts: '',
  63. storageVideoOwner: '',
  64. storageBlacklist: [],
  65. storageWhitelist: [],
  66. menuOpen: false,
  67. menuPause: false
  68. };
  69.  
  70. // get saved values
  71. Values.storageVer = await GM.getValue('byuver', '1');
  72. Values.storageSep = await GM.getValue('sep', ',');
  73. Values.storageTimer = await GM.getValue('timer', 1000);
  74. Values.storageComment = await GM.getValue('hidecomments', '');
  75. Values.storageVideo = await GM.getValue('enablepause', '');
  76. Values.storageAdd = await GM.getValue('enableadd', '');
  77. Values.storageHideShorts = await GM.getValue('hideshorts', '');
  78. Values.storageVideoOwner = await GM.getValue('videoowner', '');
  79. Values.storageBlacklist = getArray(await GM.getValue('savedblocks', ''));
  80. Values.storageWhitelist = getArray(await GM.getValue('savedwhites', ''));
  81.  
  82. // get array from string
  83. function getArray(string) {
  84. if (!string) return [];
  85. return string.split(Values.storageSep).map(v => v.trim()).filter(v => v.length);
  86. }
  87.  
  88. const Where = {
  89. renderer: `ytd-rich-item-renderer,
  90. ytd-video-renderer,
  91. ytd-channel-renderer,
  92. ytd-playlist-renderer,
  93. ytd-playlist-video-renderer,
  94. ytd-playlist-panel-video-renderer,
  95. ytd-movie-renderer,
  96. ytd-compact-video-renderer,
  97. ytd-compact-movie-renderer,
  98. ytd-compact-radio-renderer,
  99. ytd-compact-autoplay-renderer,
  100. ytd-compact-playlist-renderer,
  101. ytd-grid-video-renderer,
  102. ytd-grid-playlist-renderer,
  103. ytd-secondary-search-container-renderer,
  104. yt-lockup-view-model
  105. ${Values.storageVideoOwner ? ', ytd-video-owner-renderer' : ''}
  106. ${Values.storageHideShorts ? ', ytd-reel-shelf-renderer, ytd-rich-section-renderer' : ''}
  107. ${Values.storageComment ? ', ytd-comment-view-model.ytd-comment-replies-renderer, ytd-comment-view-model.ytd-comment-thread-renderer' : ''}`,
  108.  
  109. // home, related and page playlist: #metadata > :not([hidden]) #text.ytd-channel-name
  110. // ^ not hidden because of search video
  111. // search video: #channel-info #text.ytd-channel-name
  112. // search channel: #channel-title.ytd-channel-renderer span.ytd-channel-renderer, #info #text.ytd-channel-name, #metadata #subscribers.ytd-channel-renderer
  113. // video playlist: #byline.ytd-playlist-panel-video-renderer
  114. // playlists: .yt-content-metadata-view-model-wiz__metadata-row span.yt-core-attributed-string a[href^="/@"], .yt-content-metadata-view-model-wiz__metadata-row span.yt-core-attributed-string a[href^="/channel/"]
  115. // user video: #owner #upload-info #channel-name #text.ytd-channel-name
  116. // comment: #author-text span.ytd-comment-view-model, #name #text.ytd-channel-name
  117. user: `#metadata > :not([hidden]) #text.ytd-channel-name,
  118. #channel-info #text.ytd-channel-name,
  119. #channel-title.ytd-channel-renderer span.ytd-channel-renderer,
  120. #info #text.ytd-channel-name,
  121. #metadata #subscribers.ytd-channel-renderer,
  122. #byline.ytd-playlist-panel-video-renderer,
  123. .yt-content-metadata-view-model-wiz__metadata-row span.yt-core-attributed-string a[href^="/@"],
  124. .yt-content-metadata-view-model-wiz__metadata-row span.yt-core-attributed-string a[href^="/channel/"]
  125. ${Values.storageVideoOwner ? ', #owner #upload-info #channel-name #text.ytd-channel-name' : ''}
  126. ${Values.storageComment ? ', #author-text span.ytd-comment-view-model, #name #text.ytd-channel-name' : ''}`,
  127.  
  128. // if the above aren't found
  129. userFailSafe: 'a[href^="/@"], a[href^="/channel/"], .ytd-channel-name a, .yt-content-metadata-view-model-wiz__metadata-row:first-child span.yt-core-attributed-string',
  130.  
  131. shorts: 'a[href^="/shorts/"]',
  132.  
  133. videoPage: {
  134. parentId: 'owner',
  135. channel: '#owner #upload-info #channel-name #text.ytd-channel-name',
  136. video: '#player video.video-stream.html5-main-video'
  137. },
  138.  
  139. masthead: {
  140. parent: '#container.ytd-masthead',
  141. buttonsId: 'buttons'
  142. }
  143. };
  144.  
  145. /* INTERVAL FOR BLACKLISTING */
  146.  
  147. search();
  148. setInterval(search, Values.storageTimer);
  149.  
  150. /* CSS */
  151.  
  152. document.head.insertAdjacentHTML('beforeend', `<style>
  153. [data-block="yes"] { display: none!important; }
  154. .byu-add { float: left; margin-right: .4em; cursor: pointer; color: var(--yt-brand-youtube-red, red); font-size: .9em; background-color: var(--yt-spec-badge-chip-background); border-radius: 100%; width: 1.6em; text-align: center; font-weight: lighter; }
  155. #byu-icon { display: inline-block; position: relative; text-align: center; width: 40px; height: 24px; margin: 0 8px; font-weight: 100; }
  156. #byu-icon span { color: var(--yt-spec-icon-active-other); cursor: pointer; font-size: 20px; vertical-align: middle; }
  157. #byu-options { width: 400px; max-width: 80vw; display: flex; flex-flow: row wrap; align-items: baseline; position: fixed; right: 1em; padding: 1em; text-align: center; font-size: 1.2em; color: var(--yt-spec-text-primary); background-color: var(--yt-spec-menu-background); z-index: 99999; border-radius: 1em; box-shadow: 0 .3em 2em 0 var(--yt-spec-static-overlay-background-light); }
  158. #byu-options div { width: 33%; flex-grow: 1; box-sizing: border-box; padding: .6em; }
  159. #byu-save { font-weight: bold; cursor: pointer; color: var(--yt-brand-youtube-red, red); }
  160. #byu-pause { cursor: pointer; }
  161. #byu-options .byu-textarea { width: 100%; }
  162. #byu-options .byu-textarea span { width: 100%; text-align: center; font-weight: bold; }
  163. #byu-options .byu-textarea textarea { line-height: 1.2em; resize: vertical; width: 100%; padding: .4em; color: var(--yt-spec-text-primary); background-color: var(--yt-spec-badge-chip-background); box-sizing: border-box; border: 0; border-radius: 1em; }
  164. #byu-options .byu-textarea textarea#byu-blacklist { height: 8em; }
  165. #byu-options .byu-textarea textarea#byu-whitelist { height: 4em; }
  166. #byu-options input { color: var(--yt-spec-text-primary); background-color: var(--yt-spec-badge-chip-background); border: 0; padding: 0 2px; height: 1.6em; line-height: 1em; vertical-align: middle; box-sizing: border-box; margin: 0; border-radius: .5em; }
  167. #byu-sep { width: 1em; }
  168. #byu-timer { width: 4.5em; }
  169. #byu-video-page-black { font-size: 1.2em; padding: var(--yt-button-padding); background: var(--yt-brand-youtube-red, red); color: #fff; border-radius: 2em; margin-right: .8em; font-weight: bold; }
  170. </style>`);
  171.  
  172. /* VIDEO FIRST PAGE */
  173.  
  174. if (Values.storageVideo && /\/watch/.test(window.location.href)) {
  175. let waitUserVideo = setInterval(() => {
  176. if (document.querySelector(Where.videoPage.channel)) {
  177. clearInterval(waitUserVideo);
  178.  
  179. let username = document.querySelector(Where.videoPage.channel).textContent.trim();
  180. if (ifMatch(username.toLowerCase())) {
  181. let video = document.querySelector(Where.videoPage.video);
  182. video.pause();
  183. video.currentTime = 0;
  184.  
  185. let divBlack = document.createElement('div');
  186. divBlack.id = 'byu-video-page-black';
  187. divBlack.title = username;
  188. divBlack.textContent = 'BLACKLISTED';
  189. document.getElementById(Where.videoPage.parentId).insertAdjacentElement('afterbegin', divBlack);
  190. }
  191. }
  192. }, 1000);
  193. }
  194.  
  195. /* BLACKLIST MENU */
  196.  
  197. document.body.insertAdjacentHTML('beforeend', `<div id="byu-options" style="display: none;">
  198. <div><span id="byu-save">SAVE</span></div>
  199. <div><span id="byu-pause">pause</span></div>
  200. <div class="byu-textarea"><span>blacklist</span>
  201. <textarea spellcheck="false" id="byu-blacklist">${Values.storageBlacklist.join(`${Values.storageSep} `)}</textarea></div>
  202. <div class="byu-textarea"><span>whitelist</span>
  203. <textarea spellcheck="false" id="byu-whitelist">${Values.storageWhitelist.join(`${Values.storageSep} `)}</textarea></div>
  204. <div title="separator between usernames">
  205. <input id="byu-sep" type="text" maxlength="1" value="${Values.storageSep}"> separator</div>
  206. <div title="interval between new checks in ms">
  207. <input id="byu-timer" type="number" min="500" max="5000" step="500" title="milliseconds" value="${Values.storageTimer}"> timer</div>
  208. <div title="always show x buttons">
  209. <input id="byu-enableadd" type="checkbox" value="clickadd" ${Values.storageAdd ? 'checked' : ''}> show buttons</div>
  210. <div title="target also video owner on view page">
  211. <input id="byu-videoowner" type="checkbox" value="videoowner" ${Values.storageVideoOwner ? 'checked' : ''}> video owner</div>
  212. <div title="from direct link if user is blacklisted">
  213. <input id="byu-enablepause" type="checkbox" value="pausevideo" ${Values.storageVideo ? 'checked' : ''}> pause video</div>
  214. <div title="hide comments from specific users">
  215. <input id="byu-hidecomments" type="checkbox" value="comments" ${Values.storageComment ? 'checked' : ''}> comments</div>
  216. <div title="hide all shorts">
  217. <input id="byu-hideshorts" type="checkbox" value="hideshorts" ${Values.storageHideShorts ? 'checked' : ''}> hide all shorts</div>
  218. </div>`);
  219.  
  220. // for the B wait till the masthead buttons are added
  221. const buttonB = document.createElement('div');
  222. buttonB.id = 'byu-icon';
  223. buttonB.innerHTML = '<span>B</span>';
  224.  
  225. let waitButton = setInterval(() => {
  226. if (document.getElementById(Where.masthead.buttonsId)) {
  227. clearInterval(waitButton);
  228. document.getElementById(Where.masthead.buttonsId).insertAdjacentElement('beforebegin', buttonB);
  229. document.head.insertAdjacentHTML('beforeend', `<style>#byu-options { top:${
  230. document.querySelector(Where.masthead.parent).offsetHeight
  231. }px; }</style>`);
  232. }
  233. }, 1000);
  234.  
  235. // open / close menu
  236. buttonB.addEventListener('click', openMenu);
  237. document.addEventListener('keydown', function(e) {
  238. if (e.ctrlKey && e.altKey && e.key == 'b') {
  239. openMenu();
  240. }
  241. });
  242.  
  243. function openMenu() {
  244. let byuOpts = document.getElementById('byu-options');
  245. byuOpts.style = byuOpts.style.display === 'none' ? '' : 'display: none;';
  246. buttonB.style.fontWeight = buttonB.style.fontWeight === '500' ? '' : '500';
  247.  
  248. Values.menuOpen = !Values.menuOpen;
  249. if (Values.storageAdd) return;
  250.  
  251. if (Values.menuOpen) {
  252. search();
  253. } else {
  254. document.querySelectorAll('.byu-add').forEach(el => el.parentElement.removeChild(el));
  255. }
  256. }
  257.  
  258. // save changes
  259. document.getElementById('byu-save').addEventListener('click', function() {
  260. if (/[*"]|^$/.test(document.getElementById('byu-sep').value)) {
  261. this.text('ERROR! separator');
  262. } else {
  263. Values.storageSep = document.getElementById('byu-sep').value;
  264. Values.storageTimer = Math.max(parseInt(document.getElementById('byu-timer').value, 10), 500) || 1000;
  265. Values.storageComment = document.getElementById('byu-hidecomments').checked ? 'yes' : '';
  266. Values.storageVideo = document.getElementById('byu-enablepause').checked ? 'yes' : '';
  267. Values.storageAdd = document.getElementById('byu-enableadd').checked ? 'yes' : '';
  268. Values.storageHideShorts = document.getElementById('byu-hideshorts').checked ? 'yes' : '';
  269. Values.storageVideoOwner = document.getElementById('byu-videoowner').checked ? 'yes' : '';
  270. Values.storageBlacklist = getArray(document.getElementById('byu-blacklist').value.trim());
  271. Values.storageWhitelist = getArray(document.getElementById('byu-whitelist').value.trim());
  272. GM.setValue('sep', Values.storageSep);
  273. GM.setValue('timer', Values.storageTimer);
  274. GM.setValue('hidecomments', Values.storageComment);
  275. GM.setValue('enablepause', Values.storageVideo);
  276. GM.setValue('enableadd', Values.storageAdd);
  277. GM.setValue('hideshorts', Values.storageHideShorts);
  278. GM.setValue('videoowner', Values.storageVideoOwner);
  279. GM.setValue('savedblocks', Values.storageBlacklist.join(`${Values.storageSep} `));
  280. GM.setValue('savedwhites', Values.storageWhitelist.join(`${Values.storageSep} `));
  281. this.textContent = 'SAVED';
  282. search(true);
  283. }
  284. setTimeout(() => this.textContent = 'SAVE', 2000);
  285. });
  286.  
  287. // pause
  288. document.getElementById('byu-pause').addEventListener('click', function() {
  289. Values.menuPause = !Values.menuPause;
  290. if (Values.menuPause) {
  291. document.querySelectorAll('[data-block="yes"]').forEach(el => el.dataset.block = '');
  292. this.textContent = 'paused';
  293. } else {
  294. search(true);
  295. this.textContent = 'pause';
  296. }
  297. });
  298.  
  299. /* BLACKLISTING FUNCTIONS */
  300.  
  301. // global search
  302. function search(blacklistChanged = false) {
  303. for (el of document.querySelectorAll(Where.renderer)) {
  304. // hide shorts if option is enabled
  305. if (Values.storageHideShorts &&
  306. !Values.menuPause &&
  307. el.querySelector(Where.shorts)) {
  308.  
  309. if (el.dataset.block !== 'yes') el.dataset.block = 'yes';
  310. continue;
  311. }
  312.  
  313. // check if blacklisted
  314. findMatch(el, blacklistChanged);
  315. }
  316. }
  317.  
  318. // check if blacklisted, get username, add "x" buttons
  319. function findMatch(el, blacklistChanged) {
  320. let addButton = true;
  321.  
  322. // retrieve current username
  323. let userEl = el.querySelector(Where.user);
  324. let username = userEl?.textContent?.trim();
  325. if (!username) {
  326. // to try to not make the script completely break if yt changes
  327. // search with broader classes but don't add "x" buttons
  328. username = el.querySelector(Where.userFailSafe)?.textContent?.trim();
  329. if (!username) return;
  330. addButton = false;
  331. };
  332. username = username.toLowerCase();
  333.  
  334. // add "x" when menu is open or always add selected
  335. if (addButton &&
  336. (Values.menuOpen || Values.storageAdd) &&
  337. !el.querySelector('.byu-add')) {
  338.  
  339. let addBtn = document.createElement('div');
  340. addBtn.className = 'byu-add';
  341. addBtn.textContent = 'x';
  342. addBtn.addEventListener('click', addToBlacklist);
  343. userEl.insertAdjacentElement('beforebegin', addBtn);
  344. }
  345.  
  346. // if blacklist is paused, stop
  347. if (Values.menuPause) return;
  348.  
  349. // if blacklist or content are changed
  350. if (blacklistChanged || el.dataset.username !== username) {
  351. el.dataset.username = username;
  352.  
  353. // hide if match
  354. if (ifMatch(username)) {
  355. el.dataset.block = 'yes';
  356. // show if it was hidden with another username or deleted username from blacklist
  357. } else if (el.dataset.block) {
  358. el.dataset.block = '';
  359. }
  360. }
  361. }
  362.  
  363. // check if it needs to be blacklisted
  364. function ifMatch(u) {
  365. return (
  366. !Values.storageWhitelist.some(w => u === w.toLowerCase()) &&
  367. Values.storageBlacklist.some(b => {
  368. b = b.toLowerCase();
  369. if (b.startsWith('*')) {
  370. b = b.replace('*', '');
  371. return b && u.includes(b);
  372. } else {
  373. return u === b;
  374. }
  375. })
  376. );
  377. }
  378.  
  379. // add usernames to blacklist
  380. function addToBlacklist(e) {
  381. e.preventDefault();
  382. e.stopPropagation();
  383.  
  384. let username = this.nextElementSibling.textContent.trim().toLowerCase();
  385.  
  386. if (!Values.storageBlacklist.includes(username)) {
  387. Values.storageBlacklist.push(username);
  388. let blacks = Values.storageBlacklist.join(`${Values.storageSep} `);
  389. document.getElementById('byu-blacklist').value = blacks;
  390. GM.setValue('savedblocks', blacks);
  391. search(true);
  392. }
  393. }
  394.  
  395. /* NEW VERSION NOTIFICATION */
  396.  
  397. if (Values.storageVer !== '2.6.1') {
  398. Values.storageVer = '2.6.1';
  399. GM.setValue('byuver', Values.storageVer);
  400.  
  401. /*GM.notification({
  402. text: '\nFor more details, see the script description on Greasy Fork.',
  403. title: 'BLOCK YOUTUBE USERS [2.6.1]'
  404. });*/
  405. }
  406.  
  407. })();