NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Nova YouTube // @namespace https://github.com/raingart/Nova-YouTube-extension/ // @version 0.41.0 // @description Gives you more control on YouTube // @author raingart <raingart+scriptaddons@protonmail.com> // @license Apache-2.0 // @icon https://raw.github.com/raingart/Nova-YouTube-extension/master/icons/48.png // @homepageURL https://github.com/raingart/Nova-YouTube-extension // @supportURL https://github.com/raingart/Nova-YouTube-extension/issues // @contributionURL https://www.patreon.com/raingart // @contributionURL https://www.buymeacoffee.com/raingart // @contributionURL https://www.paypal.com/donate/?hosted_button_id=B44WLWHZ8AGU2 // @domain youtube.com // @include http*://www.youtube.com/* // @include http*://m.youtube.com/* // @include http*://*.youtube-nocookie.com/embed/* // @include http*://youtube.googleapis.com/embed/* // @include http*://raingart.github.io/options.html* // @exclude http*://*.youtube.com/*.xml* // @exclude http*://*.youtube.com/error* // @exclude http*://music.youtube.com/* // @exclude http*://accounts.youtube.com/* // @exclude http*://studio.youtube.com/* // @exclude http*://*.youtube.com/redirect?* // @exclude http*://*.youtube.com/embed/?* // @grant GM_getResourceText // @grant GM_getResourceURL // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_openInTab // @run-at document-start // @compatible chrome >=80 Violentmonkey,Tampermonkey // @compatible firefox >=74 Tampermonkey // ==/UserScript== /*jshint esversion: 6 */ if (typeof GM_info === 'undefined') { alert('Direct Chromium is not supported now'); } try { document?.body; } catch (error) { errorAlert('Your browser does not support chaining operator'); } switch (GM_info.scriptHandler) { case 'Tampermonkey': case 'Violentmonkey': case 'ScriptCat': break; case 'FireMonkey': errorAlert(GM_info.scriptHandler + ' incomplete support', true); break; case 'Greasemonkey': errorAlert(GM_info.scriptHandler + ' is not supported'); break; default: if (typeof GM_getValue !== 'function') { errorAlert('Your ' + GM_info.scriptHandler + ' does not support/no access the API being used. Contact the developer') } break; } if (!('MutationObserver' in window)) { errorAlert('MutationObserver not supported'); } function errorAlert(text = '', continue_execute) { alert(GM_info.script.name + ' Error!\n' + text); if (!continue_execute) { throw GM_info.script.name + ' crashed!\n' + text; } } window.nova_plugins = []; window.nova_plugins.push({ id: 'comments-visibility', run_on_pages: 'watch, -mobile', restart_on_location_change: true, _runtime: user_settings => { NOVA.collapseElement({ selector: '#comments', remove: (user_settings.comments_visibility_mode == 'disable') ? true : false, }); }, }); window.nova_plugins.push({ id: 'square-avatars', run_on_pages: 'all, -live_chat', _runtime: user_settings => { NOVA.css.push( [ 'yt-img-shadow', '.ytp-title-channel-logo', '#player .ytp-title-channel', 'ytm-profile-icon', 'a.ytd-thumbnail', ] .join(',\n') + ` { border-radius: 0 !important; }`); NOVA.waitUntil(() => { if (window.yt && (obj = yt?.config_?.EXPERIMENT_FLAGS) && Object.keys(obj).length) { yt.config_.EXPERIMENT_FLAGS.web_rounded_thumbnails = false; return true; } }); }, }); window.nova_plugins.push({ id: 'comments-expand', run_on_pages: 'watch, -mobile', _runtime: user_settings => { NOVA.css.push( `#expander.ytd-comment-renderer { overflow-x: hidden; }`); NOVA.watchElements({ selectors: ['#comment #expander[collapsed] #more:not([hidden])'], attr_mark: 'nova-comment-expanded', callback: btn => { const moreExpand = () => btn.click(); const comment = btn.closest('#expander[collapsed]'); switch (user_settings.comments_expand_mode) { case 'onhover': comment.addEventListener('mouseenter', moreExpand, { capture: true, once: true }); break; case 'always': moreExpand(); break; } }, }); NOVA.watchElements({ selectors: ['#replies #more-replies button'], attr_mark: 'nova-replies-expanded', callback: btn => { const moreExpand = () => btn.click(); switch (user_settings.comments_view_reply) { case 'onhover': btn.addEventListener('mouseenter', moreExpand, { capture: true, once: true }); break; case 'always': moreExpand(); break; } }, }); if (NOVA.queryURL.has('lc')) { NOVA.waitSelector('#comment #linked-comment-badge + #body #expander[collapsed] #more:not([hidden])') .then(btn => btn.click()); NOVA.waitSelector('ytd-comment-thread-renderer:has(#linked-comment-badge) #replies #more-replies button') .then(btn => btn.click()); } }, }); window.nova_plugins.push({ id: 'channel-videos-count', run_on_pages: 'watch, -mobile', restart_on_location_change: true, opt_api_key_warn: true, _runtime: user_settings => { const CACHE_PREFIX = 'nova-channel-videos-count:', SELECTOR_ID = 'nova-video-count'; switch (NOVA.currentPage) { case 'watch': NOVA.waitSelector('#upload-info #owner-sub-count, ytm-slim-owner-renderer .subhead') .then(el => setVideoCount(el)); break; } function setVideoCount(container = required()) { const channelId = NOVA.getChannelId(); if (!channelId) return console.error('setVideoCount channelId: empty', channelId); if (storage = sessionStorage.getItem(CACHE_PREFIX + channelId)) { insertToHTML({ 'text': storage, 'container': container }); } else { NOVA.request.API({ request: 'channels', params: { 'id': channelId, 'part': 'statistics' }, api_key: user_settings['user-api-key'], }) .then(res => { if (res?.error) return alert(`Error [${res.code}]: ${res.reason}\n` + res.error); res?.items?.forEach(item => { if (videoCount = NOVA.prettyRoundInt(item.statistics.videoCount)) { insertToHTML({ 'text': videoCount, 'container': container }); sessionStorage.setItem(CACHE_PREFIX + channelId, videoCount); } else console.warn('API is change', item); }); }); } function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (document.getElementById(SELECTOR_ID) || (function () { container.insertAdjacentHTML('beforeend', `<span class="date style-scope ytd-video-secondary-info-renderer" style="margin-right:5px;"> • <span id="${SELECTOR_ID}">${text}</span> videos</span>`); return document.getElementById(SELECTOR_ID); })()) .textContent = text; container.title = `${text} videos`; } } }, }); window.nova_plugins.push({ id: 'save-to-playlist', run_on_pages: 'home, feed, channel, results, watch, -mobile', _runtime: user_settings => { NOVA.waitSelector('tp-yt-paper-dialog #playlists') .then(playlists => { const container = playlists.closest('tp-yt-paper-dialog'); new IntersectionObserver(([entry]) => { const searchInput = container.querySelector('input[type=search]') if (entry.isIntersecting) { if (user_settings.save_to_playlist_sort) sortPlaylistsMenu(playlists); if (!searchInput) insertFilterInput(playlists); } else if (searchInput) { searchInput.value = ''; searchInput.dispatchEvent(new Event('change')); } }) .observe(container); }); function sortPlaylistsMenu(playlists = required()) { if (!(playlists instanceof HTMLElement)) return console.error('playlists not HTMLElement:', playlists); playlists.append( ...Array.from(playlists.childNodes) .sort(sortByLabel) ); function sortByLabel(a, b) { const getLabel = el => el.textContent.trim(); return stringLocaleCompare(getLabel(a), getLabel(b)); function stringLocaleCompare(a = required(), b = required()) { return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }); } } } function insertFilterInput(container = required()) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); const searchInput = document.createElement('input'); searchInput.setAttribute('type', 'search'); searchInput.setAttribute('placeholder', 'Playlist Filter'); Object.assign(searchInput.style, { padding: '.4em .6em', border: 0, outline: 0, width: '100%', 'margin-bottom': '1.5em', height: '2.5em', color: 'var(--ytd-searchbox-text-color)', 'background-color': 'var(--ytd-searchbox-background)', }); ['change', 'keyup'].forEach(evt => { searchInput .addEventListener(evt, function () { NOVA.searchFilterHTML({ 'keyword': this.value, 'filter_selectors': '#playlists #checkbox', 'highlight_selector': '#label', }); }); searchInput .addEventListener('click', () => { searchInput.value = ''; searchInput.dispatchEvent(new Event('change')); }); }); container.prepend(searchInput); }; }, }); window.nova_plugins.push({ id: 'header-unfixed', run_on_pages: 'all, -embed, -mobile, -live_chat', _runtime: user_settings => { const CLASS_NAME_TOGGLE = 'nova-header-unfixed', SELECTOR = 'html.' + CLASS_NAME_TOGGLE; NOVA.css.push( `${SELECTOR} #masthead-container { position: absolute !important; } ${SELECTOR} #chips-wrapper { position: sticky !important; } ${SELECTOR} #header { margin-top: 0 !important; }`); document.documentElement.classList.add(CLASS_NAME_TOGGLE); if (user_settings.header_unfixed_hotkey) { const hotkey = user_settings.header_unfixed_hotkey || 'v'; document.addEventListener('keyup', evt => { if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return; if (evt.key === hotkey) { document.documentElement.classList.toggle(CLASS_NAME_TOGGLE); } }); } if (user_settings.header_unfixed_scroll) { createArrowButton(); document.addEventListener('yt-action', evt => { if (evt.detail?.actionName == 'yt-store-grafted-ve-action' ) { scrollAfter(); } }); function scrollAfter() { if ((masthead = document.getElementById('masthead')) && (topOffset = masthead.offsetHeight) && NOVA.isInViewport(masthead) ) { window.scrollTo({ top: topOffset }); } } function createArrowButton() { const scrollDownButton = document.createElement('button'); scrollDownButton.textContent = '▼'; scrollDownButton.title = 'Scroll down'; Object.assign(scrollDownButton.style, { cursor: 'pointer', background: 'transparent', color: 'deepskyblue', border: 'none', }); scrollDownButton.onclick = scrollAfter; if (endnode = document.getElementById('end')) { endnode.parentElement.insertBefore(scrollDownButton, endnode); } } } }, }); const NOVA = { waitSelector(selector = required(), container) { if (typeof selector !== 'string') return console.error('wait > selector:', typeof selector); if (container && !(container instanceof HTMLElement)) return console.error('wait > container not HTMLElement:', container); return new Promise(resolve => { if (element = (container || document.body || document).querySelector(selector)) { return resolve(element); } new MutationObserver((mutationRecordsArray, observer) => { for (const record of mutationRecordsArray) { for (const node of record.addedNodes) { if (![1, 3, 8].includes(node.nodeType) || !(node instanceof HTMLElement)) continue; if (node.matches && node.matches(selector)) { observer.disconnect(); return resolve(node); } else if ( (parentEl = node.parentElement || node) && (parentEl instanceof HTMLElement) && (element = parentEl.querySelector(selector)) ) { observer.disconnect(); return resolve(element); } } } if (document?.readyState != 'loading' && (element = (container || document?.body || document).querySelector(selector)) ) { observer.disconnect(); return resolve(element); } }) .observe(container || document.body || document.documentElement || document, { childList: true, subtree: true, attributes: true, }); }); }, waitUntil(condition = required(), timeout = 100) { if (typeof condition !== 'function') return console.error('waitUntil > condition is not fn:', typeof condition); return new Promise((resolve) => { if (result = condition()) { resolve(result); } else { const interval = setInterval(() => { if (result = condition()) { clearInterval(interval); resolve(result); } }, timeout); } }); }, delay(ms = 100) { return new Promise(resolve => setTimeout(resolve, ms)); }, watchElements_list: {}, watchElements({ selectors = required(), attr_mark, callback = required() }) { if (!Array.isArray(selectors) && typeof selectors !== 'string') return console.error('watch > selector:', typeof selectors); if (typeof callback !== 'function') return console.error('watch > callback:', typeof callback); this.waitSelector((typeof selectors === 'string') ? selectors : selectors.join(',')) .then(video => { !Array.isArray(selectors) && (selectors = selectors.split(',').map(s => s.trim())); process(); this.watchElements_list[attr_mark] = setInterval(() => document.visibilityState == 'visible' && process(), 1000 * 1.5); function process() { selectors .forEach(selectorItem => { if (attr_mark) selectorItem += `:not([${attr_mark}])`; document.body.querySelectorAll(selectorItem) .forEach(el => { if (attr_mark) el.setAttribute(attr_mark, true); callback(el); }); }); } }); }, runOnPageInitOrTransition(callback) { if (!callback || typeof callback !== 'function') { return console.error('runOnPageInitOrTransition > callback not function:', ...arguments); } let prevURL = location.href; const isURLChange = () => (prevURL === location.href) ? false : prevURL = location.href; isURLChange() || callback(); document.addEventListener('yt-navigate-finish', () => isURLChange() && callback()); }, css: { push(css = required(), selector, important) { if (typeof css === 'object') { if (!selector) return console.error('injectStyle > empty json-selector:', ...arguments); injectCss(selector + json2css(css)); function json2css(obj) { let css = ''; Object.entries(obj) .forEach(([key, value]) => { css += key + ':' + value + (important ? ' !important' : '') + ';'; }); return `{ ${css} }`; } } else if (css && typeof css === 'string') { if (document.head) { injectCss(css); } else { window.addEventListener('load', () => injectCss(css), { capture: true, once: true }); } } else { console.error('addStyle > css:', typeof css); } function injectCss(source = required()) { let sheet; if (source.endsWith('.css')) { sheet = document.createElement('link'); sheet.rel = 'sheet'; sheet.href = source; } else { const sheetId = 'NOVA-style'; sheet = document.getElementById(sheetId) || (function () { const style = document.createElement('style'); style.type = 'text/css'; style.id = sheetId; return (document.head || document.documentElement).appendChild(style); })(); } sheet.textContent += '\n' + source .replace(/\n+\s{2,}/g, ' ') + '\n'; } }, getValue(selector = required(), prop_name = required()) { return (el = (selector instanceof HTMLElement) ? selector : document.body?.querySelector(selector)) ? getComputedStyle(el).getPropertyValue(prop_name) : null; }, }, prettyRoundInt(num) { num = +num; if (num === 0) return ''; if (num < 1000) return num; const sizes = ['', 'K', 'M', 'B']; const i = Math.floor(Math.log(Math.abs(num)) / Math.log(1000)); if (!sizes[i]) return num; return round(num / 1000 ** i, 1) + sizes[i]; function round(n, precision = 2) { const prec = 10 ** precision; return Math.floor(n * prec) / prec; } }, isInViewport(el = required()) { if (!(el instanceof HTMLElement)) return console.error('el is not HTMLElement type:', el); if (bounding = el.getBoundingClientRect()) { return ( bounding.top >= 0 && bounding.left >= 0 && bounding.bottom <= window.innerHeight && bounding.right <= window.innerWidth ); } }, collapseElement({ selector = required(), title = required(), remove }) { const selector_id = `${title.match(/[a-z]+/gi).join('')}-prevent-load-btn`; this.waitSelector(selector.toString()) .then(el => { if (remove) el.remove(); else { if (document.getElementById(selector_id)) return; el.style.display = 'none'; const btn = document.createElement('a'); btn.textContent = `Load ${title}`; btn.id = selector_id; btn.className = 'more-button style-scope ytd-video-secondary-info-renderer'; Object.assign(btn.style, { cursor: 'pointer', 'text-align': 'center', 'text-transform': 'uppercase', display: 'block', color: 'var(--yt-spec-text-secondary)', }); btn.addEventListener('click', () => { btn.remove(); el.style.display = 'unset'; window.dispatchEvent(new Event('scroll')); }); el.before(btn); } }); }, aspectRatio: { sizeToFit({ srcWidth = 0, srcHeight = 0, maxWidth = window.innerWidth, maxHeight = window.innerHeight }) { const aspectRatio = Math.min(+maxWidth / +srcWidth, +maxHeight / +srcHeight); return { width: +srcWidth * aspectRatio, height: +srcHeight * aspectRatio, }; }, getAspectRatio({ width = required(), height = required() }) { const gcd = (a, b) => b ? gcd(b, a % b) : a, divisor = gcd(width, height); return width / divisor + ':' + height / divisor; }, chooseAspectRatio({ width = required(), height = required(), layout }) { const acceptedRatioList = { 'landscape': { '1:1': 1, '3:2': 1.5, '4:3': 1.33333333333, '5:4': 1.25, '5:3': 1.66666666667, '16:9': 1.77777777778, '16:10': 1.6, '17:9': 1.88888888889, '21:9': 2.33333333333, '24:10': 2.4, }, 'portrait': { '1:1': 1, '2:3': .66666666667, '3:4': .75, '3:5': .6, '4:5': .8, '9:16': .5625, '9:17': .5294117647, '9:21': .4285714286, '10:16': .625, }, }; return choiceRatioFromList(this.getAspectRatio(...arguments)) || acceptedRatioList['landscape']['16:9']; function choiceRatioFromList(ratio = required()) { const layout_ = layout || ((ratio < 1) ? 'portrait' : 'landscape'); return acceptedRatioList[layout_][ratio]; } }, calculateHeight: (width = required(), aspectRatio = (16 / 9)) => parseFloat((width / aspectRatio).toFixed(2)), calculateWidth: (height = required(), aspectRatio = (16 / 9)) => parseFloat((height * aspectRatio).toFixed(2)), }, bezelTrigger(text) { if (!text) return; if (typeof this.fateBezel === 'number') clearTimeout(this.fateBezel); const bezelEl = document.body.querySelector('.ytp-bezel-text'); if (!bezelEl) return console.warn(`bezelTrigger ${text}=>${bezelEl}`); const bezelContainer = bezelEl.parentElement.parentElement, BEZEL_SELECTOR_TOGGLE = '.ytp-text-root'; if (!this.bezel_css_inited) { this.bezel_css_inited = true; this.css.push( `${BEZEL_SELECTOR_TOGGLE} { display: block !important; } ${BEZEL_SELECTOR_TOGGLE} .ytp-bezel-text-wrapper { pointer-events: none; z-index: 40 !important; } ${BEZEL_SELECTOR_TOGGLE} .ytp-bezel-text { display: inline-block !important; } ${BEZEL_SELECTOR_TOGGLE} .ytp-bezel { display: none !important; }`); } bezelEl.textContent = text; bezelContainer.classList.add(BEZEL_SELECTOR_TOGGLE); this.fateBezel = setTimeout(() => { bezelContainer.classList.remove(BEZEL_SELECTOR_TOGGLE); bezelEl.textContent = ''; }, 600); }, getChapterList(video_duration = required()) { if (NOVA.currentPage != 'embed' && (chapsCollect = getFromDescriptionText() || getFromDescriptionChaptersBlock()) && chapsCollect.length ) { return chapsCollect; } else { chapsCollect = getFromAPI(); } return chapsCollect; function getFromDescriptionText() { const selectorTimestampLink = 'a[href*="&t="]'; let timestampsCollect = [], nowComment, prevSec = -1; [ ( document.body.querySelector('ytd-watch-flexy')?.playerData?.videoDetails.shortDescription || document.body.querySelector('ytd-watch-metadata #description.ytd-watch-metadata')?.textContent ), //...[...document.body.querySelectorAll(`#comments #comment #comment-content:has(${selectorTimestampLink})`)] ...[...document.body.querySelectorAll(`#comments #comment #comment-content ${selectorTimestampLink} + *:last-child`)] .map(el => ({ 'source': 'comment', 'text': el.closest('#comment-content')?.textContent, })), ] .forEach(data => { if (timestampsCollect.length > 1) return; nowComment = Boolean(data?.source); (data?.text || data) ?.split('\n') .forEach(line => { line = line?.toString().trim(); if (line.length > 5 && line.length < 200 && (timestamp = /((\d?\d:){1,2}\d{2})/g.exec(line))) { timestamp = timestamp[0]; const sec = NOVA.timeFormatTo.hmsToSec(timestamp), timestampPos = line.indexOf(timestamp); if ( (nowComment ? true : (sec > prevSec && sec < +video_duration)) && (timestampPos < 5 || (timestampPos + timestamp.length) === line.length) ) { if (nowComment) prevSec = sec; timestampsCollect.push({ 'sec': sec, 'time': timestamp, 'title': line .replace(timestamp, '') .trim().replace(/^[:\-–—|]|(\[\])?|[:\-–—.;|]$/g, '') //.trim().replace(/^([:\-–—|]|(\d+[\.)]))|(\[\])?|[:\-–—.;|]$/g, '') .trim() }); } } }); }); if (timestampsCollect.length == 1 && timestampsCollect[0].sec < (video_duration / 4)) { return timestampsCollect; } else if (timestampsCollect.length > 1) { if (nowComment) { timestampsCollect = timestampsCollect.sort((a, b) => a.sec - b.sec); } return timestampsCollect; } } async function getFromDescriptionChaptersBlock() { await NOVA.delay(500); const selectorTimestampLink = 'a[href*="&t="]'; let timestampsCollect = []; document.body.querySelectorAll(`#structured-description ${selectorTimestampLink}`) .forEach(chaperLink => { if (sec = parseInt(NOVA.queryURL.get('t', chaperLink.href))) { timestampsCollect.push({ 'time': NOVA.timeFormatTo.HMS.digit(sec), 'sec': sec, 'title': chaperLink.textContent.trim().split('\n')[0].trim(), }); } }); if (timestampsCollect.length == 1 && timestampsCollect[0].sec < (video_duration / 4)) { return timestampsCollect; } else if (timestampsCollect.length > 1) { return timestampsCollect; } } function getFromAPI() { if (!window.ytPubsubPubsubInstance) { return console.warn('ytPubsubPubsubInstance is null:', ytPubsubPubsubInstance); } const data = Object.values(( ytPubsubPubsubInstance.i || ytPubsubPubsubInstance.j || ytPubsubPubsubInstance.subscriptions_ ) .find(a => a?.player) .player.app ) .find(a => a?.videoData) ?.videoData.multiMarkersPlayerBarRenderer; if (data?.markersMap?.length) { return data.markersMap[0].value.chapters ?.map(c => { const sec = +c.chapterRenderer.timeRangeStartMillis / 1000; return { 'sec': sec, 'time': NOVA.timeFormatTo.HMS.digit(sec), 'title': c.chapterRenderer.title.simpleText || c.chapterRenderer.title.runs[0].text, }; }); } } }, searchFilterHTML({ keyword = required(), filter_selectors = required(), highlight_selector }) { keyword = keyword.toString().toLowerCase(); document.body.querySelectorAll(filter_selectors) .forEach(item => { const text = item.textContent, hasText = text?.toLowerCase().includes(keyword), highlight = el => { if (el.innerHTML.includes('<mark ')) { el.innerHTML = el.innerHTML .replace(/<\/?mark[^>]*>/g, ''); } item.style.display = hasText ? '' : 'none'; if (hasText && keyword) { highlightTerm({ 'target': el, 'keyword': keyword, }); } }; (highlight_selector ? item.querySelectorAll(highlight_selector) : [item]) .forEach(highlight); }); function highlightTerm({ target = required(), keyword = required(), highlightClass }) { const content = target.innerHTML, pattern = new RegExp('(>[^<.]*)?(' + keyword + ')([^<.]*)?', 'gi'), highlightStyle = highlightClass ? `class="${highlightClass}"` : 'style="background-color:#afafaf"', replaceWith = `$1<mark ${highlightStyle}>$2</mark>$3`, marked = content.replaceAll(pattern, replaceWith); return (target.innerHTML = marked) !== content; } }, isMusic() { return checkMusicType(); function checkMusicType() { const channelName = movie_player.getVideoData().author, titleStr = movie_player.getVideoData().title.toUpperCase(), titleWordsList = titleStr?.toUpperCase().match(/\w+/g), playerData = document.body.querySelector('ytd-watch-flexy')?.playerData; return [ titleStr, location.href, channelName, playerData?.microformat?.playerMicroformatRenderer.category, playerData?.title, ] .some(i => i?.toUpperCase().includes('MUSIC')) || document.body.querySelector('#upload-info #channel-name .badge-style-type-verified-artist') || (channelName && /(VEVO|Topic|Records|AMV)$/.test(channelName)) || (channelName && /(MUSIC|ROCK|SOUNDS|SONGS)/.test(channelName.toUpperCase())) || titleWordsList?.length && ['🎵', '♫', 'SONG', 'SOUND', 'SONGS', 'SOUNDTRACK', 'LYRIC', 'LYRICS', 'AMBIENT', 'MIX', 'VEVO', 'CLIP', 'KARAOKE', 'OPENING', 'COVER', 'COVERED', 'VOCAL', 'INSTRUMENTAL', 'ORCHESTRAL', 'DJ', 'DNB', 'BASS', 'BEAT', 'HITS', 'ALBUM', 'PLAYLIST', 'DUBSTEP', 'CHILL', 'RELAX', 'CLASSIC', 'CINEMATIC'] .some(i => titleWordsList.includes(i)) || ['OFFICIAL VIDEO', 'OFFICIAL AUDIO', 'FEAT.', 'FT.', 'LIVE RADIO', 'DANCE VER', 'HIP HOP', 'ROCK N ROLL', 'HOUR VER', 'HOURS VER', 'INTRO THEME'] .some(i => titleStr.includes(i)) || titleWordsList?.length && ['OP', 'ED', 'MV', 'OST', 'NCS', 'BGM', 'EDM', 'GMV', 'AMV', 'MMD', 'MAD'] .some(i => titleWordsList.includes(i)); } }, timeFormatTo: { hmsToSec(str) { let parts = str?.split(':'), t = 0; switch (parts?.length) { case 2: t = (parts[0] * 60); break; case 3: t = (parts[0] * 60 * 60) + (parts[1] * 60); break; case 4: t = (parts[0] * 24 * 60 * 60) + (parts[1] * 60 * 60) + (parts[2] * 60); break; } return t + +parts.pop(); }, HMS: { digit(time_sec = required()) { const ts = Math.abs(+time_sec), d = ~~(ts / 86400), h = ~~((ts % 86400) / 3600), m = ~~((ts % 3600) / 60), s = Math.floor(ts % 60); return (d ? `${d}d ` : '') + (h ? (d ? h.toString().padStart(2, '0') : h) + ':' : '') + (h ? m.toString().padStart(2, '0') : m) + ':' + s.toString().padStart(2, '0'); }, abbr(time_sec = required()) { const ts = Math.abs(+time_sec), d = ~~(ts / 86400), h = ~~((ts % 86400) / 3600), m = ~~((ts % 3600) / 60), s = Math.floor(ts % 60); return (d ? `${d}d ` : '') + (h ? (d ? h.toString().padStart(2, '0') : h) + 'h' : '') + (m ? (h ? m.toString().padStart(2, '0') : m) + 'm' : '') + (s ? (m ? s.toString().padStart(2, '0') : s) + 's' : ''); }, }, ago(date = required()) { if (!(date instanceof Date)) return console.error('"date" is not Date type:', date); const samples = [ { label: 'year', seconds: 31536000 }, { label: 'month', seconds: 2592000 }, { label: 'day', seconds: 86400 }, { label: 'hour', seconds: 3600 }, { label: 'minute', seconds: 60 }, { label: 'second', seconds: 1 } ]; const now = date.getTime(), seconds = Math.floor((Date.now() - Math.abs(now)) / 1000), interval = samples.find(i => i.seconds < seconds), time = Math.floor(seconds / interval.seconds); return `${(now < 0 ? '-' : '') + time} ${interval.label}${time !== 1 ? 's' : ''}`; }, }, updateUrl: (new_url = required()) => window.history.replaceState(null, null, new_url), queryURL: { has: (query = required(), url_string) => new URL(url_string || location).searchParams.has(query.toString()), get: (query = required(), url_string) => new URL(url_string || location).searchParams.get(query.toString()), set(query_obj = {}, url_string) { if (typeof query_obj != 'object' || !Object.keys(query_obj).length) return console.error('query_obj:', query_obj) const url = new URL(url_string || location); Object.entries(query_obj).forEach(([key, value]) => url.searchParams.set(key, value)); return url.toString(); }, remove(query = required(), url_string) { const url = new URL(url_string || location); url.searchParams.delete(query.toString()); return url.toString(); }, }, request: (() => { const API_STORE_NAME = 'YOUTUBE_API_KEYS'; async function getKeys() { NOVA.log('request.API: fetch to youtube_api_keys.json'); return await fetch('https://gist.githubusercontent.com/raingart/ff6711fafbc46e5646d4d251a79d1118/raw/youtube_api_keys.json') .then(res => res.text()) .then(keys => { NOVA.log(`get and save keys in localStorage`, keys); localStorage.setItem(API_STORE_NAME, keys); return JSON.parse(keys); }) .catch(error => { localStorage.removeItem(API_STORE_NAME); throw error; }) .catch(reason => console.error('Error get keys:', reason)); } return { async API({ request = required(), params = required(), api_key }) { const YOUTUBE_API_KEYS = localStorage.hasOwnProperty(API_STORE_NAME) ? JSON.parse(localStorage.getItem(API_STORE_NAME)) : await getKeys(); if (!api_key && (!Array.isArray(YOUTUBE_API_KEYS) || !YOUTUBE_API_KEYS?.length)) { localStorage.hasOwnProperty(API_STORE_NAME) && localStorage.removeItem(API_STORE_NAME); return console.error('YOUTUBE_API_KEYS empty:', YOUTUBE_API_KEYS); } const referRandKey = arr => api_key || 'AIzaSy' + arr[Math.floor(Math.random() * arr.length)]; const query = Object.keys(params) .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) .join('&'); const URL = `https://www.googleapis.com/youtube/v3/${request}?${query}&key=` + referRandKey(YOUTUBE_API_KEYS); return await fetch(URL) .then(response => response.json()) .then(json => { if (!json?.error && Object.keys(json).length) return json; console.warn('used key:', NOVA.queryURL.get('key', URL)); if (json?.error && Object.keys(json.error).length) { throw new Error(JSON.stringify(json?.error)); } }) .catch(error => { localStorage.removeItem(API_STORE_NAME); console.error(`Request API failed:${URL}\n${error}`); if (error?.message && (err = JSON.parse(error?.message))) { return { 'code': err.code, 'reason': err.errors?.length && err.errors[0].reason, 'error': err.message, }; } }); }, }; })(), getPlayerState(state) { return { '-1': 'UNSTARTED', 0: 'ENDED', 1: 'PLAYING', 2: 'PAUSED', 3: 'BUFFERING', 5: 'CUED' }[state || movie_player.getPlayerState()]; }, videoElement: (() => { const videoSelector = '#movie_player:not(.ad-showing) video'; document.addEventListener('canplay', ({ target }) => { target.matches(videoSelector) && (NOVA.videoElement = target); }, { capture: true, once: true }); document.addEventListener('play', ({ target }) => { target.matches(videoSelector) && (NOVA.videoElement = target); }, true); })(), isFullscreen: () => ( movie_player.classList.contains('ytp-fullscreen') || (movie_player.hasOwnProperty('isFullscreen') && movie_player.isFullscreen()) ), getChannelId(api_key) { const isChannelId = id => id && /UC([a-z0-9-_]{22})$/i.test(id); let result = [ document.querySelector('meta[itemprop="channelId"][content]')?.content, (document.body.querySelector('ytd-app')?.__data?.data?.response || document.body.querySelector('ytd-app')?.data?.response || window.ytInitialData ) ?.metadata?.channelMetadataRenderer?.externalId, document.querySelector('link[itemprop="url"][href]')?.href.split('/')[4], location.pathname.split('/')[2], document.body.querySelector('#video-owner a[href]')?.href.split('/')[4], document.body.querySelector('a.ytp-ce-channel-title[href]')?.href.split('/')[4], document.body.querySelector('ytd-watch-flexy')?.playerData?.videoDetails.channelId, ] .find(i => isChannelId(i)); return result; }, storage_obj_manager: { STORAGE_NAME: 'nova-channels-state', async initStorage() { this.channelId = location.search.includes('list=') ? (NOVA.queryURL.get('list') || movie_player?.getPlaylistId()) : await NOVA.waitUntil(NOVA.getChannelId, 1000); }, read(return_all) { if (store = JSON.parse(localStorage.getItem(this.STORAGE_NAME))) { return return_all ? store : store[this.channelId]; } }, write(obj_save) { if ((storage = this.read('all') || {})) { if (Object.keys(obj_save).length) { storage = Object.assign(storage, { [this.channelId]: obj_save }); } else { delete storage[this.channelId]; } } localStorage.setItem(this.STORAGE_NAME, JSON.stringify(storage)); }, _getParam(key = required()) { if (storage = this.read()) { return storage[key]; } }, async getParam(key = required()) { if (!this.channelId) await this.initStorage(); return this._getParam(...arguments); }, save(obj_save) { if (storage = this.read()) { obj_save = Object.assign(storage, obj_save); } this.write(obj_save); }, remove(key) { if ((storage = this.read())) { delete storage[key]; this.write(storage); } }, }, log() { if (this.DEBUG && arguments.length) { console.groupCollapsed(...arguments); console.trace(); console.groupEnd(); } } } window.nova_plugins.push({ id: 'channel-default-tab', run_on_pages: 'channel', restart_on_location_change: true, _runtime: user_settings => { if (NOVA.channelTab) return; location.pathname += '/' + user_settings.channel_default_tab; }, }); window.nova_plugins.push({ id: 'rss-link', run_on_pages: 'channel, playlist, -mobile', restart_on_location_change: true, _runtime: user_settings => { const SELECTOR_ID = 'nova-rss-link', rssLinkPrefix = '/feeds/videos.xml', playlistURL = rssLinkPrefix + '?playlist_id=' + NOVA.queryURL.get('list'), genChannelURL = channelId => rssLinkPrefix + '?channel_id=' + channelId; switch (NOVA.currentPage) { case 'channel': NOVA.waitSelector('#channel-header #links-holder #primary-links') .then(container => { if (!parseInt(NOVA.css.getValue('#header div.banner-visible-area', 'height'))) { container = document.body.querySelector('#channel-header #inner-header-container #buttons'); } if (url = (document.querySelector('link[type="application/rss+xml"][href]')?.href || genChannelURL(NOVA.getChannelId(user_settings['user-api-key']))) ) { insertToHTML({ 'url': url, 'container': container }); } }); break; case 'playlist': NOVA.waitSelector('ytd-playlist-header-renderer .metadata-buttons-wrapper') .then(container => { insertToHTML({ 'url': playlistURL, 'container': container, 'is_playlist': true }); }); break; } function insertToHTML({ url = required(), container = required(), is_playlist }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (container.querySelector(`#${SELECTOR_ID}`) || (function () { const link = document.createElement('a'); link.id = SELECTOR_ID; link.target = '_blank'; link.className = `yt-spec-button-shape-next--overlay`; link.innerHTML = `<svg viewBox="-35 -35 55 55" height="100%" width="100%" style="width: auto;"> <g fill="currentColor"> <path fill="#F60" d="M-17.392 7.875c0 3.025-2.46 5.485-5.486 5.485s-5.486-2.46-5.486-5.485c0-3.026 2.46-5.486 5.486-5.486s5.486 2.461 5.486 5.486zm31.351 5.486C14.042.744 8.208-11.757-1.567-19.736c-7.447-6.217-17.089-9.741-26.797-9.708v9.792C-16.877-19.785-5.556-13.535.344-3.66a32.782 32.782 0 0 1 4.788 17.004h8.827v.017zm-14.96 0C-.952 5.249-4.808-2.73-11.108-7.817c-4.821-3.956-11.021-6.184-17.255-6.15v8.245c6.782-.083 13.432 3.807 16.673 9.774a19.296 19.296 0 0 1 2.411 9.326h8.278v-.017z"/> </g> </svg>`; Object.assign(link.style, { height: '20px', display: 'inline-block', padding: '5px', }); if (is_playlist) { Object.assign(link.style, { 'margin-right': '8px', 'border-radius': '20px', 'background-color': 'var(--yt-spec-static-overlay-button-secondary)', color: 'var(--yt-spec-static-overlay-text-primary)', padding: '8px', 'margin-right': '8px', 'white-space': 'nowrap', 'font-size': 'var(--ytd-tab-system-font-size, 1.4rem)', 'font-weight': 'var(--ytd-tab-system-font-weight, 500)', 'letter-spacing': 'var(--ytd-tab-system-letter-spacing, .007px)', 'text-transform': 'var(--ytd-tab-system-text-transform, uppercase)', }); } container.prepend(link); return link; })()) .href = url; } }, }); window.nova_plugins.push({ id: 'thumbs-hide', run_on_pages: 'home, results, feed, channel, watch, -mobile', _runtime: user_settings => { const thumbsSelectors = [ 'ytd-rich-item-renderer', 'ytd-video-renderer', 'ytd-grid-video-renderer', 'ytd-compact-video-renderer', 'ytm-compact-video-renderer', 'ytm-item-section-renderer' ] .join(','); document.addEventListener('yt-action', evt => { if ([ 'yt-append-continuation-items-action', 'ytd-update-grid-state-action', 'yt-service-request', 'ytd-rich-item-index-update-action', ] .includes(evt.detail?.actionName) ) { switch (NOVA.currentPage) { case 'home': thumbRemove.live(); thumbRemove.mix(); thumbRemove.watched(); break; case 'results': thumbRemove.live(); thumbRemove.shorts(); thumbRemove.mix(); break; case 'feed': thumbRemove.live(); thumbRemove.streamed(); thumbRemove.shorts(); thumbRemove.durationLimits(); thumbRemove.premieres(); thumbRemove.mix(); thumbRemove.watched(); break; case 'channel': thumbRemove.live(); thumbRemove.streamed(); thumbRemove.premieres(); thumbRemove.watched(); break; case 'watch': thumbRemove.live(); thumbRemove.mix(); thumbRemove.watched(); break; } } }); const thumbRemove = { shorts() { if (!user_settings.shorts_disable) return; if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'shorts') return; document.body.querySelectorAll('a#thumbnail[href*="shorts/"]') .forEach(el => el.closest(thumbsSelectors)?.remove()); }, durationLimits() { if (!+user_settings.shorts_disable_min_duration) return; const OVERLAYS_TIME_SELECTOR = '#thumbnail #overlays #text:not(:empty)'; NOVA.waitSelector(OVERLAYS_TIME_SELECTOR) .then(() => { document.body.querySelectorAll(OVERLAYS_TIME_SELECTOR) .forEach(el => { if ((thumb = el.closest(thumbsSelectors)) && (time = NOVA.timeFormatTo.hmsToSec(el.textContent.trim())) && time < (+user_settings.shorts_disable_min_duration || 60) ) { thumb.remove(); } }); }); }, premieres() { if (!user_settings.premieres_disable) return; document.body.querySelectorAll( `#thumbnail #overlays [aria-label="Premiere"], #thumbnail #overlays [aria-label="Upcoming"]` ) .forEach(el => el.closest(thumbsSelectors)?.remove()); document.body.querySelectorAll('#video-badges > [class*="live-now"]') .forEach(el => el.closest(thumbsSelectors)?.remove()); }, live() { if (!user_settings.live_disable) return; if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'streams') return; document.body.querySelectorAll('#thumbnail img[src*="_live.jpg"]') .forEach(el => el.closest(thumbsSelectors)?.remove()); }, streamed() { if (!user_settings.streamed_disable) return; if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'streams') return; document.body.querySelectorAll('#metadata-line > span:last-of-type') .forEach(el => { if (el.textContent?.split(' ').length === 4 && (thumb = el.closest(thumbsSelectors))) { thumb.remove(); } }); }, mix() { if (!user_settings.mix_disable) return; document.body.querySelectorAll( `a[href*="list="][href*="start_radio="]:not([hidden]), #video-title[title^="Mix -"]:not([hidden])` ) .forEach(el => el.closest('ytd-radio-renderer, ytd-compact-radio-renderer, ' + thumbsSelectors)?.remove()); }, watched() { if (!user_settings.watched_disable) return; if (!user_settings['thumbnails-watched']) return; const PERCENT_COMPLETE = user_settings.watched_disable_percent_complete || 90; document.body.querySelectorAll('#thumbnail #overlays #progress') .forEach(el => { if (parseInt(el.style.width) > PERCENT_COMPLETE) { el.closest(thumbsSelectors)?.remove(); } }); }, }; if (user_settings.mix_disable) { NOVA.css.push( `ytd-radio-renderer { display: none !important; }`); } }, }); window.nova_plugins.push({ id: 'shorts-redirect', run_on_pages: 'shorts', restart_on_location_change: true, _runtime: user_settings => { location.href = location.href.replace('shorts/', 'watch?v='); }, }); window.nova_plugins.push({ id: 'pause-background-tab', run_on_pages: 'watch, embed', _runtime: user_settings => { if (location.hostname.includes('youtube-nocookie.com')) location.hostname = 'youtube.com'; if (typeof window === 'undefined') return; const storeName = 'nova-playing-instanceIDTab', instanceID = String(Math.random()), removeStorage = () => localStorage.removeItem(storeName); NOVA.waitSelector('video') .then(video => { video.addEventListener('play', checkInstance); video.addEventListener('playing', checkInstance); ['pause', 'suspend', 'ended'].forEach(evt => video.addEventListener(evt, removeStorage)); window.addEventListener('beforeunload', removeStorage); window.addEventListener('storage', store => { if ((!document.hasFocus() || NOVA.currentPage == 'embed') && store.key === storeName && store.storageArea === localStorage && localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID && 'PLAYING' == NOVA.getPlayerState() ) { video.pause(); } }); if (user_settings.pause_background_tab_autoplay_onfocus) { window.addEventListener('focus', () => { if (!localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID && ['UNSTARTED', 'PAUSED'].includes(NOVA.getPlayerState()) ) { video.play(); } }); } if (user_settings.pause_background_tab_autopause_unfocus) { window.addEventListener('blur', () => { if (!document.hasFocus() && 'PLAYING' == NOVA.getPlayerState()) { video.pause(); } }); } function checkInstance() { if (localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID) { video.pause(); } else { localStorage.setItem(storeName, instanceID); } } }); }, }); window.nova_plugins.push({ id: 'rate-wheel', run_on_pages: 'watch, embed', _runtime: user_settings => { NOVA.waitSelector('#movie_player video') .then(video => { const sliderContainer = insertSlider.apply(video); video.addEventListener('ratechange', function () { NOVA.bezelTrigger(this.playbackRate + 'x'); if (Object.keys(sliderContainer).length) { sliderContainer.slider.value = this.playbackRate; sliderContainer.sliderLabel.textContent = `Speed (${this.playbackRate})`; sliderContainer.sliderCheckbox.checked = (this.playbackRate === 1) ? false : true; } }); setDefaultRate.apply(video); video.addEventListener('loadeddata', setDefaultRate); if (Object.keys(sliderContainer).length) { sliderContainer.slider.addEventListener('input', ({ target }) => playerRate.set(target.value)); sliderContainer.slider.addEventListener('change', ({ target }) => playerRate.set(target.value)); sliderContainer.slider.addEventListener('wheel', evt => { evt.preventDefault(); const rate = playerRate.adjust(+user_settings.rate_step * Math.sign(evt.wheelDelta)); }); sliderContainer.sliderCheckbox.addEventListener('change', ({ target }) => { target.checked || playerRate.set(1) }); } NOVA.runOnPageInitOrTransition(async () => { if (NOVA.currentPage == 'watch') { if (user_settings['save-channel-state']) { if (userRate = await NOVA.storage_obj_manager.getParam('speed')) { video.addEventListener('canplay', () => playerRate.set(userRate), { capture: true, once: true }); } } expandAvailableRatesMenu(); } }); }); if (user_settings.rate_hotkey) { NOVA.waitSelector('.html5-video-container') .then(container => { container.addEventListener('wheel', evt => { evt.preventDefault(); if (evt[user_settings.rate_hotkey] || (user_settings.rate_hotkey == 'none' && !evt.ctrlKey && !evt.altKey && !evt.shiftKey && !evt.metaKey)) { const rate = playerRate.adjust(+user_settings.rate_step * Math.sign(evt.wheelDelta)); } }); }); } if (+user_settings.rate_default !== 1 && user_settings.rate_default_apply_music) { NOVA.waitSelector('#upload-info #channel-name .badge-style-type-verified-artist') .then(icon => playerRate.set(1)); NOVA.waitSelector('#upload-info #channel-name a[href]') .then(channelName => { if (/(VEVO|Topic|Records|AMV)$/.test(channelName.textContent) || channelName.textContent.toUpperCase().includes('MUSIC') ) { playerRate.set(1); } }); } const playerRate = { testDefault: rate => (+rate % .25) === 0 && +rate <= 2 && +user_settings.rate_default <= 2 && (typeof movie_player !== 'undefined' && movie_player.hasOwnProperty('getPlaybackRate')), async set(level = 1) { this.log('set', ...arguments); if (this.testDefault(level)) { this.log('set:default'); movie_player.setPlaybackRate(+level) && this.saveInSession(level); } else { this.log('set:html5'); if (NOVA.videoElement) { NOVA.videoElement.playbackRate = +level; this.clearInSession(); } } }, adjust(rate_step = required()) { this.log('adjust', ...arguments); return this.testDefault(rate_step) ? this.default(+rate_step) : this.html5(+rate_step); }, default(playback_rate = required()) { this.log('default', ...arguments); const playbackRate = movie_player.getPlaybackRate(); const inRange = step => { const setRateStep = playbackRate + step; return (.1 <= setRateStep && setRateStep <= 2) && +setRateStep.toFixed(2); }; const newRate = inRange(+playback_rate); if (newRate && newRate != playbackRate) { movie_player.setPlaybackRate(newRate); if (newRate === movie_player.getPlaybackRate()) { this.saveInSession(newRate); } else { console.error('playerRate:default different: %s!=%s', newRate, movie_player.getPlaybackRate()); } } this.log('default return', newRate); return newRate === movie_player.getPlaybackRate() && newRate; }, html5(playback_rate = required()) { this.log('html5', ...arguments); if (!NOVA.videoElement) return console.error('playerRate > videoElement empty:', NOVA.videoElement); const playbackRate = NOVA.videoElement.playbackRate; const inRange = step => { const setRateStep = playbackRate + step; return (.1 <= setRateStep && setRateStep <= 3) && +setRateStep.toFixed(2); }; const newRate = inRange(+playback_rate); if (newRate && newRate != playbackRate) { NOVA.videoElement.playbackRate = newRate; if (newRate === NOVA.videoElement.playbackRate) { this.clearInSession(); } else { console.error('playerRate:html5 different: %s!=%s', newRate, NOVA.videoElement.playbackRate); } } this.log('html5 return', newRate); return newRate === NOVA.videoElement.playbackRate && newRate; }, saveInSession(level = required()) { try { sessionStorage['yt-player-playback-rate'] = JSON.stringify({ creation: Date.now(), data: level.toString(), }) this.log('playbackRate save in session:', ...arguments); } catch (err) { console.warn(`${err.name}: save "rate" in sessionStorage failed. It seems that "Block third-party cookies" is enabled`, err.message); } }, clearInSession() { const keyName = 'yt-player-playback-rate'; try { sessionStorage.hasOwnProperty(keyName) && sessionStorage.removeItem(keyName); this.log('playbackRate save in session:', ...arguments); } catch (err) { console.warn(`${err.name}: save "rate" in sessionStorage failed. It seems that "Block third-party cookies" is enabled`, err.message); } }, log() { if (this.DEBUG && arguments.length) { console.groupCollapsed(...arguments); console.trace(); console.groupEnd(); } }, }; function setDefaultRate() { if (+user_settings.rate_default !== 1) { const is_music = NOVA.isMusic(); if (this.playbackRate !== +user_settings.rate_default && (!user_settings.rate_default_apply_music || !is_music) && (!isNaN(this.duration) && this.duration > 25) ) { playerRate.set(user_settings.rate_default); } else if (this.playbackRate !== 1 && is_music) { playerRate.set(1); } } } function insertSlider() { const SELECTOR_ID = 'nova-rate-slider-menu', SELECTOR = '#' + SELECTOR_ID; NOVA.css.push( `${SELECTOR} [type="range"] { vertical-align: text-bottom; margin: '0 5px', } ${SELECTOR} [type="checkbox"] { appearance: none; outline: none; cursor: pointer; } ${SELECTOR} [type="checkbox"]:checked { background: #f00; } ${SELECTOR} [type="checkbox"]:checked:after { left: 20px; background-color: #fff; }`); const slider = document.createElement('input'); slider.className = 'ytp-menuitem-slider'; slider.type = 'range'; slider.min = +user_settings.rate_step; slider.max = Math.max(2, +user_settings.rate_default); slider.step = +user_settings.rate_step; slider.value = this.playbackRate; const sliderIcon = document.createElement('div'); sliderIcon.className = 'ytp-menuitem-icon'; const sliderLabel = document.createElement('div'); sliderLabel.className = 'ytp-menuitem-label'; sliderLabel.textContent = `Speed (${this.playbackRate})`; const sliderCheckbox = document.createElement('input'); sliderCheckbox.className = 'ytp-menuitem-toggle-checkbox'; sliderCheckbox.type = 'checkbox'; sliderCheckbox.title = 'Remember speed'; const out = {}; const right = document.createElement('div'); right.className = 'ytp-menuitem-content'; out.sliderCheckbox = right.appendChild(sliderCheckbox); out.slider = right.appendChild(slider); const speedMenu = document.createElement('div'); speedMenu.className = 'ytp-menuitem'; speedMenu.id = SELECTOR_ID; speedMenu.append(sliderIcon); out.sliderLabel = speedMenu.appendChild(sliderLabel); speedMenu.append(right); document.body.querySelector('.ytp-panel-menu') ?.append(speedMenu); return out; } function expandAvailableRatesMenu() { if (typeof _yt_player !== 'object') { return console.error('expandAvailableRatesMenu > _yt_player is empty', _yt_player); } if (path = findPathInObj({ 'obj': _yt_player, 'keys': 'getAvailablePlaybackRates' })) { setAvailableRates(_yt_player, 0, path.split('.')); } function findPathInObj({ obj = required(), keys = required(), path }) { const setPath = d => (path ? path + '.' : '') + d; for (const prop in obj) { if (obj.hasOwnProperty(prop) && obj[prop]) { if (keys === prop) { return this.path = setPath(prop) } if (obj[prop].constructor.name == 'Function' && Object.keys(obj[prop]).length) { for (const j in obj[prop]) { if (typeof obj[prop] !== 'undefined') { findPathInObj({ 'obj': obj[prop][j], 'keys': keys, 'path': setPath(prop) + '.' + j, }); } if (this.path) return this.path; } } } } } function setAvailableRates(path, idx, arr) { if (arr.length - 1 == idx) { path[arr[idx]] = () => [.25, .5, .75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4, 10]; } else { setAvailableRates(path[arr[idx]], idx + 1, arr); } } } }, }); window.nova_plugins.push({ id: 'volume-wheel', run_on_pages: 'watch, embed, -mobile', _runtime: user_settings => { NOVA.waitSelector('video') .then(video => { video.addEventListener('volumechange', function () { NOVA.bezelTrigger(movie_player.getVolume() + '%'); playerVolume.buildVolumeSlider(); if (user_settings.volume_mute_unsave) { playerVolume.saveInSession(movie_player.getVolume()); } }); if (user_settings.volume_hotkey) { document.body.querySelector('.html5-video-container') .addEventListener('wheel', evt => { evt.preventDefault(); if (evt[user_settings.volume_hotkey] || (user_settings.volume_hotkey == 'none' && !evt.ctrlKey && !evt.altKey && !evt.shiftKey && !evt.metaKey)) { if (step = +user_settings.volume_step * Math.sign(evt.wheelDelta)) { playerVolume.adjust(step); } } }); } if (+user_settings.volume_level_default) { playerVolume.set(+user_settings.volume_level_default); } if (user_settings['save-channel-state']) { NOVA.runOnPageInitOrTransition(async () => { if (NOVA.currentPage == 'watch' && (userVolume = await NOVA.storage_obj_manager.getParam('volume'))) { video.addEventListener('canplay', () => playerVolume.set(userVolume), { capture: true, once: true }); } }); } }); const playerVolume = { adjust(delta) { const level = movie_player?.getVolume() + +delta; return user_settings.volume_unlimit ? this.unlimit(level) : this.set(level); }, set(level = 50) { if (typeof movie_player === 'undefined' || !movie_player.hasOwnProperty('getVolume')) return console.error('Error getVolume'); const newLevel = Math.max(0, Math.min(100, +level)); if (newLevel !== movie_player.getVolume()) { movie_player.isMuted() && movie_player.unMute(); movie_player.setVolume(newLevel); if (newLevel === movie_player.getVolume()) { this.saveInSession(newLevel); } else { console.error('setVolumeLevel error! Different: %s!=%s', newLevel, movie_player.getVolume()); } } return newLevel === movie_player.getVolume() && newLevel; }, saveInSession(level = required()) { const storageData = { creation: Date.now(), data: { 'volume': +level, 'muted': (level ? 'false' : 'true') }, }; try { localStorage['yt-player-volume'] = JSON.stringify( Object.assign({ expiration: Date.now() + 2592e6 }, storageData) ); sessionStorage['yt-player-volume'] = JSON.stringify(storageData); } catch (err) { console.warn(`${err.name}: save "volume" in sessionStorage failed. It seems that "Block third-party cookies" is enabled`, err.message); } }, unlimit(level = 300) { if (level > 100) { if (!this.audioCtx) { this.audioCtx = new AudioContext(); const source = this.audioCtx.createMediaElementSource(NOVA.videoElement); this.node = this.audioCtx.createGain(); this.node.gain.value = 1; source.connect(this.node); this.node.connect(this.audioCtx.destination); } if (this.node.gain.value < 7) this.node.gain.value += 1; NOVA.bezelTrigger(movie_player.getVolume() * this.node.gain.value + '%'); } else { if (this.audioCtx && this.node.gain.value !== 1) { this.node.gain.value = 1; } this.set(level); } }, buildVolumeSlider(timeout_ms = 800) { if (volumeArea = movie_player?.querySelector('.ytp-volume-area')) { if (typeof this.showTimeout === 'number') clearTimeout(this.showTimeout); volumeArea.dispatchEvent(new Event('mouseover', { bubbles: true })); this.showTimeout = setTimeout(() => volumeArea.dispatchEvent(new Event('mouseout', { bubbles: true })) , timeout_ms); insertToHTML({ 'text': Math.round(movie_player.getVolume()), 'container': volumeArea, }); } function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); const SELECTOR_ID = 'nova-volume-text'; (document.getElementById(SELECTOR_ID) || (function () { const SELECTOR = '#' + SELECTOR_ID; NOVA.css.push(` ${SELECTOR} { display: none; text-indent: 2px; font-size: 110%; text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); cursor: default; } ${SELECTOR}:after { content: '%'; } .ytp-volume-control-hover:not([aria-valuenow="0"])+${SELECTOR} { display: block; }`) const el = document.createElement('span'); el.id = SELECTOR_ID; container.insertAdjacentElement('beforeend', el); return el; })()) .textContent = text; container.title = `${text} %`; } } }; }, }); window.nova_plugins.push({ id: 'video-autopause', run_on_pages: 'watch, embed', restart_on_location_change: true, _runtime: user_settings => { if (user_settings['video-stop-preload'] && !user_settings.stop_preload_embed) return; if (user_settings.video_autopause_embed && NOVA.currentPage != 'embed') return; if (NOVA.currentPage == 'embed' && window.self !== window.top && ['0', 'false'].includes(NOVA.queryURL.get('autoplay')) ) { return; } NOVA.waitSelector('#movie_player video') .then(video => { if (user_settings.video_autopause_ignore_live && movie_player.getVideoData().isLive) return; forceVideoPause.apply(video); }); function forceVideoPause() { if (user_settings.video_autopause_ignore_playlist && location.search.includes('list=')) return; this.pause(); const forceHoldPause = setInterval(() => this.paused || this.pause(), 200); document.addEventListener('click', stopForceHoldPause); document.addEventListener('keyup', keyupSpace); function stopForceHoldPause() { if (movie_player.contains(document.activeElement)) { clearInterval(forceHoldPause); document.removeEventListener('keyup', keyupSpace); document.removeEventListener('click', stopForceHoldPause); } } function keyupSpace(evt) { switch (evt.code) { case 'Space': stopForceHoldPause() break; } } } }, }); window.nova_plugins.push({ id: 'player-pin-scroll', run_on_pages: 'watch, -mobile', _runtime: user_settings => { if (!('IntersectionObserver' in window)) return alert('Nova\n\nPin player Error!\nIntersectionObserver not supported.'); const CLASS_VALUE = 'nova-player-pin', PINNED_SELECTOR = '.' + CLASS_VALUE, UNPIN_BTN_CLASS_VALUE = CLASS_VALUE + '-unpin-btn', UNPIN_BTN_SELECTOR = '.' + UNPIN_BTN_CLASS_VALUE; document.addEventListener('scroll', () => { NOVA.waitSelector('#ytd-player') .then(container => { new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { movie_player.classList.remove(CLASS_VALUE); drag.reset(); } else if (!movie_player.isFullscreen() && document.documentElement.scrollTop ) { movie_player.classList.add(CLASS_VALUE); drag?.storePos?.X && drag.setTranslate(drag.storePos); } window.dispatchEvent(new Event('resize')); }, { threshold: .5, }) .observe(container); }); }, { capture: true, once: true }); NOVA.waitSelector(PINNED_SELECTOR) .then(async player => { drag.init(player); await NOVA.waitUntil( () => (NOVA.videoElement?.videoWidth && !isNaN(NOVA.videoElement.videoWidth) && NOVA.videoElement?.videoHeight && !isNaN(NOVA.videoElement.videoHeight) ) , 500) initMiniStyles(); insertUnpinButton(player); document.addEventListener('fullscreenchange', () => NOVA.isFullscreen() && movie_player.classList.remove(CLASS_VALUE)); NOVA.waitSelector('#movie_player video') .then(video => { video.addEventListener('loadeddata', () => { if (NOVA.currentPage != 'watch') return; NOVA.waitSelector(PINNED_SELECTOR) .then(() => { const width = NOVA.aspectRatio.calculateWidth( movie_player.clientHeight, NOVA.aspectRatio.chooseAspectRatio({ 'width': NOVA.videoElement.videoWidth, 'height': NOVA.videoElement.videoHeight, 'layout': 'landscape', }), ); player.style.setProperty('--width', `${width}px !important;`); }); }); }); if (user_settings.player_float_scroll_after_fullscreen_restore_srcoll_pos) { let scrollPos = 0; document.addEventListener('fullscreenchange', () => { if (!NOVA.isFullscreen() && scrollPos && drag.storePos ) { window.scrollTo({ top: scrollPos, }); } }); document.addEventListener('yt-action', function (evt) { if (evt.detail?.actionName == 'yt-close-all-popups-action') { scrollPos = document.documentElement.scrollTop; } }); document.addEventListener('yt-navigate-start', () => scrollPos = 0); } }); function initMiniStyles() { const scrollbarWidth = (window.innerWidth - document.documentElement.clientWidth || 0) + 'px'; const miniSize = NOVA.aspectRatio.sizeToFit({ 'srcWidth': NOVA.videoElement.videoWidth, 'srcHeight': NOVA.videoElement.videoHeight, 'maxWidth': (window.innerWidth / user_settings.player_float_scroll_size_ratio), 'maxHeight': (window.innerHeight / user_settings.player_float_scroll_size_ratio), }); let initcss = { width: NOVA.aspectRatio.calculateWidth( miniSize.height, NOVA.aspectRatio.chooseAspectRatio({ 'width': miniSize.width, 'height': miniSize.height }) ) + 'px', height: miniSize.height + 'px', position: 'fixed', 'z-index': 'var(--zIndex)', 'box-shadow': '0 16px 24px 2px rgba(0, 0, 0, 0.14),' + '0 6px 30px 5px rgba(0, 0, 0, 0.12),' + '0 8px 10px -5px rgba(0, 0, 0, 0.4)', }; switch (user_settings.player_float_scroll_position) { case 'top-left': initcss.top = user_settings['header-unfixed'] ? 0 : (document.getElementById('masthead-container')?.offsetHeight || 0) + 'px'; initcss.left = 0; break; case 'top-right': initcss.top = user_settings['header-unfixed'] ? 0 : (document.getElementById('masthead-container')?.offsetHeight || 0) + 'px'; initcss.right = scrollbarWidth; break; case 'bottom-left': initcss.bottom = 0; initcss.left = 0; break; case 'bottom-right': initcss.bottom = 0; initcss.right = scrollbarWidth; break; } NOVA.css.push(initcss, PINNED_SELECTOR, 'important'); NOVA.css.push( PINNED_SELECTOR + `{ --height: ${initcss.height} !important; --width: ${initcss.width} !important; width: var(--width) !important; height: var(--height) !important; background-color: var(--yt-spec-base-background); } ${PINNED_SELECTOR} video { object-fit: contain !important; } ${PINNED_SELECTOR} .ytp-chrome-controls .nova-right-custom-button, ${PINNED_SELECTOR} .ytp-chrome-controls #nova-player-time-remaining, ${PINNED_SELECTOR} .ytp-chrome-controls button.ytp-size-button, ${PINNED_SELECTOR} .ytp-chrome-controls button.ytp-subtitles-button, ${PINNED_SELECTOR} .ytp-chrome-controls button.ytp-settings-button, ${PINNED_SELECTOR} .ytp-chrome-controls .ytp-chapter-container { display: none !important; }`); NOVA.css.push(` ${PINNED_SELECTOR} .ytp-preview, ${PINNED_SELECTOR} .ytp-scrubber-container, ${PINNED_SELECTOR} .ytp-hover-progress, ${PINNED_SELECTOR} .ytp-gradient-bottom { display:none !important; } ${PINNED_SELECTOR} .ytp-chrome-bottom { width: 96% !important; } ${PINNED_SELECTOR} .ytp-chapters-container { display: flex; }`); NOVA.css.push( `${PINNED_SELECTOR} video { width: var(--width) !important; height: var(--height) !important; left: 0 !important; top: 0 !important; } .ended-mode video { visibility: hidden; }`); } function insertUnpinButton(player = movie_player) { NOVA.css.push( PINNED_SELECTOR + ` { --zIndex: ${1 + Math.max( NOVA.css.getValue('#chat', 'z-index'), NOVA.css.getValue('.ytp-chrome-top .ytp-cards-button', 'z-index'), NOVA.css.getValue('#chat', 'z-index'), 601)}; } ${UNPIN_BTN_SELECTOR} { display: none; } ${PINNED_SELECTOR} ${UNPIN_BTN_SELECTOR} { display: initial !important; position: absolute; cursor: pointer; top: 10px; left: 10px; width: 28px; height: 28px; color: white; border: none; outline: none; opacity: .1; z-index: var(--zIndex); font-size: 24px; font-weight: bold; background-color: rgba(0, 0, 0, 0.8); } ${PINNED_SELECTOR}:hover ${UNPIN_BTN_SELECTOR} { opacity: .7; } ${UNPIN_BTN_SELECTOR}:hover { opacity: 1 !important; }`); const btnUnpin = document.createElement('button'); btnUnpin.className = UNPIN_BTN_CLASS_VALUE; btnUnpin.title = 'Unpin player'; btnUnpin.textContent = '×'; btnUnpin.addEventListener('click', () => { player.classList.remove(CLASS_VALUE); drag.reset('clear storePos'); window.dispatchEvent(new Event('resize')); }); player.append(btnUnpin); document.addEventListener('yt-navigate-start', () => { if (player.classList.contains(CLASS_VALUE)) { player.classList.remove(CLASS_VALUE); drag.reset(); } }); } const drag = { attrNametoLock: 'force_fix_preventDefault', reset(clear_storePos) { this.dragTarget?.style.removeProperty('transform'); if (clear_storePos) this.storePos = this.xOffset = this.yOffset = 0; else this.storePos = { 'X': this.xOffset, 'Y': this.yOffset }; }, init(el_target = required()) { this.log('drag init', ...arguments); if (!(el_target instanceof HTMLElement)) return console.error('el_target not HTMLElement:', el_target); this.dragTarget = el_target; document.addEventListener('mousedown', evt => { if (!el_target.classList.contains(CLASS_VALUE)) return; this.dragStart.apply(this, [evt]); }); document.addEventListener('mouseup', evt => { if (this.active) this.dragTarget.removeAttribute(this.attrNametoLock); this.dragEnd.apply(this, [evt]); }); document.addEventListener('mousemove', evt => { if (this.active && !this.dragTarget.hasAttribute(this.attrNametoLock)) { this.dragTarget.setAttribute(this.attrNametoLock, true); } this.draging.apply(this, [evt]); }); NOVA.css.push( `[${this.attrNametoLock}]:active { pointer-events: none; }`); }, dragStart(evt) { if (!this.dragTarget.contains(evt.target)) return; this.log('dragStart'); switch (evt.type) { case 'touchstart': this.initialX = evt.touches[0].clientX - (this.xOffset || 0); this.initialY = evt.touches[0].clientY - (this.yOffset || 0); break; case 'mousedown': this.initialX = evt.clientX - (this.xOffset || 0); this.initialY = evt.clientY - (this.yOffset || 0); break; } this.active = true; document.body.style.cursor = 'move'; }, dragEnd(evt) { if (!this.active) return; this.log('dragEnd'); this.initialX = this.currentX; this.initialY = this.currentY; this.active = false; document.body.style.cursor = 'default'; }, draging(evt) { if (!this.active) return; evt.preventDefault(); evt.stopImmediatePropagation(); this.log('draging'); switch (evt.type) { case 'touchmove': this.currentX = evt.touches[0].clientX - this.initialX; this.currentY = evt.touches[0].clientY - this.initialY; break; case 'mousemove': const rect = this.dragTarget.getBoundingClientRect(); if (rect.left >= document.body.clientWidth - this.dragTarget.offsetWidth) { this.currentX = Math.min( evt.clientX - this.initialX, document.body.clientWidth - this.dragTarget.offsetWidth - this.dragTarget.offsetLeft ); } else { this.currentX = Math.max(evt.clientX - this.initialX, 0 - this.dragTarget.offsetLeft); } if (rect.top >= window.innerHeight - this.dragTarget.offsetHeight) { this.currentY = Math.min( evt.clientY - this.initialY, window.innerHeight - this.dragTarget.offsetHeight - this.dragTarget.offsetTop ); } else { this.currentY = Math.max(evt.clientY - this.initialY, 0 - this.dragTarget.offsetTop); } break; } this.xOffset = this.currentX; this.yOffset = this.currentY; this.setTranslate({ 'X': this.currentX, 'Y': this.currentY }); }, setTranslate({ X = required(), Y = required() }) { this.log('setTranslate', ...arguments); this.dragTarget.style.transform = `translate3d(${X}px, ${Y}px, 0)`; }, log() { if (this.DEBUG && arguments.length) { console.groupCollapsed(...arguments); console.trace(); console.groupEnd(); } }, }; }, }); window.nova_plugins.push({ id: 'video-quality', run_on_pages: 'watch, embed', _runtime: user_settings => { const qualityFormatListWidth = { highres: 4320, hd2880: 2880, hd2160: 2160, hd1440: 1440, hd1080: 1080, hd720: 720, large: 480, medium: 360, small: 240, tiny: 144, }; let selectedQuality = user_settings.video_quality; NOVA.waitSelector('#movie_player') .then(movie_player => { if (user_settings.video_quality_manual_save_in_tab && NOVA.currentPage == 'watch' ) { movie_player.addEventListener('onPlaybackQualityChange', quality => { if (document.activeElement.getAttribute('role') == 'menuitemradio' && quality !== selectedQuality ) { console.info(`keep quality "${quality}" in the session`); selectedQuality = quality; user_settings.video_quality_in_music_playlist = false; } }); } if (user_settings['save-channel-state']) { NOVA.runOnPageInitOrTransition(async () => { if (NOVA.currentPage == 'watch' && (userQuality = await NOVA.storage_obj_manager.getParam('quality'))) { selectedQuality = userQuality; } }); } setQuality(); movie_player.addEventListener('onStateChange', setQuality); }); function setQuality(state) { if (!selectedQuality) return console.error('selectedQuality unavailable', selectedQuality); if (user_settings.video_quality_in_music_playlist && location.search.includes('list=') && NOVA.isMusic() ) { selectedQuality = user_settings.video_quality_in_music_quality; } if (['PLAYING', 'BUFFERING'].includes(NOVA.getPlayerState(state)) && !setQuality.quality_busy) { setQuality.quality_busy = true; const waitQuality = setInterval(() => { let availableQualityLevels = movie_player.getAvailableQualityLevels(); const maxWidth = (NOVA.currentPage == 'watch') ? window.screen.width : window.innerWidth; const maxQualityIdx = availableQualityLevels .findIndex(i => qualityFormatListWidth[i] <= (maxWidth * 1.3)); availableQualityLevels = availableQualityLevels.slice(maxQualityIdx); if (availableQualityLevels?.length) { clearInterval(waitQuality); const maxAvailableQuality = Math.max(availableQualityLevels.indexOf(selectedQuality), 0); const newQuality = availableQualityLevels[maxAvailableQuality]; if (movie_player.hasOwnProperty('setPlaybackQuality')) { movie_player.setPlaybackQuality(newQuality); } if (movie_player.hasOwnProperty('setPlaybackQualityRange')) { movie_player.setPlaybackQualityRange(newQuality, newQuality); } } }, 50); } else if (state <= 0) { setQuality.quality_busy = false; } } NOVA.waitSelector('.ytp-error [class*="reason"]') .then(error_reason_el => { if (alertText = error_reason_el.textContent) { throw alertText; } }); }, }); window.nova_plugins.push({ id: 'player-resume-playback', run_on_pages: 'watch, embed', _runtime: user_settings => { if (!navigator.cookieEnabled && NOVA.currentPage == 'embed') return; const CACHE_PREFIX = 'nova-resume-playback-time', getCacheName = () => CACHE_PREFIX + ':' + (NOVA.queryURL.get('v') || movie_player.getVideoData().video_id); let cacheName; NOVA.waitSelector('video') .then(video => { cacheName = getCacheName(); resumePlayback.apply(video); video.addEventListener('loadeddata', resumePlayback.bind(video)); video.addEventListener('timeupdate', savePlayback.bind(video)); video.addEventListener('ended', () => sessionStorage.removeItem(cacheName)); if (user_settings.player_resume_playback_url_mark && NOVA.currentPage != 'embed') { if (NOVA.queryURL.has('t')) { document.addEventListener('yt-navigate-finish', connectSaveStateInURL.bind(video), { capture: true, once: true }); } else { connectSaveStateInURL.apply(video); } } }); function savePlayback() { if (this.currentTime > 5 && this.duration > 30 && !movie_player.classList.contains('ad-showing')) { sessionStorage.setItem(cacheName, ~~this.currentTime); } } async function resumePlayback() { if (NOVA.queryURL.has('t') || (user_settings['save-channel-state'] && await NOVA.storage_obj_manager.getParam('ignore-playback')) ) { return; } cacheName = getCacheName(); if ((time = +sessionStorage.getItem(cacheName)) && (time < (this.duration - 1)) ) { this.currentTime = time; } } function connectSaveStateInURL() { let delaySaveOnPauseURL; this.addEventListener('pause', () => { if (this.currentTime < (this.duration - 1) && this.currentTime > 5 && this.duration > 10) { delaySaveOnPauseURL = setTimeout(() => { NOVA.updateUrl(NOVA.queryURL.set({ 't': ~~this.currentTime + 's' })); }, 100); } }); this.addEventListener('play', () => { if (typeof delaySaveOnPauseURL === 'number') clearTimeout(delaySaveOnPauseURL); if (NOVA.queryURL.has('t')) NOVA.updateUrl(NOVA.queryURL.remove('t')); }); } }, }); window.nova_plugins.push({ id: 'video-stop-preload', run_on_pages: 'watch, embed', _runtime: user_settings => { if (user_settings.stop_preload_embed && NOVA.currentPage != 'embed') return; if (location.hostname == 'youtube.googleapis.com') return; if (NOVA.currentPage == 'embed' && window.self !== window.top && ['0', 'false'].includes(NOVA.queryURL.get('autoplay')) ) { return; } NOVA.waitSelector('#movie_player') .then(async movie_player => { let disableStop; document.addEventListener('yt-navigate-start', () => disableStop = false); await NOVA.waitUntil(() => typeof movie_player === 'object' && typeof movie_player.stopVideo === 'function'); movie_player.stopVideo(); movie_player.addEventListener('onStateChange', onPlayerStateChange.bind(this)); function onPlayerStateChange(state) { if (user_settings.stop_preload_ignore_playlist && location.search.includes('list=')) return; if (user_settings.stop_preload_ignore_live && movie_player.getVideoData().isLive) return; if (!disableStop && state > 0 && state < 5) { movie_player.stopVideo(); } } document.addEventListener('click', disableHoldStop); document.addEventListener('keyup', ({ code }) => (code == 'Space') && disableHoldStop()); function disableHoldStop() { if (!disableStop && movie_player.contains(document.activeElement)) { disableStop = true; movie_player.playVideo(); } } }); }, }); window.nova_plugins.push({ id: 'subtitle-style', run_on_pages: 'watch, embed, -mobile', _runtime: user_settings => { const SELECTOR = '.ytp-caption-segment'; let css = {} if (user_settings.subtitle_transparent) { css = { 'background': 'Transparent', 'text-shadow': `rgb(0, 0, 0) 0 0 .1em, rgb(0, 0, 0) 0 0 .2em, rgb(0, 0, 0) 0 0 .4em`, }; } if (user_settings.subtitle_bold) css['font-weight'] = 'bold'; if (user_settings.subtitle_fixed) { NOVA.css.push( `.caption-window { margin-bottom: 1px !important; bottom: 1% !important; }`); } if (user_settings.subtitle_selectable) { NOVA.watchElements({ selectors: [ SELECTOR, '#caption-window-1', ] .map(i => i + ':not(:empty)'), callback: el => { el.addEventListener('mousedown', evt => evt.stopPropagation(), true); el.setAttribute('draggable', 'false'); el.setAttribute('selectable', 'true'); el.style.userSelect = 'text'; el.style.WebkitUserSelect = 'text'; el.style.cursor = 'text'; } }); } if (Object.keys(css).length) { NOVA.css.push(css, SELECTOR, 'important'); } }, }); window.nova_plugins.push({ id: 'video-unblock-region', run_on_pages: 'watch, -mobile', _runtime: user_settings => { NOVA.waitSelector('ytd-watch-flexy[player-unavailable]') .then(el => el.querySelector('yt-player-error-message-renderer #button.yt-player-error-message-renderer button') || redirect()); function redirect(new_tab_url) { if (new_tab_url) { window.open(`${location.protocol}//${user_settings.video_unblock_region_domain || 'hooktube.com'}${location.port ? ':' + location.port : ''}/watch?v=` + movie_player.getVideoData().video_id); } else { location.hostname = user_settings.video_unblock_region_domain || 'hooktube.com'; } if (user_settings.video_unblock_region_open_map) { window.open(`https://watannetwork.com/tools/blocked/#url=${NOVA.queryURL.get('v')}:~:text=Allowed%20countries`); } } }, }); window.nova_plugins.push({ id: 'player-quick-buttons', run_on_pages: 'watch, embed, -mobile', _runtime: user_settings => { const SELECTOR_BTN_CLASS_NAME = 'nova-right-custom-button', SELECTOR_BTN = '.' + SELECTOR_BTN_CLASS_NAME; NOVA.waitSelector('.ytp-right-controls') .then(async container => { NOVA.videoElement = await NOVA.waitSelector('video'); NOVA.css.push( `${SELECTOR_BTN} { user-select: none; } ${SELECTOR_BTN}:hover { color: #66afe9 !important; } ${SELECTOR_BTN}:active { color: #2196f3 !important; }`); NOVA.css.push( `${SELECTOR_BTN}[tooltip]:hover::before { content: attr(tooltip); position: absolute; top: -3em; transform: translateX(-30%); line-height: normal; background-color: rgba(28,28,28,.9); border-radius: 2px; padding: 5px 9px; color: #fff; font-weight: bold; white-space: nowrap; } html[data-cast-api-enabled] ${SELECTOR_BTN}[tooltip]:hover::before { font-weight: normal; }`); if (user_settings.player_buttons_custom_items?.includes('picture-in-picture')) { const pipBtn = document.createElement('button'); pipBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; pipBtn.setAttribute('tooltip', 'Open in PictureInPicture'); pipBtn.innerHTML = createSVG(); pipBtn.addEventListener('click', () => document.pictureInPictureElement ? document.exitPictureInPicture() : NOVA.videoElement.requestPictureInPicture() ); container.prepend(pipBtn); NOVA.videoElement?.addEventListener('enterpictureinpicture', () => pipBtn.innerHTML = createSVG(2)); NOVA.videoElement?.addEventListener('leavepictureinpicture', () => pipBtn.innerHTML = createSVG()); function createSVG(alt) { const svg = document.createElement('svg'); svg.setAttribute('width', '100%'); svg.setAttribute('height', '100%'); svg.setAttribute('viewBox', '-8 -6 36 36'); const path = document.createElement('path'); path.setAttribute('fill', 'currentColor'); path.setAttribute('d', alt ? 'M18.5,11H18v1h.5A1.5,1.5,0,0,1,20,13.5v5A1.5,1.5,0,0,1,18.5,20h-8A1.5,1.5,0,0,1,9,18.5V18H8v.5A2.5,2.5,0,0,0,10.5,21h8A2.5,2.5,0,0,0,21,18.5v-5A2.5,2.5,0,0,0,18.5,11Z M14.5,4H2.5A2.5,2.5,0,0,0,0,6.5v8A2.5,2.5,0,0,0,2.5,17h12A2.5,2.5,0,0,0,17,14.5v-8A2.5,2.5,0,0,0,14.5,4Z' : 'M2.5,17A1.5,1.5,0,0,1,1,15.5v-9A1.5,1.5,0,0,1,2.5,5h13A1.5,1.5,0,0,1,17,6.5V10h1V6.5A2.5,2.5,0,0,0,15.5,4H2.5A2.5,2.5,0,0,0,0,6.5v9A2.5,2.5,0,0,0,2.5,18H7V17Z M18.5,11h-8A2.5,2.5,0,0,0,8,13.5v5A2.5,2.5,0,0,0,10.5,21h8A2.5,2.5,0,0,0,21,18.5v-5A2.5,2.5,0,0,0,18.5,11Z'); svg.append(path); return svg.outerHTML; } } if (user_settings.player_buttons_custom_items?.indexOf('popup') !== -1 && !NOVA.queryURL.has('popup')) { const popupBtn = document.createElement('button'); popupBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; popupBtn.setAttribute('tooltip', 'Open in popup'); popupBtn.innerHTML = `<svg viewBox="-8 -8 36 36" height="100%" width="100%"> <g fill="currentColor"> <path d="M18 2H6v4H2v12h12v-4h4V2z M12 16H4V8h2v6h6V16z M16 12h-2h-2H8V8V6V4h8V12z" /> </g> </svg>`; popupBtn.addEventListener('click', () => { const width = screen.width / (+user_settings.player_buttons_custom_popup_width || 4), aspectRatio = NOVA.aspectRatio.getAspectRatio({ 'width': NOVA.videoElement.videoWidth, 'height': NOVA.videoElement.videoHeight }), height = Math.round(width / aspectRatio); url = new URL( document.querySelector('link[itemprop="embedUrl"][href]')?.href || (location.origin + '/embed/' + movie_player.getVideoData().video_id) ); if (currentTime = ~~NOVA.videoElement?.currentTime) url.searchParams.set('start', currentTime); url.searchParams.set('autoplay', 1); url.searchParams.set('popup', true); openPopup({ 'url': url.href, 'title': document.title, 'width': width, 'height': height }); }); container.prepend(popupBtn); function openPopup({ url, title, width, height }) { const left = (screen.width / 2) - (width / 2); const top = (screen.height / 2) - (height / 2); const newWindow = window.open(url, '_blank', `popup=1,toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=${width},height=${height},top=${top},left=${left}`); return; } } if (user_settings.player_buttons_custom_items?.includes('screenshot')) { const SELECTOR_SCREENSHOT_ID = 'nova-screenshot-result', SELECTOR_SCREENSHOT = '#' + SELECTOR_SCREENSHOT_ID; NOVA.css.push( SELECTOR_SCREENSHOT + ` { --width: 400px; --height: 400px; position: fixed; top: 0; right: 0; overflow: hidden; margin: 36px 30px; box-shadow: 0 0 15px #000; max-width: var(--width); max-height: var(--height); } ${SELECTOR_SCREENSHOT} canvas { max-width: var(--width); max-height: var(--height); } ${SELECTOR_SCREENSHOT} .close-btn { position: absolute; bottom: 0; right: 0; background-color: rgba(0, 0, 0, .5); color: #FFF; cursor: pointer; font-size: 12px; display: grid; height: 100%; width: 25%; } ${SELECTOR_SCREENSHOT} .close-btn:hover { background-color: rgba(0, 0, 0, .65); } ${SELECTOR_SCREENSHOT} .close-btn > * { margin: auto; }`); const screenshotBtn = document.createElement('button'); screenshotBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; screenshotBtn.setAttribute('tooltip', 'Take screenshot'); screenshotBtn.innerHTML = `<svg viewBox="0 -166 512 860" height="100%" width="100%"> <g fill="currentColor"> <circle cx="255.811" cy="285.309" r="75.217" /> <path d="M477,137H352.718L349,108c0-16.568-13.432-30-30-30H191c-16.568,0-30,13.432-30,30l-3.718,29H34 c-11.046,0-20,8.454-20,19.5v258c0,11.046,8.954,20.5,20,20.5h443c11.046,0,20-9.454,20-20.5v-258C497,145.454,488.046,137,477,137 z M255.595,408.562c-67.928,0-122.994-55.066-122.994-122.993c0-67.928,55.066-122.994,122.994-122.994 c67.928,0,122.994,55.066,122.994,122.994C378.589,353.495,323.523,408.562,255.595,408.562z M474,190H369v-31h105V190z" /> </g> </svg>`; screenshotBtn.addEventListener('click', () => { const container = document.getElementById(SELECTOR_SCREENSHOT_ID) || document.createElement('a'), canvas = container.querySelector('canvas') || document.createElement('canvas'); canvas.width = NOVA.videoElement.videoWidth; canvas.height = NOVA.videoElement.videoHeight canvas.getContext('2d').drawImage(NOVA.videoElement, 0, 0, canvas.width, canvas.height); canvas.title = 'Click to save'; try { canvas.toBlob(blob => container.href = URL.createObjectURL(blob)); } catch (error) { } if (!container.id) { container.id = SELECTOR_SCREENSHOT_ID; container.target = '_blank'; if (headerContainer = document.getElementById('masthead-container')) { container.style.marginTop = (headerContainer?.offsetHeight || 0) + 'px'; container.style.zIndex = +getComputedStyle(headerContainer)['z-index'] + 1; } canvas.addEventListener('click', evt => { evt.preventDefault(); downloadCanvasAsImage(evt.target); container.remove(); }); container.append(canvas); const close = document.createElement('a'); close.className = 'close-btn' close.innerHTML = '<span>CLOSE</span>'; close.title = 'Close'; close.addEventListener('click', evt => { evt.preventDefault(); container.remove(); }); container.append(close); document.body.append(container); } }); function downloadCanvasAsImage(canvas) { const downloadLink = document.createElement('a'), downloadFileName = [ movie_player.getVideoData().title .replace(/[\\/:*?"<>|]+/g, '') .replace(/\s+/g, ' ').trim(), `[${NOVA.timeFormatTo.HMS.abbr(NOVA.videoElement.currentTime)}]`, ] .join(' '); downloadLink.href = canvas.toBlob(blob => URL.createObjectURL(blob)); downloadLink.download = downloadFileName + '.png'; downloadLink.click(); } container.prepend(screenshotBtn); } if (user_settings.player_buttons_custom_items?.includes('thumbnail')) { const thumbBtn = document.createElement('button'); thumbBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; thumbBtn.setAttribute('tooltip', 'View Thumbnail'); thumbBtn.innerHTML = `<svg viewBox="0 -10 21 40" height="100%" width="100%"> <g fill="currentColor"> <circle cx='8' cy='7.2' r='2'/> <path d='M0 2v16h20V2H0z M18 16H2V4h16V16z'/> <polygon points='17 10.9 14 7.9 9 12.9 6 9.9 3 12.9 3 15 17 15' /> </g> </svg>`; thumbBtn.addEventListener('click', async () => { const videoId = movie_player.getVideoData().video_id || NOVA.queryURL.get('v'), thumbsSizesTemplate = [ 'maxres', 'sd', 'hq', 'mq', '' ]; document.body.style.cursor = 'wait'; for (const resPrefix of thumbsSizesTemplate) { const imgUrl = `https://i.ytimg.com/vi/${videoId}/${resPrefix}default.jpg`, response = await fetch(imgUrl); if (response.status === 200) { document.body.style.cursor = 'default'; window.open(imgUrl); break; } } }); container.prepend(thumbBtn); } if (user_settings.player_buttons_custom_items?.includes('rotate')) { const rotateBtn = document.createElement('button'); rotateBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; rotateBtn.setAttribute('tooltip', 'Rotate video'); Object.assign(rotateBtn.style, { padding: '0 1.1em', }); rotateBtn.innerHTML = `<svg viewBox="0 0 1536 1536" height="100%" width="100%"> <g fill="currentColor"> <path d="M1536 128v448q0 26-19 45t-45 19h-448q-42 0-59-40-17-39 14-69l138-138Q969 256 768 256q-104 0-198.5 40.5T406 406 296.5 569.5 256 768t40.5 198.5T406 1130t163.5 109.5T768 1280q119 0 225-52t179-147q7-10 23-12 14 0 25 9l137 138q9 8 9.5 20.5t-7.5 22.5q-109 132-264 204.5T768 1536q-156 0-298-61t-245-164-164-245T0 768t61-298 164-245T470 61 768 0q147 0 284.5 55.5T1297 212l130-129q29-31 70-14 39 17 39 59z"/> </path> </g> </svg>`; rotateBtn.addEventListener('click', () => { let angle = parseInt(NOVA.videoElement.style.transform.replace(/\D+/, '')) || 0; const scale = (angle === 0 || angle === 180) ? movie_player.clientHeight / NOVA.videoElement.clientWidth : 1; angle += 90; NOVA.videoElement.style.transform = (angle === 360) ? '' : `rotate(${angle}deg) scale(${scale})`; }); container.prepend(rotateBtn); } if (user_settings.player_buttons_custom_items?.includes('aspect-ratio')) { const aspectRatioBtn = document.createElement('a'), aspectRatioList = [ { '16:9': 1.335 }, { '4:3': .75 }, { '9:16': 1.777777778 }, { 'auto': 1 }, ], genTooltip = (key = 0) => `Switch aspect ratio to ` + Object.keys(aspectRatioList[key]); aspectRatioBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; aspectRatioBtn.style.textAlign = 'center'; aspectRatioBtn.style.fontWeight = 'bold'; aspectRatioBtn.setAttribute('tooltip', genTooltip()); aspectRatioBtn.innerHTML = '1:1'; aspectRatioBtn.addEventListener('click', () => { if (!NOVA.videoElement) return; const getNextIdx = () => (this.listIdx < aspectRatioList.length - 1) ? this.listIdx + 1 : 0; this.listIdx = getNextIdx(); NOVA.videoElement.style.transform = `scaleX(${Object.values(aspectRatioList[this.listIdx])})`; aspectRatioBtn.setAttribute('tooltip', genTooltip(getNextIdx())); aspectRatioBtn.textContent = Object.keys(aspectRatioList[this.listIdx]); }); container.prepend(aspectRatioBtn); } if (user_settings.player_buttons_custom_items?.includes('watch-later')) { NOVA.waitSelector('.ytp-watch-later-button') .then(watchLaterDefault => { NOVA.css.push( `.${SELECTOR_BTN_CLASS_NAME} .ytp-spinner-container { position: relative; top: 0; left: 0; scale: .5; margin: 0; } .${SELECTOR_BTN_CLASS_NAME}.watch-later-btn svg { scale: .85; }`); const watchLaterBtn = document.createElement('button'); watchLaterBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME} watch-later-btn`; watchLaterBtn.setAttribute('tooltip', 'Watch later'); renderIcon(); watchLaterBtn.addEventListener('click', () => { watchLaterDefault.click(); renderIcon(); const waitStatus = setInterval(() => { if (watchLaterDefault.querySelector('svg')) { clearInterval(waitStatus); renderIcon(); } }, 100); }); [...document.getElementsByClassName(SELECTOR_BTN_CLASS_NAME)].pop() ?.after(watchLaterBtn); function renderIcon() { watchLaterBtn.innerHTML = watchLaterDefault.querySelector('.ytp-watch-later-icon')?.innerHTML; } }); } if (user_settings.player_buttons_custom_items?.includes('card-switch') && !user_settings.player_hide_elements?.includes('videowall_endscreen') && !user_settings.player_hide_elements?.includes('card_endscreen') ) { const cardAttrName = 'nova-hide-endscreen', cardBtn = document.createElement('button'); NOVA.css.push( `#movie_player[${cardAttrName}] .videowall-endscreen, #movie_player[${cardAttrName}] .ytp-pause-overlay, #movie_player[${cardAttrName}] [class^="ytp-ce-"] { display: none !important; }`); cardBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; cardBtn.innerHTML = createSVG(); if (user_settings.player_buttons_custom_card_switch) { switchState(movie_player.toggleAttribute(cardAttrName)); } cardBtn.addEventListener('click', () => switchState(movie_player.toggleAttribute(cardAttrName))); function switchState(state = required()) { cardBtn.innerHTML = createSVG(state) cardBtn.setAttribute('tooltip', `The cards are currently ${state ? 'hidden' : 'showing'}`); } function createSVG(alt) { const svg = document.createElement('svg'); svg.setAttribute('width', '100%'); svg.setAttribute('height', '100%'); svg.setAttribute('viewBox', '-200 0 912 512'); const g = document.createElement('g'); g.setAttribute('fill', 'currentColor'); g.innerHTML = alt ? '<path d="M 409 57.104 C 407.625 57.641, 390.907 73.653, 371.848 92.687 L 337.196 127.293 323.848 120.738 C 301.086 109.561, 283.832 103.994, 265.679 101.969 C 217.447 96.591, 148.112 134.037, 59.026 213.577 C 40.229 230.361, 4.759 265.510, 2.089 270 C -0.440 274.252, -0.674 281.777, 1.575 286.516 C 4.724 293.153, 67.054 352.112, 89.003 369.217 L 92.490 371.934 63.330 401.217 C 37.873 426.781, 34.079 430.988, 33.456 434.346 C 31.901 442.720, 38.176 452.474, 46.775 455.051 C 56.308 457.907, 41.359 471.974, 244.317 269.173 C 350.152 163.421, 429.960 82.914, 431.067 80.790 C 436.940 69.517, 428.155 55.840, 415.185 56.063 C 413.158 56.098, 410.375 56.566, 409 57.104 M 245.500 137.101 C 229.456 139.393, 201.143 151.606, 177.500 166.433 C 151.339 182.839, 120.778 206.171, 89.574 233.561 C 72.301 248.723, 42 277.649, 42 278.977 C 42 280.637, 88.281 323.114, 108.367 339.890 L 117.215 347.279 139.209 325.285 L 161.203 303.292 159.601 293.970 C 157.611 282.383, 157.570 272.724, 159.465 261.881 C 165.856 225.304, 193.011 195.349, 229.712 184.389 C 241.299 180.929, 261.648 179.996, 272.998 182.405 L 280.496 183.996 295.840 168.652 L 311.183 153.309 303.342 149.583 C 292.100 144.242, 277.007 139.186, 267.205 137.476 C 257.962 135.865, 254.565 135.806, 245.500 137.101 M 377.500 163.164 C 374.231 164.968, 369.928 169.297, 368.295 172.423 C 366.203 176.431, 366.351 184.093, 368.593 187.889 C 369.597 189.587, 375.944 195.270, 382.699 200.516 C 406.787 219.226, 444.129 252.203, 462.500 270.989 L 470.500 279.170 459 290.204 C 374.767 371.030, 302.827 418.200, 259.963 420.709 C 239.260 421.921, 213.738 412.918, 179.575 392.352 C 167.857 385.298, 166.164 384.571, 161.448 384.571 C 154.702 384.571, 149.091 388.115, 146.121 394.250 C 143.531 399.600, 143.472 403.260, 145.890 408.500 C 148.270 413.656, 150.468 415.571, 162 422.535 C 198.520 444.590, 230.555 455.992, 256 455.992 C 305.062 455.992, 376.663 414.097, 462 335.458 C 483.584 315.567, 509.652 289.051, 510.931 285.685 C 512.694 281.042, 512.218 273.876, 509.889 270 C 507.494 266.017, 484.252 242.741, 463.509 223.552 C 437.964 199.922, 398.967 167.566, 391.300 163.639 C 387.656 161.773, 380.470 161.526, 377.500 163.164 M 235.651 219.459 C 231.884 220.788, 226.369 223.351, 223.395 225.153 C 216.405 229.389, 206.759 239.019, 202.502 246.010 C 198.959 251.828, 193.677 266.197, 194.194 268.611 C 194.372 269.437, 205.637 258.890, 220.993 243.519 C 249.683 214.801, 249.910 214.427, 235.651 219.459 M 316.962 223.250 C 313.710 224.890, 311.876 226.720, 310.200 230 C 307.188 235.893, 307.781 240.006, 313.805 255 C 317.867 265.109, 318.470 267.589, 318.790 275.500 C 319.554 294.378, 313.786 309.236, 300.522 322.557 C 287.282 335.854, 274.164 341.408, 256 341.408 C 244.216 341.408, 238.392 340.027, 226.837 334.489 C 214.541 328.596, 204.996 330.563, 200.250 339.966 C 191.301 357.697, 210.339 372.220, 247.484 375.998 C 301.141 381.456, 350.063 339.760, 353.664 285.500 C 354.618 271.136, 351.039 249.928, 345.577 237.579 C 342.933 231.601, 337.061 224.600, 332.875 222.435 C 328.782 220.319, 322.095 220.661, 316.962 223.250" fill-rule="evenodd" />' : `<path d="M 377.5 163.164 C 374.231 164.968 375.944 195.27 382.699 200.516 C 406.787 219.226 444.129 252.203 462.5 270.989 L 470.5 279.17 L 459 290.204 C 374.767 371.03 302.827 418.2 259.963 420.709 C 239.26 421.921 213.738 412.918 179.575 392.352 C 167.857 385.298 166.164 384.571 161.448 384.571 C 154.702 384.571 149.091 388.115 146.121 394.25 C 143.531 399.6 143.472 403.26 145.89 408.5 C 148.27 413.656 150.468 415.571 162 422.535 C 198.52 444.59 230.555 455.992 256 455.992 C 305.062 455.992 376.663 414.097 462 335.458 C 483.584 315.567 509.652 289.051 510.931 285.685 C 512.694 281.042 512.218 273.876 509.889 270 C 507.494 266.017 484.252 242.741 463.509 223.552 C 437.964 199.922 398.967 167.566 391.3 163.639 C 387.656 161.773 380.47 161.526 377.5 163.164 M 316.962 223.25 C 313.71 224.89 311.876 226.72 310.2 230 C 307.188 235.893 307.781 240.006 313.805 255 C 317.867 265.109 318.47 267.589 318.79 275.5 C 319.554 294.378 313.786 309.236 300.522 322.557 C 287.282 335.854 274.164 341.408 256 341.408 C 244.216 341.408 238.392 340.027 226.837 334.489 C 214.541 328.596 204.996 330.563 200.25 339.966 C 191.301 357.697 210.339 372.22 247.484 375.998 C 301.141 381.456 350.063 339.76 353.664 285.5 C 354.618 271.136 351.039 249.928 345.577 237.579 C 342.933 231.601 337.061 224.6 332.875 222.435 C 328.782 220.319 322.095 220.661 316.962 223.25"></path> <path d="M 377.487 163.483 C 374.218 165.287 369.915 169.616 368.282 172.742 C 366.19 176.75 366.338 184.412 368.58 188.208 C 369.584 189.906 375.931 195.589 382.686 200.835 C 406.774 219.545 444.116 252.522 462.487 271.308 L 470.487 279.489 L 458.987 290.523 C 374.754 371.349 302.814 418.519 259.95 421.028 C 239.247 422.24 213.725 413.237 179.562 392.671 C 167.844 385.617 166.151 384.89 161.435 384.89 C 154.689 384.89 149.078 388.434 146.108 394.569 C 143.518 399.919 143.459 403.579 145.877 408.819 C 148.257 413.975 150.455 415.89 161.987 422.854 C 198.507 444.909 230.542 456.311 255.987 456.311 C 305.049 456.311 376.65 414.416 461.987 335.777 C 483.571 315.886 509.639 289.37 510.918 286.004 C 512.681 281.361 512.205 274.195 509.876 270.319 C 507.481 266.336 484.239 243.06 463.496 223.871 C 437.951 200.241 398.954 167.885 391.287 163.958 C 387.643 162.092 380.457 161.845 377.487 163.483 M 316.949 223.569 C 313.697 225.209 311.863 227.039 310.187 230.319 C 307.175 236.212 307.768 240.325 313.792 255.319 C 317.854 265.428 318.457 267.908 318.777 275.819 C 319.541 294.697 313.773 309.555 300.509 322.876 C 287.269 336.173 274.151 341.727 255.987 341.727 C 244.203 341.727 238.379 340.346 226.824 334.808 C 214.528 328.915 204.983 330.882 200.237 340.285 C 191.288 358.016 210.326 372.539 247.471 376.317 C 301.128 381.775 350.05 340.079 353.651 285.819 C 354.605 271.455 351.026 250.247 345.564 237.898 C 342.92 231.92 337.048 224.919 332.862 222.754 C 328.769 220.638 322.082 220.98 316.949 223.569" transform="matrix(-1, 0, 0, -1, 512.000305, 558.092285)"></path>`; svg.append(g); return svg.outerHTML; } container.prepend(cardBtn); } if (user_settings.player_buttons_custom_items?.includes('quick-quality')) { const SELECTOR_QUALITY_CLASS_NAME = 'nova-quick-quality', SELECTOR_QUALITY = '.' + SELECTOR_QUALITY_CLASS_NAME, qualityContainerBtn = document.createElement('a'), SELECTOR_QUALITY_LIST_ID = SELECTOR_QUALITY_CLASS_NAME + '-list', SELECTOR_QUALITY_LIST = '#' + SELECTOR_QUALITY_LIST_ID, listQuality = document.createElement('ul'), SELECTOR_QUALITY_TITLE_ID = SELECTOR_QUALITY_CLASS_NAME + '-title', qualityBtn = document.createElement('span'), qualityFormatList = { highres: { label: '4320p', badge: '8K' }, hd2880: { label: '2880p', badge: '5K' }, hd2160: { label: '2160p', badge: '4K' }, hd1440: { label: '1440p', badge: 'QHD' }, hd1080: { label: '1080p', badge: 'FHD' }, hd720: { label: '720p', badge: 'ᴴᴰ' }, large: { label: '480p' }, medium: { label: '360p' }, small: { label: '240p' }, tiny: { label: '144p' }, auto: { label: 'auto' }, }; NOVA.css.push( SELECTOR_QUALITY + ` { overflow: visible !important; position: relative; text-align: center !important; vertical-align: top; font-weight: bold; } ${SELECTOR_QUALITY_LIST} { position: absolute; bottom: 2.5em !important; left: -2.2em; list-style: none; padding-bottom: 1.5em !important; z-index: ${1 + Math.max(NOVA.css.getValue('.ytp-progress-bar', 'z-index'), 31)}; } html[data-cast-api-enabled] ${SELECTOR_QUALITY_LIST} { margin: 0; padding: 0; bottom: 3.3em; } ${SELECTOR_QUALITY}:not(:hover) ${SELECTOR_QUALITY_LIST} { display: none; } ${SELECTOR_QUALITY_LIST} li { cursor: pointer; white-space: nowrap; line-height: 1.4; background: rgba(28, 28, 28, 0.9); margin: .3em 0; padding: .5em 3em; border-radius: .3em; color: #fff; } ${SELECTOR_QUALITY_LIST} li .quality-menu-item-label-badge { position: absolute; right: 1em; width: 1.7em; } ${SELECTOR_QUALITY_LIST} li.active { background: #720000; } ${SELECTOR_QUALITY_LIST} li.disable { color: #666; } ${SELECTOR_QUALITY_LIST} li:hover:not(.active) { background: #c00; }`); qualityContainerBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME} ${SELECTOR_QUALITY_CLASS_NAME}`; qualityBtn.id = SELECTOR_QUALITY_TITLE_ID; qualityBtn.textContent = qualityFormatList[movie_player.getPlaybackQuality()]?.label || '[out of range]'; listQuality.id = SELECTOR_QUALITY_LIST_ID; movie_player.addEventListener('onPlaybackQualityChange', quality => { document.getElementById(SELECTOR_QUALITY_TITLE_ID) .textContent = qualityFormatList[quality]?.label || '[out of range]'; }); qualityContainerBtn.prepend(qualityBtn); qualityContainerBtn.append(listQuality); container.prepend(qualityContainerBtn); fillQualityMenu(); NOVA.videoElement?.addEventListener('canplay', fillQualityMenu); function fillQualityMenu() { if (qualityList = document.getElementById(SELECTOR_QUALITY_LIST_ID)) { qualityList.innerHTML = ''; movie_player.getAvailableQualityLevels() .forEach(quality => { const qualityItem = document.createElement('li'); if (qualityData = qualityFormatList[quality]) { qualityItem.textContent = qualityData.label; if (badge = qualityData.badge) { qualityItem.insertAdjacentHTML('beforeend', `<span class="quality-menu-item-label-badge">${badge}</span>`); } if (movie_player.getPlaybackQuality() == quality) { qualityItem.className = 'active'; } else { const maxWidth = (NOVA.currentPage == 'watch') ? window.screen.width : window.innerWidth; if (+(qualityData.label.replace(/[^0-9]/g, '') || 0) <= (maxWidth * 1.3)) { qualityItem.addEventListener('click', () => { movie_player.setPlaybackQualityRange(quality, quality); if (quality == 'auto') return; qualityList.innerHTML = ''; }); } else { qualityItem.className = 'disable'; qualityItem.title = 'Max (window viewport + 30%)'; } } qualityList.append(qualityItem); } }); } } } if (user_settings.player_buttons_custom_items?.includes('clock')) { const clockEl = document.createElement('span'); clockEl.className = 'ytp-time-display'; clockEl.title = 'Now time'; container.prepend(clockEl); setInterval(() => { if (document.visibilityState == 'hidden' || movie_player.classList.contains('ytp-autohide') ) { return; } const time = new Date().toTimeString().slice(0, 8); clockEl.textContent = time; }, 1000); } if (user_settings.player_buttons_custom_items?.includes('toggle-speed')) { const speedBtn = document.createElement('a'), hotkey = user_settings.player_buttons_custom_hotkey_toggle_speed || 'a', defaultRateText = '1x', genTooltip = () => `Switch to ${NOVA.videoElement.playbackRate}>${speedBtn.textContent} (${hotkey})`; let rateOrig = {}; speedBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`; speedBtn.style.textAlign = 'center'; speedBtn.style.fontWeight = 'bold'; speedBtn.innerHTML = defaultRateText; speedBtn.setAttribute('tooltip', genTooltip()); document.addEventListener('keyup', evt => { if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return; if (evt.key === hotkey) { switchRate(); } }); speedBtn.addEventListener('click', switchRate); function switchRate() { if (Object.keys(rateOrig).length) { playerRate.set(rateOrig); rateOrig = {}; speedBtn.innerHTML = defaultRateText; } else { rateOrig = (movie_player && NOVA.videoElement.playbackRate % .25) === 0 ? { 'default': movie_player.getPlaybackRate() } : { 'html5': NOVA.videoElement.playbackRate }; let resetRate = Object.assign({}, rateOrig); resetRate[Object.keys(resetRate)[0]] = 1; playerRate.set(resetRate); speedBtn.textContent = rateOrig[Object.keys(rateOrig)[0]] + 'x'; } speedBtn.setAttribute('tooltip', genTooltip()); } const playerRate = { set(obj) { if (obj.hasOwnProperty('html5') || !movie_player) { NOVA.videoElement.playbackRate = obj.html5; } else { movie_player.setPlaybackRate(obj.default); } }, }; container.prepend(speedBtn); visibilitySwitch(); NOVA.videoElement?.addEventListener('ratechange', visibilitySwitch); NOVA.videoElement?.addEventListener('loadeddata', () => { rateOrig = {}; speedBtn.textContent = defaultRateText; visibilitySwitch(); }); function visibilitySwitch() { if (!Object.keys(rateOrig).length) { speedBtn.style.display = (NOVA.videoElement?.playbackRate === 1) ? 'none' : ''; } } } }); }, }); window.nova_plugins.push({ id: 'player-float-progress-bar', run_on_pages: 'watch, embed, -mobile', _runtime: user_settings => { if (NOVA.currentPage == 'embed' && window.self.location.href.includes('live_stream') ) return; if (NOVA.currentPage == 'embed' && ['0', 'false'].includes(NOVA.queryURL.get('controls'))) return; const SELECTOR_ID = 'nova-player-float-progress-bar', SELECTOR = '#' + SELECTOR_ID, CHAPTERS_MARK_WIDTH_PX = '2px'; NOVA.waitSelector('#movie_player.ytp-autohide video') .then(video => { const container = insertFloatBar(Math.max( NOVA.css.getValue('.ytp-chrome-bottom', 'z-index'), 59 ) + 1), bufferEl = document.getElementById(`${SELECTOR_ID}-buffer`), progressEl = document.getElementById(`${SELECTOR_ID}-progress`); renderChapters.init(video); video.addEventListener('loadeddata', resetBar); video.addEventListener('timeupdate', function () { if (notInteractiveToRender()) return; if (!isNaN(this.duration)) { progressEl.style.transform = `scaleX(${this.currentTime / this.duration})`; } }); video.addEventListener('progress', renderBuffer.bind(video)); video.addEventListener('seeking', renderBuffer.bind(video)); function renderBuffer() { if (notInteractiveToRender()) return; if ((totalDuration = movie_player.getDuration()) && !isNaN(totalDuration)) { bufferEl.style.transform = `scaleX(${movie_player.getVideoLoadedFraction()})`; } } function resetBar() { container.style.display = movie_player.getVideoData().isLive ? 'none' : 'initial'; container.classList.remove('transition'); bufferEl.style.transform = 'scaleX(0)'; progressEl.style.transform = 'scaleX(0)'; container.classList.add('transition'); renderChapters.init(video); } function notInteractiveToRender() { if (user_settings['player-control-below'] && NOVA.isFullscreen()) return; return (document.visibilityState == 'hidden' || movie_player.getVideoData().isLive ); } }); function insertFloatBar(z_index = 60) { return document.getElementById(SELECTOR_ID) || (function () { movie_player.insertAdjacentHTML('beforeend', `<div id="${SELECTOR_ID}" class="transition"> <div class="container"> <div id="${SELECTOR_ID}-buffer" class="ytp-load-progress"></div> <div id="${SELECTOR_ID}-progress" class="ytp-swatch-background-color"></div> </div> <div id="${SELECTOR_ID}-chapters"></div> </div>`); NOVA.css.push( `[id|=${SELECTOR_ID}] { position: absolute; bottom: 0; } ${SELECTOR} { --opacity: ${+user_settings.player_float_progress_bar_opacity || .7}; --height: ${+user_settings.player_float_progress_bar_height || 3}px; --bg-color: ${NOVA.css.getValue('.ytp-progress-list', 'background-color') || 'rgba(255,255,255,.2)'}; --zindex: ${z_index}; opacity: var(--opacity); z-index: var(--zindex); background-color: var(--bg-color); width: 100%; visibility: hidden; } #movie_player.ytp-autohide ${SELECTOR} { visibility: visible; } ${SELECTOR}.transition [id|=${SELECTOR_ID}] { transition: transform .2s linear; } ${SELECTOR}-progress, ${SELECTOR}-buffer { width: 100%; height: var(--height); transform-origin: 0 0; transform: scaleX(0); } ${SELECTOR}-progress { z-index: calc(var(--zindex) + 1); } ${SELECTOR}-chapters { position: relative; width: 100%; display: flex; justify-content: flex-end; } ${SELECTOR}-chapters span { height: var(--height); z-index: calc(var(--zindex) + 1); box-sizing: border-box; padding: 0; margin: 0; } ${SELECTOR}-chapters span:not(:first-child) { border-left: ${CHAPTERS_MARK_WIDTH_PX} solid rgba(255,255,255,.7); }`); return document.getElementById(SELECTOR_ID); })(); } const renderChapters = { async init(vid) { if (NOVA.currentPage == 'watch' && !(vid instanceof HTMLElement)) { return console.error('vid not HTMLElement:', chaptersContainer); } await NOVA.waitUntil(() => !isNaN(vid.duration), 1000); switch (NOVA.currentPage) { case 'watch': this.from_description(vid.duration); break; case 'embed': await NOVA.waitUntil(() => ( chaptersContainer = document.body.querySelector('.ytp-chapters-container')) && chaptersContainer?.children.length > 1 , 1000); ( this.renderChaptersMarks(vid.duration) || this.from_div(chaptersContainer) ); break; } }, from_description(duration = required()) { if (Math.sign(duration) !== 1) return console.error('duration not positive number:', duration); const selectorTimestampLink = 'a[href*="&t="]'; NOVA.waitSelector(`ytd-watch-metadata #description.ytd-watch-metadata ${selectorTimestampLink}`) .then(() => this.renderChaptersMarks(duration)); NOVA.waitSelector(`#comments #comment #comment-content ${selectorTimestampLink}`) .then(() => this.renderChaptersMarks(duration)); }, from_div(chaptersContainer = required()) { if (!(chaptersContainer instanceof HTMLElement)) return console.error('container not HTMLElement:', chaptersContainer); const progressContainerWidth = parseInt(getComputedStyle(chaptersContainer).width), chaptersOut = document.getElementById(`${SELECTOR_ID}-chapters`); for (const chapter of chaptersContainer.children) { const newChapter = document.createElement('span'), { width, marginLeft, marginRight } = getComputedStyle(chapter), chapterMargin = parseInt(marginLeft) + parseInt(marginRight); newChapter.style.width = (((parseInt(width) + chapterMargin) / progressContainerWidth) * 100) + '%'; chaptersOut.append(newChapter); } }, renderChaptersMarks(duration) { if (isNaN(duration)) return console.error('duration isNaN:', duration); if (chaptersContainer = document.getElementById(`${SELECTOR_ID}-chapters`)) { chaptersContainer.innerHTML = ''; } const chapterList = NOVA.getChapterList(duration); chapterList ?.forEach((chapter, i, chapters_list) => { const newChapter = document.createElement('span'); const nextChapterSec = chapters_list[i + 1]?.sec || duration; newChapter.style.width = ((nextChapterSec - chapter.sec) / duration) * 100 + '%'; if (chapter.title) newChapter.title = chapter.title; newChapter.setAttribute('time', chapter.time); chaptersContainer.append(newChapter); }); return chapterList; }, }; }, }); window.nova_plugins.push({ id: 'time-remaining', run_on_pages: 'watch, embed, -mobile', _runtime: user_settings => { const SELECTOR_ID = 'nova-player-time-remaining'; NOVA.waitSelector('.ytp-time-duration, ytm-time-display .time-display-content') .then(container => { NOVA.waitSelector('video') .then(video => { video.addEventListener('timeupdate', setRemaining.bind(video)); video.addEventListener('ratechange', setRemaining.bind(video)); ['suspend', 'ended'].forEach(evt => { video.addEventListener(evt, () => insertToHTML({ 'container': container })); }); document.addEventListener('yt-navigate-finish', () => insertToHTML({ 'container': container })); }); function setRemaining() { if (isNaN(this.duration) || movie_player.getVideoData().isLive || (NOVA.currentPage == 'embed' && window.self.location.href.includes('live_stream')) || document.visibilityState == 'hidden' || movie_player.classList.contains('ytp-autohide') ) return; const getProgressPt = () => { const floatRound = pt => (this.duration > 3600) ? pt.toFixed(2) : (this.duration > 1500) ? pt.toFixed(1) : Math.round(pt); return floatRound((this.currentTime / this.duration) * 100) + '%'; }, getLeftTime = () => '-' + NOVA.timeFormatTo.HMS.digit((this.duration - this.currentTime) / this.playbackRate); let text; switch (user_settings.time_remaining_mode) { case 'pt': text = ' • ' + getProgressPt(); break; case 'time': text = getLeftTime(); break; default: text = getLeftTime(); text += text && ` (${getProgressPt()})`; } if (text) { insertToHTML({ 'text': text, 'container': container }); } } function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (document.getElementById(SELECTOR_ID) || (function () { container.insertAdjacentHTML('afterend', ` <span id="${SELECTOR_ID}">${text}</span>`); return document.getElementById(SELECTOR_ID); })()) .textContent = text; } }); }, }); window.nova_plugins.push({ id: 'related-visibility', run_on_pages: 'watch, -mobile', _runtime: user_settings => { NOVA.collapseElement({ selector: '#secondary #related', remove: (user_settings.related_visibility_mode == 'disable') ? true : false, }); }, }); window.nova_plugins.push({ id: 'playlist-duration', run_on_pages: 'watch, playlist, -mobile', restart_on_location_change: true, _runtime: user_settings => { const SELECTOR_ID = 'nova-playlist-duration', playlistId = NOVA.queryURL.get('list'); if (!playlistId) return; switch (NOVA.currentPage) { case 'playlist': NOVA.waitSelector('#owner-text a') .then(el => { if (duration = getPlaylistDuration()) { insertToHTML({ 'container': el, 'text': duration }); } else { getPlaylistDurationFromThumbnails({ 'items_selector': '#primary .ytd-thumbnail-overlay-time-status-renderer:not(:empty)', }) .then(duration => insertToHTML({ 'container': el, 'text': duration })); } function getPlaylistDuration() { const vids_list = (document.body.querySelector('ytd-app')?.data?.response || window.ytInitialData) .contents.twoColumnBrowseResultsRenderer ?.tabs[0].tabRenderer?.content?.sectionListRenderer ?.contents[0].itemSectionRenderer ?.contents[0].playlistVideoListRenderer?.contents; const duration = vids_list?.reduce((acc, vid) => acc + (isNaN(vid.playlistVideoRenderer?.lengthSeconds) ? 0 : parseInt(vid.playlistVideoRenderer.lengthSeconds)), 0); if (duration) { return outFormat(duration); } } }); break; case 'watch': NOVA.waitSelector('#secondary .index-message-wrapper') .then(el => { const waitPlaylist = setInterval(() => { const playlistLength = movie_player.getPlaylist()?.length, playlistList = document.querySelector('yt-playlist-manager')?.currentPlaylistData_?.contents .filter(e => e.playlistPanelVideoRenderer?.lengthText?.simpleText) .map(e => NOVA.timeFormatTo.hmsToSec(e.playlistPanelVideoRenderer.lengthText.simpleText)); console.assert(playlistList?.length === playlistLength, 'playlist loading:', playlistList?.length + '/' + playlistLength); if (playlistList?.length === playlistLength) { clearInterval(waitPlaylist); if (duration = getPlaylistDuration(playlistList)) { insertToHTML({ 'container': el, 'text': duration }); } else if (!user_settings.playlist_duration_progress_type) { getPlaylistDurationFromThumbnails({ 'container': document.body.querySelector('#secondary #playlist'), 'items_selector': '#playlist-items #unplayableText[hidden]', }) .then(duration => insertToHTML({ 'container': el, 'text': duration })); } } }, 1000); function getPlaylistDuration(total_list) { const currentIndex = movie_player.getPlaylistIndex(); let elapsedList = [...total_list]; switch (user_settings.playlist_duration_progress_type) { case 'done': elapsedList.splice(currentIndex); break; case 'left': elapsedList.splice(0, currentIndex); break; } const sumArr = arr => arr.reduce((acc, time) => acc + +time, 0); return outFormat( sumArr(elapsedList), user_settings.playlist_duration_percentage ? sumArr(total_list) : false ); } }); break; } function getPlaylistDurationFromThumbnails({ items_selector = required(), container }) { if (container && !(container instanceof HTMLElement)) { return console.error('container not HTMLElement:', container); } return new Promise(resolve => { let forcePlaylistRun = false; const waitThumbnails = setInterval(() => { const playlistLength = movie_player.getPlaylist()?.length || document.body.querySelector('ytd-player')?.player_?.getPlaylist()?.length || document.body.querySelectorAll(items_selector)?.length, timeStampList = (container || document.body) .querySelectorAll('.ytd-thumbnail-overlay-time-status-renderer:not(:empty)'), duration = getTotalTime(timeStampList); console.assert(timeStampList.length === playlistLength, 'playlist loading:', timeStampList.length + '/' + playlistLength); if (+duration && timeStampList.length && (timeStampList.length === playlistLength || forcePlaylistRun) ) { clearInterval(waitThumbnails); resolve(outFormat(duration)); } else if (!forcePlaylistRun) { setTimeout(() => forcePlaylistRun = true, 1000 * 3); } }, 500); }); function getTotalTime(nodes) { const arr = [...nodes] .map(e => NOVA.timeFormatTo.hmsToSec(e.textContent)) .filter(Number); return arr.length && arr.reduce((acc, time) => acc + +time, 0); } } function outFormat(duration = 0, total) { let outArr = [ NOVA.timeFormatTo.HMS.digit( (NOVA.currentPage == 'watch' && NOVA.videoElement?.playbackRate) ? (duration / NOVA.videoElement.playbackRate) : duration ) ]; if (total) { outArr.push(`(${~~(duration * 100 / total) + '%'})`); if (user_settings.playlist_duration_progress_type) { outArr.push(user_settings.playlist_duration_progress_type); } } return ' - ' + outArr.join(' '); } function insertToHTML({ text = '', container = required() }) { if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container); (container.querySelector(`#${SELECTOR_ID}`) || (function () { const el = document.createElement('span'); el.id = SELECTOR_ID; return container.appendChild(el); })()) .textContent = ' ' + text; } }, }); const Plugins = { run: ({ user_settings, app_ver }) => { if (!window.nova_plugins?.length) return console.error('nova_plugins empty', window.nova_plugins); if (!user_settings) return console.error('user_settings empty', user_settings); NOVA.currentPage = (function () { const [page, channelTab] = location.pathname.split('/').filter(Boolean); NOVA.channelTab = channelTab; return (page != 'live_chat') && (['channel', 'c', 'user'].includes(page) || page?.startsWith('@') || /[A-Z\d_]/.test(page) || ['featured', 'videos', 'shorts', 'streams', 'playlists', 'community', 'channels', 'about'].includes(channelTab) ) ? 'channel' : (page == 'clip') ? 'watch' : page || 'home'; })(); NOVA.isMobile = location.host == 'm.youtube.com'; let logTableArray = [], logTableStatus, logTableTime; window.nova_plugins?.forEach(plugin => { const pagesAllowList = plugin?.run_on_pages?.split(',').map(p => p.trim().toLowerCase()).filter(Boolean); logTableTime = 0; logTableStatus = false; if (!pluginChecker(plugin)) { console.error('Plugin invalid\n', plugin); alert('Plugin invalid: ' + plugin?.id); logTableStatus = 'INVALID'; } else if (plugin.was_init && !plugin.restart_on_location_change) { logTableStatus = 'skiped'; } else if (!user_settings.hasOwnProperty(plugin.id)) { logTableStatus = 'off'; } else if ( ( pagesAllowList?.includes(NOVA.currentPage) || (pagesAllowList?.includes('all') && !pagesAllowList?.includes('-' + NOVA.currentPage)) ) && (!NOVA.isMobile || (NOVA.isMobile && !pagesAllowList?.includes('-mobile'))) ) { try { const startTableTime = performance.now(); plugin.was_init = true; plugin._runtime(user_settings); logTableTime = (performance.now() - startTableTime).toFixed(2); logTableStatus = true; } catch (err) { console.groupEnd('plugins status'); console.error(`[ERROR PLUGIN] ${plugin.id}\n${err.stack}\n\nPlease report the bug: https://github.com/raingart/Nova-YouTube-extension/issues/new?body=` + encodeURIComponent(app_ver + ' | ' + navigator.userAgent)); if (user_settings.report_issues && _pluginsCaptureException) { _pluginsCaptureException({ 'trace_name': plugin.id, 'err_stack': err.stack, 'app_ver': app_ver, 'confirm_msg': `ERROR in Nova YouTube™\n\nCrash plugin: "${plugin.id}"\nPlease report the bug or disable the plugin\n\nSend the bug raport to developer?`, }); } console.groupCollapsed('plugins status'); logTableStatus = 'ERROR'; } } logTableArray.push({ 'launched': logTableStatus, 'name': plugin?.id, 'time init (ms)': logTableTime, }); }); console.table(logTableArray); console.groupEnd('plugins status'); function pluginChecker(plugin) { const result = plugin?.id && plugin.run_on_pages && 'function' === typeof plugin._runtime; if (!result) { console.error('plugin invalid:\n', { 'id': plugin?.id, 'run_on_pages': plugin?.run_on_pages, '_runtime': 'function' === typeof plugin?._runtime, }); } return result; } }, } console.log('%c ', 'color:#0096fa; font-weight:bold;', GM_info.script.name + ' v.' + GM_info.script.version); const configPage = 'https://raingart.github.io/options.html', configStoreName = 'user_settings', user_settings = GM_getValue(configStoreName, null); if (user_settings?.exclude_iframe && (window.frameElement || window.self !== window.top)) { return console.warn(GM_info.script.name + ': processed in the iframe disable'); } console.debug(`current ${configStoreName}:`, user_settings); const keyRenameTemplate = { 'shorts_thumbnails_time': 'shorts-thumbnails-time', } for (const oldKey in user_settings) { if (newKey = keyRenameTemplate[oldKey]) { console.log(oldKey, '=>', newKey); delete Object.assign(user_settings, { [newKey]: user_settings[oldKey] })[oldKey]; } GM_setValue(configStoreName, user_settings); } registerMenuCommand(); if (location.hostname === new URL(configPage).hostname) setupConfigPage(); else { if (!user_settings?.disable_setting_button) insertSettingButton(); if (!user_settings || !Object.keys(user_settings).length) { if (confirm('Active plugins undetected. Open the settings page now?')) GM_openInTab(configPage); user_settings['report_issues'] = 'on'; GM_setValue(configStoreName, user_settings); } else landerPlugins(); } function setupConfigPage() { document.addEventListener('submit', event => { event.preventDefault(); let obj = {}; for (const [key, value] of new FormData(event.target)) { if (obj.hasOwnProperty(key)) { obj[key] += ',' + value; obj[key] = obj[key].split(','); } else { switch (value) { case 'true': obj[key] = true; break; case 'false': obj[key] = false; break; case 'undefined': obj[key] = undefined; break; default: obj[key] = value; } }; } console.debug(`update ${configStoreName}:`, obj); GM_setValue(configStoreName, obj); }); window.addEventListener('DOMContentLoaded', () => { localizePage(user_settings?.lang_code); storeData = user_settings; }); window.addEventListener('load', () => { document.body?.classList?.remove('preload'); document.body.querySelector('a[href$="issues/new"]') .addEventListener('click', ({ target }) => { target.href += '?body=' + encodeURIComponent(GM_info.script.version + ' | ' + navigator.userAgent); }); }); } function landerPlugins() { processLander(); function processLander() { const plugins_lander = setInterval(() => { const domLoaded = document?.readyState != 'loading'; if (!domLoaded) return console.debug('waiting, page loading..'); clearInterval(plugins_lander); console.groupCollapsed('plugins status'); Plugins.run({ 'user_settings': user_settings, 'app_ver': GM_info.script.version, }); }, 500); } let prevURL = location.href; const isURLChanged = () => prevURL == location.href ? false : prevURL = location.href; if (isMobile = (location.host == 'm.youtube.com')) { window.addEventListener('transitionend', ({ target }) => target.id == 'progress' && isURLChange() && processLander()); } else { document.addEventListener('yt-navigate-start', () => isURLChanged() && processLander()); } } function registerMenuCommand() { GM_registerMenuCommand('Settings', () => GM_openInTab(configPage)); GM_registerMenuCommand('Import settings', () => { const f = document.createElement('input'); f.type = 'file'; f.accept = 'application/JSON'; f.style.display = 'none'; f.addEventListener('change', function () { if (f.files.length !== 1) return alert('file empty'); const rdr = new FileReader(); rdr.addEventListener('load', function () { try { GM_setValue(configStoreName, JSON.parse(rdr.result)); alert('Settings imported'); location.reload(); } catch (err) { alert(`Error parsing settings\n${err.name}: ${err.message}`); } }); rdr.addEventListener('error', error => alert('Error loading file\n' + rdr?.error || error)); rdr.readAsText(f.files[0]); }); document.body.append(f); f.click(); f.remove(); }); GM_registerMenuCommand('Export settings', () => { let d = document.createElement('a'); d.style.display = 'none'; d.download = 'nova-settings.json'; d.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(user_settings)); document.body.append(d); d.click(); d.remove(); }); } function insertSettingButton() { NOVA.waitSelector('#masthead #end') .then(menu => { const titleMsg = 'Nova Settings', a = document.createElement('a'), SETTING_BTN_ID = 'nova_settings_button'; a.id = SETTING_BTN_ID; a.href = configPage + '?tabs=tab-plugins'; a.target = '_blank'; a.innerHTML = `<yt-icon-button class="style-scope ytd-button-renderer style-default size-default"> <svg viewBox="-4 0 20 16"> <radialGradient id="nova-gradient" gradientUnits="userSpaceOnUse" cx="6" cy="22" r="18.5"> <stop class="nova-gradient-start" offset="0"/> <stop class="nova-gradient-stop" offset="1"/> </radialGradient> <g fill="deepskyblue"> <polygon points="0,16 14,8 0,0"/> </g> </svg> </yt-icon-button>`; a.addEventListener('click', () => { setTimeout(() => document.body.click(), 200); }); a.title = titleMsg; const tooltip = document.createElement('tp-yt-paper-tooltip'); tooltip.className = 'style-scope ytd-topbar-menu-button-renderer'; tooltip.textContent = titleMsg; a.appendChild(tooltip); menu.prepend(a); NOVA.css.push( `#${SETTING_BTN_ID}[tooltip]:hover:after { position: absolute; top: 50px; transform: translateX(-50%); content: attr(tooltip); text-align: center; min-width: 3em; max-width: 21em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 1.8ch 1.2ch; border-radius: 0.6ch; background-color: #616161; box-shadow: 0 1em 2em -0.5em rgb(0 0 0 / 35%); color: #fff; z-index: 1000; } #${SETTING_BTN_ID} { position: relative; opacity: .3; transition: opacity .3s ease-out; } #${SETTING_BTN_ID}:hover { opacity: 1 !important; } #${SETTING_BTN_ID} path, #${SETTING_BTN_ID} polygon { fill: url(#nova-gradient); } #${SETTING_BTN_ID} .nova-gradient-start, #${SETTING_BTN_ID} .nova-gradient-stop { transition: .6s; stop-color: #7a7cbd; } #${SETTING_BTN_ID}:hover .nova-gradient-start { stop-color: #0ff; } #${SETTING_BTN_ID}:hover .nova-gradient-stop { stop-color: #0095ff; }`); }); } function _pluginsCaptureException({ trace_name, err_stack, confirm_msg, app_ver }) { }