// ==UserScript==
// @name YouTube +
// @name:en YouTube +
// @name:de YouTube +
// @name:ja YouTube +
// @name:tr YouTube +
// @name:zh-CN YouTube +
// @name:zh-TW YouTube +
// @name:fr YouTube +
// @name:ko YouTube +
// @namespace by
// @version 2.2
// @author diorhc
// @description Вкладки для информации, комментариев, видео, плейлиста и скачивание видео и другие функции ↴
// @description:en Tabview YouTube and Download and others features ↴
// @description:de Tabview YouTube und Download und andere Funktionen ↴
// @description:fr Tabview YouTube et Télécharger et autres fonctionnalités ↴
// @description:zh-CN 标签视图 YouTube、下载及其他功能 ↴
// @description:zh-TW 標籤檢視 YouTube 及下載及其他功能 ↴
// @description:ko Tabview YouTube 및 다운로드 및 기타 기능 ↴
// @description:ja タブビューYouTubeとダウンロードおよびその他の機能 ↴
// @description:tr Sekmeli Görünüm YouTube ve İndir ve diğer özellikler ↴
// @match https://*.youtube.com/*
// @match https://music.youtube.com/*
// @match *://myactivity.google.com/*
// @include *://www.youtube.com/feed/history/*
// @include https://www.youtube.com
// @include *://*.youtube.com/**
// @exclude *://accounts.youtube.com/*
// @exclude *://www.youtube.com/live_chat_replay*
// @exclude *://www.youtube.com/persist_identity*
// @exclude /^https?://\w+\.youtube\.com\/live_chat.*$/
// @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @license MIT
// @require https://cdn.jsdelivr.net/npm/@preact/signals-core@1.12.1/dist/signals-core.min.js
// @require https://cdn.jsdelivr.net/npm/browser-id3-writer@4.4.0/dist/browser-id3-writer.min.js
// @require https://cdn.jsdelivr.net/npm/preact@10.27.2/dist/preact.min.js
// @require https://cdn.jsdelivr.net/npm/preact@10.27.2/hooks/dist/hooks.umd.js
// @require https://cdn.jsdelivr.net/npm/@preact/signals@2.5.0/dist/signals.min.js
// @require https://cdn.jsdelivr.net/npm/dayjs@1.11.19/dayjs.min.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect api.livecounts.io
// @connect cnv.cx
// @connect mp3yt.is
// @connect web.archive.org
// @connect *
// @connect ytplaylist.robert.wesner.io
// @connect youtube.com
// @connect googlevideo.com
// @connect self
// @run-at document-start
// @noframes
// @homepageURL https://github.com/diorhc/YTP
// @supportURL https://github.com/diorhc/YTP/issues
// @downloadURL none
// ==/UserScript==
const MODULE_PREFIX = '[YouTube+]';
const MODULE_NAMES = {
ADBLOCKER: `${MODULE_PREFIX}[Ad]`,
BASIC: `${MODULE_PREFIX}[B]`,
COMMENT: `${MODULE_PREFIX}[C]`,
ENHANCED: `${MODULE_PREFIX}[E]`,
ERROR_BOUNDARY: `${MODULE_PREFIX}[Err]`,
I18N: `${MODULE_PREFIX}[i18n]`,
MAIN: `${MODULE_PREFIX}[Main]`,
MUSIC: `${MODULE_PREFIX}[Mus]`,
PERFORMANCE: `${MODULE_PREFIX}[Perf]`,
PIP: `${MODULE_PREFIX}[PIP]`,
PLAYLIST_SEARCH: `${MODULE_PREFIX}[PL]`,
REPORT: `${MODULE_PREFIX}[Rep]`,
SHORTS: `${MODULE_PREFIX}[S]`,
STATS: `${MODULE_PREFIX}[St]`,
STYLE: `${MODULE_PREFIX}[Sty]`,
THUMBNAIL: `${MODULE_PREFIX}[Th]`,
TIMECODE: `${MODULE_PREFIX}[TC]`,
UPDATE: `${MODULE_PREFIX}[Upd]`,
UTILS: `${MODULE_PREFIX}[U]`,
};
const DOWNLOAD_SITES = {
Y2MATE: {
name: 'Y2Mate',
url: 'https://www.y2mate.com/youtube/{videoId}',
},
};
const SVG_NS = 'http://www.w3.org/2000/svg';
const SELECTORS = {
VIDEO_PLAYER: '.html5-video-player',
VIDEO_ELEMENT: 'video',
PLAYER_CONTAINER: '#movie_player',
PRIMARY: '#primary',
SECONDARY: '#secondary',
COMMENTS: '#comments',
DESCRIPTION: '#description',
TITLE: 'h1.ytd-watch-metadata',
CHANNEL_NAME: 'ytd-channel-name',
SUBSCRIBE_BUTTON: '#subscribe-button',
LIKE_BUTTON: 'like-button-view-model',
};
const CLASS_NAMES = {
YTP_BUTTON: 'ytp-button',
YTP_SETTINGS_BUTTON: 'ytp-settings-button',
HIDDEN: 'hidden',
ACTIVE: 'active',
};
const STORAGE_KEYS = {
SETTINGS: 'youtube_plus_settings',
TIMECODE_SETTINGS: 'youtube_timecode_settings',
COMMENT_SETTINGS: 'youtube_comment_manager_settings',
THEME: 'youtube_plus_theme',
LANGUAGE: 'youtube_plus_language',
};
const API_URLS = {
GITHUB_REPO: 'https://github.com/diorhc/YTP',
GITHUB_API: 'https://api.github.com/repos/diorhc/YTP/releases/latest',
GREASYFORK: 'https://greasyfork.org/scripts/YOUR_SCRIPT_ID',
};
const TIMING = {
DEBOUNCE_SHORT: 100,
DEBOUNCE_MEDIUM: 250,
DEBOUNCE_LONG: 500,
THROTTLE: 100,
ANIMATION_DURATION: 300,
TOAST_DURATION: 3000,
RETRY_DELAY: 1000,
OBSERVER_DELAY: 100,
};
const LIMITS = {
MAX_PLAYLIST_ITEMS: 5000,
MAX_COMMENT_LENGTH: 10000,
MAX_TITLE_LENGTH: 100,
MAX_DESCRIPTION_LENGTH: 500,
MAX_RETRIES: 3,
RATE_LIMIT_REQUESTS: 10,
RATE_LIMIT_WINDOW: 60000,
};
const ERROR_MESSAGES = {
INVALID_KEY: 'Key must be a non-empty string',
OBSERVER_DISCONNECT_FAILED: 'Observer disconnect failed',
FETCH_FAILED: 'Failed to fetch data',
INVALID_VIDEO_ID: 'Invalid video ID',
STORAGE_FAILED: 'Failed to save to localStorage',
PARSE_FAILED: 'Failed to parse JSON',
};
const URL_PATTERNS = {
VIDEO_ID: /[?&]v=([^&]+)/,
PLAYLIST_ID: /[?&]list=([^&]+)/,
SHORTS: /\/shorts\/([^/?]+)/,
TIMESTAMP: /[?&]t=(\d+)/,
CHANNEL_ID: /\/(channel|c|user)\/([^/?]+)/,
};
const UI_IDS = {
DOWNLOAD_BUTTON: '.ytp-download-button',
RIGHT_TABS_TOP_BUTTON: 'right-tabs-top-button',
UNIVERSAL_TOP_BUTTON: 'universal-top-button',
PLAYLIST_PANEL_TOP_BUTTON: 'playlist-panel-top-button',
YTMUSIC_SIDE_PANEL_TOP_BUTTON: 'ytmusic-side-panel-top-button',
STATS_MENU_CONTAINER: '.stats-menu-container',
TIMECODE_PANEL: 'ytplus-timecode-panel',
THUMBNAIL_STYLES: 'ytplus-thumbnail-styles',
THUMBNAIL_MODAL_ACTION_BTN: 'thumbnail-modal-action-btn',
SETTINGS_NAV_ITEM: 'ytp-plus-settings-nav-item',
};
const SVG_ICONS = {
ARROW_UP:
'',
SETTINGS:
'',
};
const STORAGE_PREFIXES = {
TIMECODE: 'youtube_timecode_',
COMMENT: 'youtube_comment_',
SETTINGS: 'youtubeEnhancer',
};
if (typeof window !== 'undefined') {
window.YouTubePlusConstants = {
MODULE_NAMES,
DOWNLOAD_SITES,
SVG_NS,
SELECTORS,
CLASS_NAMES,
STORAGE_KEYS,
API_URLS,
TIMING,
LIMITS,
ERROR_MESSAGES,
URL_PATTERNS,
UI_IDS,
SVG_ICONS,
STORAGE_PREFIXES,
};
}
window.YouTubePlusConfig = {
PRODUCTION_MODE: false,
LOG_LEVELS: {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
NONE: 4,
},
get currentLogLevel() {
return this.PRODUCTION_MODE ? this.LOG_LEVELS.WARN : this.LOG_LEVELS.DEBUG;
},
FEATURES: {
PERFORMANCE_MONITORING: true,
ERROR_BOUNDARY: true,
AUTO_UPDATE_CHECK: true,
ANALYTICS: false,
},
VERSION: '2.2',
BUILD_DATE: new Date().toISOString(),
shouldLog(level) {
return level >= this.currentLogLevel;
},
log: {
debug(...args) {
if (window.YouTubePlusConfig.shouldLog(window.YouTubePlusConfig.LOG_LEVELS.DEBUG)) {
console.log('[YouTube+][DEBUG]', ...args);
}
},
info(...args) {
if (window.YouTubePlusConfig.shouldLog(window.YouTubePlusConfig.LOG_LEVELS.INFO)) {
console.info('[YouTube+][INFO]', ...args);
}
},
warn(...args) {
if (window.YouTubePlusConfig.shouldLog(window.YouTubePlusConfig.LOG_LEVELS.WARN)) {
console.warn('[YouTube+][WARN]', ...args);
}
},
error(...args) {
if (window.YouTubePlusConfig.shouldLog(window.YouTubePlusConfig.LOG_LEVELS.ERROR)) {
console.error('[YouTube+][ERROR]', ...args);
}
},
},
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = window.YouTubePlusConfig;
}
(function () {
'use strict';
const DebugConfig = {
enabled: false,
performance: false,
errors: true,
domOperations: false,
navigation: false,
modules: false,
attachDetach: false,
tabOperations: false,
api: false,
storage: false,
userActions: false,
};
const debugLog = (category, ...args) => {
if (!DebugConfig.enabled) return;
if (!DebugConfig[category]) return;
console.log(`[YouTube+ Debug:${category}]`, ...args);
};
const debugWarn = (category, ...args) => {
if (!DebugConfig.enabled) return;
if (!DebugConfig[category]) return;
console.warn(`[YouTube+ Debug:${category}]`, ...args);
};
const debugError = (category, ...args) => {
if (!DebugConfig.errors) return;
console.error(`[YouTube+ Debug:${category}]`, ...args);
};
const debugTime = label => {
if (!DebugConfig.enabled || !DebugConfig.performance) {
return () => {};
}
const startTime = performance.now();
return () => {
const endTime = performance.now();
console.log(`[YouTube+ Perf] ${label}: ${(endTime - startTime).toFixed(2)}ms`);
};
};
const isDebugEnabled = category => {
return DebugConfig.enabled && DebugConfig[category];
};
if (typeof window !== 'undefined') {
window.YouTubePlusDebug = {
config: DebugConfig,
log: debugLog,
warn: debugWarn,
error: debugError,
time: debugTime,
isEnabled: isDebugEnabled,
get DEBUG_5084() {
return isDebugEnabled('attachDetach');
},
get DEBUG_5085() {
return isDebugEnabled('tabOperations');
},
};
console.log(
'[YouTube+] Debug system initialized. Use window.YouTubePlusDebug.config to configure.'
);
}
})();
(function () {
'use strict';
const LogLevel = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
CRITICAL: 4,
NONE: 5,
};
const config = {
level: LogLevel.INFO,
enabled: true,
includeTimestamp: false,
includeStack: true,
maxStackLines: 5,
customHandler: null,
};
const getTimestamp = () => {
const now = new Date();
return now.toISOString().substring(11, 23);
};
const formatStack = error => {
if (!error || !error.stack) return '';
const lines = error.stack.split('\n');
const relevantLines = lines.slice(0, config.maxStackLines);
return `\n${relevantLines.join('\n')}`;
};
const formatMessage = (module, level, args) => {
const parts = ['[YouTube+]'];
if (config.includeTimestamp) {
parts.push(`[${getTimestamp()}]`);
}
parts.push(`[${level}]`);
if (module) {
parts.push(`[${module}]`);
}
return [parts.join(' '), ...args];
};
const shouldLog = level => {
return config.enabled && level >= config.level;
};
const addStackTraceIfNeeded = (formattedArgs, level, args) => {
if (level >= LogLevel.ERROR && config.includeStack) {
const lastArg = args[args.length - 1];
if (lastArg instanceof Error) {
formattedArgs.push(formatStack(lastArg));
}
}
};
const outputLog = (module, level, levelName, consoleFn, formattedArgs) => {
if (config.customHandler) {
config.customHandler(module, level, levelName, formattedArgs);
} else if (typeof consoleFn === 'function') {
consoleFn(...formattedArgs);
}
};
const log = (module, level, levelName, consoleFn, args) => {
if (!shouldLog(level)) return;
try {
const formattedArgs = formatMessage(module, levelName, args);
addStackTraceIfNeeded(formattedArgs, level, args);
outputLog(module, level, levelName, consoleFn, formattedArgs);
} catch (err) {
if (typeof console !== 'undefined' && console.error) {
console.error('[YouTube+] Logger error:', err);
}
}
};
class ModuleLogger {
constructor(moduleName) {
this.moduleName = moduleName || 'Unknown';
}
debug(...args) {
log(this.moduleName, LogLevel.DEBUG, 'DEBUG', console.log, args);
}
info(...args) {
log(this.moduleName, LogLevel.INFO, 'INFO', console.log, args);
}
warn(...args) {
log(this.moduleName, LogLevel.WARN, 'WARN', console.warn, args);
}
error(...args) {
log(this.moduleName, LogLevel.ERROR, 'ERROR', console.error, args);
}
critical(...args) {
log(this.moduleName, LogLevel.CRITICAL, 'CRITICAL', console.error, args);
}
log(level, ...args) {
const levelMap = {
debug: () => this.debug(...args),
info: () => this.info(...args),
warn: () => this.warn(...args),
error: () => this.error(...args),
critical: () => this.critical(...args),
};
const logFn = levelMap[level.toLowerCase()];
if (logFn) {
logFn();
} else {
this.info(...args);
}
}
}
const createLogger = moduleName => {
return new ModuleLogger(moduleName);
};
const configure = options => {
if (typeof options !== 'object' || options === null) return;
if (typeof options.level === 'number') {
config.level = options.level;
}
if (typeof options.enabled === 'boolean') {
config.enabled = options.enabled;
}
if (typeof options.includeTimestamp === 'boolean') {
config.includeTimestamp = options.includeTimestamp;
}
if (typeof options.includeStack === 'boolean') {
config.includeStack = options.includeStack;
}
if (typeof options.maxStackLines === 'number') {
config.maxStackLines = options.maxStackLines;
}
if (typeof options.customHandler === 'function') {
config.customHandler = options.customHandler;
}
};
const setLevel = level => {
if (typeof level === 'number') {
config.level = level;
} else if (typeof level === 'string') {
const levelMap = {
debug: LogLevel.DEBUG,
info: LogLevel.INFO,
warn: LogLevel.WARN,
error: LogLevel.ERROR,
critical: LogLevel.CRITICAL,
none: LogLevel.NONE,
};
const levelValue = levelMap[level.toLowerCase()];
if (levelValue !== undefined) {
config.level = levelValue;
}
}
};
const setEnabled = enabled => {
config.enabled = !!enabled;
};
if (typeof window !== 'undefined') {
(window).YouTubePlusLogger = {
createLogger,
configure,
setLevel,
setEnabled,
LogLevel,
logger: createLogger('YouTube+'),
};
}
console.log('[YouTube+] Logger system initialized');
})();
(function () {
'use strict';
const PATTERNS = {
URL: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/,
VIDEO_ID: /^[a-zA-Z0-9_-]{11}$/,
EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
STORAGE_KEY: /^[a-zA-Z0-9_.-]{1,100}$/,
SAFE_STRING: /^[a-zA-Z0-9\s._-]{0,1000}$/,
};
const MAX_LENGTHS = {
URL: 2048,
VIDEO_ID: 11,
EMAIL: 254,
STORAGE_KEY: 100,
STORAGE_VALUE: 5242880,
HTML_CONTENT: 1000000,
USER_INPUT: 10000,
TITLE: 500,
DESCRIPTION: 5000,
};
const DANGEROUS_PATTERNS = [
/