您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filters your YouTube subscriptions feed.
当前为
- // ==UserScript==
- // @name YouTube Sub Feed Filter 2
- // @version 0.1
- // @description Filters your YouTube subscriptions feed.
- // @author Callum Latham
- // @namespace https://greasyfork.org/users/696211-ctl2
- // @license MIT
- // @match *://www.youtube.com/*
- // @match *://youtube.com/*
- // @require https://greasyfork.org/scripts/446506-tree-frame-2/code/Tree%20Frame%202.js?version=1061073
- // @grant GM.setValue
- // @grant GM.getValue
- // @grant GM.deleteValue
- // ==/UserScript==
- //TODO Include filters for badges (Verified, Official Artist Channel, ...)
- // Also view counts, video lengths, ...
- // User config
- const LONG_PRESS_TIME = 400;
- const REGEXP_FLAGS = 'i';
- // Dev config
- const VIDEO_TYPE_IDS = {
- 'GROUPS': {
- 'ALL': 'All',
- 'STREAMS': 'Streams',
- 'PREMIERS': 'Premiers',
- 'NONE': 'None'
- },
- 'INDIVIDUALS': {
- 'STREAMS_SCHEDULED': 'Scheduled Streams',
- 'STREAMS_LIVE': 'Live Streams',
- 'STREAMS_FINISHED': 'Finished Streams',
- 'PREMIERS_SCHEDULED': 'Scheduled Premiers',
- 'PREMIERS_LIVE': 'Live Premiers',
- 'SHORTS': 'Shorts',
- 'NORMAL': 'Basic Videos'
- }
- };
- const FRAME_STYLE = {
- 'OUTER': {'zIndex': 10000},
- 'INNER': {
- 'headBase': '#ff0000',
- 'headButtonExit': '#000000',
- 'borderHead': '#ffffff',
- 'nodeBase': ['#222222', '#111111'],
- 'borderTooltip': '#570000'
- }
- };
- const TITLE = 'YouTube Sub Feed Filter';
- const KEY_TREE = 'YTSFF_TREE';
- const KEY_IS_ACTIVE = 'YTSFF_IS_ACTIVE';
- function getVideoTypes(children) {
- const registry = new Set();
- const register = (value) => {
- if (registry.has(value)) {
- throw new Error(`Overlap found at '${value}'.`);
- }
- registry.add(value);
- };
- for (const {value} of children) {
- switch (value) {
- case VIDEO_TYPE_IDS.GROUPS.ALL:
- Object.values(VIDEO_TYPE_IDS.INDIVIDUALS).forEach(register);
- break;
- case VIDEO_TYPE_IDS.GROUPS.STREAMS:
- register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED);
- register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE);
- register(VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED);
- break;
- case VIDEO_TYPE_IDS.GROUPS.PREMIERS:
- register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED);
- register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE);
- break;
- default:
- register(value);
- }
- }
- return registry;
- }
- const DEFAULT_TREE = (() => {
- const regexPredicate = (value) => {
- try {
- RegExp(value);
- } catch (e) {
- return 'Value must be a valid regular expression.';
- }
- return true;
- };
- return {
- 'children': [
- {
- 'label': 'Filters',
- 'children': [],
- 'seed': {
- 'label': 'Filter Name',
- 'value': '',
- 'children': [
- {
- 'label': 'Channel Regex',
- 'children': [],
- 'seed': {
- 'value': '^',
- 'predicate': regexPredicate
- }
- },
- {
- 'label': 'Video Regex',
- 'children': [],
- 'seed': {
- 'value': '^',
- 'predicate': regexPredicate
- }
- },
- {
- 'label': 'Video Types',
- 'children': [{
- 'value': VIDEO_TYPE_IDS.GROUPS.ALL,
- 'predicate': Object.values(VIDEO_TYPE_IDS.GROUPS).concat(Object.values(VIDEO_TYPE_IDS.INDIVIDUALS))
- }],
- 'seed': {
- 'value': VIDEO_TYPE_IDS.GROUPS.NONE,
- 'predicate': Object.values(VIDEO_TYPE_IDS.GROUPS).concat(Object.values(VIDEO_TYPE_IDS.INDIVIDUALS))
- },
- 'childPredicate': (children) => {
- try {
- getVideoTypes(children);
- } catch ({message}) {
- return message;
- }
- return true;
- }
- }
- ]
- }
- }
- // {
- // 'label': 'Options',
- // 'children': [
- // {
- // // <div id="progress" class="style-scope ytd-thumbnail-overlay-resume-playback-renderer" style="width: 45%;"></div>
- // 'label': 'Watched Cutoff (%)',
- // 'value': 100,
- // 'predicate': (value) => value > 0 ? true : 'Value must be greater than 0'
- // }
- // ]
- // }
- ]
- };
- })();
- // Video element helpers
- function getAllSections() {
- return [...document
- .querySelector('.ytd-page-manager[page-subtype="subscriptions"]')
- .querySelectorAll('ytd-item-section-renderer')
- ];
- }
- function getAllVideos(section) {
- return [...section.querySelectorAll('ytd-grid-video-renderer')];
- }
- function firstWordEquals(element, word) {
- return element.innerText.split(' ')[0] === word;
- }
- function getVideoBadges(video) {
- const container = video.querySelector('#video-badges');
- return container ? container.querySelectorAll('.badge') : [];
- }
- function getMetadataLine(video) {
- return video.querySelector('#metadata-line');
- }
- // Video hiding predicates
- class SectionSplitter {
- static splitters = {
- [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED]: (video) => {
- const [schedule] = getMetadataLine(video).children;
- return firstWordEquals(schedule, 'Scheduled');
- },
- [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE]: (video) => {
- for (const badge of getVideoBadges(video)) {
- if (firstWordEquals(badge.querySelector('span.ytd-badge-supported-renderer'), 'LIVE')) {
- return true;
- }
- }
- return false;
- },
- [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED]: (video) => {
- const metaDataLine = getMetadataLine(video);
- return metaDataLine.children.length > 1 && firstWordEquals(metaDataLine.children[1], 'Streamed');
- },
- [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED]: (video) => {
- const [schedule] = getMetadataLine(video).children;
- return firstWordEquals(schedule, 'Premieres');
- },
- [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE]: (video) => {
- for (const badge of getVideoBadges(video)) {
- const text = badge.querySelector('span.ytd-badge-supported-renderer');
- if (firstWordEquals(text, 'PREMIERING') || firstWordEquals(text, 'PREMIERE')) {
- return true;
- }
- }
- return false;
- },
- [VIDEO_TYPE_IDS.INDIVIDUALS.SHORTS]: (video) => {
- return new Promise(async (resolve) => {
- let icon = video.querySelector('[overlay-style]');
- // Stop searching if it gets a live badge
- const predicate = () => getVideoBadges(video).length || (icon && icon.getAttribute('overlay-style'));
- if (!predicate()) {
- await new Promise((resolve) => {
- const observer = new MutationObserver(() => {
- icon = video.querySelector('[overlay-style]');
- if (predicate()) {
- observer.disconnect();
- resolve();
- }
- });
- observer.observe(video, {
- 'childList': true,
- 'subtree': true,
- 'attributes': true
- });
- });
- }
- resolve(icon && icon.getAttribute('overlay-style') === 'SHORTS');
- });
- },
- [VIDEO_TYPE_IDS.INDIVIDUALS.NORMAL]: (video) => {
- const [, {innerText}] = getMetadataLine(video).children;
- return new RegExp('^\\d+ .+ ago$').test(innerText);
- }
- };
- hideables = [];
- promises = [];
- constructor(section) {
- this.videos = getAllVideos(section);
- }
- addHideables(channelRegex, titleRegex, videoType) {
- const predicate = SectionSplitter.splitters[videoType];
- const promises = [];
- for (const video of this.videos) {
- if (
- channelRegex.test(video.querySelector('a.yt-formatted-string').innerText) &&
- titleRegex.test(video.querySelector('a#video-title').innerText)
- ) {
- promises.push(new Promise(async (resolve) => {
- if (await predicate(video)) {
- this.hideables.push(video);
- }
- resolve();
- }));
- }
- }
- this.promises.push(Promise.all(promises));
- }
- }
- // Hider functions
- function hideSection(section, doHide = true) {
- if (section.matches(':first-child')) {
- const title = section.querySelector('#title');
- const videoContainer = section.querySelector('#contents').querySelector('#contents');
- if (doHide) {
- title.style.display = 'none';
- videoContainer.style.display = 'none';
- section.style.borderBottom = 'none';
- } else {
- title.style.removeProperty('display');
- videoContainer.style.removeProperty('display');
- section.style.removeProperty('borderBottom');
- }
- } else {
- if (doHide) {
- section.style.display = 'none';
- } else {
- section.style.removeProperty('display');
- }
- }
- }
- function hideVideo(video, doHide = true) {
- if (doHide) {
- video.style.display = 'none';
- } else {
- video.style.removeProperty('display');
- }
- }
- function getConfig([filters, options]) {
- return {
- 'filters': (() => {
- const getRegex = ({children}) => new RegExp(children.length === 0 ? '' :
- children.map(({value}) => `(${value})`).join('|'), REGEXP_FLAGS);
- return filters.children.map(({'children': [channel, video, type]}) => ({
- 'channels': getRegex(channel),
- 'videos': getRegex(video),
- 'types': type.children.length === 0 ? Object.values(VIDEO_TYPE_IDS.INDIVIDUALS) : getVideoTypes(type.children)
- }));
- })(),
- 'options': {
- // 'time': options.children[0].value
- }
- };
- }
- function hideFromSections(config, sections = getAllSections()) {
- for (const section of sections) {
- if (section.matches('ytd-continuation-item-renderer')) {
- continue;
- }
- const splitter = new SectionSplitter(section);
- // Separate the section's videos by hideability
- for (const {channels, videos, types} of config.filters) {
- for (const type of types) {
- splitter.addHideables(channels, videos, type);
- }
- }
- Promise.all(splitter.promises)
- .then(() => {
- // Hide hideable videos
- for (const video of splitter.hideables) {
- hideVideo(video);
- }
- });
- }
- }
- async function hideFromMutations(mutations) {
- const sections = [];
- for (const {addedNodes} of mutations) {
- for (const section of addedNodes) {
- sections.push(section);
- }
- }
- hideFromSections(getConfig(await getForest(KEY_TREE, DEFAULT_TREE)), sections);
- }
- function resetConfig() {
- for (const section of getAllSections()) {
- hideSection(section, false);
- for (const video of getAllVideos(section)) {
- hideVideo(video, false);
- }
- }
- }
- // Button
- function getButtonDock() {
- return document
- .querySelector('ytd-browse[page-subtype="subscriptions"]')
- .querySelector('#title-container')
- .querySelector('#top-level-buttons-computed');
- }
- class ClickHandler {
- constructor(button, onShortClick, onLongClick) {
- this.onShortClick = (function() {
- onShortClick();
- window.clearTimeout(this.longClickTimeout);
- window.removeEventListener('mouseup', this.onShortClick);
- }).bind(this);
- this.onLongClick = (function() {
- window.removeEventListener('mouseup', this.onShortClick);
- onLongClick();
- }).bind(this);
- this.longClickTimeout = window.setTimeout(this.onLongClick, LONG_PRESS_TIME);
- window.addEventListener('mouseup', this.onShortClick);
- }
- }
- class Button {
- constructor(pageManager) {
- this.pageManager = pageManager;
- this.element = this.getNewButton();
- this.element.addEventListener('mousedown', this.onMouseDown.bind(this));
- GM.getValue(KEY_IS_ACTIVE, true).then((isActive) => {
- this.isActive = isActive;
- if (isActive) {
- this.setButtonActive();
- this.pageManager.start();
- }
- });
- }
- addToDOM(button = this.element) {
- const {parentElement} = getButtonDock();
- parentElement.appendChild(button);
- }
- getNewButton() {
- const openerTemplate = getButtonDock().children[1];
- const button = openerTemplate.cloneNode(false);
- this.addToDOM(button);
- button.innerHTML = openerTemplate.innerHTML;
- button.querySelector('button').innerHTML = openerTemplate.querySelector('button').innerHTML;
- button.querySelector('a').removeAttribute('href');
- // TODO Build the svg via javascript
- 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>`;
- return button;
- }
- hide() {
- this.element.style.display = 'none';
- }
- show() {
- this.element.parentElement.appendChild(this.element);
- this.element.style.removeProperty('display');
- }
- setButtonActive() {
- if (this.isActive) {
- this.element.classList.add('style-blue-text');
- this.element.classList.remove('style-opacity');
- } else {
- this.element.classList.add('style-opacity');
- this.element.classList.remove('style-blue-text');
- }
- }
- toggleActive() {
- this.isActive = !this.isActive;
- this.setButtonActive();
- GM.setValue(KEY_IS_ACTIVE, this.isActive);
- if (this.isActive) {
- this.pageManager.start();
- } else {
- this.pageManager.stop();
- }
- }
- onLongClick() {
- editForest(KEY_TREE, DEFAULT_TREE, TITLE, FRAME_STYLE.INNER, FRAME_STYLE.OUTER)
- .then((forest) => {
- if (this.isActive) {
- resetConfig();
- // Hide filtered videos
- hideFromSections(getConfig(forest));
- }
- })
- .catch((error) => {
- console.error(error);
- if (window.confirm(
- 'An error was thrown by Tree Frame; Your data may be corrupted.\n' +
- 'Error Message: ' + error + '\n\n' +
- 'Would you like to clear your saved configs?'
- )) {
- GM.deleteValue(KEY_TREE);
- }
- });
- }
- async onMouseDown(event) {
- if (event.button === 0) {
- new ClickHandler(this.element, this.toggleActive.bind(this), this.onLongClick.bind(this));
- }
- }
- }
- // Page load/navigation handler
- class PageManager {
- constructor() {
- // Don't run in frames (e.g. stream chat frame)
- if (window.parent !== window) {
- return;
- }
- this.videoObserver = new MutationObserver(hideFromMutations);
- const emitter = document.getElementById('page-manager');
- const event = 'yt-action';
- const onEvent = ({detail}) => {
- if (detail.actionName === 'ytd-update-grid-state-action') {
- this.onLoad();
- emitter.removeEventListener(event, onEvent);
- }
- };
- emitter.addEventListener(event, onEvent);
- }
- start() {
- getForest(KEY_TREE, DEFAULT_TREE).then(forest => {
- // Call hide function when new videos are loaded
- this.videoObserver.observe(
- document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'),
- {childList: true}
- );
- hideFromSections(getConfig(forest));
- });
- }
- stop() {
- this.videoObserver.disconnect();
- resetConfig();
- }
- isSubPage() {
- return new RegExp('^.*youtube.com/feed/subscriptions(\\?flow=1|\\?pbjreload=\\d+)?$').test(document.URL);
- }
- isGridView() {
- return document.querySelector('ytd-expanded-shelf-contents-renderer') === null;
- }
- onLoad() {
- // Allow configuration
- if (this.isSubPage() && this.isGridView()) {
- this.button = new Button(this);
- this.button.show();
- }
- document.body.addEventListener('yt-navigate-finish', (function({detail}) {
- this.onNavigate(detail);
- }).bind(this));
- document.body.addEventListener('popstate', (function({state}) {
- this.onNavigate(state);
- }).bind(this));
- }
- onNavigate({endpoint}) {
- if (endpoint.browseEndpoint) {
- const {params, browseId} = endpoint.browseEndpoint;
- if ((params === 'MAE%3D' || (!params && this.isGridView())) && browseId === 'FEsubscriptions') {
- if (!this.button) {
- this.button = new Button(this);
- }
- this.button.show();
- this.start();
- } else {
- if (this.button) {
- this.button.hide();
- }
- this.videoObserver.disconnect();
- }
- }
- }
- }
- // Main
- new PageManager();