YouTube Sub Feed Filter 2

Filters your YouTube subscriptions feed.

当前为 2022-07-31 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Sub Feed Filter 2
  3. // @version 0.2
  4. // @description Filters your YouTube subscriptions feed.
  5. // @author Callum Latham
  6. // @namespace https://greasyfork.org/users/696211-ctl2
  7. // @license MIT
  8. // @match *://www.youtube.com/*
  9. // @match *://youtube.com/*
  10. // @require https://greasyfork.org/scripts/446506-tree-frame-2/code/Tree%20Frame%202.js?version=1076104
  11. // @grant GM.setValue
  12. // @grant GM.getValue
  13. // @grant GM.deleteValue
  14. // ==/UserScript==
  15.  
  16. // User config
  17.  
  18. const LONG_PRESS_TIME = 400;
  19. const REGEXP_FLAGS = 'i';
  20.  
  21. // Dev config
  22.  
  23. const VIDEO_TYPE_IDS = {
  24. 'GROUPS': {
  25. 'ALL': 'All',
  26. 'STREAMS': 'Streams',
  27. 'PREMIERS': 'Premiers',
  28. 'NONE': 'None'
  29. },
  30. 'INDIVIDUALS': {
  31. 'STREAMS_SCHEDULED': 'Scheduled Streams',
  32. 'STREAMS_LIVE': 'Live Streams',
  33. 'STREAMS_FINISHED': 'Finished Streams',
  34. 'PREMIERS_SCHEDULED': 'Scheduled Premiers',
  35. 'PREMIERS_LIVE': 'Live Premiers',
  36. 'SHORTS': 'Shorts',
  37. 'NORMAL': 'Basic Videos'
  38. }
  39. };
  40.  
  41. const FRAME_STYLE = {
  42. 'OUTER': {'zIndex': 10000},
  43. 'INNER': {
  44. 'headBase': '#ff0000',
  45. 'headButtonExit': '#000000',
  46. 'borderHead': '#ffffff',
  47. 'nodeBase': ['#222222', '#111111'],
  48. 'borderTooltip': '#570000'
  49. }
  50. };
  51. const TITLE = 'YouTube Sub Feed Filter';
  52. const KEY_TREE = 'YTSFF_TREE';
  53. const KEY_IS_ACTIVE = 'YTSFF_IS_ACTIVE';
  54.  
  55. function getVideoTypes(children) {
  56. const registry = new Set();
  57. const register = (value) => {
  58. if (registry.has(value)) {
  59. throw new Error(`Overlap found at '${value}'.`);
  60. }
  61.  
  62. registry.add(value);
  63. };
  64.  
  65. for (const {value} of children) {
  66. switch (value) {
  67. case VIDEO_TYPE_IDS.GROUPS.ALL:
  68. Object.values(VIDEO_TYPE_IDS.INDIVIDUALS).forEach(register);
  69. break;
  70.  
  71. case VIDEO_TYPE_IDS.GROUPS.STREAMS:
  72. register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED);
  73. register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE);
  74. register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED);
  75. break;
  76.  
  77. case VIDEO_TYPE_IDS.GROUPS.PREMIERS:
  78. register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED);
  79. register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE);
  80. break;
  81.  
  82. default:
  83. register(value);
  84. }
  85. }
  86.  
  87. return registry;
  88. }
  89.  
  90. const CUTOFF_VALUES = [
  91. 'Minimum',
  92. 'Maximum'
  93. ];
  94.  
  95. const BADGE_VALUES = [
  96. 'Exclude',
  97. 'Include',
  98. 'Require'
  99. ];
  100.  
  101. const DEFAULT_TREE = (() => {
  102. const regexPredicate = (value) => {
  103. try {
  104. RegExp(value);
  105. } catch (e) {
  106. return 'Value must be a valid regular expression.';
  107. }
  108.  
  109. return true;
  110. };
  111.  
  112. return {
  113. 'children': [
  114. {
  115. 'label': 'Filters',
  116. 'children': [],
  117. 'seed': {
  118. 'label': 'Filter Name',
  119. 'value': '',
  120. 'children': [
  121. {
  122. 'label': 'Channel Regex',
  123. 'children': [],
  124. 'seed': {
  125. 'value': '^',
  126. 'predicate': regexPredicate
  127. }
  128. },
  129. {
  130. 'label': 'Video Regex',
  131. 'children': [],
  132. 'seed': {
  133. 'value': '^',
  134. 'predicate': regexPredicate
  135. }
  136. },
  137. {
  138. 'label': 'Video Types',
  139. 'children': [{
  140. 'value': VIDEO_TYPE_IDS.GROUPS.ALL,
  141. 'predicate': Object.values(VIDEO_TYPE_IDS.GROUPS).concat(Object.values(VIDEO_TYPE_IDS.INDIVIDUALS))
  142. }],
  143. 'seed': {
  144. 'value': VIDEO_TYPE_IDS.GROUPS.NONE,
  145. 'predicate': Object.values(VIDEO_TYPE_IDS.GROUPS).concat(Object.values(VIDEO_TYPE_IDS.INDIVIDUALS))
  146. },
  147. 'childPredicate': (children) => {
  148. try {
  149. getVideoTypes(children);
  150. } catch ({message}) {
  151. return message;
  152. }
  153.  
  154. return true;
  155. }
  156. }
  157. ]
  158. }
  159. },
  160. {
  161. 'label': 'Cutoffs',
  162. 'children': [
  163. {
  164. 'label': 'Watched (%)',
  165. 'children': [],
  166. 'seed': {
  167. 'childPredicate': ([{'value': boundary}, {value}]) => {
  168. if (boundary === CUTOFF_VALUES[0]) {
  169. return value < 100 ? true : 'Minimum must be less than 100%';
  170. }
  171.  
  172. return value > 0 ? true : 'Maximum must be greater than 0%';
  173. },
  174. 'children': [
  175. {
  176. 'value': CUTOFF_VALUES[1],
  177. 'predicate': CUTOFF_VALUES
  178. },
  179. {
  180. 'value': 100
  181. }
  182. ]
  183. }
  184. },
  185. {
  186. 'label': 'View Count',
  187. 'children': [],
  188. 'seed': {
  189. 'childPredicate': ([{'value': boundary}, {value}]) => {
  190. if (boundary === CUTOFF_VALUES[1]) {
  191. return value > 0 ? true : 'Maximum must be greater than 0';
  192. }
  193.  
  194. return true;
  195. },
  196. 'children': [
  197. {
  198. 'value': CUTOFF_VALUES[0],
  199. 'predicate': CUTOFF_VALUES
  200. },
  201. {
  202. 'value': 0,
  203. 'predicate': (value) => Math.floor(value) === value ? true : 'Value must be an integer'
  204. }
  205. ]
  206. }
  207. },
  208. {
  209. 'label': 'Duration (Minutes)',
  210. 'children': [],
  211. 'seed': {
  212. 'childPredicate': ([{'value': boundary}, {value}]) => {
  213. if (boundary === CUTOFF_VALUES[1]) {
  214. return value > 0 ? true : 'Maximum must be greater than 0';
  215. }
  216.  
  217. return true;
  218. },
  219. 'children': [
  220. {
  221. 'value': CUTOFF_VALUES[0],
  222. 'predicate': CUTOFF_VALUES
  223. },
  224. {
  225. 'value': 0
  226. }
  227. ]
  228. }
  229. }
  230. ]
  231. },
  232. {
  233. 'label': 'Badges',
  234. 'children': [
  235. {
  236. 'label': 'Verified',
  237. 'value': BADGE_VALUES[1],
  238. 'predicate': BADGE_VALUES
  239. },
  240. {
  241. 'label': 'Official Artist',
  242. 'value': BADGE_VALUES[1],
  243. 'predicate': BADGE_VALUES
  244. }
  245. ]
  246. }
  247. ]
  248. };
  249. })();
  250.  
  251. function getConfig([filters, cutoffs, badges]) {
  252. return {
  253. 'filters': (() => {
  254. const getRegex = ({children}) => new RegExp(children.length === 0 ? '' :
  255. children.map(({value}) => `(${value})`).join('|'), REGEXP_FLAGS);
  256.  
  257. return filters.children.map(({'children': [channel, video, type]}) => ({
  258. 'channels': getRegex(channel),
  259. 'videos': getRegex(video),
  260. 'types': type.children.length === 0 ? Object.values(VIDEO_TYPE_IDS.INDIVIDUALS) : getVideoTypes(type.children)
  261. }));
  262. })(),
  263. 'cutoffs': cutoffs.children.map(({children}) => {
  264. const boundaries = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
  265.  
  266. for (const {'children': [{'value': boundary}, {value}]} of children) {
  267. boundaries[boundary === CUTOFF_VALUES[0] ? 0 : 1] = value;
  268. }
  269.  
  270. return boundaries;
  271. }),
  272. 'badges': badges.children.map(({value}) => BADGE_VALUES.indexOf(value))
  273. };
  274. }
  275.  
  276. // Video element helpers
  277.  
  278. function getAllSections() {
  279. return [...document
  280. .querySelector('.ytd-page-manager[page-subtype="subscriptions"]')
  281. .querySelectorAll('ytd-item-section-renderer')
  282. ];
  283. }
  284.  
  285. function getAllVideos(section) {
  286. return [...section.querySelectorAll('ytd-grid-video-renderer')];
  287. }
  288.  
  289. function firstWordEquals(element, word) {
  290. return element.innerText.split(' ')[0] === word;
  291. }
  292.  
  293. function getVideoBadges(video) {
  294. const container = video.querySelector('#video-badges');
  295.  
  296. return container ? [...container.querySelectorAll('.badge')] : [];
  297. }
  298.  
  299. function getChannelBadges(video) {
  300. const container = video.querySelector('ytd-badge-supported-renderer.ytd-channel-name');
  301.  
  302. return container ? [...container.querySelectorAll('.badge')] : [];
  303. }
  304.  
  305. function getMetadataLine(video) {
  306. return video.querySelector('#metadata-line');
  307. }
  308.  
  309. function isScheduled(video) {
  310. return VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED](video) ||
  311. VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED](video);
  312. }
  313.  
  314. function isLive(video) {
  315. return VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE](video) ||
  316. VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE](video);
  317. }
  318.  
  319. // Config testers
  320.  
  321. const VIDEO_PREDICATES = {
  322. [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED]: (video) => {
  323. const [schedule] = getMetadataLine(video).children;
  324.  
  325. return firstWordEquals(schedule, 'Scheduled');
  326. },
  327. [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE]: (video) => {
  328. for (const badge of getVideoBadges(video)) {
  329. if (firstWordEquals(badge.querySelector('span.ytd-badge-supported-renderer'), 'LIVE')) {
  330. return true;
  331. }
  332. }
  333.  
  334. return false;
  335. },
  336. [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED]: (video) => {
  337. const {children} = getMetadataLine(video);
  338.  
  339. return children.length > 1 && firstWordEquals(children[1], 'Streamed');
  340. },
  341. [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED]: (video) => {
  342. const [schedule] = getMetadataLine(video).children;
  343.  
  344. return firstWordEquals(schedule, 'Premieres');
  345. },
  346. [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE]: (video) => {
  347. for (const badge of getVideoBadges(video)) {
  348. const text = badge.querySelector('span.ytd-badge-supported-renderer');
  349.  
  350. if (firstWordEquals(text, 'PREMIERING') || firstWordEquals(text, 'PREMIERE')) {
  351. return true;
  352. }
  353. }
  354.  
  355. return false;
  356. },
  357. [VIDEO_TYPE_IDS.INDIVIDUALS.SHORTS]: (video) => {
  358. let icon = video.querySelector('[overlay-style]');
  359.  
  360. return icon && icon.getAttribute('overlay-style') === 'SHORTS';
  361. },
  362. [VIDEO_TYPE_IDS.INDIVIDUALS.NORMAL]: (video) => {
  363. const [, {innerText}] = getMetadataLine(video).children;
  364.  
  365. return new RegExp('^\\d+ .+ ago$').test(innerText);
  366. }
  367. };
  368.  
  369. const CUTOFF_GETTERS = [
  370. // Watched %
  371. (video) => {
  372. const progressBar = video.querySelector('#progress');
  373.  
  374. if (!progressBar) {
  375. return 0;
  376. }
  377.  
  378. return Number.parseInt(progressBar.style.width.slice(0, -1));
  379. },
  380. // View count
  381. (video) => {
  382. if (isScheduled(video)) {
  383. return 0;
  384. }
  385.  
  386. const [{innerText}] = getMetadataLine(video).children;
  387. const [valueString] = innerText.split(' ');
  388. const lastChar = valueString.slice(-1);
  389.  
  390. if (/\d/.test(lastChar)) {
  391. return Number.parseInt(valueString);
  392. }
  393.  
  394. const valueNumber = Number.parseFloat(valueString.slice(0, -1));
  395.  
  396. switch (lastChar) {
  397. case 'B':
  398. return valueNumber * 1000000000;
  399. case 'M':
  400. return valueNumber * 1000000;
  401. case 'K':
  402. return valueNumber * 1000;
  403. }
  404.  
  405. return valueNumber;
  406. },
  407. // Duration (minutes)
  408. (video) => {
  409. const timeElement = video.querySelector('ytd-thumbnail-overlay-time-status-renderer');
  410.  
  411. let minutes = 0;
  412.  
  413. if (timeElement) {
  414. const timeParts = timeElement.innerText.split(':').map((_) => Number.parseInt(_));
  415.  
  416. let timeValue = 1 / 60;
  417.  
  418. for (let i = timeParts.length - 1; i >= 0; --i) {
  419. minutes += timeParts[i] * timeValue;
  420.  
  421. timeValue *= 60;
  422. }
  423. }
  424.  
  425. return Number.isNaN(minutes) ? 0 : minutes;
  426. }
  427. ];
  428.  
  429. const BADGE_PREDICATES = [
  430. // Verified
  431. (video) => getChannelBadges(video)
  432. .some((badge) => badge.classList.contains('badge-style-type-verified')),
  433. // Official Artist
  434. (video) => getChannelBadges(video)
  435. .some((badge) => badge.classList.contains('badge-style-type-verified-artist'))
  436. ];
  437.  
  438. // Hider functions
  439.  
  440. function hideSection(section, doHide = true) {
  441. if (section.matches(':first-child')) {
  442. const title = section.querySelector('#title');
  443.  
  444. if (doHide) {
  445. section.style.height = '0';
  446. section.style.borderBottom = 'none';
  447. title.style.display = 'none';
  448. } else {
  449. section.style.removeProperty('height');
  450. section.style.removeProperty('borderBottom');
  451. title.style.removeProperty('display');
  452. }
  453. } else {
  454. if (doHide) {
  455. section.style.display = 'none';
  456. } else {
  457. section.style.removeProperty('display');
  458. }
  459. }
  460. }
  461.  
  462. function hideVideo(video, doHide = true) {
  463. if (doHide) {
  464. video.style.display = 'none';
  465. } else {
  466. video.style.removeProperty('display');
  467. }
  468. }
  469.  
  470. function loadVideo(video) {
  471. return new Promise((resolve) => {
  472. const test = () => {
  473. if (video.querySelector('#interaction.yt-icon-button')) {
  474. resolve();
  475. }
  476. };
  477.  
  478. test();
  479.  
  480. new MutationObserver(test)
  481. .observe(video, {
  482. 'childList ': true,
  483. 'subtree': true,
  484. 'attributes': true,
  485. 'attributeOldValue': true
  486. });
  487. });
  488. }
  489.  
  490. function shouldHide({filters, cutoffs, badges}, video) {
  491. for (let i = 0; i < BADGE_PREDICATES.length; ++i) {
  492. if (badges[i] !== 1 && Boolean(badges[i]) !== BADGE_PREDICATES[i](video)) {
  493. return true;
  494. }
  495. }
  496.  
  497. for (let i = 0; i < CUTOFF_GETTERS.length; ++i) {
  498. const [lowerBound, upperBound] = cutoffs[i];
  499. const value = CUTOFF_GETTERS[i](video);
  500.  
  501. if (value < lowerBound || value > upperBound) {
  502. return true;
  503. }
  504. }
  505.  
  506. // Separate the section's videos by hideability
  507. for (const {'channels': channelRegex, 'videos': videoRegex, types} of filters) {
  508. if (
  509. channelRegex.test(video.querySelector('a.yt-formatted-string').innerText) &&
  510. videoRegex.test(video.querySelector('a#video-title').innerText)
  511. ) {
  512. for (const type of types) {
  513. if (VIDEO_PREDICATES[type](video)) {
  514. return true;
  515. }
  516. }
  517. }
  518. }
  519.  
  520. return false;
  521. }
  522.  
  523. async function hideFromSections(config, sections = getAllSections()) {
  524. for (const section of sections) {
  525. if (section.matches('ytd-continuation-item-renderer')) {
  526. continue;
  527. }
  528.  
  529. const videos = getAllVideos(section);
  530.  
  531. let hiddenCount = 0;
  532.  
  533. // Process all videos in the section in parallel
  534. await Promise.all(videos.map((video) => new Promise(async (resolve) => {
  535. await loadVideo(video);
  536.  
  537. if (shouldHide(config, video)) {
  538. hideVideo(video);
  539.  
  540. hiddenCount++;
  541. }
  542.  
  543. resolve();
  544. })));
  545.  
  546. // Hide hideable videos
  547. if (hiddenCount === videos.length) {
  548. hideSection(section);
  549. }
  550. }
  551. }
  552.  
  553. async function hideFromMutations(mutations) {
  554. const sections = [];
  555.  
  556. for (const {addedNodes} of mutations) {
  557. for (const section of addedNodes) {
  558. sections.push(section);
  559. }
  560. }
  561.  
  562. hideFromSections(getConfig(await getForest(KEY_TREE, DEFAULT_TREE)), sections);
  563. }
  564.  
  565. // Helpers
  566.  
  567. function resetConfig() {
  568. for (const section of getAllSections()) {
  569. hideSection(section, false);
  570.  
  571. for (const video of getAllVideos(section)) {
  572. hideVideo(video, false);
  573. }
  574. }
  575. }
  576.  
  577. function getButtonDock() {
  578. return document
  579. .querySelector('ytd-browse[page-subtype="subscriptions"]')
  580. .querySelector('#title-container')
  581. .querySelector('#top-level-buttons-computed');
  582. }
  583.  
  584. // Button
  585.  
  586. class ClickHandler {
  587. constructor(button, onShortClick, onLongClick) {
  588. this.onShortClick = (function() {
  589. onShortClick();
  590.  
  591. window.clearTimeout(this.longClickTimeout);
  592.  
  593. window.removeEventListener('mouseup', this.onShortClick);
  594. }).bind(this);
  595.  
  596. this.onLongClick = (function() {
  597. window.removeEventListener('mouseup', this.onShortClick);
  598.  
  599. onLongClick();
  600. }).bind(this);
  601.  
  602. this.longClickTimeout = window.setTimeout(this.onLongClick, LONG_PRESS_TIME);
  603.  
  604. window.addEventListener('mouseup', this.onShortClick);
  605. }
  606. }
  607.  
  608. class Button {
  609. constructor(pageManager) {
  610. this.pageManager = pageManager;
  611. this.element = this.getNewButton();
  612.  
  613. this.element.addEventListener('mousedown', this.onMouseDown.bind(this));
  614.  
  615. GM.getValue(KEY_IS_ACTIVE, true).then((isActive) => {
  616. this.isActive = isActive;
  617.  
  618. if (isActive) {
  619. this.setButtonActive();
  620.  
  621. this.pageManager.start();
  622. }
  623. });
  624. }
  625.  
  626. addToDOM(button = this.element) {
  627. const {parentElement} = getButtonDock();
  628. parentElement.appendChild(button);
  629. }
  630.  
  631. getNewButton() {
  632. const openerTemplate = getButtonDock().children[1];
  633. const button = openerTemplate.cloneNode(false);
  634.  
  635. this.addToDOM(button);
  636.  
  637. button.innerHTML = openerTemplate.innerHTML;
  638.  
  639. button.querySelector('button').innerHTML = openerTemplate.querySelector('button').innerHTML;
  640.  
  641. button.querySelector('a').removeAttribute('href');
  642.  
  643. // TODO Build the svg via javascript
  644. button.querySelector('yt-icon').innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" focusable="false" viewBox="-50 -50 400 400"><g><path d="M128.25,175.6c1.7,1.8,2.7,4.1,2.7,6.6v139.7l60-51.3v-88.4c0-2.5,1-4.8,2.7-6.6L295.15,65H26.75L128.25,175.6z"/><rect x="13.95" y="0" width="294" height="45"/></g></svg>`;
  645.  
  646. return button;
  647. }
  648.  
  649. hide() {
  650. this.element.style.display = 'none';
  651. }
  652.  
  653. show() {
  654. this.element.parentElement.appendChild(this.element);
  655. this.element.style.removeProperty('display');
  656. }
  657.  
  658. setButtonActive() {
  659. if (this.isActive) {
  660. this.element.classList.add('style-blue-text');
  661. this.element.classList.remove('style-opacity');
  662. } else {
  663. this.element.classList.add('style-opacity');
  664. this.element.classList.remove('style-blue-text');
  665. }
  666. }
  667.  
  668. toggleActive() {
  669. this.isActive = !this.isActive;
  670.  
  671. this.setButtonActive();
  672.  
  673. GM.setValue(KEY_IS_ACTIVE, this.isActive);
  674.  
  675. if (this.isActive) {
  676. this.pageManager.start();
  677. } else {
  678. this.pageManager.stop();
  679. }
  680. }
  681.  
  682. onLongClick() {
  683. editForest(KEY_TREE, DEFAULT_TREE, TITLE, FRAME_STYLE.INNER, FRAME_STYLE.OUTER)
  684. .then((forest) => {
  685. if (this.isActive) {
  686. resetConfig();
  687.  
  688. // Hide filtered videos
  689. hideFromSections(getConfig(forest));
  690. }
  691. })
  692. .catch((error) => {
  693. console.error(error);
  694.  
  695. if (window.confirm(
  696. `[${TITLE}]` +
  697. '\n\nYour config\'s structure is invalid.' +
  698. '\nThis could be due to a script update or your data being corrupted.' +
  699. '\n\nError Message:' +
  700. `\n${error}` +
  701. '\n\nWould you like to erase your data?'
  702. )) {
  703. GM.deleteValue(KEY_TREE);
  704. }
  705. });
  706. }
  707.  
  708. async onMouseDown(event) {
  709. if (event.button === 0) {
  710. new ClickHandler(this.element, this.toggleActive.bind(this), this.onLongClick.bind(this));
  711. }
  712. }
  713. }
  714.  
  715. // Page load/navigation handler
  716.  
  717. class PageManager {
  718. constructor() {
  719. // Don't run in frames (e.g. stream chat frame)
  720. if (window.parent !== window) {
  721. return;
  722. }
  723.  
  724. this.videoObserver = new MutationObserver(hideFromMutations);
  725.  
  726. const emitter = document.getElementById('page-manager');
  727. const event = 'yt-action';
  728. const onEvent = ({detail}) => {
  729. if (detail.actionName === 'ytd-update-grid-state-action') {
  730. this.onLoad();
  731.  
  732. emitter.removeEventListener(event, onEvent);
  733. }
  734. };
  735.  
  736. emitter.addEventListener(event, onEvent);
  737. }
  738.  
  739. start() {
  740. getForest(KEY_TREE, DEFAULT_TREE).then(forest => {
  741. // Call hide function when new videos are loaded
  742. this.videoObserver.observe(
  743. document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'),
  744. {childList: true}
  745. );
  746.  
  747. try {
  748. hideFromSections(getConfig(forest));
  749. } catch (error) {
  750. console.error(error);
  751.  
  752. window.alert(
  753. `[${TITLE}]` +
  754. '\n\nUnable to execute filter; Expected config structure may have changed.' +
  755. '\nTry opening and closing the config editor to update your data\'s structure.'
  756. );
  757. }
  758. });
  759. }
  760.  
  761. stop() {
  762. this.videoObserver.disconnect();
  763.  
  764. resetConfig();
  765. }
  766.  
  767. isSubPage() {
  768. return new RegExp('^.*youtube.com/feed/subscriptions(\\?flow=1|\\?pbjreload=\\d+)?$').test(document.URL);
  769. }
  770.  
  771. isGridView() {
  772. return document.querySelector('ytd-expanded-shelf-contents-renderer') === null;
  773. }
  774.  
  775. onLoad() {
  776. // Allow configuration
  777. if (this.isSubPage() && this.isGridView()) {
  778. this.button = new Button(this);
  779.  
  780. this.button.show();
  781. }
  782.  
  783. document.body.addEventListener('yt-navigate-finish', (function({detail}) {
  784. this.onNavigate(detail);
  785. }).bind(this));
  786.  
  787. document.body.addEventListener('popstate', (function({state}) {
  788. this.onNavigate(state);
  789. }).bind(this));
  790. }
  791.  
  792. onNavigate({endpoint}) {
  793. if (endpoint.browseEndpoint) {
  794. const {params, browseId} = endpoint.browseEndpoint;
  795.  
  796. if ((params === 'MAE%3D' || (!params && this.isGridView())) && browseId === 'FEsubscriptions') {
  797. if (!this.button) {
  798. this.button = new Button(this);
  799. }
  800.  
  801. this.button.show();
  802.  
  803. this.start();
  804. } else {
  805. if (this.button) {
  806. this.button.hide();
  807. }
  808.  
  809. this.videoObserver.disconnect();
  810. }
  811. }
  812. }
  813. }
  814.  
  815. // Main
  816.  
  817. new PageManager();