YouTube Sub Feed Filter 2

Filters your YouTube subscriptions feed.

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

  1. // ==UserScript==
  2. // @name YouTube Sub Feed Filter 2
  3. // @version 1.2
  4. // @description Filters your YouTube subscriptions feed.
  5. // @author Callum Latham
  6. // @namespace https://gf.qytechs.cn/users/696211-ctl2
  7. // @license MIT
  8. // @match *://www.youtube.com/*
  9. // @match *://youtube.com/*
  10. // @require https://gf.qytechs.cn/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. // Allow the page to update before moving on to the next section
  552. await new Promise((resolve) => {
  553. window.setTimeout(resolve, 0);
  554. });
  555. }
  556. }
  557.  
  558. async function hideFromMutations(mutations) {
  559. const sections = [];
  560.  
  561. for (const {addedNodes} of mutations) {
  562. for (const section of addedNodes) {
  563. sections.push(section);
  564. }
  565. }
  566.  
  567. hideFromSections(getConfig(await getForest(KEY_TREE, DEFAULT_TREE)), sections);
  568. }
  569.  
  570. // Helpers
  571.  
  572. function resetConfig() {
  573. for (const section of getAllSections()) {
  574. hideSection(section, false);
  575.  
  576. for (const video of getAllVideos(section)) {
  577. hideVideo(video, false);
  578. }
  579. }
  580. }
  581.  
  582. function getButtonDock() {
  583. return document
  584. .querySelector('ytd-browse[page-subtype="subscriptions"]')
  585. .querySelector('#title-container')
  586. .querySelector('#top-level-buttons-computed');
  587. }
  588.  
  589. // Button
  590.  
  591. class ClickHandler {
  592. constructor(button, onShortClick, onLongClick) {
  593. this.onShortClick = (function() {
  594. onShortClick();
  595.  
  596. window.clearTimeout(this.longClickTimeout);
  597.  
  598. window.removeEventListener('mouseup', this.onShortClick);
  599. }).bind(this);
  600.  
  601. this.onLongClick = (function() {
  602. window.removeEventListener('mouseup', this.onShortClick);
  603.  
  604. onLongClick();
  605. }).bind(this);
  606.  
  607. this.longClickTimeout = window.setTimeout(this.onLongClick, LONG_PRESS_TIME);
  608.  
  609. window.addEventListener('mouseup', this.onShortClick);
  610. }
  611. }
  612.  
  613. class Button {
  614. constructor(pageManager) {
  615. this.pageManager = pageManager;
  616. this.element = this.getNewButton();
  617.  
  618. this.element.addEventListener('mousedown', this.onMouseDown.bind(this));
  619.  
  620. GM.getValue(KEY_IS_ACTIVE, true).then((isActive) => {
  621. this.isActive = isActive;
  622.  
  623. if (isActive) {
  624. this.setButtonActive();
  625.  
  626. this.pageManager.start();
  627. }
  628. });
  629. }
  630.  
  631. addToDOM(button = this.element) {
  632. const {parentElement} = getButtonDock();
  633. parentElement.appendChild(button);
  634. }
  635.  
  636. getNewButton() {
  637. const openerTemplate = getButtonDock().children[1];
  638. const button = openerTemplate.cloneNode(false);
  639.  
  640. this.addToDOM(button);
  641.  
  642. button.innerHTML = openerTemplate.innerHTML;
  643.  
  644. button.querySelector('button').innerHTML = openerTemplate.querySelector('button').innerHTML;
  645.  
  646. button.querySelector('a').removeAttribute('href');
  647.  
  648. // TODO Build the svg via javascript
  649. 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>`;
  650.  
  651. return button;
  652. }
  653.  
  654. hide() {
  655. this.element.style.display = 'none';
  656. }
  657.  
  658. show() {
  659. this.element.parentElement.appendChild(this.element);
  660. this.element.style.removeProperty('display');
  661. }
  662.  
  663. setButtonActive() {
  664. if (this.isActive) {
  665. this.element.classList.add('style-blue-text');
  666. this.element.classList.remove('style-opacity');
  667. } else {
  668. this.element.classList.add('style-opacity');
  669. this.element.classList.remove('style-blue-text');
  670. }
  671. }
  672.  
  673. toggleActive() {
  674. this.isActive = !this.isActive;
  675.  
  676. this.setButtonActive();
  677.  
  678. GM.setValue(KEY_IS_ACTIVE, this.isActive);
  679.  
  680. if (this.isActive) {
  681. this.pageManager.start();
  682. } else {
  683. this.pageManager.stop();
  684. }
  685. }
  686.  
  687. onLongClick() {
  688. editForest(KEY_TREE, DEFAULT_TREE, TITLE, FRAME_STYLE.INNER, FRAME_STYLE.OUTER)
  689. .then((forest) => {
  690. if (this.isActive) {
  691. resetConfig();
  692.  
  693. // Hide filtered videos
  694. hideFromSections(getConfig(forest));
  695. }
  696. })
  697. .catch((error) => {
  698. console.error(error);
  699.  
  700. if (window.confirm(
  701. `[${TITLE}]` +
  702. '\n\nYour config\'s structure is invalid.' +
  703. '\nThis could be due to a script update or your data being corrupted.' +
  704. '\n\nError Message:' +
  705. `\n${error}` +
  706. '\n\nWould you like to erase your data?'
  707. )) {
  708. GM.deleteValue(KEY_TREE);
  709. }
  710. });
  711. }
  712.  
  713. async onMouseDown(event) {
  714. if (event.button === 0) {
  715. new ClickHandler(this.element, this.toggleActive.bind(this), this.onLongClick.bind(this));
  716. }
  717. }
  718. }
  719.  
  720. // Page load/navigation handler
  721.  
  722. class PageManager {
  723. constructor() {
  724. // Don't run in frames (e.g. stream chat frame)
  725. if (window.parent !== window) {
  726. return;
  727. }
  728.  
  729. this.videoObserver = new MutationObserver(hideFromMutations);
  730.  
  731. const emitter = document.getElementById('page-manager');
  732. const event = 'yt-action';
  733. const onEvent = ({detail}) => {
  734. if (detail.actionName === 'ytd-update-grid-state-action') {
  735. this.onLoad();
  736.  
  737. emitter.removeEventListener(event, onEvent);
  738. }
  739. };
  740.  
  741. emitter.addEventListener(event, onEvent);
  742. }
  743.  
  744. start() {
  745. getForest(KEY_TREE, DEFAULT_TREE).then(forest => {
  746. // Call hide function when new videos are loaded
  747. this.videoObserver.observe(
  748. document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'),
  749. {childList: true}
  750. );
  751.  
  752. try {
  753. hideFromSections(getConfig(forest));
  754. } catch (error) {
  755. console.error(error);
  756.  
  757. window.alert(
  758. `[${TITLE}]` +
  759. '\n\nUnable to execute filter; Expected config structure may have changed.' +
  760. '\nTry opening and closing the config editor to update your data\'s structure.'
  761. );
  762. }
  763. });
  764. }
  765.  
  766. stop() {
  767. this.videoObserver.disconnect();
  768.  
  769. resetConfig();
  770. }
  771.  
  772. isSubPage() {
  773. return new RegExp('^.*youtube.com/feed/subscriptions(\\?flow=1|\\?pbjreload=\\d+)?$').test(document.URL);
  774. }
  775.  
  776. isGridView() {
  777. return document.querySelector('ytd-expanded-shelf-contents-renderer') === null;
  778. }
  779.  
  780. onLoad() {
  781. // Allow configuration
  782. if (this.isSubPage() && this.isGridView()) {
  783. this.button = new Button(this);
  784.  
  785. this.button.show();
  786. }
  787.  
  788. document.querySelector('ytd-app').addEventListener('yt-navigate-finish', (function({detail}) {
  789. this.onNavigate(detail);
  790. }).bind(this));
  791.  
  792. document.body.addEventListener('popstate', (function({state}) {
  793. this.onNavigate(state);
  794. }).bind(this));
  795. }
  796.  
  797. onNavigate({endpoint}) {
  798. if (endpoint.browseEndpoint) {
  799. const {params, browseId} = endpoint.browseEndpoint;
  800.  
  801. if ((params === 'MAE%3D' || (!params && this.isGridView())) && browseId === 'FEsubscriptions') {
  802. if (!this.button) {
  803. this.button = new Button(this);
  804. }
  805.  
  806. this.button.show();
  807.  
  808. this.start();
  809. } else {
  810. if (this.button) {
  811. this.button.hide();
  812. }
  813.  
  814. this.videoObserver.disconnect();
  815. }
  816. }
  817. }
  818. }
  819.  
  820. // Main
  821.  
  822. new PageManager();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址