返回首頁 

YouTube Sub Feed Filter 2

Set up filters for your sub feed


Install this script?
// ==UserScript==// @name        YouTube Sub Feed Filter 2// @version     1.47// @description Set up filters for your sub feed// @author      Callum Latham// @namespace   https://greasyfork.org/users/696211-ctl2// @license     MIT// @match       *://www.youtube.com/*// @match       *://youtube.com/*// @exclude     *://www.youtube.com/embed/*// @exclude     *://youtube.com/embed/*// @require     https://update.greasyfork.org/scripts/446506/1537901/%24Config.js// @grant       GM.setValue// @grant       GM.getValue// @grant       GM.deleteValue// ==/UserScript==/* global $Config */(() => {// Don't run in frames (e.g. stream chat frame)if (window.parent !== window) {// noinspection JSAnnotatorreturn;}// User configconst LONG_PRESS_TIME = 400;const REGEXP_FLAGS = 'i';// Dev configconst 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',FUNDRAISERS: 'Fundraisers',NORMAL: 'Basic Videos',},};const CUTOFF_VALUES = ['Minimum','Maximum',];const BADGE_VALUES = ['Exclude','Include','Require',];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 videoTypeOptions = Object.values({...VIDEO_TYPE_IDS.GROUPS,...VIDEO_TYPE_IDS.INDIVIDUALS,});return {get: (_, configs) => Object.assign(...configs),children: [{label: 'Filters',get: (() => {const getRegex = ({children}) => children.length === 0 ?null :new RegExp(children.map(({value}) => `(${value})`).join('|'), REGEXP_FLAGS);return ({children}) => ({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),})),});})(),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,options: videoTypeOptions,},],seed: {value: VIDEO_TYPE_IDS.GROUPS.NONE,options: videoTypeOptions,},childPredicate: (children) => {try {getVideoTypes(children);} catch ({message}) {return message;}return true;},},],},},{label: 'Cutoffs',get: ({children}) => ({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;}),}),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],options: 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],options: 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],options: CUTOFF_VALUES,},{value: 0},],},},],},{label: 'Badges',get: ({children}) => ({badges: children.map(({value}) => BADGE_VALUES.indexOf(value))}),children: [{label: 'Verified',value: BADGE_VALUES[1],options: BADGE_VALUES,},{label: 'Official Artist',value: BADGE_VALUES[1],options: BADGE_VALUES,},],},],};})(),{headBase: '#c80000',headButtonExit: '#000000',borderHead: '#ffffff',borderTooltip: '#c80000',},{zIndex: 10000,scrollbarColor: 'initial',},);const KEY_IS_ACTIVE = 'YTSFF_IS_ACTIVE';// Statelet button;// Video element helpersfunction getSubPage() {return document.querySelector('.ytd-page-manager[page-subtype="subscriptions"]');}function getAllVideos() {const subPage = getSubPage();return [...subPage.querySelectorAll('#primary > ytd-rich-grid-renderer > #contents > :not(:first-child):not(ytd-continuation-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 isShorts(video) {return video.matches('[is-shorts] *');}function getMetadataLine(video) {return video.querySelector(isShorts(video) ? '.shortsLockupViewModelHostOutsideMetadata' : '#metadata-line');}function isScheduled(video) {if (isShorts(video)) {return false;}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 testersconst 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]: isShorts,[VIDEO_TYPE_IDS.INDIVIDUALS.NORMAL]: (video) => {const uploadTimeNode = getUploadTimeNode(video);return uploadTimeNode ? new RegExp('^\\d+ .+ ago$').test(uploadTimeNode.innerText) : false;},[VIDEO_TYPE_IDS.INDIVIDUALS.FUNDRAISERS]: (video) => {for (const badge of getVideoBadges(video)) {if (firstWordEquals(badge, 'Fundraiser')) {return true;}}return 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') || child.matches('div[aria-label~=views]'),);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 functionsfunction loadVideo(video) {return new Promise((resolve) => {const test = () => {if (video.querySelector('#interaction')) {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')|| video.querySelector('.shortsLockupViewModelHostOutsideMetadataTitle')).innerText;for (const {'channels': channelRegex, 'videos': videoRegex, types} of filters) {if ((!channelRegex || channelName && channelRegex.test(channelName)) && (!videoRegex || 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'(element, doHide = true) {if (button.isActive) {hasReverted = false;}list.push({element, doHide, wasHidden: element.hidden});if (button.isActive) {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);}},};})();const showList = (() => {const ATTRIBUTE = 'is-in-first-column';const list = [];const observers = [];let rowLength;let rowRemaining = 1;let hasReverted = true;function disconnectObservers() {for (const observer of observers) {observer.disconnect();}observers.length = 0;}function show(element, isFirst) {const act = isFirst ?() => element.setAttribute(ATTRIBUTE, true) :() => element.removeAttribute(ATTRIBUTE);act();const observer = new MutationObserver(() => {observer.disconnect();act();// Avoids observation cycle that I can't figure out the cause ofwindow.setTimeout(() => {observer.observe(element, {attributeFilter: [ATTRIBUTE]});}, 0);});observer.observe(element, {attributeFilter: [ATTRIBUTE]});observers.push(observer);}return {'add'(element) {if (list.length === 0) {rowLength = element.itemsPerRow ?? 3;}if (button.isActive) {hasReverted = false;}const isFirst = --rowRemaining === 0;if (isFirst) {rowRemaining = rowLength;}list.push({element, isFirst, wasFirst: element.hasAttribute(ATTRIBUTE)});if (button.isActive) {show(element, isFirst);}},'revert'(doErase) {if (!hasReverted) {hasReverted = true;disconnectObservers();for (const {element, wasFirst} of list) {show(element, wasFirst);}}if (doErase) {list.length = 0;rowRemaining = 1;}},'ensure'() {if (!hasReverted) {return;}hasReverted = false;for (const {element, isFirst} of list) {show(element, isFirst);}},'lineFeed'() {rowRemaining = 1;},};})();async function hideVideo(element, config) {// video, else shorts containerif (element.tagName === 'YTD-RICH-ITEM-RENDERER') {await loadVideo(element);if (shouldHide(config, element)) {hideList.add(element);} else {showList.add(element);}return;}let doHide = true;for (const video of element.querySelectorAll('ytd-rich-item-renderer')) {await loadVideo(video);if (shouldHide(config, video)) {hideList.add(video);} else {showList.add(video);doHide = false;}}if (doHide) {hideList.add(element);} else {showList.lineFeed();}}async function hideVideos(videos = getAllVideos()) {const config = $config.get();for (const video of videos) {await Promise.all([hideVideo(video, config),// Allow the page to update visually before moving onnew Promise((resolve) => {window.setTimeout(resolve, 0);}),]);}}// Helpersfunction resetConfig(fullReset = true) {hideList.revert(fullReset);showList.revert(fullReset);}function hideFromMutations(mutations) {const videos = [];for (const {addedNodes} of mutations) {for (const node of addedNodes) {switch (node.tagName) {case 'YTD-RICH-ITEM-RENDERER':case 'YTD-RICH-SECTION-RENDERER':videos.push(node);}}}hideVideos(videos);}function getButtonDock() {return document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('#contents').querySelector('#title-container').querySelector('#top-level-buttons-computed');}// Buttonclass 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');}// 🤷‍♀️const policy = trustedTypes?.createPolicy('policy', {createHTML: (string) => string}) ?? {createHTML: (string) => string};parentElement.appendChild(button);button.innerHTML = policy.createHTML(openerTemplate.innerHTML);button.querySelector('yt-button-shape').innerHTML = policy.createHTML(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);videoObserver.observe(document.querySelector('ytd-browse[page-subtype="subscriptions"]').querySelector('div#contents'),{childList: true},);hideVideos();});let resizeCount = 0;window.addEventListener('resize', () => {const resizeId = ++resizeCount;this.forceInactive();const listener = ({detail}) => {// column size changedif (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 shownif (this.isDormant) {return;}resetConfig();hideVideos();}, 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 calledif (this.wasActive === undefined) {// Saves a GM.getValue call laterthis.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();showList.ensure();} else {hideList.revert(false);showList.revert(false);}}async onLongClick() {await $config.edit();resetConfig();hideVideos();}onMouseDown(event) {if (event.button === 0) {new ClickHandler(this.element, this.toggleActive.bind(this), this.onLongClick.bind(this));}}}// Main(() => {const loadButton = async () => {if (button) {button.isDormant = false;hideVideos();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 {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-r###lts-renderer ytd-rich-grid-renderer ytd-rich-item-renderer ytd-rich-grid-media'),);};function onNavigate({detail}) {if (detail.endpoint.browseEndpoint) {const {params, browseId} = detail.endpoint.browseEndpoint;// Handle navigation to the sub feedif ((params === 'MAE%3D' || !params && (!button || isGridView())) && browseId === 'F###bscriptions') {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 feedif (button) {button.isDormant = true;hideList.revert(true);showList.revert(true);}}document.body.addEventListener('yt-navigate-finish', onNavigate);})();})();