您需要先安装一个扩展,例如 篡改猴、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} ); try { hideFromSections(getConfig(forest)); } catch (e) { debugger; } }); } 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();