您需要先安装一个扩展,例如 篡改猴、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();
- });