您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filters your YouTube subscriptions feed.
当前为
// ==UserScript== // @name YouTube Sub Feed Filter 2 // @version 1.16 // @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://update.greasyfork.org/scripts/446506/1298241/%24Config.js // @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', 'PREMIERES': 'Premieres', 'NONE': 'None', }, 'INDIVIDUALS': { 'STREAMS_SCHEDULED': 'Scheduled Streams', 'STREAMS_LIVE': 'Live Streams', 'STREAMS_FINISHED': 'Finished Streams', 'PREMIERES_SCHEDULED': 'Scheduled Premieres', 'PREMIERES_LIVE': 'Live Premieres', 'SHORTS': 'Shorts', 'NORMAL': 'Basic Videos', }, }; const CUTOFF_VALUES = [ 'Minimum', 'Maximum', ]; const BADGE_VALUES = [ 'Exclude', 'Include', 'Require', ]; const TITLE = 'YouTube Sub Feed Filter'; 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.PREMIERES: register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_SCHEDULED); register(VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_LIVE); break; default: register(value); } } return registry; } const $config = new $Config( 'YTSFF_TREE', (() => { const regexPredicate = (value) => { try { RegExp(value); } catch (_) { return 'Value must be a valid regular expression.'; } return true; }; const videoTypePredicate = Object.values({ ...VIDEO_TYPE_IDS.GROUPS, ...VIDEO_TYPE_IDS.INDIVIDUALS, }); 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': videoTypePredicate, }], 'seed': { 'value': VIDEO_TYPE_IDS.GROUPS.NONE, 'predicate': videoTypePredicate, }, '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, }, ], }, ], }; })(), ([filters, cutoffs, badges]) => ({ '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)), }), TITLE, { 'headBase': '#ff0000', 'headButtonExit': '#000000', 'borderHead': '#ffffff', 'nodeBase': ['#222222', '#111111'], 'borderTooltip': '#570000', }, {'zIndex': 10000}, ); const KEY_IS_ACTIVE = 'YTSFF_IS_ACTIVE'; // Removing row styling (() => { const styleElement = document.createElement('style'); document.head.appendChild(styleElement); const styleSheet = styleElement.sheet; const rules = [ ['ytd-rich-grid-row #contents.ytd-rich-grid-row', [ ['display', 'contents'], ]], ['ytd-rich-grid-row', [ ['display', 'contents'], ]], ]; for (let rule of rules) { styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value} !important;`).join('')}}`); } })(); // Video element helpers function getSubPage() { return document.querySelector('.ytd-page-manager[page-subtype="subscriptions"]'); } function getAllRows() { const subPage = getSubPage(); return subPage ? [...subPage.querySelectorAll('ytd-rich-grid-row')] : []; } function getAllSections() { const subPage = getSubPage(); return subPage ? [...subPage.querySelectorAll('ytd-rich-section-renderer:not(:first-child)')] : []; } function getAllVideos(row) { return [...row.querySelectorAll('ytd-rich-item-renderer')]; } function firstWordEquals(element, word) { return element.innerText.split(' ')[0] === word; } function getVideoBadges(video) { return video.querySelectorAll('.video-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.PREMIERES_SCHEDULED](video); } function getUploadTimeNode(video) { const children = [...getMetadataLine(video).children].filter((child) => child.matches('.inline-metadata-item')); return children.length > 1 ? children[1] : null; } // Config testers const VIDEO_PREDICATES = { [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_SCHEDULED]: (video) => { const metadataLine = getMetadataLine(video); return firstWordEquals(metadataLine, 'Scheduled'); }, [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_LIVE]: (video) => { for (const badge of getVideoBadges(video)) { if (firstWordEquals(badge, 'LIVE')) { return true; } } return false; }, [VIDEO_TYPE_IDS.INDIVIDUALS.STREAMS_FINISHED]: (video) => { const uploadTimeNode = getUploadTimeNode(video); return uploadTimeNode && firstWordEquals(uploadTimeNode, 'Streamed'); }, [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_SCHEDULED]: (video) => { const metadataLine = getMetadataLine(video); return firstWordEquals(metadataLine, 'Premieres'); }, [VIDEO_TYPE_IDS.INDIVIDUALS.PREMIERES_LIVE]: (video) => { for (const badge of getVideoBadges(video)) { if (firstWordEquals(badge, 'PREMIERING') || firstWordEquals(badge, 'PREMIERE')) { return true; } } return false; }, [VIDEO_TYPE_IDS.INDIVIDUALS.SHORTS]: (video) => { return video.querySelector('ytd-rich-grid-slim-media')?.isShort ?? false; }, [VIDEO_TYPE_IDS.INDIVIDUALS.NORMAL]: (video) => { const uploadTimeNode = getUploadTimeNode(video); return uploadTimeNode ? new RegExp('^\\d+ .+ ago$').test(uploadTimeNode.innerText) : false; }, }; 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].find((child) => child.matches('.inline-metadata-item')); 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 loadVideo(video) { return new Promise((resolve) => { const test = () => { if (video.querySelector('#interaction.yt-icon-button')) { observer.disconnect(); resolve(); } }; const observer = new MutationObserver(test); observer.observe(video, { 'childList': true, 'subtree': true, 'attributes': true, 'attributeOldValue': true, }); test(); }); } 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; } } const channelName = video.querySelector('ytd-channel-name#channel-name')?.innerText; const videoName = video.querySelector('#video-title').innerText; for (const {'channels': channelRegex, 'videos': videoRegex, types} of filters) { if ( (!channelName || channelRegex.test(channelName)) && videoRegex.test(videoName) ) { for (const type of types) { if (VIDEO_PREDICATES[type](video)) { return true; } } } } return false; } const hideList = (() => { const list = []; let hasReverted = true; function hide(element, doHide) { element.hidden = false; if (doHide) { element.style.display = 'none'; } else { element.style.removeProperty('display'); } } return { 'add'(doAct, element, doHide = true) { if (doAct) { hasReverted = false; } list.push({element, doHide, 'wasHidden': element.hidden}); if (doAct) { hide(element, doHide); } }, 'revert'(doErase) { if (!hasReverted) { hasReverted = true; for (const {element, doHide, wasHidden} of list) { hide(element, !doHide); element.hidden = wasHidden; } } if (doErase) { list.length = 0; } }, 'ensure'() { if (!hasReverted) { return; } hasReverted = false; for (const {element, doHide} of list) { hide(element, doHide); } }, }; })(); async function hideFromRows(config, doAct, groups = getAllRows()) { for (const group of groups) { const videos = getAllVideos(group); // Process all videos in the row in parallel await Promise.all(videos.map((video) => new Promise(async (resolve) => { await loadVideo(video); if (shouldHide(config, video)) { hideList.add(doAct, video); } resolve(); }))); // Allow the page to update visually before moving on to the next row await new Promise((resolve) => { window.setTimeout(resolve, 0); }); } } const hideFromSections = (() => { return async (config, doAct, groups = getAllSections()) => { for (const group of groups) { const shownVideos = []; const backupVideos = []; for (const video of getAllVideos(group)) { await loadVideo(video); if (video.hidden) { if (!shouldHide(config, video)) { backupVideos.push(video); } } else { shownVideos.push(video); } } let lossCount = 0; // Process all videos in the row in parallel await Promise.all(shownVideos.map((video) => new Promise(async (resolve) => { await loadVideo(video); if (shouldHide(config, video)) { hideList.add(doAct, video); if (backupVideos.length > 0) { hideList.add(doAct, backupVideos.shift(), false); } else { lossCount++; } } resolve(); }))); if (lossCount >= shownVideos.length) { hideList.add(doAct, group); } // Allow the page to update visually before moving on to the next row await new Promise((resolve) => { window.setTimeout(resolve, 0); }); } }; })(); function hideAll(doAct = true, rows, sections, config = $config.get()) { return Promise.all([ hideFromRows(config, doAct, rows), hideFromSections(config, doAct, sections), ]); } // Helpers async function hideFromMutations(isActive, mutations) { const rows = []; const sections = []; for (const {addedNodes} of mutations) { for (const node of addedNodes) { switch (node.tagName) { case 'YTD-RICH-GRID-ROW': rows.push(node); break; case 'YTD-RICH-SECTION-RENDERER': sections.push(node); } } } hideAll(isActive(), rows, sections); } function resetConfig(fullReset = true) { hideList.revert(fullReset); } function getButtonDock() { return document .querySelector('ytd-browse[page-subtype="subscriptions"]') .querySelector('#contents') .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 { wasActive; isActive = false; isDormant = false; constructor() { this.element = (() => { const getSVG = () => { const svgNamespace = 'http://www.w3.org/2000/svg'; const bottom = document.createElementNS(svgNamespace, 'path'); bottom.setAttribute('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'); const top = document.createElementNS(svgNamespace, 'rect'); top.setAttribute('x', '13.95'); top.setAttribute('width', '294'); top.setAttribute('height', '45'); const g = document.createElementNS(svgNamespace, 'g'); g.appendChild(bottom); g.appendChild(top); const svg = document.createElementNS(svgNamespace, 'svg'); svg.setAttribute('viewBox', '-50 -50 400 400'); svg.setAttribute('focusable', 'false'); svg.appendChild(g); return svg; }; const getNewButton = () => { const {parentElement, 'children': [, openerTemplate]} = getButtonDock(); const button = openerTemplate.cloneNode(false); if (openerTemplate.innerText) { throw new Error('too early'); } parentElement.appendChild(button); button.innerHTML = openerTemplate.innerHTML; button.querySelector('yt-button-shape').innerHTML = openerTemplate.querySelector('yt-button-shape').innerHTML; button.querySelector('a').removeAttribute('href'); button.querySelector('yt-icon').appendChild(getSVG()); button.querySelector('tp-yt-paper-tooltip').remove(); return button; }; return getNewButton(); })(); this.element.addEventListener('mousedown', this.onMouseDown.bind(this)); GM.getValue(KEY_IS_ACTIVE, true).then((isActive) => { this.isActive = isActive; this.update(); const videoObserver = new MutationObserver(hideFromMutations.bind(null, () => this.isActive)); videoObserver.observe( document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'), {childList: true}, ); hideAll(isActive); }); let resizeCount = 0; window.addEventListener('resize', () => { const resizeId = ++resizeCount; this.forceInactive(); const listener = ({detail}) => { // column size changed if (detail.actionName === 'yt-window-resized') { window.setTimeout(() => { if (resizeId !== resizeCount) { return; } this.forceInactive(false); // Don't bother re-running filters if the sub page isn't shown if (this.isDormant) { return; } resetConfig(); hideAll(this.isActive); }, 1000); document.body.removeEventListener('yt-action', listener); } }; document.body.addEventListener('yt-action', listener); }); } forceInactive(doForce = true) { if (doForce) { // if wasActive isn't undefined, forceInactive was already called if (this.wasActive === undefined) { // Saves a GM.getValue call later this.wasActive = this.isActive; this.isActive = false; } } else { this.isActive = this.wasActive; this.wasActive = undefined; } } update() { if (this.isActive) { this.setButtonActive(); } } setButtonActive() { if (this.isActive) { this.element.querySelector('svg').style.setProperty('fill', 'var(--yt-spec-call-to-action)'); } else { this.element.querySelector('svg').style.setProperty('fill', 'currentcolor'); } } toggleActive() { this.isActive = !this.isActive; this.setButtonActive(); GM.setValue(KEY_IS_ACTIVE, this.isActive); if (this.isActive) { hideList.ensure(); } else { hideList.revert(false); } } async onLongClick() { await $config.edit(); resetConfig(); hideAll(this.isActive); } async onMouseDown(event) { if (event.button === 0) { new ClickHandler(this.element, this.toggleActive.bind(this), this.onLongClick.bind(this)); } } } // Main (() => { let button; const loadButton = async () => { if (button) { button.isDormant = false; hideAll(button.isActive); return; } try { await $config.ready; } catch (error) { if (!$config.reset) { throw error; } if (!window.confirm(`${error.message}\n\nWould you like to erase your data?`)) { return; } $config.reset(); } try { getButtonDock(); button = new Button(); } catch (e) { const emitter = document.getElementById('page-manager'); const bound = () => { loadButton(); emitter.removeEventListener('yt-action', bound); }; emitter.addEventListener('yt-action', bound); } }; const isGridView = () => { return Boolean( document.querySelector('ytd-browse[page-subtype="subscriptions"]:not([hidden])') && document.querySelector('ytd-browse > ytd-two-column-browse-results-renderer ytd-rich-grid-row ytd-rich-item-renderer ytd-rich-grid-media'), ); }; async function onNavigate({detail}) { if (detail.endpoint.browseEndpoint) { const {params, browseId} = detail.endpoint.browseEndpoint; // Handle navigation to the sub feed if ((params === 'MAE%3D' || (!params && (!button || isGridView()))) && browseId === 'FEsubscriptions') { const emitter = document.querySelector('ytd-app'); const event = 'yt-action'; if (button || isGridView()) { loadButton(); } else { const listener = ({detail}) => { if (detail.actionName === 'ytd-update-grid-state-action') { if (isGridView()) { loadButton(); } emitter.removeEventListener(event, listener); } }; emitter.addEventListener(event, listener); } return; } } // Handle navigation away from the sub feed if (button) { button.isDormant = true; hideList.revert(); } } document.body.addEventListener('yt-navigate-finish', onNavigate); })();