您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filters your YouTube subscriptions feed.
当前为
// ==UserScript== // @name YouTube Sub Feed Filter 2 // @version 1.4 // @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=1076104 // @grant GM.setValue // @grant GM.getValue // @grant GM.deleteValue // ==/UserScript== // Don't run in frames (e.g. stream chat frame) if (window.parent !== window) { // noinspection JSAnnotator return; } // 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 CUTOFF_VALUES = [ 'Minimum', 'Maximum' ]; const BADGE_VALUES = [ 'Exclude', 'Include', 'Require' ]; const DEFAULT_TREE = (() => { const regexPredicate = (value) => { try { RegExp(value); } catch (_) { 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': 'Cutoffs', 'children': [ { 'label': 'Watched (%)', 'children': [], 'seed': { 'childPredicate': ([{'value': boundary}, {value}]) => { if (boundary === CUTOFF_VALUES[0]) { return value < 100 ? true : 'Minimum must be less than 100%'; } return value > 0 ? true : 'Maximum must be greater than 0%'; }, 'children': [ { 'value': CUTOFF_VALUES[1], 'predicate': CUTOFF_VALUES }, { 'value': 100 } ] } }, { 'label': 'View Count', 'children': [], 'seed': { 'childPredicate': ([{'value': boundary}, {value}]) => { if (boundary === CUTOFF_VALUES[1]) { return value > 0 ? true : 'Maximum must be greater than 0'; } return true; }, 'children': [ { 'value': CUTOFF_VALUES[0], 'predicate': CUTOFF_VALUES }, { 'value': 0, 'predicate': (value) => Math.floor(value) === value ? true : 'Value must be an integer' } ] } }, { 'label': 'Duration (Minutes)', 'children': [], 'seed': { 'childPredicate': ([{'value': boundary}, {value}]) => { if (boundary === CUTOFF_VALUES[1]) { return value > 0 ? true : 'Maximum must be greater than 0'; } return true; }, 'children': [ { 'value': CUTOFF_VALUES[0], 'predicate': CUTOFF_VALUES }, { 'value': 0 } ] } } ] }, { 'label': 'Badges', 'children': [ { 'label': 'Verified', 'value': BADGE_VALUES[1], 'predicate': BADGE_VALUES }, { 'label': 'Official Artist', 'value': BADGE_VALUES[1], 'predicate': BADGE_VALUES } ] } ] }; })(); function getConfig([filters, cutoffs, badges]) { 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) })); })(), 'cutoffs': cutoffs.children.map(({children}) => { const boundaries = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY]; for (const {'children': [{'value': boundary}, {value}]} of children) { boundaries[boundary === CUTOFF_VALUES[0] ? 0 : 1] = value; } return boundaries; }), 'badges': badges.children.map(({value}) => BADGE_VALUES.indexOf(value)) }; } // Video element helpers function getAllSections() { const subPage = document.querySelector('.ytd-page-manager[page-subtype="subscriptions"]'); return subPage ? [...subPage.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 getChannelBadges(video) { const container = video.querySelector('ytd-badge-supported-renderer.ytd-channel-name'); return container ? [...container.querySelectorAll('.badge')] : []; } function getMetadataLine(video) { return video.querySelector('#metadata-line'); } function isScheduled(video) { return VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED](video) || VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_SCHEDULED](video); } function isLive(video) { return VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE](video) || VIDEO_PREDICATES[VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERS_LIVE](video); } // Config testers const VIDEO_PREDICATES = { [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 {children} = getMetadataLine(video); return children.length > 1 && firstWordEquals(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) => { let icon = video.querySelector('[overlay-style]'); return icon && icon.getAttribute('overlay-style') === 'SHORTS'; }, [VIDEO_TYPE_IDS.INDIVIDUALS.NORMAL]: (video) => { const [, {innerText}] = getMetadataLine(video).children; return new RegExp('^\\d+ .+ ago$').test(innerText); } }; const CUTOFF_GETTERS = [ // Watched % (video) => { const progressBar = video.querySelector('#progress'); if (!progressBar) { return 0; } return Number.parseInt(progressBar.style.width.slice(0, -1)); }, // View count (video) => { if (isScheduled(video)) { return 0; } const [{innerText}] = getMetadataLine(video).children; const [valueString] = innerText.split(' '); const lastChar = valueString.slice(-1); if (/\d/.test(lastChar)) { return Number.parseInt(valueString); } const valueNumber = Number.parseFloat(valueString.slice(0, -1)); switch (lastChar) { case 'B': return valueNumber * 1000000000; case 'M': return valueNumber * 1000000; case 'K': return valueNumber * 1000; } return valueNumber; }, // Duration (minutes) (video) => { const timeElement = video.querySelector('ytd-thumbnail-overlay-time-status-renderer'); let minutes = 0; if (timeElement) { const timeParts = timeElement.innerText.split(':').map((_) => Number.parseInt(_)); let timeValue = 1 / 60; for (let i = timeParts.length - 1; i >= 0; --i) { minutes += timeParts[i] * timeValue; timeValue *= 60; } } return Number.isNaN(minutes) ? 0 : minutes; } ]; const BADGE_PREDICATES = [ // Verified (video) => getChannelBadges(video) .some((badge) => badge.classList.contains('badge-style-type-verified')), // Official Artist (video) => getChannelBadges(video) .some((badge) => badge.classList.contains('badge-style-type-verified-artist')) ]; // Hider functions function hideSection(section, doHide = true) { if (section.matches(':first-child')) { const title = section.querySelector('#title'); if (doHide) { section.style.height = '0'; section.style.borderBottom = 'none'; title.style.display = 'none'; } else { section.style.removeProperty('height'); section.style.removeProperty('borderBottom'); title.style.removeProperty('display'); } } 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 loadVideo(video) { return new Promise((resolve) => { const test = () => { if (video.querySelector('#interaction.yt-icon-button')) { resolve(); } }; test(); new MutationObserver(test) .observe(video, { 'childList ': true, 'subtree': true, 'attributes': true, 'attributeOldValue': true }); }); } function shouldHide({filters, cutoffs, badges}, video) { for (let i = 0; i < BADGE_PREDICATES.length; ++i) { if (badges[i] !== 1 && Boolean(badges[i]) !== BADGE_PREDICATES[i](video)) { return true; } } for (let i = 0; i < CUTOFF_GETTERS.length; ++i) { const [lowerBound, upperBound] = cutoffs[i]; const value = CUTOFF_GETTERS[i](video); if (value < lowerBound || value > upperBound) { return true; } } // Separate the section's videos by hideability for (const {'channels': channelRegex, 'videos': videoRegex, types} of filters) { if ( channelRegex.test(video.querySelector('a.yt-formatted-string').innerText) && videoRegex.test(video.querySelector('a#video-title').innerText) ) { for (const type of types) { if (VIDEO_PREDICATES[type](video)) { return true; } } } } return false; } async function hideFromSections(config, sections = getAllSections()) { for (const section of sections) { if (section.matches('ytd-continuation-item-renderer')) { continue; } const videos = getAllVideos(section); let hiddenCount = 0; // Process all videos in the section in parallel await Promise.all(videos.map((video) => new Promise(async (resolve) => { await loadVideo(video); if (shouldHide(config, video)) { hideVideo(video); hiddenCount++; } resolve(); }))); // Hide hideable videos if (hiddenCount === videos.length) { hideSection(section); } // Allow the page to update before moving on to the next section await new Promise((resolve) => { window.setTimeout(resolve, 0); }); } } 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); } // Helpers function resetConfig() { for (const section of getAllSections()) { hideSection(section, false); for (const video of getAllVideos(section)) { hideVideo(video, false); } } } function getButtonDock() { return document .querySelector('ytd-browse[page-subtype="subscriptions"]') .querySelector('#title-container') .querySelector('#top-level-buttons-computed'); } // Button 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; this.update(); }); } update() { if (this.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( `[${TITLE}]` + '\n\nYour config\'s structure is invalid.' + '\nThis could be due to a script update or your data being corrupted.' + '\n\nError Message:' + `\n${error}` + '\n\nWould you like to erase your data?' )) { 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() { this.videoObserver = new MutationObserver(hideFromMutations); document .querySelector('ytd-app') .addEventListener('yt-navigate-finish', ({detail}) => { this.onNavigate(detail); }); document .body .addEventListener('popstate', ({state}) => { this.onNavigate(state); }); const canDock = () => this.isSubPage() && this.isGridView(); if (canDock()) { this.loadButton(); } const emitter = document.getElementById('page-manager'); const event = 'yt-action'; const onEvent = ({detail}) => { if (detail.actionName === 'ytd-update-grid-state-action') { if (canDock()) { this.loadButton(); } emitter.removeEventListener(event, onEvent); } }; emitter.addEventListener(event, onEvent); } loadButton() { if (!this.button) { this.button = new Button(this); } else { this.button.update(); } this.button.show(); } 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} ); try { hideFromSections(getConfig(forest)); } catch (error) { console.error(error); window.alert( `[${TITLE}]` + '\n\nUnable to execute filter; Expected config structure may have changed.' + '\nTry opening and closing the config editor to update your data\'s structure.' ); } }); } 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-item-section-renderer:not([hidden]) ytd-expanded-shelf-contents-renderer') === null; } onNavigate({endpoint}) { if (endpoint.browseEndpoint) { const {params, browseId} = endpoint.browseEndpoint; if ((params === 'MAE%3D' || (!params && this.isGridView())) && browseId === 'FEsubscriptions') { this.loadButton(); } else { if (this.button) { this.button.hide(); } this.stop(); } } } } // Main window.addEventListener('load', () => { new PageManager(); });