raingart / Nova YouTube

// ==UserScript==
// @name            Nova YouTube
// @namespace       https://github.com/raingart/Nova-YouTube-extension/
// @version         0.50.0.1
// @description     Powerful control on YouTube
// @name:en         Nova YouTube
// @name:ru 操你妈的
// @name:uk 操你妈的
// @description:ru 你妈死了
// @description:uk 你妈死了

// @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*
// @include         http*://raingart.github.io/nova/*

// @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*://*.youtubetranscript.com/*

// @grant           GM_getResourceText
// @grant           GM_getResourceURL
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           GM_registerMenuCommand
// @grant           GM_notification
// @grant           GM_openInTab
// @grant           unsafeWindow

// @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');
}
if (!('MutationObserver' in window)) {
errorAlert('MutationObserver not supported');
}
try {
document?.body;
} catch (error) {
errorAlert('Your browser does not support chaining operator');
}
switch (GM_info.scriptHandler) {
case 'Tampermonkey':
case 'Violentmonkey':
case 'ScriptCat':
case 'OrangeMonkey':
break;
case 'FireMonkey':
errorAlert(GM_info.scriptHandler + ' incomplete support', false);
break;
case 'Greasemonkey':
errorAlert(GM_info.scriptHandler + ' is not supported');
break;
case 'Stay':
errorAlert(GM_info.scriptHandler + ' is not tested!\nPlease inform the author about the working status');
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;
}
function errorAlert(text = '', stop_execute = true) {
alert(GM_info.script.name + ' Error!\n' + text);
if (stop_execute) {
throw GM_info.script.name + ' crashed!\n' + text;
}
}
window.nova_plugins = [];
window.nova_plugins.push({
id: 'comments-visibility',
title: 'Collapse comments section',
'title:zh': '收起评论区',
'title:ja': 'コメント欄を折りたたむ',
'title:pl': 'Zwiń sekcję komentarzy',
run_on_pages: 'watch, -mobile',
restart_on_location_change: true,
section: 'comments',
_runtime: user_settings => {
NOVA.collapseElement({
selector: '#comments',
label: 'comments',
remove: (user_settings.comments_visibility_mode == 'disable') ? true : false,
});
},
options: {
comments_visibility_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'collapse', value: 'hide', selected: true,
'label:pl': 'zwiń',
},
{
label: 'remove', value: 'disable',
'label:pl': 'usuń',
},
],
},
}
});
window.nova_plugins.push({
id: 'square-avatars',
title: 'Square avatars',
'title:zh': '方形头像',
'title:ja': '正方形のアバター',
'title:pl': 'Kwadratowe awatary',
run_on_pages: '*, -live_chat',
section: 'comments',
desc: 'Make user images squared',
'desc:zh': '方形用户形象',
'desc:ja': 'ユーザー画像を二乗する',
'desc:pl': 'Awatary użytkowniów będą kwadratowe',
_runtime: user_settings => {
NOVA.css.push(
[
'yt-img-shadow',
'.ytp-title-channel-logo',
'#player .ytp-title-channel',
'ytm-profile-icon',
'#ytd-player.ytd-watch-flexy',
'a.ytd-thumbnail',
'#search .ytd-searchbox',
]
.join(',\n') + ` {
border-radius: 0 !important;
}
html {
--yt-button-border-radius: 0;
}`);
NOVA.waitUntil(() => {
if (window.yt && (obj = yt?.config_?.EXPERIMENT_FLAGS) && Object.keys(obj).length) {
yt.config_.EXPERIMENT_FLAGS.web_rounded_thumbnails = false;
return true;
}
}, 100);
},
});
window.nova_plugins.push({
id: 'comments-expand',
title: 'Expand comments',
'title:zh': '展开评论',
'title:ja': 'コメントを展開',
'title:pl': 'Rozwiń komentarze',
run_on_pages: 'watch, -mobile',
section: 'comments',
_runtime: user_settings => {
NOVA.css.push(
`#expander.ytd-comment-renderer {
overflow-x: hidden;
}`);
NOVA.watchElements({
selectors: ['#comments #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', '#replies #expander-contents ytd-continuation-item-renderer 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());
}
},
options: {
comments_expand_mode: {
_tagName: 'select',
label: 'Expand comment',
'label:zh': '展开评论',
'label:ja': 'コメントを展開',
'label:pl': 'Rozwiń komentarz',
options: [
{
label: 'always', value: 'always', selected: true,
'label:zh': '每次',
'label:ja': 'いつも',
'label:pl': 'zawsze',
},
{
label: 'on hover', value: 'onhover',
'label:zh': '悬停时',
'label:ja': 'ホバー時に',
'label:pl': 'przy najechaniu',
},
{
label: 'disable', value: false,
},
],
},
comments_view_reply: {
_tagName: 'select',
label: 'Expand reply',
'label:zh': '展开回复',
'label:ja': '返信を展開',
'label:pl': 'Rozwiń odpowiedź',
options: [
{
label: 'always', value: 'always',
'label:zh': '每次',
'label:ja': 'いつも',
'label:pl': 'zawsze',
},
{
label: 'on hover', value: 'onhover', selected: true,
'label:zh': '悬停时',
'label:ja': 'ホバー時に',
'label:pl': 'przy najechaniu',
},
{
label: 'disable', value: false,
},
],
},
}
});
window.nova_plugins.push({
id: 'comments-popup',
title: 'Comments section in popup',
'title:zh': '弹出窗口中的评论部分',
'title:ja': 'ポップアップのコメントセクション',
'title:pl': 'Sekcja komentarzy w osobnym oknie',
run_on_pages: 'watch, -mobile',
section: 'comments',
_runtime: user_settings => {
if (user_settings['comments_visibility_mode'] == 'disable') return;
const
COMMENTS_SELECTOR = 'html:not(:fullscreen) #page-manager #comments:not([hidden]):not(:empty)',
counterAttrName = 'data-counter';
NOVA.runOnPageLoad(() => {
if (NOVA.currentPage == 'watch') {
NOVA.waitSelector('ytd-comments-header-renderer #title #count:not(:empty)', { destroy_after_page_leaving: true })
.then(countEl => {
if (count = NOVA.extractAsNum.int(countEl.textContent)) {
document.body.querySelector(COMMENTS_SELECTOR)
?.setAttribute(counterAttrName, NOVA.numberFormat.abbr(count));
}
});
}
});
NOVA.waitSelector('#masthead-container')
.then(masthead => {
NOVA.css.push(
`${COMMENTS_SELECTOR},
${COMMENTS_SELECTOR}:before {
position: fixed;
top: ${masthead.offsetHeight || 56}px;
right: 0;
z-index: ${1 + Math.max(getComputedStyle(masthead || movie_player)['z-index'], 601)};
}
${COMMENTS_SELECTOR}:not(:hover):before {
content: attr(${counterAttrName}) " comments ▼";
cursor: pointer;
visibility: visible;
right: 3em;
padding: 0 6px 2px;
line-height: normal;
font-family: Roboto, Arial, sans-serif;
font-size: 11px;
color: #eee;
background-color: rgba(0, 0, 0, .3);
}
${COMMENTS_SELECTOR} {
${(user_settings.comments_popup_width === 100) ? 'margin: 0 1%;' : ''}
padding: 0 15px;
background-color: var(--yt-spec-brand-background-primary);
background-color: var(--yt-spec-menu-background);
background-color: var(--yt-spec-raised-background);
color: var(--yt-spec-text-primary);;
border: 1px solid #333;
max-width: ${user_settings.comments_popup_width || 40}%;
${user_settings['square-avatars'] ? '' : 'border-radius: 12px'};
}
${COMMENTS_SELECTOR}:not(:hover) {
visibility: collapse;
}
${COMMENTS_SELECTOR}:hover {
visibility: visible !important;
}
${COMMENTS_SELECTOR} > #sections > #contents {
overflow-y: auto;
max-height: 82.5vh;
padding-top: 1em;
}
#expander.ytd-comment-renderer {
overflow-x: hidden;
}
${COMMENTS_SELECTOR} #sections {
min-width: 500px;
}
${COMMENTS_SELECTOR} #contents::-webkit-scrollbar {
height: 8px;
width: 10px;
}
${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-button {
height: 0;
width: 0;
}
${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-corner {
background-color: transparent;
}
${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-thumb {
background-color: #e1e1e1;
border: 0;
border-radius: 0;
}
${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-thumb {}
${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-track {
background-color: #666;
border: 0;
border-radius: 0;
}
${COMMENTS_SELECTOR} #contents::-webkit-scrollbar-track:hover {
background-color: #666;
}
ytd-comments-header-renderer {
margin: 10px 0 !important;
}`);
if (user_settings.comments_popup_hide_textarea) {
NOVA.css.push(
`${COMMENTS_SELECTOR} > #sections > #contents {
overflow-y: auto;
max-height: 88vh;
border-top: 1px solid #333;
padding-top: 1em;
}
${COMMENTS_SELECTOR} #header #simple-box {
display: none;
}
ytd-comments-header-renderer #title {
margin: 0 !important;
}`);
}
});
},
options: {
comments_popup_width: {
_tagName: 'input',
label: 'Width',
'label:zh': '宽度',
'label:ja': '幅',
'label:pl': 'Szerokość',
type: 'number',
title: '% of the screen width',
placeholder: '%',
step: 5,
min: 10,
max: 100,
value: 40,
},
comments_popup_hide_textarea: {
_tagName: 'input',
label: 'Hide textarea',
'label:zh': '隐藏文本区域',
'label:ja': 'テキストエリアを隠す',
'label:pl': 'Ukryj obszar tekstowy',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'comments-sort',
title: 'Comments sort',
'title:zh': '评论排序',
'title:ja': 'コメントの並べ替え',
'title:pl': 'Sortowanie komentarzy',
run_on_pages: 'watch, -mobile',
section: 'comments',
opt_api_key_warn: true,
desc: 'add modal',
_runtime: user_settings => {
const
MAX_COMMENTS = (user_settings['user-api-key'] && +user_settings.comments_sort_max) || 250,
MODAL_NAME_SELECTOR_ID = 'nova-modal-comments',
MODAL_CONTENT_SELECTOR_ID = 'modal-content',
NOVA_REPLYS_SELECTOR_ID = 'nova-replys',
NOVA_REPLYS_SWITCH_CLASS_NAME = NOVA_REPLYS_SELECTOR_ID + '-switch',
BLOCK_KEYWORDS = NOVA.strToArray(user_settings.comments_sort_blocklist?.toLowerCase());
NOVA.waitSelector('#movie_player')
.then(insertButton);
function insertButton() {
NOVA.waitSelector(
user_settings['comments-popup']
? '#masthead-container'
: '#comments ytd-comments-header-renderer #title'
)
.then(menu => {
const btn = document.createElement('span');
btn.setAttribute('data-open-modal', MODAL_NAME_SELECTOR_ID);
btn.title = 'Nova Comments';
btn.textContent = '►';
btn.addEventListener('click', () => {
if (!document.body.querySelector(`#${MODAL_CONTENT_SELECTOR_ID} table`)) {
getComments();
}
btn.dispatchEvent(new CustomEvent(MODAL_NAME_SELECTOR_ID, { bubbles: true, detail: 'test' }));
});
Object.assign(btn.style,
user_settings['comments-popup']
? {
position: 'fixed',
right: '0',
top: 'var(--ytd-masthead-height)',
visibility: 'visible',
'z-index': 1 + Math.max(
NOVA.css.get('.ytp-chrome-top', 'z-index'),
60),
'font-size': '18px',
}
: {
'font-size': '24px',
'text-decoration': 'none',
padding: '0 10px',
'background-color': 'transparent',
border: 'none',
},
{
color: 'orange',
cursor: 'pointer',
});
user_settings['comments-popup']
? menu.append(btn)
: menu.prepend(btn);
insertModal();
NOVA.runOnPageLoad(() => {
if (NOVA.currentPage == 'watch') {
document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML = '<pre>Loading data...</pre>';
}
});
});
}
let commentList = [];
function getComments(next_page_token) {
const params = {
'videoId': NOVA.queryURL.get('v') || movie_player.getVideoData().video_id,
'part': 'snippet,replies',
'maxResults': Math.min(+user_settings.comments_sort_max || 100, 100),
'order': 'relevance',
};
if (next_page_token) {
params['pageToken'] = next_page_token;
}
NOVA.request.API({
request: 'commentThreads',
params: params,
api_key: user_settings['user-api-key'],
})
.then(res => {
if (res?.error) {
if (res.reason) {
document.getElementById(MODAL_NAME_SELECTOR_ID)
.dispatchEvent(new CustomEvent(MODAL_NAME_SELECTOR_ID, { bubbles: true, detail: 'test' }));
return alert(`Error [${res.code}]: ${res.reason}`);
}
else {
return document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML =
`<pre>Error [${res.code}]: ${res.reason}</pre>
<pre>${res.error}</pre>`;
}
}
res?.items?.forEach(item => {
if (comment = item.snippet?.topLevelComment?.snippet) {
commentList.push(
Object.assign(
{ 'totalReplyCount': item.snippet.totalReplyCount },
{ 'id': item.id },
comment,
item.replies,
)
);
}
else {
console.warn('API is change', item);
}
});
if (commentList.length >= MAX_COMMENTS) {
genTable();
}
else if (res?.nextPageToken) {
document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML = `<pre>Loading: ${commentList.length + (user_settings['user-api-key'] ? '' : '/' + MAX_COMMENTS)}</pre>`;
getComments(res?.nextPageToken);
}
else {
genTable();
}
});
}
function genTable() {
if (!commentList.length) {
return document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML = `<pre>Comments empty</pre>`;
}
const ul = document.createElement('tbody');
const channelName = (href = document.body.querySelector('#owner #upload-info #channel-name a[href]')?.href) && new URL(href).pathname;
commentList
.sort((a, b) => b.likeCount - a.likeCount)
.forEach(comment => {
try {
if (!(comment.textDisplay = filterStr(comment.textDisplay, comment.authorDisplayName))) return;
if (comment.textOriginal.length > 100 && comment.textOriginal.split(' ')?.some(word => word.length > 100)) {
console.debug('comment istoo long:\n', comment.textOriginal);
return;
}
const
replyInputName = `${NOVA_REPLYS_SELECTOR_ID}-${comment.id}`,
li = document.createElement('tr');
let replyCount = 0;
li.className = 'item';
if (channelName && comment.authorChannelUrl.includes(channelName)) li.classList.add('author');
li.innerHTML =
`<td>${comment.likeCount}</td>
<td sorttable_customkey="${comment.totalReplyCount}" class="${NOVA_REPLYS_SWITCH_CLASS_NAME}">
${comment.comments?.length
? `<a href="https://www.youtube.com/watch?v=${comment.videoId}&lc=${comment.id}" target="_blank" title="Open comment link">${comment.totalReplyCount}</a> <label for="${replyInputName}"></label>`
: ''}</td>
<td sorttable_customkey="${new Date(comment.publishedAt).getTime()}">${NOVA.formatTimeOut.ago(new Date(comment.publishedAt))}</td>
<td>
<a href="${comment.authorChannelUrl}" target="_blank" title="${comment.authorDisplayName}">
<img src="${comment.authorProfileImageUrl}" alt="${comment.authorDisplayName}" />
</a>
</td>
<td sorttable_customkey="${comment.textOriginal.length}">
<span class="text-overflow-dynamic-ellipsis">${comment.textDisplay}</span>
${appendReplies()}
</td>`;
ul.append(li);
if (replyCount) {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = checkbox.name = replyInputName;
checkbox.addEventListener('change', ({ target }) => {
document.body.querySelector(`table[${NOVA_REPLYS_SELECTOR_ID}="${target.name}"]`)
.classList.toggle('nova-hide');
});
li.querySelector('td label[for]')?.before(checkbox);
}
function appendReplies() {
if (!+comment.totalReplyCount) return '';
const table = document.createElement('table');
table.className = 'nova-hide';
table.setAttribute(NOVA_REPLYS_SELECTOR_ID, replyInputName);
comment.comments
?.forEach(reply => {
if (!(reply.snippet.textDisplay = filterStr(reply.snippet.textDisplay, reply.snippet.authorDisplayName))) return;
replyCount++;
const li = document.createElement('tr');
if (channelName && reply.snippet.authorChannelUrl.includes(channelName)) li.classList.add('author');
li.innerHTML =
`<td>
<a href="${reply.snippet.authorChannelUrl}" target="_blank" title="${reply.snippet.authorDisplayName}">
<img src="${reply.snippet.authorProfileImageUrl}" alt="${reply.snippet.authorDisplayName}" />
</a>
</td>
<td>
<span class="text-overflow-dynamic-ellipsis">
<div class="nova-reply-time-text">${reply.snippet.likeCount
? `${reply.snippet.likeCount} likes` : ''}</div>
<div>${reply.snippet.textDisplay}</div>
</span>
</td>`;
table.append(li);
});
return table.outerHTML;
}
} catch (error) {
console.error('Error comment generate:\n', error.stack + '\n', comment);
}
});
function filterStr(str, user) {
if (keyword = BLOCK_KEYWORDS?.find(keyword => ((user && keyword?.startsWith('@')) ? user : str)
.toLowerCase().includes(keyword))
) {
console.log('comment filter:', `"${keyword}\n"`, str.replace(keyword, `[${keyword}]`));
return;
}
const countWords = (str = '') => str.trim().split(/\s+/).length,
clearOfEmoji = str => str
.replace(/[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2580-\u27BF]|\uD83E[\uDD10-\uDDFF]/g, ' ')
.replace(/(?![*#0-9]+)[\p{Emoji}]/gu, ' ')
.replace(/([=:;/.()]{2,}|\))$/g, ' ')
.replace(/\s{2,}/g, ' ')
.replace(/(<br>){3,}/g, '<br><br>')
.replace(/<a[^>]+><\/a>/g, '')
.trim();
if (user_settings.comments_sort_clear_emoji) {
str = clearOfEmoji(str);
if (!str.length) return;
if (+user_settings.comments_sort_min_words
&& countWords(str) <= +user_settings.comments_sort_min_words
) {
return;
}
}
return str;
}
const MODAL_CONTENT_FILTER_SELECTOR_ID = 'nova-search-comment';
document.getElementById(MODAL_CONTENT_SELECTOR_ID).innerHTML =
`<table class="sortable" border="0" cellspacing="0" cellpadding="0">
<thead id="${MODAL_CONTENT_FILTER_SELECTOR_ID}">
<tr>
<th class="sorttable_numeric">likes</th>
<th class="sorttable_numeric">replys</th>
<th class="sorttable_numeric">date</th>
<th class="sorttable_nosort">avatar</th>
<th class="sorttable_numeric">comments (${commentList.length})</th>
</tr>
</thead>
<!-- $ {ul.innerHTML} -->
</table>`;
document.getElementById(MODAL_CONTENT_FILTER_SELECTOR_ID).after(ul);
connectSortable().makeSortable(document.body.querySelector('table.sortable'));
document.body.querySelector(`table.sortable thead`)
.addEventListener('click', ({ target }) => {
if (['input', 'textarea', 'select'].includes(target.localName) || target.isContentEditable) return;
if (containerScroll = document.body.querySelector('.modal-container')) containerScroll.scrollTop = 0;
});
insertFilterInput(MODAL_CONTENT_FILTER_SELECTOR_ID);
NOVA.css.push(
`.nova-hide {
display: none;
}
table[${NOVA_REPLYS_SELECTOR_ID}] {
border: 1px solid #444;
width: auto !important;
}
table[${NOVA_REPLYS_SELECTOR_ID}] td {
padding: auto 10px;
}
.nova-reply-time-text {
font-size: .5em;
font-style: italic;
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox] {
--height: 1.5em;
--disabled-opacity: .7;
background-color: var(--dark-theme-divider-color);
color: var(--dark-theme-text-color);
--off-hover-bg: var(--light-theme-secondary-color);
--checked-bg: #e85717;
--checked-bg-active: var(--dark-theme-divider-color);
--checked-color: var(--dark-theme-text-color);
--text-on: 'HIDE';
--text-on-press: 'SHOW';
--text-off: 'ANS';
--text-off-press: 'HIDE?';
appearance: none;
-webkit-appearance: none;
position: relative;
cursor: pointer;
outline: 0;
border: none;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
font-size: 1em;
width: 4em;
height: 1.7em;
font-weight: bold;
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:hover:before {
background-color: var(--off-hover-bg);
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:after,
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:before {
position: absolute;
transition: left 200ms ease-in-out;
width: 100%;
line-height: 1.8em;
text-align: center;
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:after {
left: 100%;
content: var(--text-on);
font-weight: bold;
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:before {
left: 0;
content: var(--text-off);
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:active {
background-color: var(--checked-bg);
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:active:before {
left: -10%;
content: var(--text-on-press);
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:checked {
color: var(--checked-color);
background-color: var(--checked-bg);
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:checked:before {
left: -100%;
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:checked:after {
left: 0;
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox]:checked:active:after {
left: 10%;
content: var(--text-off-press);
background-color: var(--checked-bg-active);
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox] [disabled] {
cursor: not-allowed;
}
.${NOVA_REPLYS_SWITCH_CLASS_NAME} input[type=checkbox] [disabled] {
opacity: var(--disabled-opacity);
}
`);
}
function insertFilterInput(parent_selector_id = required()) {
if (typeof parent_selector_id !== 'string') {
return console.error('typeof "parent_selector_id":', (typeof parent_selector_id));
}
NOVA.css.push(
`#${parent_selector_id} input {
position: absolute;
top: 0;
right: 0;
}
#${parent_selector_id} input[type=search]:focus,
#${parent_selector_id} input[type=text]:focus {
outline: 1px solid #00b7fc;
}
.nova-mark-text {
background-color: #ff0;
background-color: mark;
}`);
const searchInput = document.createElement('input');
searchInput.setAttribute('type', 'search');
searchInput.setAttribute('placeholder', 'Filter');
['change', 'keyup'].forEach(evt => {
searchInput
.addEventListener(evt, function () {
NOVA.searchFilterHTML({
'keyword': this.value,
'filter_selectors': 'tr.item',
'highlight_selector': '.text-overflow-dynamic-ellipsis',
'highlight_class': 'nova-mark-text',
});
});
searchInput
.addEventListener('click', () => {
searchInput.value = '';
searchInput.dispatchEvent(new Event('change'));
});
});
document.getElementById(parent_selector_id).append(searchInput);
};
function insertModal() {
NOVA.css.push(
`.modal {
--animation-time: .2s;
z-index: 9999;
position: fixed;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, .8);
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
box-sizing: border-box;
visibility: hidden;
opacity: 0;
}
.modal.modal-visible {
animation: microModalFadeIn var(--animation-time) cubic-bezier(0, 0, .2, 1);
backdrop-filter: blur(1em);
visibility: visible;
opacity: 1;
}
@keyframes microModalFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-container {
border-radius: 4px;
background-color: silver;
position: relative;
display: flex;
box-sizing: border-box;
overflow-y: auto;
max-width: 70%;
max-height: 100vh;
transform: scale(0.9);
transition: scale var(--animation-time) ease-out;
}
.modal.modal-visible .modal-container {
transform: scale(1);
}
.modal-close {
position: absolute;
top: 0;
right: 0;
cursor: pointer;
font-size: 2em;
padding: 0 5px;
transition: background-color var(--animation-time) ease-out;
}
.modal-close:hover {
background-color: #ea3c3c;
}
.modal-content {
padding: 2rem;
}`);
NOVA.css.push(
`.modal {}
.modal-container {
background-color: var(--yt-spec-brand-background-primary);
background-color: var(--yt-spec-menu-background);
background-color: var(--yt-spec-raised-background);
color: var(--yt-spec-text-primary);
}
.modal-content {
font-size: 12px;
}`);
document.body
.insertAdjacentHTML('beforeend',
`<div id="${MODAL_NAME_SELECTOR_ID}" class="modal" data-modal>
<div class="modal-container">
<div class="modal-close" data-close-modal>✕</div>
<div class="modal-content" id="${MODAL_CONTENT_SELECTOR_ID}"></div>
</div>
</div>`);
const modalShowClass = 'modal-visible';
document.getElementById(MODAL_NAME_SELECTOR_ID)
.addEventListener('click', ({ target }) => {
target.dispatchEvent(new CustomEvent(MODAL_NAME_SELECTOR_ID, { bubbles: true, detail: 'test' }));
});
document.addEventListener(MODAL_NAME_SELECTOR_ID, ({ target }) => {
const
attrModal = target.hasAttribute('data-modal'),
attrOpen = target.getAttribute('data-open-modal'),
attrClose = target.hasAttribute('data-close-modal');
if (attrModal) {
target.classList.remove(modalShowClass);
}
else if (attrOpen && (modal = document.getElementById(attrOpen))) {
modal.classList.add(modalShowClass);
}
else if (attrClose && (modal = target.closest('[data-modal]'))) {
modal.classList.remove(modalShowClass);
}
});
}
function connectSortable() {
NOVA.css.push(
`table.sortable table {
width: 100%;
}
table.sortable thead {
position: sticky;
top: 0px
}
table.sortable th {
text-transform: uppercase;
white-space: nowrap;
}
table.sortable th:not(.sorttable_nosort) {
cursor: pointer;
}
table.sortable th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):hover:after {
position: absolute;
content: " \\25B4\\25BE";
}
thead, th, td {
text-align: center;
}
table tbody {
counter-reset: sortabletablescope;
}
`);
NOVA.css.push(
`#${MODAL_CONTENT_SELECTOR_ID} table {}
#${MODAL_CONTENT_SELECTOR_ID} thead {
background-color: #555;
z-index: 1;
}
#${MODAL_CONTENT_SELECTOR_ID} th {
padding: 5px 3px;
font-weight: 500;
}
#${MODAL_CONTENT_SELECTOR_ID} tr:nth-child(even) {
background-color: var(--yt-spec-menu-background);
}
#${MODAL_CONTENT_SELECTOR_ID} td .text-overflow-dynamic-ellipsis {
display: block;
max-height: 25vh;
overflow-y: auto;
scrollbar-width: thin;
text-align: left;
font-size: 1.2em;
line-height: 1.4;
padding: 10px 5px;
text-overflow: ellipsis;
word-wrap: break-word;
}
#${MODAL_CONTENT_SELECTOR_ID} tr.author {
}
#${MODAL_CONTENT_SELECTOR_ID} .author > td > .text-overflow-dynamic-ellipsis {
background-color: rgba(0, 47, 144, .2);
}
#${MODAL_CONTENT_SELECTOR_ID} td a {
text-decoration: none;
color: var(--yt-spec-call-to-action);
}`);
return sorttable = { selector_tables: "table.sortable", class_sort_bottom: "sortbottom", class_no_sort: "sorttable_nosort", class_sorted: "sorttable_sorted", class_sorted_reverse: "sorttable_sorted_reverse", id_sorttable_sortfwdind: "sorttable_sortfwdind", id_sorttable_sortfrevind: "sorttable_sortrevind", icon_up: "&nbsp;&#x25B4;", icon_down: "&nbsp;&#x25BE;", regex_non_decimal: /[^0-9\.\-]/g, regex_trim: /^\s+|\s+$/g, regex_any_sorttable_class: /\bsorttable_([a-z0-9]+)\b/, init: function () { arguments.callee.done || (arguments.callee.done = !0, sorttable.forEach(document.querySelectorAll(sorttable.selector_tables), sorttable.makeSortable)) }, insert_thead_in_table: function (t) { 0 === t.getElementsByTagName("thead").length && (thead_element = document.createElement("thead"), thead_element.appendChild(t.rows[0]), t.insertBefore(thead_element, t.firstChild)) }, forEach: function (t, e, r) { if (t) { var s = Object; if (t instanceof Function) s = Function; else { if (t.forEach instanceof Function) return void t.forEach(e, r); "string" == typeof t ? s = String : "number" == typeof t.length && (s = Array) } s.forEach(t, e, r) } }, innerSortFunction: function (t) { if (this.classList.contains(sorttable.class_sorted)) return sorttable.reverse(this.sorttable_tbody), this.classList.remove(sorttable.class_sorted), this.classList.add(sorttable.class_sorted_reverse), this.removeChild(document.getElementById(sorttable.id_sorttable_sortfwdind)), sortrevind = document.createElement("span"), sortrevind.id = sorttable.id_sorttable_sortfrevind, sortrevind.innerHTML = sorttable.icon_up, this.appendChild(sortrevind), void t.preventDefault(); if (this.classList.contains(sorttable.class_sorted_reverse)) return sorttable.reverse(this.sorttable_tbody), this.classList.remove(sorttable.class_sorted_reverse), this.classList.add(sorttable.class_sorted), this.removeChild(document.getElementById(sorttable.id_sorttable_sortfrevind)), sortfwdind = document.createElement("span"), sortfwdind.id = sorttable.id_sorttable_sortfwdind, sortfwdind.innerHTML = sorttable.icon_down, this.appendChild(sortfwdind), void t.preventDefault(); theadrow = this.parentNode, sorttable.forEach(theadrow.childNodes, (function (t) { 1 == t.nodeType && (t.classList.remove(sorttable.class_sorted_reverse), t.classList.remove(sorttable.class_sorted)) })), sortfwdind = document.getElementById(sorttable.id_sorttable_sortfwdind), sortfwdind && sortfwdind.parentNode.removeChild(sortfwdind), sortrevind = document.getElementById(sorttable.id_sorttable_sortfrevind), sortrevind && sortrevind.parentNode.removeChild(sortrevind), this.classList.add(sorttable.class_sorted), sortfwdind = document.createElement("span"), sortfwdind.id = sorttable.id_sorttable_sortfwdind, sortfwdind.innerHTML = sorttable.icon_down, this.appendChild(sortfwdind), row_array = [], col = this.sorttable_columnindex, rows = this.sorttable_tbody.rows; for (var e = 0; e < rows.length; e++)row_array[row_array.length] = [sorttable.getInnerText(rows[e].cells[col]), rows[e]]; row_array.sort(this.sorttable_sortfunction), tb = this.sorttable_tbody; for (e = 0; e < row_array.length; e++)tb.appendChild(row_array[e][1]); t.preventDefault(), delete row_array }, makeSortable: function (t) { if (sorttable.insert_thead_in_table(t), null == t.tHead && (t.tHead = t.getElementsByTagName("thead")[0]), 1 == t.tHead.rows.length) { for (var e = [], r = 0; r < t.rows.length; r++)t.rows[r].classList.contains(sorttable.class_sort_bottom) && (e[e.length] = t.rows[r]); if (e) { if (null == t.tFoot) { var s = document.createElement("tfoot"); t.appendChild(s) } for (r = 0; r < e.length; r++)s.appendChild(e[r]) } var o = t.tHead.rows[0].cells; for (r = 0; r < o.length; r++)o[r].classList.contains(sorttable.class_no_sort) || (mtch = o[r].className.match(sorttable.regex_any_sorttable_class), mtch && (override = mtch[1]), mtch && "function" == typeof sorttable["sort_" + override] ? o[r].sorttable_sortfunction = sorttable["sort_" + override] : o[r].sorttable_sortfunction = sorttable.guessType(t, r), o[r].sorttable_columnindex = r, o[r].sorttable_tbody = t.tBodies[0], o[r].addEventListener("click", sorttable.innerSortFunction)) } }, guessType: function (t, e) { return sorttable.sort_alpha }, getInnerText: function (t) { if (!t) return ""; if (void 0 !== t.dataset && void 0 !== t.dataset.value) return t.dataset.value; if (hasInputs = "function" == typeof t.getElementsByTagName && t.getElementsByTagName("input").length, null != t.getAttribute("sorttable_customkey")) return t.getAttribute("sorttable_customkey"); if (void 0 !== t.textContent && !hasInputs) return t.textContent.replace(sorttable.regex_trim, ""); if (void 0 !== t.innerText && !hasInputs) return t.innerText.replace(sorttable.regex_trim, ""); if (void 0 !== t.text && !hasInputs) return t.text.replace(sorttable.regex_trim, ""); switch (t.nodeType) { case 3: if ("input" == t.nodeName.toLowerCase()) return t.value.replace(sorttable.regex_trim, ""); case 4: return t.nodeValue.replace(sorttable.regex_trim, ""); case 1: case 11: for (var e = "", r = 0; r < t.childNodes.length; r++)e += sorttable.getInnerText(t.childNodes[r]); return e.replace(sorttable.regex_trim, ""); default: return "" } }, reverse: function (t) { for (var e = [], r = 0; r < t.rows.length; r++)e[e.length] = t.rows[r]; for (r = e.length - 1; r >= 0; r--)t.appendChild(e[r]) }, sort_numeric: function (t, e) { var r = parseFloat(t[0].replace(sorttable.regex_non_decimal, "")); isNaN(r) && (r = 0); var s = parseFloat(e[0].replace(sorttable.regex_non_decimal, "")); return isNaN(s) && (s = 0), r - s }, sort_alpha: function (t, e) { return t[0] == e[0] ? 0 : t[0] < e[0] ? -1 : 1 }, shaker_sort: function (t, e) { for (var r = 0, s = t.length - 1, o = !0; o;) { o = !1; for (var a = r; a < s; ++a)if (e(t[a], t[a + 1]) > 0) { var n = t[a]; t[a] = t[a + 1], t[a + 1] = n, o = !0 } if (s-- , !o) break; for (a = s; a > r; --a)if (e(t[a], t[a - 1]) < 0) { n = t[a]; t[a] = t[a - 1], t[a - 1] = n, o = !0 } r++ } } };
}
},
options: {
comments_sort_clear_emoji: {
_tagName: 'input',
label: 'Clear of emoji',
type: 'checkbox',
},
comments_sort_min_words: {
_tagName: 'input',
label: 'Min words count',
'label:zh': '最少字数',
'label:ja': '最小単語数',
'label:es': 'Recuento mínimo de palabras',
'label:pl': 'Minimalna liczba słów',
type: 'number',
title: '0 - disable',
placeholder: '0-10',
min: 0,
max: 10,
value: 2,
'data-dependent': { 'comments_sort_clear_emoji': true },
},
comments_sort_max: {
_tagName: 'input',
label: 'Max comments',
type: 'number',
title: '0 - disable',
placeholder: '0-1200',
min: 0,
max: 1200,
value: 100,
},
comments_sort_blocklist: {
_tagName: 'textarea',
label: 'Words/users blocklist',
'label:zh': '被阻止的单词列表',
'label:ja': 'ブロックされた単語のリスト',
'label:pl': 'Lista blokowanych słów',
title: 'separator: "," or ";" or "new line"',
'title:zh': '分隔器: "," 或 ";" 或 "新队"',
'title:ja': 'セパレータ: "," または ";" または "改行"',
'title:pl': 'separator: "," lub ";" lub "now linia"',
placeholder: 'text1\n@userA',
},
},
});
window.nova_plugins.push({
id: 'player-loop',
title: 'Add repeat (loop) playback button',
'title:zh': '添加循环播放按钮',
'title:ja': 'ループ再生ボタンを追加する',
'title:pl': 'Dodaj przycisk odtwarzania pętli',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
_runtime: user_settings => {
NOVA.waitSelector('#movie_player .ytp-left-controls .ytp-play-button')
.then(container => {
const
SELECTOR_CLASS = 'nova-right-custom-button',
btn = document.createElement('button');
btn.className = `ytp-button ${SELECTOR_CLASS}`;
btn.style.opacity = .5;
btn.style.minWidth = getComputedStyle(container).width || '48px';
btn.title = 'Repeat';
btn.innerHTML =
`<svg viewBox="-6 -6 36 36" height="100%" width="100%">
<g fill="currentColor">
<path d="M 7 7 L 17 7 L 17 10 L 21 6 L 17 2 L 17 5 L 5 5 L 5 11 L 7 11 L 7 7 Z M 7.06 17 L 7 14 L 3 18 L 7 22 L 7 19 L 19 19 L 19 13 L 17 13 L 17 17 L 7.06 17 Z"/>
</g>
</svg>`;
btn.addEventListener('click', toggleLoop);
container.after(btn);
NOVA.waitSelector('#movie_player video')
.then(video => {
video.addEventListener('loadeddata', ({ target }) => {
if (movie_player.classList.contains('ad-showing')) return;
if (btn.style.opacity == 1 && !target.loop) target.loop = true;
if (target.loop) btn.style.opacity = 1;
});
});
if (user_settings.player_loop_hotkey) {
const hotkey = user_settings.player_loop_hotkey;
document.addEventListener('keyup', evt => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
if ((hotkey.length === 1 ? evt.key : evt.code) === hotkey) {
toggleLoop();
}
});
}
function toggleLoop() {
if (!NOVA.videoElement) return console.error('btn > videoElement empty:', NOVA.videoElement);
NOVA.videoElement.loop = !NOVA.videoElement.loop;
btn.style.opacity = NOVA.videoElement.loop ? 1 : .5;
NOVA.showOSD('Loop is ' + Boolean(NOVA.videoElement.loop));
}
});
},
options: {
player_loop_hotkey: {
_tagName: 'select',
label: 'Hotkey',
options: [
{ label: 'none', },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
},
}
});
window.nova_plugins.push({
id: 'player-live-duration',
title: 'Show duration on live video',
'title:zh': '显示直播视频的时长',
'title:ja': 'ライブビデオの表示時間',
'title:pl': 'Pokaż czas trwania wideo na żywo',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
_runtime: user_settings => {
NOVA.waitSelector('#movie_player video')
.then(video => {
video.addEventListener('canplay', () => {
if (movie_player.getVideoData().isLive
&& (el = document.body.querySelector('#movie_player .ytp-chrome-controls .ytp-live .ytp-time-current'))
) {
el.style.cssText = 'display: block !important; margin-right: 5px;';
}
});
NOVA.css.push(
`#movie_player .ytp-chrome-controls .ytp-time-display.ytp-live {
display: flex !important;
}`);
});
},
});
window.nova_plugins.push({
id: 'player-control-autohide',
title: 'Hide player control panel if not hovered',
'title:zh': '播放器上的自动隐藏控件',
'title:ja': 'プレーヤーのコントロールを自動非表示',
'title:pl': 'Ukrywaj elementy w odtwarzaczu',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
desc: 'Hover controls to display it',
'desc:zh': '将鼠标悬停在它上面以显示它',
'desc:ja': 'カーソルを合わせると表示されます',
'desc:pl': 'Najedź, aby wyświetlić',
'plugins-conflict': 'player-control-below',
_runtime: user_settings => {
if (user_settings['player-control-below']) return;
let selectorHover, selectorGradientHide;
switch (user_settings.player_control_autohide_container) {
case 'player':
selectorHover = 'ytd-watch-flexy:not([fullscreen]) #movie_player:hover .ytp-chrome-bottom';
selectorGradientHide = '#movie_player:not(:hover) .ytp-gradient-bottom';
NOVA.waitSelector('#movie_player')
.then(movie_player => {
triggerOnHoverElement({
'element': movie_player,
'callback': function (hovered) {
if (hovered) fixControlFreeze.mouseMoveIntervalId = fixControlFreeze();
else clearInterval(fixControlFreeze.mouseMoveIntervalId);
},
});
});
break;
default:
selectorHover = '.ytp-chrome-bottom:hover';
selectorGradientHide = '#movie_player:has(.ytp-chrome-bottom:not(:hover)) .ytp-gradient-bottom';
break;
}
NOVA.css.push(
`.ytp-chrome-bottom {
opacity: 0;
}
${selectorHover} {
opacity: 1;
}`);
NOVA.css.push(
`${selectorGradientHide} {
opacity: 0;
}`);
if (user_settings.player_control_autohide_show_on_seek) {
let timeout;
document.addEventListener('seeked', ({ target }) => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (el = document.body.querySelector('#movie_player .ytp-chrome-bottom')) {
clearTimeout(timeout);
el.style.opacity = 1;
timeout = setTimeout(() => el.style.removeProperty('opacity'), 1500);
}
});
}
function triggerOnHoverElement({ element = required(), callback = required() }) {
if (!(element instanceof HTMLElement)) return console.error('triggerOnHoverElement:', typeof element);
if (typeof callback !== 'function') return console.error('triggerOnHoverElement callback:', typeof callback);
const isHover = e => e.parentElement.querySelector(':hover') === e;
document.addEventListener('mousemove', function checkHover() {
const hovered = isHover(element);
if (hovered !== checkHover.hovered) {
checkHover.hovered = hovered;
return callback(hovered);
}
});
}
function fixControlFreeze(ms = 2000) {
return setInterval(() => {
if ((NOVA.currentPage === 'watch' || NOVA.currentPage === 'embed')
&& document.visibilityState == 'visible'
&& movie_player.classList.contains('playing-mode')
&& !document.fullscreenElement
) {
movie_player.wakeUpControls();
}
}, ms);
}
},
options: {
player_control_autohide_container: {
_tagName: 'select',
label: 'Hover container',
options: [
{
label: 'player', value: 'player', selected: true,
},
{
label: 'control', value: 'control',
},
],
},
player_control_autohide_show_on_seek: {
_tagName: 'input',
label: 'Show on seeked',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'player-control-below',
title: 'Control panel below the player',
'title:zh': '控制面板位于播放器下方',
'title:ja': 'プレーヤーの下にあるコントロールパネル',
'title:pl': 'Panel sterowania pod odtwarzaczem',
run_on_pages: 'watch, -mobile',
section: 'control-panel',
_runtime: user_settings => {
NOVA.waitSelector('.ytp-chrome-bottom')
.then(async control_panel => {
if ((heightPanel = NOVA.css.get(control_panel, 'height'))
&& (heightProgressBar = NOVA.css.get('.ytp-progress-bar-container', 'height'))
) {
const height = `calc(${heightPanel} + ${heightProgressBar})` || '51px';
let SELECTOR_CONTAINER = 'ytd-watch-flexy:not([fullscreen])';
if (['force', 'offset'].includes(user_settings.player_full_viewport_mode)) {
SELECTOR_CONTAINER += `:not([theater])`;
}
NOVA.css.push(
`
${SELECTOR_CONTAINER} .caption-window {
margin-bottom: 0;
}
${SELECTOR_CONTAINER} .ytp-gradient-bottom {
transform: translateY(${height});
display: block !important;
opacity: 1 !important;
height: ${height} !important;
padding: 0;
background-color: #0f0f0f; 
}
${SELECTOR_CONTAINER} .ytp-chrome-bottom {
transform: translateY(${height});
opacity: 1 !important;
}
${SELECTOR_CONTAINER} .html5-video-player {
overflow: visible;
}
${SELECTOR_CONTAINER} .ytp-player-content.ytp-iv-player-content {
bottom: ${NOVA.css.get('.ytp-player-content.ytp-iv-player-content', 'left') || '12px'};
}
${SELECTOR_CONTAINER} .ytp-tooltip,
${SELECTOR_CONTAINER} .ytp-settings-menu {
transform: translateY(${height});
}
${SELECTOR_CONTAINER}[theater] > #columns,
${SELECTOR_CONTAINER}:not([theater]) #below {
margin-top: ${height} !important;
}
#ytd-player {
overflow: visible !important;
}
`);
if (user_settings['player-float-progress-bar']) {
NOVA.css.push(
`#movie_player.ytp-autohide .ytp-chrome-bottom .ytp-progress-bar-container {
display: none !important;
}`);
}
fixControlFreeze();
}
});
function fixControlFreeze(ms = 2000) {
if (user_settings.player_hide_elements?.includes('time_display')
|| (user_settings['theater-mode'] && ['force', 'offset'].includes(user_settings.player_full_viewport_mode))
) {
return;
}
return setInterval(() => {
if (user_settings['theater-mode']
&& user_settings.player_full_viewport_mode == 'smart'
&& NOVA.css.get(movie_player, 'z-index') != '2020'
&& NOVA.css.get(movie_player, 'position') != 'fixed'
) {
return;
}
if (NOVA.currentPage == 'watch'
&& document.visibilityState == 'visible'
&& movie_player.classList.contains('playing-mode')
&& !document.fullscreenElement
) {
movie_player.wakeUpControls();
}
}, ms);
}
},
});
window.nova_plugins.push({
id: 'player-hide-elements',
title: 'Hide some player buttons/elements',
'title:zh': '隐藏一些播放器按钮/元素',
'title:ja': '一部のプレーヤーのボタン/要素を非表示にする',
'title:pl': 'Ukryj niektóre przyciski/elementy odtwarzacza',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
_runtime: user_settings => {
const SELECTORS = {
'ambient': '#cinematics-container',
'videowall_endscreen': '.videowall-endscreen',
'card_endscreen': '[class^="ytp-ce-"]',
'watch_later_button': '.ytp-chrome-top-buttons button.ytp-watch-later-button',
'info_button': '.ytp-chrome-top-buttons button.ytp-cards-button',
'prev_button': '.ytp-chrome-bottom .ytp-prev-button',
'play_button': '.ytp-chrome-bottom .ytp-play-button',
'next_button': '.ytp-chrome-bottom .ytp-next-button',
'volume_area': '.ytp-chrome-bottom .ytp-volume-area',
'time_display': '.ytp-chrome-bottom .ytp-time-display'
+ (user_settings['time-remaining'] ? ' span > span:not([id])' : ''),
'time_duration_display': '.ytp-chrome-bottom .ytp-time-duration, .ytp-chrome-bottom .ytp-time-separator',
'chapter_container': '.ytp-chrome-bottom .ytp-chapter-container',
'autonav_toggle_button': '.ytp-chrome-bottom button.ytp-button[data-tooltip-target-id="ytp-autonav-toggle-button"]',
'subtitles_button': '.ytp-chrome-bottom button.ytp-subtitles-button',
'settings_button': '.ytp-chrome-bottom button.ytp-settings-button',
'cast_button': '.ytp-chrome-bottom button.ytp-remote-button',
'size_button': '.ytp-chrome-bottom button.ytp-size-button',
'miniplayer_button': '.ytp-chrome-bottom button.ytp-miniplayer-button',
'logo_button': '.ytp-chrome-bottom .yt-uix-sessionlink',
'fullscreen_button': '.ytp-chrome-bottom button.ytp-fullscreen-button',
'brave_jump_button': '.ytp-chrome-bottom button.ytp-jump-button',
};
const SELECTOR_CONTAINER = '#movie_player';
const toArray = a => Array.isArray(a) ? a : [a];
let list = [];
toArray(user_settings.player_hide_elements)
.forEach(el => (data = SELECTORS[el]) && list.push(`${SELECTOR_CONTAINER} ${data}`));
if (list.length) {
NOVA.css.push(
list.join(',\n') + ` {
display: none !important;
}`);
}
},
options: {
player_hide_elements: {
_tagName: 'select',
label: 'Items',
title: '[Ctrl+Click] to select several',
'title:zh': '[Ctrl+Click] 选择多个',
'title:ja': '「Ctrl+Click」して、いくつかを選択します',
'title:pl': 'Ctrl+kliknięcie, aby zaznaczyć kilka',
multiple: null,
required: true,
size: 10,
options: [
{
label: 'ambient', value: 'ambient',
},
{
label: 'videowall (thumbs)', value: 'videowall_endscreen',
},
{
label: 'card', value: 'card_endscreen',
},
{
label: 'watch-later', value: 'watch_later_button',
},
{
label: 'info (embed)', value: 'info_button',
},
{
label: 'prev', value: 'prev_button',
},
{
label: 'play / stop live', value: 'play_button',
},
{
label: 'next', value: 'next_button',
},
{
label: 'jump (for Brave)', value: 'brave_jump_button',
title: 'Seek backwards/forward 10 seconds'
},
{
label: 'volume', value: 'volume_area',
},
{
label: 'time', value: 'time_display',
},
{
label: 'time duration', value: 'time_duration_display',
},
{
label: 'chapter', value: 'chapter_container',
},
{
label: 'autoplay next', value: 'autonav_toggle_button',
},
{
label: 'subtitles', value: 'subtitles_button',
},
{
label: 'settings', value: 'settings_button',
},
{
label: 'cast', value: 'cast_button',
},
{
label: 'size', value: 'size_button',
},
{
label: 'miniplayer', value: 'miniplayer_button',
},
{
label: 'logo (embed)', value: 'logo_button',
},
{
label: 'fullscreen', value: 'fullscreen_button',
},
],
},
}
});
window.nova_plugins.push({
id: 'player-hotkeys-focused',
title: 'Player shortcuts always active',
'title:zh': '播放器热键始终处于活动状态',
'title:ja': 'プレーヤーのホットキーは常にアクティブです',
'title:pl': 'Klawisze skrótów dla graczy zawsze aktywne',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
_runtime: user_settings => {
document.addEventListener('keyup', evt => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
setPlayerFocus(evt.target);
if (user_settings.hotkeys_disable_numpad && evt.code.startsWith('Numpad')) {
evt.preventDefault();
evt.stopPropagation();
evt.stopImmediatePropagation();
}
});
document.addEventListener('click', evt => evt.isTrusted && setPlayerFocus(evt.target));
function setPlayerFocus(target) {
if (['input', 'textarea', 'select'].includes(target.localName) || target.isContentEditable) return;
movie_player.focus({ preventScroll: true });
}
},
options: {
hotkeys_disable_numpad: {
_tagName: 'input',
label: 'Disable numpad',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'player-progress-bar-color',
title: 'Player progress bar color',
'title:zh': '播放器进度条颜色',
'title:ja': 'プレーヤーのプログレスバーの色',
'title:pl': 'Kolor paska postępu gracza',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
_runtime: user_settings => {
NOVA.css.push(
`.ytp-swatch-background-color {
background-color: ${user_settings.player_progress_bar_color || '#f00'} !important;
}`);
},
options: {
player_progress_bar_color: {
_tagName: 'input',
type: 'color',
value: '#0089ff',
label: 'Color',
'label:zh': '颜色',
'label:ja': '色',
'label:pl': 'Kolor',
},
}
});
window.nova_plugins.push({
id: 'player-float-progress-bar',
title: 'Float player progress bar',
'title:zh': '浮动播放器进度条',
'title:ja': 'フロートプレーヤーのプログレスバー',
'title:pl': 'Pływający pasek postępu odtwarzacza',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
_runtime: user_settings => {
if (NOVA.currentPage == 'embed') {
if (
document.URL.includes('live_stream')
|| ['0', 'false'].includes(NOVA.queryURL.get('controls'))
) {
return;
}
}
const
SELECTOR_CONTAINER = '#movie_player.ytp-autohide',
SELECTOR_ID = 'nova-player-float-progress-bar',
SELECTOR = '#' + SELECTOR_ID,
CHAPTERS_MARK_WIDTH_PX = '2px',
CHP_JUMP_TOGGLE_CLASS_VALUE = 'nova-chapters-jump-active';
NOVA.waitSelector(`${user_settings['player-control-autohide'] ? '#movie_player' : SELECTOR_CONTAINER} video`)
.then(video => {
const
container = insertFloatBar({
'init_container': movie_player,
'z_index': Math.max(NOVA.css.get('.ytp-chrome-bottom', 'z-index'), 59)
}),
bufferEl = document.getElementById(`${SELECTOR_ID}-buffer`),
progressEl = document.getElementById(`${SELECTOR_ID}-progress`);
renderChapters.init(video);
video.addEventListener('progress', () => container.classList.add('transition'), { capture: true, once: true });
video.addEventListener('loadeddata', resetBar);
document.addEventListener('yt-navigate-finish', resetBar);
video.addEventListener('timeupdate', function () {
if (notInteractiveToRender()) return;
if (!isNaN(this.duration)) {
progressEl.style.transform = `scaleX(${this.currentTime / this.duration})`;
}
});
renderBuffer.apply(video);
video.addEventListener('progress', renderBuffer.bind(video));
video.addEventListener('seeking', renderBuffer.bind(video));
function renderBuffer() {
if (notInteractiveToRender()) return;
if (!isNaN(this.duration) && this.buffered?.length) {
bufferEl.style.transform = `scaleX(${this.buffered.end(this.buffered.length - 1) / this.duration})`;
}
}
function resetBar() {
container.style.display = movie_player.getVideoData().isLive ? 'none' : 'inherit';
container.classList.remove('transition');
bufferEl.style.transform = 'scaleX(0)';
progressEl.style.transform = 'scaleX(0)';
container.classList.add('transition');
renderChapters.init(video);
}
function notInteractiveToRender() {
return (document.visibilityState == 'hidden'
|| movie_player.getVideoData().isLive
);
}
if (user_settings.player_float_progress_bar_hotkey) connectChapterJump();
});
function insertFloatBar({ init_container = movie_player, z_index = 60 }) {
if (!(init_container instanceof HTMLElement)) {
return console.error('vid not HTMLElement:', init_container);
}
return document.getElementById(SELECTOR_ID) || (function () {
init_container.insertAdjacentHTML('beforeend',
`<div id="${SELECTOR_ID}" class="">
<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.get('.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%;
height: var(--height);
visibility: hidden;
}
${SELECTOR_CONTAINER} ${SELECTOR} {
visibility: visible;
}
${SELECTOR_CONTAINER} ${SELECTOR}.transition [id|=${SELECTOR_ID}] {
transition: transform 200ms linear;
}
${SELECTOR}-progress, ${SELECTOR}-buffer {
width: 100%;
height: 100%;
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:first-child:not([time$="0:00"]), 
${SELECTOR}-chapters > span:not(:first-child) {
border-left: ${CHAPTERS_MARK_WIDTH_PX} solid rgba(255,255,255,.7);
}
${SELECTOR}-chapters > span {
position: relative;
}
${SELECTOR}-chapters > span > span {
position: absolute;
}
.${CHP_JUMP_TOGGLE_CLASS_VALUE} {
visibility: visible !important;
--height: 20px !important;
}
.${CHP_JUMP_TOGGLE_CLASS_VALUE}:not(:hover) {
--bg-color: coral !important;
}
.${CHP_JUMP_TOGGLE_CLASS_VALUE} ${SELECTOR}-chapters span:hover {
border-left: ${CHAPTERS_MARK_WIDTH_PX} solid cornflowerblue !important;
cursor: pointer;
background-color: rgba(255,255,255,.7);
}`);
if (user_settings['player-control-autohide']) {
switch (user_settings.player_control_autohide_container) {
case 'player':
NOVA.css.push(
`${SELECTOR_CONTAINER}:not(:hover) ${SELECTOR} {
visibility: visible !important;
}`);
break;
case 'control':
NOVA.css.push(
`.ytp-chrome-bottom:not(:hover) ~ ${SELECTOR} {
visibility: visible !important;
}`);
break;
}
if (user_settings.player_control_autohide_show_on_seek) {
NOVA.css.push(
`[style*="opacity: 1"] ~ ${SELECTOR} {
visibility: hidden !important;
}`);
}
}
return document.getElementById(SELECTOR_ID);
})();
}
function connectChapterJump() {
let hotkeyActivated;
document.addEventListener('keydown', showChapterSwitch);
document.addEventListener('keyup', showChapterSwitch);
function showChapterSwitch(evt) {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if ((el = document.getElementById(SELECTOR_ID))
&& el.querySelector('span[time]')
) {
switch (evt.type) {
case 'keydown':
const hotkey = user_settings.player_float_progress_bar_hotkey.length === 1 ? evt.key : evt.code;
if (user_settings.player_float_progress_bar_hotkey == hotkey && !hotkeyActivated) {
el.classList.add(CHP_JUMP_TOGGLE_CLASS_VALUE);
hotkeyActivated = true;
}
break;
case 'keyup':
if (hotkeyActivated) {
hotkeyActivated = false;
el.classList.remove(CHP_JUMP_TOGGLE_CLASS_VALUE);
}
break;
}
}
}
document.getElementById(SELECTOR_ID)
.addEventListener('click', ({ target }) => {
if (!(secTime = target.getAttribute('time'))) return;
const sec = NOVA.formatTimeOut.hmsToSec(secTime);
if (typeof movie_player.seekBy === 'function') {
movie_player.seekTo(sec);
}
else if (NOVA.videoElement) {
NOVA.videoElement.currentTime = sec;
}
}, { capture: true });
}
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':
let chaptersContainer;
await NOVA.waitUntil(() => (
chaptersContainer = document.body.querySelector('.ytp-chapters-container'))
&& chaptersContainer?.children.length > 1
, 1000);
this.renderChaptersMarkers(vid.duration) || this.from_div(chaptersContainer);
break;
}
},
from_description(duration = required()) {
if (isNaN(duration)) return console.error('duration isNaN:', duration);
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}`, { destroy_after_page_leaving: true })
.then(() => this.renderChaptersMarkers(duration));
NOVA.waitSelector(`#comments #comment #comment-content ${selectorTimestampLink}`, { destroy_after_page_leaving: true })
.then(() => {
if (document.body.querySelector(`${SELECTOR}-chapters > span[time]`)) return;
this.renderChaptersMarkers(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) * 100 / progressContainerWidth) + '%';
chaptersOut.append(newChapter);
}
},
renderChaptersMarkers(duration = required()) {
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) * 100 / duration) + '%';
if (chapter.title) newChapter.title = chapter.title;
newChapter.setAttribute('time', chapter.time);
chaptersContainer && chaptersContainer.append(newChapter);
});
return chapterList;
},
};
},
options: {
player_float_progress_bar_height: {
_tagName: 'input',
label: 'Height',
'label:zh': '高度',
'label:ja': '身長',
'label:pl': 'Wysokość',
type: 'number',
title: 'in pixels',
placeholder: 'px',
min: 1,
max: 9,
value: 3,
},
player_float_progress_bar_opacity: {
_tagName: 'input',
label: 'Opacity',
'label:zh': '不透明度',
'label:ja': '不透明度',
'label:pl': 'Przejrzystość',
type: 'number',
placeholder: '0-1',
step: .05,
min: 0,
max: 1,
value: .7,
},
player_float_progress_bar_hotkey: {
_tagName: 'select',
label: 'Hotkey to chapters jump (by click)',
'label:zh': '章节跳转热键(点击)',
'label:ja': '章にジャンプするホットキー (クリックによる)',
options: [
{ label: 'none', },
{ label: 'ShiftL', value: 'ShiftLeft' },
{ label: 'ShiftR', value: 'ShiftRight' },
{ label: 'CtrlL', value: 'ControlLeft' },
{ label: 'CtrlR', value: 'ControlRight' },
{ label: 'AltL', value: 'AltLeft' },
{ label: 'AltR', value: 'AltRight' },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
},
}
});
window.nova_plugins.push({
id: 'player-quick-buttons',
title: 'Add custom player buttons',
'title:zh': 'カスタム プレーヤー ボタンを追加する',
'title:ja': 'カスタム プレーヤー ボタンを追加する',
'title:pl': 'Dodaj własne przyciski odtwarzacza',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
_runtime: user_settings => {
const
SELECTOR_BTN_CLASS_NAME = 'nova-right-custom-button',
SELECTOR_BTN = '.' + SELECTOR_BTN_CLASS_NAME;
NOVA.waitSelector('#movie_player .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: .3em;
padding: 5px 9px;
color: white;
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', 'Picture in Picture (PiP)');
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, height } = NOVA.aspectRatio.sizeToFit({
'srcWidth': NOVA.videoElement.videoWidth,
'srcHeight': NOVA.videoElement.videoHeight,
});
url = new URL(
document.head.querySelector('link[itemprop="embedUrl"][href]')?.href
|| (location.origin + '/embed/' + movie_player.getVideoData().video_id)
);
if (currentTime = Math.trunc(NOVA.videoElement?.currentTime)) url.searchParams.set('start', currentTime);
url.searchParams.set('autoplay', 1);
url.searchParams.set('popup', true);
NOVA.openPopup({ 'url': url.href, 'width': width, 'height': height });
});
container.prepend(popupBtn);
}
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 black;
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: white;
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'),
context = canvas.getContext('2d'),
mime = `image/${user_settings.player_buttons_custom_screenshot || 'png'}`;
canvas.width = NOVA.videoElement.videoWidth;
canvas.height = NOVA.videoElement.videoHeight;
context.drawImage(NOVA.videoElement, 0, 0, canvas.width, canvas.height);
canvas.title = 'Click to save';
if (textString = document.body.querySelector('.caption-window')?.innerText) {
context.font = `bold ${Math.trunc(canvas.height * .05)}px Arial`;
context.textAlign = 'buttom';
context.textBaseline = 'middle';
context.fillStyle = user_settings.player_buttons_custom_screenshot_subtitle_color || 'white';
context.strokeStyle = user_settings.player_buttons_custom_screenshot_subtitle_shadow_color || 'black';
context.lineWidth = canvas.height / 1000;
let h = canvas.height * .9;
textString
.split('\n')
.forEach((text, i) => {
const
metrics = context.measureText(text),
lineHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent,
textWidth = context.measureText(text).width,
w = (canvas.width / 2) - (textWidth / 2);
context.fillText(text, w, h);
context.strokeText(text, w, h);
h += lineHeight;
});
}
try {
canvas.toBlob(blob => {
container.href = URL.createObjectURL(blob);
if (user_settings.player_buttons_custom_screenshot_to_clipboard && navigator.clipboard?.write) {
navigator.clipboard.write([new ClipboardItem({ [mime]: blob })]);
}
}, mime);
} catch (error) {
}
if (user_settings.player_buttons_custom_screenshot_to_clipboard && navigator.clipboard?.write) {
return NOVA.showOSD('Screenshot copied to clipboard');
}
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, mime);
container.remove();
}, { capture: true });
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, mime = 'image/png') {
const
downloadLink = document.createElement('a'),
downloadFileName =
[
movie_player.getVideoData().title
.replace(/[\\/:*?"<>|]+/g, '')
.replace(/\s+/g, ' ').trim(),
(time = NOVA.formatTimeOut.HMS.abbr(NOVA.videoElement.currentTime)) ? `(${time})` : '',
]
.join(' ');
downloadLink.href = canvas.toDataURL(mime).replace(mime, 'image/octet-stream');
downloadLink.download = `${downloadFileName}.${user_settings.player_buttons_custom_screenshot || '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 = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id,
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) {
const imageBlob = await response.blob();
const img = new Image();
img.src = URL.createObjectURL(imageBlob);
img.addEventListener('load', () => {
NOVA.openPopup({
'url': imgUrl,
'width': img.width,
'height': img.height,
});
});
document.body.style.removeProperty('cursor');
break;
}
}
});
container.prepend(thumbBtn);
}
if (user_settings.player_buttons_custom_items?.includes('rotate')) {
const
hotkey = user_settings.player_buttons_custom_hotkey_rotate || 'KeyR',
rotateBtn = document.createElement('button');
rotateBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
rotateBtn.setAttribute('tooltip', `Rotate video (${hotkey.replace('Key', '')})`);
rotateBtn.style.cssText = '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', rotateVideo);
document.addEventListener('keyup', evt => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
if ((hotkey.length === 1 ? evt.key : evt.code) === hotkey) {
rotateVideo();
}
});
function rotateVideo() {
let angle = NOVA.extractAsNum.int(NOVA.videoElement.style.transform) || 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': 'scaleX(1.3333)' },
{ '4:3': 'scaleX(.75)' },
{ '9:16': 'scaleX(1.777777778)' },
{ '21:9': 'scaleY(.7168)' },
{ 'default': 'scale(1)' },
,],
genTooltip = (key = 0) => `next ` + 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 = 'default';
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 = 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',
qualitySpan = 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.get('.ytp-progress-bar', 'z-index'), 31)};
}
html[data-cast-api-enabled] ${SELECTOR_QUALITY_LIST} {
margin: 0;
padding: 0;
bottom: 3.3em;
}
.ytp-big-mode .ytp-menuitem-toggle-checkbox {
width: 3.5em;
height: 1.6em;
}
${SELECTOR_QUALITY}:not(:hover) ${SELECTOR_QUALITY_LIST} {
display: none;
}
${SELECTOR_QUALITY_LIST} li {
cursor: pointer;
white-space: nowrap;
line-height: 1.4;
background-color: rgba(28, 28, 28, 0.9);
margin: .3em 0;
padding: .5em 3em;
border-radius: .3em;
color: white;
}
${SELECTOR_QUALITY_LIST} li .quality-menu-item-label-badge {
position: absolute;
right: 1em;
width: 1.7em;
}
${SELECTOR_QUALITY_LIST} li.active { background-color: #720000; }
${SELECTOR_QUALITY_LIST} li.disable { color: #666; }
${SELECTOR_QUALITY_LIST} li:hover:not(.active) { background-color: #c00; }`);
qualityContainerBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME} ${SELECTOR_QUALITY_CLASS_NAME}`;
qualitySpan.id = SELECTOR_QUALITY_TITLE_ID;
qualitySpan.textContent = qualityFormatList[movie_player.getPlaybackQuality()]?.label || '[N/A]'
listQuality.id = SELECTOR_QUALITY_LIST_ID;
movie_player.addEventListener('onPlaybackQualityChange', quality => {
document.getElementById(SELECTOR_QUALITY_TITLE_ID)
.textContent = qualityFormatList[quality]?.label || '[N/A]'
});
qualityContainerBtn.prepend(qualitySpan);
qualityContainerBtn.append(listQuality);
container.prepend(qualityContainerBtn);
fillQualityMenu();
NOVA.videoElement?.addEventListener('loadeddata', 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) {
const labelBadge = document.createElement('span');
labelBadge.className = 'quality-menu-item-label-badge';
labelBadge.textContent = badge;
qualityItem.append(labelBadge);
}
if (movie_player.getPlaybackQuality() == quality) {
qualityItem.className = 'active';
}
else {
const maxWidth = (NOVA.currentPage == 'watch'
|| (user_settings['embed-popup'] && NOVA.queryURL.has('popup'))
)
? screen.width
: window.innerWidth;
if ((NOVA.extractAsNum.int(qualityData.label) || 0) <= (maxWidth * 1.3)) {
qualityItem.addEventListener('click', () => {
movie_player.setPlaybackQualityRange(quality, quality);
}, { capture: true });
}
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);
let clockInterval;
if (user_settings.player_buttons_custom_clock_fullcreen) {
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) setIntervalClock();
else {
clearInterval(clockInterval);
clockEl.textContent = '';
}
});
}
else setIntervalClock();
function setIntervalClock() {
clockInterval = setInterval(() => {
if (document.visibilityState == 'hidden'
|| movie_player.classList.contains('ytp-autohide')
) {
return;
}
const formatLength = user_settings.player_buttons_custom_clock_seconds ? 8 : 5;
const time = new Date().toTimeString().slice(0, formatLength);
clockEl.textContent = time;
}, 1000);
}
}
if (user_settings.player_buttons_custom_items?.includes('range-speed')) {
const
speedSlider = document.createElement('input'),
SELECTOR_RANGE_CLASS_NAME = 'nova-range-speed-input',
SELECTOR_RANGE = '.' + SELECTOR_RANGE_CLASS_NAME;
NOVA.css.push(
`${SELECTOR_RANGE}[type="range"] {
height: 100%;
}`);
speedSlider.className = `${SELECTOR_BTN_CLASS_NAME} ${SELECTOR_RANGE_CLASS_NAME}`;
speedSlider.title = 'Playback Rate';
speedSlider.type = 'range';
speedSlider.min = speedSlider.step = +user_settings.rate_step || .1;
speedSlider.max = user_settings.range_speed_unlimit ? +user_settings.rate_default : 2;
speedSlider.value = NOVA.videoElement.playbackRate;
updateTitleForSpeedSlider(NOVA.videoElement.playbackRate);
NOVA.videoElement.addEventListener('ratechange', function () {
speedSlider.value = this.playbackRate;
updateTitleForSpeedSlider(this.playbackRate);
});
speedSlider.addEventListener('change', ({ target }) => playerRate(target.value));
speedSlider.addEventListener('wheel', evt => {
evt.preventDefault();
const rate = NOVA.videoElement.playbackRate + (speedSlider.step * Math.sign(evt.wheelDelta));
playerRate(rate);
speedSlider.value = rate;
}, { capture: true });
container.prepend(speedSlider);
function playerRate(rate) {
if (!user_settings.range_speed_unlimit && rate > 2) return;
NOVA.videoElement.playbackRate = (+rate).toFixed(2);
updateTitleForSpeedSlider(rate);
}
function updateTitleForSpeedSlider(rate) {
speedSlider.title = `Speed (${rate})`;
speedSlider.setAttribute('tooltip', `Speed (${rate})`);
}
}
if (user_settings.player_buttons_custom_items?.includes('toggle-speed')) {
const
speedBtn = document.createElement('a'),
hotkey = user_settings.player_buttons_custom_hotkey_toggle_speed || 'KeyA',
defaultRateText = '1x',
genTooltip = () => `Switch to ${NOVA.videoElement.playbackRate}>${speedBtn.textContent} (${hotkey.replace('Key', '')})`;
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 (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
if ((hotkey.length === 1 ? evt.key : evt.code) === hotkey) {
switchRate();
}
});
speedBtn.addEventListener('click', switchRate);
NOVA.videoElement.addEventListener('ratechange', function () {
speedBtn.setAttribute('tooltip', genTooltip());
if (!user_settings['video-rate']) NOVA.showOSD(this.playbackRate + 'x');
});
function switchRate() {
if (Object.keys(rateOrig).length) {
playerRate.set(rateOrig);
rateOrig = {};
speedBtn.innerHTML = defaultRateText;
}
else {
rateOrig = (typeof movie_player === 'object'
&& (NOVA.videoElement.playbackRate % .25 === 0)
&& (NOVA.videoElement.playbackRate <= 2))
? { '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' : '';
}
}
}
});
},
options: {
player_buttons_custom_items: {
_tagName: 'select',
label: 'Buttons',
'label:zh': '纽扣',
'label:ja': 'ボタン',
'label:pl': 'Przyciski',
title: '[Ctrl+Click] to select several',
'title:zh': '[Ctrl+Click] 选择多个',
'title:ja': '「Ctrl+Click」して、いくつかを選択します',
'title:pl': 'Ctrl+kliknięcie, aby zaznaczyć kilka',
multiple: null,
required: true,
size: 7,
options: [
{
label: 'clock', value: 'clock',
},
{
label: 'quick quality', value: 'quick-quality',
'label:zh': '质量',
'label:ja': '品質',
'label:pl': 'jakość',
},
{
label: 'range speed', value: 'range-speed',
},
{
label: 'toggle speed', value: 'toggle-speed',
'label:zh': '切换速度',
'label:ja': 'トグル速度',
'label:pl': 'szybkość',
},
{
label: 'card-switch', value: 'card-switch',
},
{
label: 'screenshot', value: 'screenshot',
'label:zh': '截屏',
'label:ja': 'スクリーンショット',
},
{
label: 'picture-in-picture', value: 'picture-in-picture',
'label:pl': 'obraz w obrazie',
},
{
label: 'popup', value: 'popup',
'label:zh': '弹出式播放器',
'label:ja': 'ポップアッププレーヤー',
'label:pl': 'w okienku',
},
{
label: 'rotate', value: 'rotate',
'label:zh': '旋转',
'label:ja': '回転する',
'label:pl': 'obróć',
},
{
label: 'aspect-ratio', value: 'aspect-ratio',
},
{
label: 'watch later', value: 'watch-later',
},
{
label: 'preview cover', value: 'thumbnail',
'label:zh': '缩略图',
'label:ja': 'サムネイル',
'label:pl': 'miniaturka',
},
],
},
player_buttons_custom_hotkey_toggle_speed: {
_tagName: 'select',
label: 'Hotkey toggle speed',
'label:zh': '热键切换速度',
'label:ja': '速度を切り替えるためのホットボタン',
'label:pl': 'Skrót przełączania prędkości',
options: [
{ label: 'none', value: false },
{ label: 'A', value: 'KeyA', selected: true },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
'data-dependent': { 'player_buttons_custom_items': ['toggle-speed'] },
},
player_buttons_custom_hotkey_rotate: {
_tagName: 'select',
label: 'Hotkey rotate',
options: [
{ label: 'none', value: false },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR', selected: true },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
'data-dependent': { 'player_buttons_custom_items': ['rotate'] },
},
player_buttons_custom_card_switch: {
_tagName: 'select',
label: 'Default card state',
options: [
{
label: 'show', value: false, selected: true,
},
{
label: 'hide', value: true,
},
],
'data-dependent': { 'player_buttons_custom_items': ['card-switch'] },
},
player_buttons_custom_screenshot: {
_tagName: 'select',
label: 'Screenshot format',
options: [
{
label: 'png', value: 'png', selected: true,
},
{
label: 'jpg', value: 'jpg',
},
{
label: 'webp', value: 'webp',
},
],
'data-dependent': { 'player_buttons_custom_items': ['screenshot'] },
},
player_buttons_custom_screenshot_to_clipboard: {
_tagName: 'input',
label: 'Screenshot copy to clipboard',
type: 'checkbox',
'data-dependent': { 'player_buttons_custom_items': ['screenshot'] },
},
player_buttons_custom_screenshot_subtitle_color: {
_tagName: 'input',
type: 'color',
value: '#ffffff',
label: 'Screenshot subtitle color',
'data-dependent': { 'player_buttons_custom_items': ['screenshot'] },
},
player_buttons_custom_screenshot_subtitle_shadow_color: {
_tagName: 'input',
type: 'color',
value: '#000000',
label: 'Screenshot subtitle shadow color',
'data-dependent': { 'player_buttons_custom_items': ['screenshot'] },
},
range_speed_unlimit: {
_tagName: 'input',
label: 'Range speed unlimit',
type: 'checkbox',
'data-dependent': { 'player_buttons_custom_items': ['range-speed'] },
},
range_speed_unlimit: {
_tagName: 'input',
label: 'Range speed unlimit',
type: 'checkbox',
'data-dependent': { 'player_buttons_custom_items': ['range-speed'] },
},
player_buttons_custom_clock_seconds: {
_tagName: 'input',
label: 'Clock show seconds',
type: 'checkbox',
'data-dependent': { 'player_buttons_custom_items': ['clock'] },
},
player_buttons_custom_clock_fullcreen: {
_tagName: 'input',
label: 'Clock only fullscreen',
type: 'checkbox',
'data-dependent': { 'player_buttons_custom_items': ['clock'] },
},
}
});
window.nova_plugins.push({
id: 'save-channel-state',
title: 'Add button "Save params for the channel"',
'title:zh': '특정 채널에 저장',
'title:ja': '特定のチャンネル用に保存',
'title:pl': 'Zapisz dla określonego kanału',
run_on_pages: 'watch, embed',
section: 'control-panel',
_runtime: user_settings => {
const
SELECTOR_BTN_ID = 'nova-channels-state',
SELECTOR_BTN = '#' + SELECTOR_BTN_ID,
SELECTOR_BTN_CLASS_NAME = 'nova-right-custom-button',
SELECTOR_BTN_LIST_ID = SELECTOR_BTN_CLASS_NAME + '-list',
SELECTOR_BTN_LIST = '#' + SELECTOR_BTN_LIST_ID,
SELECTOR_BTN_TITLE_ID = SELECTOR_BTN_CLASS_NAME + '-title';
NOVA.waitSelector('#movie_player .ytp-right-controls')
.then(container => {
initStyles();
NOVA.runOnPageLoad(async () => {
if (NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed') {
await NOVA.storage_obj_manager.initStorage();
if (btn = document.getElementById(SELECTOR_BTN_ID)) {
btn.append(genList());
}
else {
const btn = document.createElement('button');
btn.id = SELECTOR_BTN_ID;
btn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME}`;
btn.title = 'Save channel state';
const btnTitle = document.createElement('span');
btnTitle.id = SELECTOR_BTN_TITLE_ID;
btnTitle.style.display = 'flex';
btnTitle.innerHTML =
`<svg width="100%" height="100%" viewBox="0 0 36 36">
<g fill="currentColor">
<path d="M23.4 24.2c-.3.8-1.1 1.4-2 1.4-.9 0-1.7-.6-2-1.4H9.3c-.3 0-.6-.3-.6-.6v-.3c0-.3.3-.6.6-.6h10.1c.3-.9 1.1-1.5 2.1-1.5s1.8.6 2.1 1.5h3.2c.3 0 .6.3.6.6v.3c0 .3-.3.6-.6.6h-3.4zm-7.7-5.3c-.3.9-1.1 1.5-2.1 1.5s-1.8-.6-2.1-1.5H9.3c-.3 0-.6-.3-.6-.6V18c0-.3.3-.6.6-.6h2.2c.3-.8 1.1-1.4 2.1-1.4s1.8.6 2.1 1.4h11.1c.3 0 .6.3.6.6v.3c0 .3-.3.6-.6.6H15.7zm7.9-5.4c-.3.8-1.1 1.4-2.1 1.4-.9 0-1.7-.6-2.1-1.4H9.3c-.3 0-.6-.3-.6-.6v-.3c0-.3.3-.6.6-.6h10.1c.3-.9 1.1-1.6 2.1-1.6s1.9.7 2.1 1.6h3.1c.3 0 .6.3.6.6v.3c0 .3-.3.6-.6.6h-3.1z" />
</g>
</svg>`;
btn.prepend(btnTitle);
btn.append(genList());
container.prepend(btn);
}
btnTitleStateUpdate(Boolean(NOVA.storage_obj_manager.read()));
}
});
});
function btnTitleStateUpdate(state) {
document.getElementById(SELECTOR_BTN_TITLE_ID)
.style.setProperty('opacity', state ? 1 : .3);
}
function genList() {
const ul = document.createElement('ul');
ul.id = SELECTOR_BTN_LIST_ID;
let listItem = [];
listItem.push({
name: 'subtitles',
getCurrentState: () => {
movie_player.toggleSubtitlesOn();
return true;
},
customApply: () => {
document.addEventListener('playing', () => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
movie_player.toggleSubtitlesOn();
}, { capture: true, once: true });
},
});
if (user_settings['video-quality']) {
listItem.push({ name: 'quality', getCurrentState: movie_player.getPlaybackQuality });
}
if (user_settings['video-rate']) {
listItem.push({ name: 'speed', getCurrentState: () => NOVA.videoElement.playbackRate });
}
if (user_settings['video-volume']) {
listItem.push({ name: 'volume', getCurrentState: () => Math.round(movie_player.getVolume()) });
}
if (user_settings['player-resume-playback']) {
listItem.push({ name: 'ignore-playback', label: 'unsave playback time', getCurrentState: () => true });
}
if (user_settings['player-loop']) {
listItem.push({ name: 'loop' });
}
if (user_settings['transcript']) {
listItem.push({ name: 'transcript' });
}
if (user_settings['video-zoom']) {
listItem.push({
name: 'zoom', getCurrentState: () => NOVA.extractAsNum.float(
document.body.querySelector('.html5-video-container').style.transform
)
});
}
listItem.forEach(async element => {
const storage = NOVA.storage_obj_manager._getParam(element.name);
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `checkbox-${element.name}`;
checkbox.checked = Boolean(storage);
checkbox.className = 'ytp-menuitem-toggle-checkbox';
const li = document.createElement('li');
li.innerHTML =
`<label for="checkbox-${element.name}">
${element.label || element.name} <span>${storage || ''}</span>
</label>`;
li.title = storage ? `Currently stored value ${storage}` : 'none';
if (Boolean(storage) && element.hasOwnProperty('customApply') && typeof element.customApply === 'function') {
element.customApply();
}
checkbox.addEventListener('change', () => {
let state;
if (checkbox.checked && (state = element.hasOwnProperty('getCurrentState') ? element.getCurrentState() : true)) {
NOVA.storage_obj_manager.save({ [element.name]: state });
}
else {
NOVA.storage_obj_manager.remove(element.name);
}
li.title = state ? `Currently stored value ${state}` : 'none';
li.querySelector('span').textContent = state || '';
btnTitleStateUpdate(Boolean(state));
});
li.prepend(checkbox);
ul.append(li);
});
if (user_settings['time-jump']) {
const
SLIDER_LABEL = 'skip into',
SLIDER_STORAGE_NAME = 'skip-into',
storage = +NOVA.storage_obj_manager._getParam(SLIDER_STORAGE_NAME);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = 0;
slider.max = 120;
slider.step = 1;
slider.value = storage || 0;
const li = document.createElement('li');
li.innerHTML =
`<label for="checkbox-${SLIDER_STORAGE_NAME}">
${SLIDER_LABEL} <span>${storage || ''}</span>
</label>`;
li.title = 'Simple alternative SponsorBlock';
slider.addEventListener('change', sliderChange);
slider.addEventListener('input', sliderChange);
slider.addEventListener('wheel', evt => {
evt.preventDefault();
evt.target.value = +evt.target.value + Math.sign(evt.wheelDelta);
sliderChange(evt);
});
li.prepend(slider);
ul.append(li);
function sliderChange({ target }) {
if (state = +target.value) {
NOVA.storage_obj_manager.save({ [SLIDER_STORAGE_NAME]: +target.value });
}
else {
NOVA.storage_obj_manager.remove(SLIDER_STORAGE_NAME);
}
li.title = state ? `Currently stored value ${state}` : 'none';
li.querySelector('span').textContent = state || '';
btnTitleStateUpdate(Boolean(state));
}
}
return ul;
}
function initStyles() {
NOVA.css.push(
SELECTOR_BTN + ` {
overflow: visible !important;
position: relative;
text-align: center !important;
vertical-align: top;
font-weight: bold;
}
.ytp-left-controls {
overflow: visible !important;
}
${SELECTOR_BTN_LIST} {
position: absolute;
bottom: 2.5em !important;
left: -2.2em;
list-style: none;
padding-bottom: 1.5em !important;
z-index: calc(${+NOVA.css.get('.ytp-progress-bar', 'z-index')} + 1);
}
html[data-cast-api-enabled] ${SELECTOR_BTN_LIST} {
margin: 0;
padding: 0;
bottom: 3.3em;
}
${SELECTOR_BTN}:not(:hover) ${SELECTOR_BTN_LIST} {
display: none;
}
${SELECTOR_BTN_LIST} li {
cursor: pointer;
white-space: nowrap;
line-height: 1.4;
background-color: rgba(28, 28, 28, .9);
margin: .3em 0;
padding: .5em 1em;
border-radius: .3em;
color: white;
text-align: left !important;
display: grid;
grid-template-columns: auto auto;
align-items: center;
justify-content: start;
}
${SELECTOR_BTN_LIST} li label {
cursor: pointer;
padding-left: 5px;
}
${SELECTOR_BTN_LIST} li.active { background-color: #720000; }
${SELECTOR_BTN_LIST} li.disable { color: #666; }
${SELECTOR_BTN_LIST} li:not(:hover) { opacity: .8; }
${SELECTOR_BTN_LIST} li span:not(:empty):before { content: '('; }
${SELECTOR_BTN_LIST} li span:not(:empty):after { content: ')'; }
${SELECTOR_BTN_LIST} [type="checkbox"] {
appearance: none;
outline: none;
cursor: pointer;
}
${SELECTOR_BTN_LIST} [type="checkbox"]:checked {
background-color: #f00;
}
${SELECTOR_BTN_LIST} [type="checkbox"]:checked:after {
left: 20px;
background-color: white;
}`);
}
},
});
window.nova_plugins.push({
id: 'time-remaining',
title: 'Remaining time',
'title:zh': '剩余时间',
'title:ja': '余日',
'title:pl': 'Pozostały czas',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
desc: 'Remaining time until the end of the video',
'desc:zh': '距离视频结束的剩余时间',
'desc:ja': 'ビデオの終わりまでの残り時間',
'desc:pl': 'Czas pozostały do końca filmu',
_runtime: user_settings => {
const SELECTOR_ID = 'nova-player-time-remaining';
let selectorOutAfter;
switch (user_settings.time_remaining_position) {
case 'description': selectorOutAfter = '#title h1'; break;
default: selectorOutAfter = '.ytp-time-duration, ytm-time-display .time-display-content'; break;
}
NOVA.waitSelector(selectorOutAfter)
.then(container => {
NOVA.waitSelector('video')
.then(video => {
video.addEventListener('timeupdate', setRemaining.bind(video));
video.addEventListener('ratechange', setRemaining.bind(video));
video.addEventListener('ended', () => insertToHTML({ 'container': container }));
document.addEventListener('yt-navigate-finish', () => insertToHTML({ 'container': container }));
});
function setRemaining() {
if (isNaN(this.duration)
|| movie_player.getVideoData().isLive
|| (NOVA.currentPage == 'embed' && document.URL.includes('live_stream'))
|| document.visibilityState == 'hidden'
|| ((user_settings.time_remaining_position != 'description') && movie_player.classList.contains('ytp-autohide'))
) return;
const
currentTime = Math.trunc(this.currentTime),
duration = Math.trunc(this.duration),
delta_ = duration - currentTime,
getPercent = percentage_type_left => {
const
floatRound = pt => (this.duration > 3600)
? pt.toFixed(2)
: (this.duration > 1500)
? pt.toFixed(1)
: Math.round(pt),
calcPercentage = percentage_type_left
? delta_ * 100 / duration
: currentTime * 100 / duration;
return floatRound(calcPercentage) + '%';
},
getTimeLeft = () => NOVA.formatTimeOut.HMS.digit(delta_),
getTimeLeftByRate = () => '-' + NOVA.formatTimeOut.HMS.digit(delta_ / this.playbackRate);
const text = user_settings.time_remaining_format
.replace(/duration(\*speed)|left(\*speed|%)?|done(%)?|'([^']|'')*'/g, partPattern => {
let out;
switch (partPattern) {
case 'left*speed': out = getTimeLeftByRate(); break;
case 'left': out = getTimeLeft(); break;
case 'left%': out = getPercent('L'); break;
case 'done': out = currentTime; break;
case 'done%': out = getPercent(); break;
case 'duration*speed': out = NOVA.formatTimeOut.HMS.digit(duration / this.playbackRate); break;
case 'duration': out = duration; break;
}
return out;
});
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 () {
const el = document.createElement('span');
el.id = SELECTOR_ID;
container.after(el);
return el;
})())
.textContent = ' ' + text;
}
});
},
options: {
time_remaining_format: {
_tagName: 'select',
label: 'Time pattern',
options: [
{
label: 'left*speed', value: 'left*speed', selected: true,
},
{
label: 'left*speed (done%)', value: 'left*speed (done%)',
},
{
label: 'left*speed (left%)', value: 'left*speed (left%)',
},
{
label: 'left', value: 'left',
},
{
label: 'left%', value: 'left%',
},
{
label: 'done%', value: 'done%',
},
{
label: 'left/left*speed', value: 'left/left*speed (done%)',
},
{
label: 'left*speed/duration*speed (done%)', value: 'left*speed/duration*speed (done%)',
},
],
},
time_remaining_position: {
_tagName: 'select',
label: 'Render position',
options: [
{
label: 'player', value: 'player', selected: true,
},
{
label: 'description', value: 'description',
},
],
},
}
});
window.nova_plugins.push({
id: 'embed-show-control-force',
title: 'Force enable control panel (for embed)',
'title:zh': '埋め込みでコントロール パネルを強制的に有効にする',
'title:ja': '强制启用嵌入的控制面板',
'title:pl': 'Wymuś włączenie panelu sterowania w osadzeniu',
run_on_pages: 'embed',
section: 'control-panel',
_runtime: user_settings => {
const href = location.href.replace(/&amp;/g, '&');
if (['0', 'false'].includes(NOVA.queryURL.get('controls', href))) {
NOVA.updateUrl(NOVA.queryURL.remove('controls', href));
}
},
});
window.nova_plugins.push({
id: 'time-jump',
title: 'Jump time/chapter',
'title:zh': '时间跳跃',
'title:ja': 'タイムジャンプ',
'title:pl': 'Skok czasowy',
run_on_pages: 'watch, embed, -mobile',
section: 'control-panel',
desc: 'Use to skip the intro or ad inserts',
'desc:zh': '用于跳过介绍或广告插入',
'desc:ja': 'イントロや広告挿入をスキップするために使用します',
'desc:pl': 'Służy do pomijania wstępu lub wstawek reklamowych',
_runtime: user_settings => {
if (user_settings.time_jump_title_offset) addTitleOffset();
NOVA.waitSelector('#movie_player video')
.then(video => {
let chapterList;
video.addEventListener('loadeddata', () => chapterList = []);
doubleKeyPressListener(timeLeap, user_settings.time_jump_hotkey);
function timeLeap() {
if (movie_player.getVideoData().isLive
|| (NOVA.currentPage == 'embed' && document.URL.includes('live_stream'))
) return;
if (chapterList !== null && !chapterList?.length) {
chapterList = NOVA.getChapterList(movie_player.getDuration()) || null;
}
const
currentTime = movie_player.getCurrentTime(),
nextChapterIndex = chapterList?.findIndex(c => c.sec > currentTime),
separator = ' • ';
let msg;
if (chapterList?.length
&& nextChapterIndex !== -1
) {
const nextChapterData = chapterList?.find(({ sec }) => sec >= currentTime);
seekTime(nextChapterData.sec + .5);
msg = nextChapterData.title + separator + nextChapterData.time;
}
else {
seekTime(+user_settings.time_jump_step + currentTime);
msg = `+${user_settings.time_jump_step} sec` + separator + NOVA.formatTimeOut.HMS.digit(currentTime);
}
NOVA.showOSD(msg);
}
function seekTime(sec) {
if (typeof movie_player.seekBy === 'function') {
movie_player.seekTo(sec);
}
else if (NOVA.videoElement) {
NOVA.videoElement.currentTime = sec;
}
else {
const errorText = '[time-jump] > "seekTime" detect player error';
console.error(errorText);
throw errorText;
}
}
});
function addTitleOffset() {
NOVA.css.push(
`.ytp-tooltip-text:after {
content: attr(data-before);
color: #ffcc00;
}`);
NOVA.waitSelector('.ytp-progress-bar')
.then(progressContainer => {
if (tooltipEl = document.body.querySelector('.ytp-tooltip-text')) {
progressContainer.addEventListener('mousemove', () => {
if (movie_player.getVideoData().isLive
|| (NOVA.currentPage == 'embed' && document.URL.includes('live_stream'))
) return;
const
cursorTime = NOVA.formatTimeOut.hmsToSec(tooltipEl.textContent),
offsetTime = cursorTime - NOVA.videoElement?.currentTime,
sign = (offsetTime >= 1) ? '+' : (Math.sign(offsetTime) === -1) ? '-' : '';
tooltipEl.setAttribute('data-before', ` ${sign + NOVA.formatTimeOut.HMS.digit(offsetTime)}`);
});
progressContainer.addEventListener('mouseleave', () => tooltipEl.removeAttribute('data-before'));
}
});
}
function doubleKeyPressListener(callback = required(), keyNameFilter = required()) {
let
pressed,
isDoublePress,
lastWhich,
lastPressed = keyNameFilter;
document.addEventListener('keyup', keyPress);
function keyPress(evt) {
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
pressed = (keyNameFilter.length === 1) || ['Control', 'Shift'].includes(keyNameFilter) ? evt.key : evt.code;
if (isDoublePress && (lastWhich === evt.which) && (pressed === lastPressed)) {
isDoublePress = false;
if (callback && typeof callback === 'function') return callback(evt);
}
else {
isDoublePress = true;
setTimeout(() => isDoublePress = false, 500);
}
if (!keyNameFilter) lastPressed = pressed;
lastWhich = evt.which;
}
}
if (user_settings['save-channel-state']) {
NOVA.waitSelector('#movie_player video')
.then(video => {
NOVA.runOnPageLoad(async () => {
const
CACHE_PREFIX = 'nova-resume-playback-time',
getCacheName = () => CACHE_PREFIX + ':' + (NOVA.queryURL.get('v') || movie_player.getVideoData().video_id);
if ((NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed')
&& !+sessionStorage.getItem(getCacheName())
&& (!NOVA.queryURL.has('t') && !NOVA.queryURL.getHashParam('t'))
&& (userSeek = await NOVA.storage_obj_manager.getParam('skip-into'))
) {
video.addEventListener('playing', timeLeapInto.apply(video, [userSeek]), { capture: true, once: true });
}
});
});
}
else if (+user_settings.skip_into_sec && (!NOVA.queryURL.has('t') && !NOVA.queryURL.getHashParam('t'))) {
NOVA.waitSelector('#movie_player video')
.then(video => {
NOVA.runOnPageLoad(() => {
if (NOVA.currentPage == 'watch') {
video.addEventListener('playing', timeLeapInto.bind(video, user_settings.skip_into_sec), { capture: true, once: true });
}
});
});
}
function timeLeapInto(time_seek = 10) {
if (!time_seek && !user_settings.skip_into_sec_in_music && NOVA.isMusic()) return;
const
CACHE_PREFIX = 'resume-playback-time',
getCacheName = () => CACHE_PREFIX + ':' + (NOVA.queryURL.get('v') || movie_player.getVideoData().video_id);
if (user_settings['player-resume-playback']
&& (saveTime = +sessionStorage.getItem(getCacheName()))
&& (saveTime > (this.duration - 3))
) return;
if ((isNaN(this.duration) || this.duration > 30)
&& (this.currentTime < +time_seek)
) {
this.currentTime = +time_seek;
}
}
},
options: {
time_jump_step: {
_tagName: 'input',
label: 'Step time',
'label:ja': 'ステップ時間',
'label:zh': '步骤时间',
'label:pl': 'Krok czasowy',
type: 'number',
title: 'In seconds',
placeholder: 'sec',
min: 3,
max: 300,
value: 30,
},
time_jump_hotkey: {
_tagName: 'select',
label: 'Hotkey (double click)',
'label:zh': '热键(双击)',
'label:ja': 'Hotkey (ダブルプレス)',
'label:pl': 'Klawisz skrótu (podwójne kliknięcie)',
title: 'by default【Ctrl + Arrows】',
options: [
{ label: 'Shift (any)', value: 'Shift' },
{ label: 'ShiftL', value: 'ShiftLeft' },
{ label: 'ShiftR', value: 'ShiftRight' },
{ label: 'Ctrl (any)', value: 'Control' },
{ label: 'CtrlL', value: 'ControlLeft' },
{ label: 'CtrlR', value: 'ControlRight', selected: true },
{ label: 'AltL', value: 'AltLeft' },
{ label: 'AltR', value: 'AltRight' },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
},
time_jump_title_offset: {
_tagName: 'input',
label: 'Show time offset on progress bar',
'label:zh': '在进度条中显示时间偏移',
'label:ja': 'プログレスバーに時間オフセットを表示する',
'label:pl': 'Pokaż przesunięcie czasu na pasku postępu',
type: 'checkbox',
title: 'Time offset from current playback time',
'title:zh': '与当前播放时间的时间偏移',
'title:ja': '現在の再生時間からの時間オフセット',
'title:pl': 'Przesunięcie czasu względem bieżącego czasu odtwarzania',
},
skip_into_sec: {
_tagName: 'input',
label: 'Start playback at',
'label:zh': '设置开始时间',
'label:ja': '開始時刻を設定',
'label:pl': 'Ustaw czas rozpoczęcia',
type: 'number',
title: 'in sec / 0 - disable',
placeholder: '1-30',
step: 1,
min: 0,
max: 30,
value: 0,
},
skip_into_sec_in_music: {
_tagName: 'input',
label: 'Apply for music genre',
type: 'checkbox',
'data-dependent': { 'skip_into_sec': "!0" },
},
}
});
window.nova_plugins.push({
id: 'download-video',
title: 'Download video',
'title:zh': '下载视频',
'title:ja': 'ビデオをダウンロードする',
run_on_pages: 'watch, -mobile',
section: 'control-panel',
_runtime: user_settings => {
NOVA.waitSelector('#movie_player .ytp-right-controls')
.then(container => {
const
SELECTOR_BTN_CLASS_NAME = 'nova-video-download',
SELECTOR_BTN = '.' + SELECTOR_BTN_CLASS_NAME,
containerBtn = document.createElement('a'),
SELECTOR_BTN_LIST_ID = SELECTOR_BTN_CLASS_NAME + '-list',
SELECTOR_BTN_LIST = '#' + SELECTOR_BTN_LIST_ID,
dropdownMenu = document.createElement('ul'),
SELECTOR_BTN_TITLE_ID = SELECTOR_BTN_CLASS_NAME + '-title',
SELECTOR_BTN_TITLE = '#' + SELECTOR_BTN_TITLE_ID,
dropdownSpan = document.createElement('span');
NOVA.runOnPageLoad(() => {
if (NOVA.currentPage == 'watch') {
containerBtn.removeEventListener('click', generateMenu);
dropdownMenu.innerHTML = '';
containerBtn.addEventListener('click', generateMenu, { capture: true, once: true });
}
});
NOVA.css.push(
`${SELECTOR_BTN_TITLE} {
display: block;
height: inherit;
}
${SELECTOR_BTN_TITLE}[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: .3em;
padding: 5px 9px;
color: white;
font-weight: bold;
white-space: nowrap;
}
html[data-cast-api-enabled] ${SELECTOR_BTN_TITLE}[tooltip]:hover::before {
font-weight: normal;
}`);
NOVA.css.push(
SELECTOR_BTN + ` {
overflow: visible !important;
position: relative;
text-align: center !important;
vertical-align: top;
font-weight: bold;
}
${SELECTOR_BTN}:hover { color: #66afe9 !important; }
${SELECTOR_BTN}:active { color: #2196f3 !important; }
${SELECTOR_BTN_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.get('.ytp-progress-bar', 'z-index'), 31)};
}
html[data-cast-api-enabled] ${SELECTOR_BTN_LIST} {
margin: 0;
padding: 0;
bottom: 3.3em;
}
${SELECTOR_BTN}:not(:hover) ${SELECTOR_BTN_LIST} {
display: none;
}
${SELECTOR_BTN_LIST} li {
cursor: pointer;
white-space: nowrap;
line-height: 1.4;
background-color: rgba(28, 28, 28, .9);
margin: .1em 0;
padding: .5em 2em;
border-radius: .3em;
color: white;
}
${SELECTOR_BTN_LIST} li:hover { background-color: #c00; }`);
containerBtn.className = `ytp-button ${SELECTOR_BTN_CLASS_NAME} ${SELECTOR_BTN_CLASS_NAME}`;
dropdownSpan.id = SELECTOR_BTN_TITLE_ID;
dropdownSpan.setAttribute('tooltip', 'Nova video download 🡇');
dropdownSpan.innerHTML =
`<svg viewBox="0 0 120 120" width="100%" height="100%" style="scale: .6;">
<g fill="currentColor">
<path d="M96.215 105h-72.18c-3.33 0-5.94-2.61-5.94-5.94V75.03c0-3.33 2.61-5.94 5.94-5.94 3.33 0 5.94 2.61 5.94 5.94v18h60.03v-18c0-3.33 2.61-5.94 5.94-5.94 3.33 0 5.94 2.61 5.94 5.94v24.03c.27 3.33-2.34 5.94-5.67 5.94Zm-32.4-34.47c-2.07 1.89-5.4 1.89-7.56 0l-18.72-17.19c-2.07-1.89-2.07-4.86 0-6.84 2.07-1.98 5.4-1.89 7.56 0l8.91 8.19V20.94c0-3.33 2.61-5.94 5.94-5.94 3.33 0 5.94 2.61 5.94 5.94V54.6l8.91-8.19c2.07-1.89 5.4-1.89 7.56 0 2.07 1.89 2.07 4.86 0 6.84l-18.54 17.28Z" />
</g>
</svg>`;
dropdownMenu.id = SELECTOR_BTN_LIST_ID;
containerBtn.prepend(dropdownSpan);
containerBtn.append(dropdownMenu);
container.prepend(containerBtn);
async function generateMenu() {
if (menuList = document.getElementById(SELECTOR_BTN_LIST_ID)) {
APIs.videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id;
const dropdownSpanOrig = dropdownSpan.outerHTML;
dropdownSpan.textContent = '🕓';
let downloadVideoList = [];
switch (user_settings.download_video_mode) {
case 'cobalt':
downloadVideoList = APIs.Cobalt();
break;
case 'loader.to':
downloadVideoList = APIs.loaderTo();
break;
case 'third_party_methods':
downloadVideoList = APIs.third_party();
break;
case 'direct':
downloadVideoList = await APIs.getInternalListUrls()
break;
}
downloadVideoList
.filter(i => i?.codec)
.forEach((item, idx) => {
const menuItem = document.createElement('li');
if (item.quality) {
menuItem.textContent = `${item.codec} / ${item.quality}`;
}
else menuItem.textContent = item.codec;
menuItem.addEventListener('click', () => {
if (item.custom_fn && typeof item.custom_fn === 'function') {
item.custom_fn(item);
}
else if (item.link_new_tab) {
window.open(item.link_new_tab, '_blank');
}
else {
downloadFile(item.link);
}
}, { capture: true });
menuList.append(menuItem);
});
dropdownSpan.innerHTML = dropdownSpanOrig;
}
}
});
const APIs = {
getQualityAvailableList() {
const qualityList = {
highres: 4320,
hd2880: 2880,
hd2160: 2160,
hd1440: 1440,
hd1080: 1080,
hd720: 720,
large: 480,
medium: 360,
small: 240,
tiny: 144,
};
return movie_player.getAvailableQualityData().map(i => qualityList[i.quality]);
},
Cobalt() {
const qualityAvailableList = this.getQualityAvailableList();
let vidlist = [];
['h264', 'vp9']
.forEach(codec => {
qualityAvailableList.forEach(quality => {
vidlist.push(...[
{
codec: codec.toLocaleUpperCase(),
quality: quality,
'data': { 'vCodec': codec, 'vQuality': String(quality) },
'custom_fn': CobaltAPI,
},
]);
});
});
return [
...vidlist,
{ codec: 'mp3', data: { isAudioOnly: true, cCodec: 'mp3' }, custom_fn: CobaltAPI },
{ codec: 'ogg', data: { isAudioOnly: true, cCodec: 'ogg' }, custom_fn: CobaltAPI },
{ codec: 'wav', data: { isAudioOnly: true, cCodec: 'wav' }, custom_fn: CobaltAPI },
{ codec: 'opus', data: { isAudioOnly: true, cCodec: 'opus' }, custom_fn: CobaltAPI },
];
async function CobaltAPI(item) {
const dlink = await fetch('https://co.wuk.sh/api/json',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
url: encodeURI('https://www.youtube.com/watch?v=' + APIs.videoId),
filenamePattern: 'basic',
disableMetadata: true,
isNoTTWatermark: true,
...item.data,
}),
})
.then(response => response.json())
.then(json => json.url)
.catch(error => {
console.warn(`Cobalt API: failed fetching: ${error}`)
});
if (!dlink) return console.debug('CobaltAPI empty dlink:', dlink);
downloadFile(dlink);
}
},
loaderTo() {
const genLink = format => `https://loader.to/api/button/?url=${APIs.videoId}&f=${format}&color=0af`;
const qualityAvailableList = this.getQualityAvailableList()?.filter(i => i > 240);
let vidlist = [];
['MP4']
.forEach(codec => {
qualityAvailableList.forEach(quality => {
vidlist.push({
'codec': codec.toLocaleUpperCase(),
'quality': quality,
'link': genLink(quality),
'custom_fn': openPopup,
});
});
});
return [
...vidlist,
{ codec: 'WEBM', quality: '4K', link: genLink('4k'), custom_fn: openPopup },
{ codec: 'WEBM', quality: '8K', link: genLink('8k'), custom_fn: openPopup },
{ codec: 'MP3', link: genLink('mp3'), custom_fn: openPopup },
{ codec: 'M4A', link: genLink('m4a'), custom_fn: openPopup },
{ codec: 'WEBM', link: genLink('webm'), custom_fn: openPopup },
{ codec: 'AAC', link: genLink('aac'), custom_fn: openPopup },
{ codec: 'FLAC', link: genLink('flac'), custom_fn: openPopup },
{ codec: 'OPUS', link: genLink('opus'), custom_fn: openPopup },
{ codec: 'OGG', link: genLink('ogg'), custom_fn: openPopup },
{ codec: 'WAV', link: genLink('wav'), custom_fn: openPopup },
];
function openPopup(item) {
NOVA.openPopup({ 'url': item.url, width: 420, height: 80 });
}
},
third_party() {
return [
{
quality: 'mp3,mp4',
codec: 'yt-download.org',
link_new_tab: 'https://yt-download.org/api/widgetv2?url=https://www.youtube.com/watch?v=' + APIs.videoId,
},
{
quality: 'mp3,mp4',
codec: 'Y2Mate.tools',
link_new_tab: 'https://www.y2mate.com/youtube/' + APIs.videoId,
},
{
quality: 'mp3,mp4',
codec: 'TubeMP3.to',
link_new_tab: 'https://tubemp3.to/' + APIs.videoId,
},
{
quality: 'mp3,mp4',
codec: 'yloader.ws',
link_new_tab: 'https://yloader.ws/yturlmp4/' + APIs.videoId,
},
{
quality: 'mp3,mp4,ogg',
codec: 'yt5s.com',
link_new_tab: 'https://yt5s.com/watch?v=' + APIs.videoId,
},
{
quality: 'mp3,mp4,ogg',
codec: 'x2download.app',
link_new_tab: 'https://x2download.app/watch?v=' + APIs.videoId,
},
{
quality: 'mp3,mp4,ogg',
codec: 'savefrom.net',
link_new_tab: 'https://savefrom.net/https://www.youtube.com/watch?v=' + APIs.videoId,
},
{
quality: 'mp3,mp4',
codec: 'yt1s.ltd',
codec: 'yt1s.com',
link_new_tab: 'https://yt1s.com/watch?v=' + APIs.videoId,
},
{
quality: 'MP3,MP4,M4A,MP4,MKV',
codec: 'clipconverter.cc',
link_new_tab: 'https://www.clipconverter.cc/3/?url=https://www.youtube.com/watch?v=' + APIs.videoId,
},
{
quality: 'mp3',
codec: 'conv2.be',
link_new_tab: 'https://conv2.be/watch?v=' + APIs.videoId,
},
{
quality: 'mp3',
codec: 'YTMP3X.com',
link_new_tab: 'https://ytmp3x.com/' + APIs.videoId,
},
];
},
async getInternalListUrls() {
let decryptSigFn;
const
URL = NOVA.queryURL.set({ 'pbj': 1 }),
headers = {
'x-youtube-client-name': 1,
'x-youtube-client-version': window.ytcfg.data_.INNERTUBE_CONTEXT_CLIENT_VERSION,
};
if (token = window.ytcfg?.data_?.ID_TOKEN) {
headers['x-youtube-identity-token'] = token;
};
return await fetch(URL, { 'headers': headers })
.then(res => res.json())
.then(data => data?.find(i => i.playerResponse?.streamingData)?.playerResponse.streamingData)
.then(async streamingData => {
console.debug('streamingData', streamingData);
const vidListData = [...streamingData.formats, ...streamingData.adaptiveFormats];
decryptSigFn = vidListData.find(o => (o.cipher || o.signatureCipher)) && await getDecryptSigFn();
return vidListData
.map(obj => {
if (dict = parseQuery(obj.cipher || obj.signatureCipher)) {
obj.url = `${dict.url}&${dict.sp}=${encodeURIComponent(decsig(dict.s))}`;
}
if (obj.url) {
let label = obj.mimeType?.match(/codecs="(.*?)"/i)[1].split('.')[0].toLocaleUpperCase();
if (!obj.mimeType?.includes('mp4a') && !obj.mimeType?.includes('audio')) {
label += ' / No Sound';
}
obj.mimeType?.includes('audio')
? obj.qualityLabel = fmtBitrate(obj.bitrate)
: obj.qualityLabel += ' ' + fmtSize(obj.contentLength);
return {
'codec': label,
'quality': obj.qualityLabel,
'link_new_tab': obj.url,
};
}
})
})
.catch(error => {
console.error('Error get vids:', error);
throw error;
});
function parseQuery(str) {
return str && Object.fromEntries(
str
.split(/&/)
.map(c => {
const [key, ...v] = c.split('=');
return [key, decodeURIComponent(v.join('='))];
}) || []
);
}
async function getDecryptSigFn() {
const
basejsUrl = getBasejs() || document.querySelector('script[src$="/base.js"]')?.src,
basejsBlob = await fetch(basejsUrl);
return parseDecSig(await basejsBlob.text());
function getBasejs() {
if (typeof ytplayer === 'object'
&& (endpoint = ytplayer.config?.assets?.js
|| ytplayer.web_player_context_config?.jsUrl)
) {
return 'https://' + location.host + endpoint;
}
}
function parseDecSig(text_content) {
const escapeRegExp = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
try {
if (text_content.startsWith('var script')) {
const obj = {};
eval(text_content);
text_content = obj.innerHTML;
}
const fnNameResult = /=([a-zA-Z0-9\$_]+?)\(decodeURIComponent/.exec(text_content);
const fnName = fnNameResult[1];
const _argNameFnBodyResult = new RegExp(escapeRegExp(fnName) + '=function\\((.+?)\\){((.+)=\\2.+?)}')
.exec(text_content);
const [_, argname, fnBody] = _argNameFnBodyResult;
const helperNameResult = /;([a-zA-Z0-9$_]+?)\..+?\(/.exec(fnBody);
const helperName = helperNameResult[1];
const helperResult = new RegExp('var ' + escapeRegExp(helperName) + '={[\\s\\S]+?};').exec(text_content);
const helper = helperResult[0];
return new Function([argname], helper + '\n' + fnBody);
} catch (error) {
console.error('parseDecSig', error);
}
}
}
function decsig(_sig) {
const sig = eval("(" + decryptSigFn + ") (\"" + _sig + "\")");
return sig;
}
},
};
function downloadFile(url = required()) {
const d = document.createElement('a');
d.style.display = 'none';
d.download = (movie_player.getVideoData().title
.replace(/[\\/:*?"<>|]+/g, '')
.replace(/\s+/g, ' ').trim()) + '.mp4';
d.href = url;
document.body.append(d);
d.click();
d.remove();
}
function fmtBitrate(size) {
return fmtSize(size, ['kbps', 'Mbps', 'Gbps'], 1000);
}
function fmtSize(size, units = ['kB', 'MB', 'GB'], divisor = 1024) {
size = Math.abs(+size);
if (size === 0) return 'n/a';
size /= divisor;
for (let i = 0; i < units.length; ++i) {
if (size < 10) return Math.round(size * 100) / 100 + units[i];
else if (size < 100) return Math.round(size * 10) / 10 + units[i];
else if (size < 1000 || i == (units.length - 1)) return Math.round(size) + units[i];
}
}
function convertSizeToBytes(size) {
const units = {
B: 1,
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
};
const regex = /^(\d+(?:\.\d+)?)\s*([A-Z]+)$/i;
const match = size.match(regex);
if (!match) return 0;
const value = parseFloat(match[1]);
const unit = match[2].toUpperCase();
if (!units.hasOwnProperty(unit)) return 0;
return value * units[unit];
}
},
options: {
download_video_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'Cobalt', value: 'cobalt', selected: true,
},
{
label: 'loader.to', value: 'loader.to',
},
{
label: 'multi 3rd party', value: 'third_party_methods',
},
{
label: 'direct', value: 'direct',
},
],
},
}
});
window.nova_plugins.push({
id: 'auto-likes',
title: 'Auto-like',
run_on_pages: 'watch, -mobile',
section: 'details-buttons',
_runtime: user_settings => {
if (user_settings['details-buttons']
&& (user_settings.details_buttons_hide?.includes('all') || user_settings.details_buttons_hide.includes('like_dislike'))
) {
return;
}
const SELECTOR_LIKE_BTN = 'ytd-watch-metadata #actions like-button-view-model button';
NOVA.waitSelector('#movie_player video')
.then(video => {
video.addEventListener('loadeddata', () => {
if (user_settings.auto_likes_for_subscribed
|| movie_player.getVideoData().isLive
) {
Timer.disable = true;
}
else Timer.reset.bind(Timer)
});
video.addEventListener('playing', Timer.start.bind(Timer, video.playbackRate));
video.addEventListener('pause', Timer.pause.bind(Timer));
video.addEventListener('timeupdate', function () {
if (Timer.disable || isNaN(this.duration)) return;
if ((+Timer.progressTime / this.duration) > ((Math.trunc(user_settings.auto_likes_percent) / 100) || .8)) {
Timer.disable = true;
setLike();
NOVA.showOSD('Auto-like is activation');
}
});
});
NOVA.runOnPageLoad(async () => {
if (NOVA.currentPage != 'watch') return;
NOVA.waitSelector(`${SELECTOR_LIKE_BTN}[aria-pressed="true"]`, { destroy_after_page_leaving: true })
.then(() => {
if (Timer.disable) return;
Timer.disable = true;
NOVA.showOSD('Auto-like is deactivated');
});
if (user_settings.auto_likes_for_subscribed) {
NOVA.waitSelector('#subscribe-button [subscribed]', { destroy_after_page_leaving: true })
.then(() => {
Timer.disable = false;
NOVA.showOSD('Auto-like is enable');
});
}
});
function setLike() {
const likeBtn = document.body.querySelector(SELECTOR_LIKE_BTN);
if (!isLiked()) likeBtn.click();
function isLiked() {
return likeBtn.getAttribute('aria-pressed') == 'true';
}
}
const Timer = {
progressTime: 0,
start(delta = 1) {
if (this.disable) return;
this.timer = setInterval(function () {
Timer.progressTime += 1 * delta;
}, 1000);
},
pause() {
if (typeof this.timer === 'number') clearInterval(this.timer);
},
reset() {
this.disable = false;
this.progressTime = 0;
},
};
},
options: {
auto_likes_percent: {
_tagName: 'input',
label: 'Watch threshold in %',
'label:zh': '观察阈值(%)',
'label:ja': '監視しきい値 (%)',
'label:pl': 'Próg oglądania w%',
type: 'number',
title: '10-90%',
title: 'Percentage of views at which a video is liked',
'title:zh': '视频在时间进度后被点赞',
'title:ja': '時間の経過後にビデオが「いいね!」される',
placeholder: '%',
step: 5,
min: 10,
max: 90,
value: 80,
},
auto_likes_for_subscribed: {
_tagName: 'input',
label: 'Only for subscribed',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'video-date-format',
title: 'Date format display',
'title:zh': '显示日期格式',
'title:ja': '日付形式の表示',
run_on_pages: 'watch, -mobile',
section: 'details',
opt_api_key_warn: true,
_runtime: user_settings => {
const
CACHE_PREFIX = 'nova-video-date:',
DATE_SELECTOR_ID = 'nova-video-published-date';
NOVA.runOnPageLoad(async () => {
if (NOVA.currentPage == 'watch') {
await NOVA.waitUntil(() => typeof movie_player === 'object', 1000);
NOVA.waitSelector('#title h1', { destroy_after_page_leaving: true })
.then(el => setVideoDate(el));
}
});
function setVideoDate(container = required()) {
const videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id;
if ((storage = sessionStorage.getItem(CACHE_PREFIX + videoId))
&& storage.format == user_settings.video_date_format
) {
return insertToHTML({ 'text': storage.date, 'container': container });
}
NOVA.request.API({
request: 'videos',
params: {
'id': videoId,
'part': 'snippet,liveStreamingDetails'
+ (user_settings.video_view_count ? ',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 => {
let outList = [];
if (user_settings.video_view_count && item.statistics.viewCount) {
switch (user_settings.video_view_count) {
case 'friendly':
outList.push(NOVA.numberFormat.friendly(item.statistics.viewCount), 'views');
break;
default:
outList.push(NOVA.numberFormat.abbr(item.statistics.viewCount), 'views');
break;
}
}
if (item.liveStreamingDetails) {
if (movie_player.getVideoData().isLive || item.snippet.liveBroadcastContent == 'live') {
outList.push('Active Livestream',
NOVA.dateFormat.apply(new Date(item.liveStreamingDetails.actualStartTime), [user_settings.video_date_format])
);
}
else if (item.liveStreamingDetails.actualEndTime) {
const
timeStart = new Date(item.liveStreamingDetails.actualStartTime),
timeEnd = new Date(item.liveStreamingDetails.actualEndTime),
sameDate = timeStart.getDay() === timeEnd.getDay();
outList.push(
document.body.querySelector('ytd-watch-flexy')?.playerData?.videoDetails?.isLiveContent
? 'Streamed'
: 'Premiered'
);
if (!sameDate) outList.push('from');
outList.push(NOVA.dateFormat.apply(timeStart, [user_settings.video_date_format]));
if (!sameDate) {
outList.push('until',
NOVA.dateFormat.apply(timeEnd, [user_settings.video_date_format])
);
}
}
else if (item.snippet.liveBroadcastContent == 'upcoming') {
outList.push('Scheduled',
NOVA.dateFormat.apply(new Date(item.liveStreamingDetails.scheduledStartTime), [user_settings.video_date_format])
);
}
}
else if (item.snippet.publishedAt) {
const publishedDate = new Date(item.snippet.publishedAt);
if (user_settings.video_date_format == 'ago') {
outList.push(NOVA.formatTimeOut.ago(publishedDate), 'ago');
}
else {
outList.push(NOVA.dateFormat.apply(publishedDate, [user_settings.video_date_format]));
}
}
if (outList.length) {
insertToHTML({ 'text': outList.join(' '), 'container': container });
sessionStorage.setItem(CACHE_PREFIX + videoId, JSON.stringify({
'date': outList.join(' '),
'format': user_settings.video_date_format
}));
}
});
});
function insertToHTML({ text = '', container = required() }) {
if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
(document.getElementById(DATE_SELECTOR_ID) || (() => {
const el = document.createElement('span');
el.id = DATE_SELECTOR_ID;
el.className = 'style-scope yt-formatted-string bold';
el.style.cssText = 'font-size: 1.35rem; line-height: 2rem; font-weight:400;';
container.after(el);
return el;
})())
.textContent = text;
}
}
},
options: {
video_view_count: {
_tagName: 'select',
label: 'Show views count format',
options: [
{ label: 'disable', value: false, },
{ label: '9.9K', value: 'abbr', selected: true },
{ label: '9,999', value: 'friendly' },
],
},
video_date_format: {
_tagName: 'select',
label: 'Date pattern',
options: [
{ label: 'ago', value: 'ago' },
{ label: 'January 20, 1999', value: 'MMMM D, YYYY' },
{ label: '20 Jan 1999', value: 'D MMM YYYY' },
{ label: '20 Jan 1999 at 23:59', value: 'D MMM YYYY at H:mm', selected: true },
{ label: 'Mon 20/01/1999 23:59', value: 'DDD DD/MM/YYYY H:mm' },
{ label: 'Monday 20/01/1999 23:59', value: 'DDDD DD/MM/YYYY H:mm' },
{ label: '1999/01/20', value: 'YYYY/MM/DD' },
{ label: '1999/01/20 at 23:59', value: 'YYYY/MM/DD at H:mm' },
{ label: '1999-01-20', value: 'YYYY-MM-D' },
{ label: '1999-01-20 at 23:59', value: 'YYYY-MM-D at H:mm' },
{ label: '1999.1.20', value: 'YYYY.M.D' },
{ label: '1999.1.20 at 23:59', value: 'YYYY.M.D at H:mm' },
{ label: '01/20/1999', value: 'MM/DD/YYYY' },
{ label: '01/20/1999 at 23:59', value: 'MM/DD/YYYY at H:mm' },
{ label: '01-20-1999', value: 'MM-D-YYYY' },
{ label: '01-20-1999 at 23:59', value: 'MM-D-YYYY at H:mm' },
{ label: '01.20.1999', value: 'MM.D.YYYY' },
{ label: '01.20.1999 at 23:59', value: 'MM.D.YYYY at H:mm' },
],
},
}
});
window.nova_plugins.push({
id: 'transcript',
title: 'Show transcript',
run_on_pages: 'watch, -mobile',
section: 'details-buttons',
_runtime: user_settings => {
const
BTN_SELECTOR_ID = 'nova-transcript-button',
BTN_SELECTOR = '#' + BTN_SELECTOR_ID;
NOVA.runOnPageLoad(async () => {
if (NOVA.currentPage != 'watch') return;
if (await NOVA.storage_obj_manager.getParam('transcript')) {
NOVA.waitSelector(BTN_SELECTOR, { destroy_after_page_leaving: true })
.then(btn => {
btn.style.display = 'flex';
switch (user_settings.transcript_visibility_mode) {
case 'button': transcriptExpand(); break;
case 'external':
case 'external-popup':
transcriptOpenLink();
break;
}
});
return;
}
switch (user_settings.transcript_visibility_mode) {
case 'expand':
NOVA.waitSelector('[target-id="engagement-panel-searchable-transcript"][visibility="ENGAGEMENT_PANEL_VISIBILITY_HIDDEN"]', { destroy_after_page_leaving: true })
.then(transcriptEl => {
transcriptEl.setAttribute('visibility', 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED');
});
break;
default:
NOVA.waitSelector(BTN_SELECTOR, { destroy_after_page_leaving: true })
.then(btn => {
btn.style.display = document.body.querySelector('#description ytd-video-description-transcript-section-renderer button, [target-id="engagement-panel-searchable-transcript"]') ? 'flex' : 'none';
});
break;
}
});
switch (user_settings.transcript_visibility_mode) {
case 'button':
NOVA.waitSelector('ytd-watch-metadata #actions #top-level-buttons-computed')
.then(container => {
insertToHTML({ 'container': container, 'position': 'beforebegin' })
.addEventListener('click', transcriptExpand);
});
break;
case 'external':
case 'external-popup':
NOVA.waitSelector('ytd-watch-metadata #actions #top-level-buttons-computed')
.then(container => {
insertToHTML({ 'container': container, 'position': 'beforebegin' })
.addEventListener('click', transcriptOpenLink);
});
break;
}
function transcriptExpand() {
if (btn = document.body.querySelector('#description ytd-video-description-transcript-section-renderer button')) {
btn.click()
}
else if (transcriptEl = document.body.querySelector('[target-id="engagement-panel-searchable-transcript"][visibility="ENGAGEMENT_PANEL_VISIBILITY_HIDDEN"]')) {
transcriptEl.setAttribute('visibility', 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED');
}
}
function transcriptOpenLink() {
const url = 'https://www.youtubetranscript.com/' + location.search;
window.open(url, '_blank', user_settings.transcript_visibility_mode == 'external-popup'
? `popup=1,toolbar=no,location=no,directories=no,status=no,menubar=no,resizable=yes,copyhistory=no`
: '')
}
function insertToHTML({ container = required(), position = 'beforebegin' }) {
if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
return (document.getElementById(BTN_SELECTOR_ID) || (function () {
NOVA.css.push(
`${BTN_SELECTOR} {
border: 0;
cursor: pointer;
text-decoration: none;
font-weight: bold;
margin: 0 var(--ytd-subscribe-button-margin, 12px);
}`);
container.insertAdjacentHTML(position,
`<button id="${BTN_SELECTOR_ID}" style="display:flex" title="Show Transcript" class="style-scope yt-formatted-string bold yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m">
<span class="yt-spec-button-shape-next__icon" style="height:100%">
<svg viewBox="0 0 24 24" height="100%" width="100%">
<g fill="currentColor">
<path d="M20 12V13C20 17.4183 16.4183 21 12 21C7.58172 21 4 17.4183 4 13V12M12 17C9.79086 17 8 15.2091 8 13V7C8 4.79086 9.79086 3 12 3C14.2091 3 16 4.79086 16 7V13C16 15.2091 14.2091 17 12 17Z" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>
</span>
<span class="yt-spec-button-shape-next__button-text-content" style="align-self:center;">Transcript</span>
</button>`);
return document.getElementById(BTN_SELECTOR_ID);
})());
}
},
options: {
transcript_visibility_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'expand default section', selected: true,
},
{
label: 'add button', value: 'button',
},
{
label: 'link to external', value: 'external',
},
{
label: 'link to external (popup)', value: 'external-popup',
},
],
},
}
});
window.nova_plugins.push({
id: 'video-title-hashtag',
title: 'Title hashtag',
run_on_pages: 'watch',
section: 'details',
_runtime: user_settings => {
let cssObj = {};
switch (user_settings.title_hashtag_visibility_mode) {
case 'uncolorize':
cssObj['color'] = 'var(--yt-endpoint-color, var(--yt-spec-text-primary))';
break;
default:
cssObj['display'] = 'none';
break;
}
if (Object.keys(cssObj).length) {
NOVA.css.push(cssObj, 'h1 a[href*="/hashtag/"]', 'important');
}
},
options: {
title_hashtag_visibility_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'hide', selected: true,
},
{
label: 'uncolorize', value: 'uncolorize',
},
],
},
}
});
window.nova_plugins.push({
id: 'channel-videos-count',
title: 'Show channel videos count',
'title:zh': '显示频道上的视频数量',
'title:ja': 'チャンネルの動画数を表示する',
'title:pl': 'Pokaż liczbę filmów na kanale',
run_on_pages: 'watch, -mobile',
restart_on_location_change: true,
section: 'details',
opt_api_key_warn: true,
desc: 'Display uploaded videos on channel',
'desc:zh': '在频道上显示上传的视频',
'desc:ja': 'アップロードした動画をチャンネルに表示',
'desc:pl': 'Wyświetla przesłane filmy na kanale',
_runtime: user_settings => {
const
CACHE_PREFIX = 'nova-channel-videos-count:',
SELECTOR_ID = 'nova-video-count';
NOVA.waitSelector('#upload-info #owner-sub-count, ytm-slim-owner-renderer .subhead', { destroy_after_page_leaving: true })
.then(el => setVideoCount(el));
async function setVideoCount(container = required()) {
await NOVA.delay(500);
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.numberFormat.abbr(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',
title: 'Add sort/filter to "Save to playlist" menu',
'title:zh': '将排序/过滤器添加到“保存到播放列表”菜单',
'title:ja': '「プレイリストに保存」メニューにソート/フィルターを追加',
'title:pl': 'Dodaj sortowanie/filtr do menu „Zapisz na liście odtwarzania”.',
run_on_pages: 'home, feed, results, channel, watch, -mobile',
section: 'details-buttons',
_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(
document.body.querySelector('ytd-add-to-playlist-renderer #header ytd-menu-title-renderer')
);
}
}
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.innerText.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', 'Playlists Filter');
Object.assign(searchInput.style, {
padding: '.4em .6em',
border: 0,
outline: 0,
'min-width': '250px',
width: '100%',
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'));
});
});
const containerDiv = document.createElement('div');
Object.assign(containerDiv.style, {
'margin-top': '.5em',
display: 'flex',
gap: '10px',
});
if (!user_settings.save_to_playlist_sort) {
const sortButton = document.createElement('button');
sortButton.textContent = 'A-Z ↓';
Object.assign(sortButton.style, {
padding: '.4em .6em',
border: 0,
outline: 0,
'border-radius': '4px',
color: 'var(--ytd-searchbox-text-color)',
'background-color': 'var(--ytd-searchbox-background)',
'white-space': 'nowrap',
'cursor': 'pointer',
});
sortButton.addEventListener('click', () => {
sortButton.remove();
sortPlaylistsMenu(document.body.querySelector('tp-yt-paper-dialog #playlists'));
}, { capture: true, once: true });
containerDiv.append(sortButton);
}
containerDiv.append(searchInput);
container.append(containerDiv);
};
},
options: {
save_to_playlist_sort: {
_tagName: 'input',
label: 'Default sorting alphabetically',
'label:zh': '默认按字母顺序排序',
'label:ja': 'デフォルトのアルファベット順のソート',
'label:pl': 'Domyślne sortowanie alfabetyczne',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'description-popup',
title: 'Description section in popup',
'title:zh': '弹出窗口中的描述部分',
'title:ja': 'ポップアップの説明セクション',
'title:pl': 'Opis w osobnym oknie',
run_on_pages: 'watch, -mobile',
section: 'details',
'plugins-conflict': 'description-timestamps-scroll',
_runtime: user_settings => {
const
DESCRIPTION_SELECTOR = 'html:not(:fullscreen) ytd-watch-metadata #description.ytd-watch-metadata:not([hidden]):not(:empty)',
DATE_SELECTOR_ID = 'nova-description-date';
NOVA.waitSelector('#masthead-container')
.then(masthead => {
NOVA.css.push(
`${DESCRIPTION_SELECTOR},
${DESCRIPTION_SELECTOR}:before {
position: fixed;
top: ${masthead.offsetHeight || 56}px;
right: 0;
z-index: ${1 + Math.max(getComputedStyle(masthead || movie_player)['z-index'], 601)};
}
${DESCRIPTION_SELECTOR}:not(:hover):before {
content: "info ▼";
cursor: pointer;
visibility: visible;
right: 12.5em;
padding: 0 8px 2px;
line-height: normal;
font-family: Roboto, Arial, sans-serif;
font-size: 11px;
color: #eee;
background-color: rgba(0, 0, 0, .3);
}
${DESCRIPTION_SELECTOR} {
margin: 0 1%;
overflow-y: auto;
max-height: 88vh;
max-width: 55%;
background-color: var(--yt-spec-brand-background-primary);
background-color: var(--yt-spec-menu-background);
background-color: var(--yt-spec-raised-background);
color: var(--yt-spec-text-primary);;
border: 1px solid #333;
${user_settings['square-avatars'] ? 'border-radius: 0' : ''};
}
${DESCRIPTION_SELECTOR}:not(:hover) {
visibility: collapse;
overflow: hidden;
}
${DESCRIPTION_SELECTOR}:hover {
visibility: visible !important;
}
${DESCRIPTION_SELECTOR}::-webkit-scrollbar {
height: 8px;
width: 10px;
}
${DESCRIPTION_SELECTOR}::-webkit-scrollbar-button {
height: 0;
width: 0;
}
${DESCRIPTION_SELECTOR}::-webkit-scrollbar-corner {
background-color: transparent;
}
${DESCRIPTION_SELECTOR}::-webkit-scrollbar-thumb {
background-color: #e1e1e1;
border: 0;
border-radius: 0;
}
${DESCRIPTION_SELECTOR}::-webkit-scrollbar-track {
background-color: #666;
border: 0;
border-radius: 0;
}
${DESCRIPTION_SELECTOR}::-webkit-scrollbar-track:hover {
background-color: #666;
}`);
});
NOVA.waitSelector(DESCRIPTION_SELECTOR)
.then(descriptionEl => {
descriptionEl.addEventListener('mouseenter', evt => {
document.body.querySelector('#meta [collapsed] #more, [description-collapsed] #description #expand')
?.click();
});
});
if (!user_settings['video-date-format']) {
NOVA.runOnPageLoad(() => (NOVA.currentPage == 'watch') && restoreDateLine());
}
let oldDateText;
function restoreDateLine() {
NOVA.waitSelector('#title h1')
.then(container => {
NOVA.waitSelector('ytd-watch-metadata #description.ytd-watch-metadata')
.then(async textDateEl => {
await NOVA.waitUntil(() => {
if ((text = [...textDateEl.querySelectorAll('span.bold.yt-formatted-string:not(:empty)')]
.map(e => e.textContent)
?.join('').trim()
)
&& text != oldDateText
) {
oldDateText = text;
insertToHTML({ 'text': oldDateText, 'container': container });
return true;
}
}, 1000);
});
});
function insertToHTML({ text = '', container = required() }) {
if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
(document.getElementById(DATE_SELECTOR_ID) || (function () {
const el = document.createElement('span');
el.id = DATE_SELECTOR_ID;
el.className = 'style-scope yt-formatted-string bold';
el.style.cssText = 'font-size: 1.35rem; line-height: 2rem; font-weight:400;';
container.after(el);
return el;
})())
.textContent = text;
}
}
},
});
window.nova_plugins.push({
id: 'details-buttons-visibility',
title: 'Buttons hide',
'title:zh': '按钮隐藏',
'title:ja': 'ボタンを非表示にする',
run_on_pages: 'watch, -mobile',
section: 'details-buttons',
_runtime: user_settings => {
const SELECTOR_BTN_CONTAINER = 'ytd-watch-metadata #actions';
if (user_settings.details_buttons_hide?.length
&& (stylesList = getHideButtonsList())
&& stylesList.length
) {
NOVA.css.push(stylesList.join(',\n') + ` {
display: none !important;
background-color: red;
}`);
}
function getHideButtonsList() {
let stylesList = [];
if (user_settings.details_buttons_hide?.includes('subscribe')) {
stylesList.push('#owner #subscribe-button');
}
if (user_settings.details_buttons_hide.includes('join')) {
stylesList.push('#sponsor-button');
}
if (user_settings.details_buttons_hide?.includes('all')) {
stylesList.push(`${SELECTOR_BTN_CONTAINER} button`);
return stylesList;
}
if (user_settings.details_buttons_hide.includes('like_dislike')) {
stylesList.push(`${SELECTOR_BTN_CONTAINER} segmented-like-dislike-button-view-model`);
}
else if (user_settings.details_buttons_hide.includes('dislike')) {
stylesList.push(`${SELECTOR_BTN_CONTAINER} dislike-button-view-model, ${SELECTOR_BTN_CONTAINER} .yt-spec-button-shape-next--segmented-start::after`);
NOVA.css.push(
`${SELECTOR_BTN_CONTAINER} segmented-like-dislike-button-view-model button {
border-radius: 20px;
}`);
}
if (user_settings.details_buttons_hide.includes('download')) {
stylesList.push(`${SELECTOR_BTN_CONTAINER} ytd-download-button-renderer`);
NOVA.css.push(`#flexible-item-buttons { width: inherit; }`);
}
if (CSS.supports('selector(:has(*))')) {
const buttonSelectors = [
`${SELECTOR_BTN_CONTAINER} ytd-button-renderer`,
`${SELECTOR_BTN_CONTAINER} button`,
'ytd-popup-container ytd-menu-service-item-renderer',
];
if (user_settings.details_buttons_hide.includes('share')) {
stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="M15 5.63 20.66"])`));
}
if (user_settings.details_buttons_hide.includes('thanks')) {
stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="M11 17h2v-1h1c.55"])`));
}
if (user_settings.details_buttons_hide.includes('clip')) {
stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="M8 7c0 .55-.45"])`));
}
if (user_settings.details_buttons_hide.includes('save')) {
stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="M22 13h-4v4h"])`));
}
if (user_settings.details_buttons_hide.includes('report')) {
stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="m13.18 4 .24 "])`));
}
if (user_settings.details_buttons_hide.includes('transcript')) {
stylesList.push(buttonSelectors.map(e => `\n${e}:has(path[d^="M5,11h2v2H5V11z"])`));
}
}
return stylesList;
}
let stylesTextHideLabel = '';
if (user_settings.details_buttons_label_hide) {
stylesTextHideLabel +=
`${SELECTOR_BTN_CONTAINER} button [class*=text] {
display: none;
}
${SELECTOR_BTN_CONTAINER} button .yt-spec-button-shape-next__icon {
margin: 0 !important;
}
${SELECTOR_BTN_CONTAINER} segmented-like-dislike-button-view-model button,
${SELECTOR_BTN_CONTAINER} segmented-like-dislike-button-view-model ~ * button,
${SELECTOR_BTN_CONTAINER} button.yt-spec-button-shape-next--size-m {
padding: 0 7px;
}
${SELECTOR_BTN_CONTAINER} ytd-menu-renderer[has-items] yt-button-shape.ytd-menu-renderer {
margin: 0 !important;
}`;
}
if (+user_settings.details_buttons_opacity) {
stylesTextHideLabel +=
`#owner #subscribe-button:not(:hover),
${SELECTOR_BTN_CONTAINER} #menu:not(:hover) {
transition: opacity .2s ease-in-out;
opacity: ${user_settings.details_buttons_opacity || .1};
}`;
}
if (stylesTextHideLabel.length) {
NOVA.css.push(stylesTextHideLabel);
}
},
options: {
details_buttons_label_hide: {
_tagName: 'input',
label: 'Buttons without labels',
'label:zh': '没有标签的按钮',
'label:ja': 'ラベルのないボタン',
'label:pl': 'Guziki bez etykiet',
type: 'checkbox',
title: 'Requires support for css tag ":has()"',
},
details_buttons_opacity: {
_tagName: 'input',
label: 'Opacity',
'label:zh': '不透明度',
'label:ja': '不透明度',
'label:pl': 'Przejrzystość',
type: 'number',
title: '0 - disable',
placeholder: '0-1',
step: .05,
min: 0,
max: 1,
value: .9,
},
details_buttons_hide: {
_tagName: 'select',
label: 'Hide items',
title: '[Ctrl+Click] to select several',
'title:zh': '[Ctrl+Click] 选择多个',
'title:ja': '「Ctrl+Click」して、いくつかを選択します',
'title:pl': 'Ctrl+kliknięcie, aby zaznaczyć kilka',
multiple: null,
size: 8,
options: [
{
label: 'subscribe', value: 'subscribe',
},
{
label: 'join', value: 'join',
},
{
label: 'all (below)', value: 'all',
},
{
label: 'like+dislike', value: 'like_dislike',
},
{
label: 'dislike', value: 'dislike',
},
{
label: 'share', value: 'share',
},
{
label: 'clip', value: 'clip',
},
{
label: 'save', value: 'save',
},
{
label: 'download', value: 'download',
},
{
label: 'thanks', value: 'thanks',
},
{
label: 'report', value: 'report',
},
{
label: 'transcript', value: 'transcript',
},
],
},
}
});
window.nova_plugins.push({
id: 'redirect-disable',
title: 'Clear links from redirect',
'title:zh': '清除重定向中的链接',
'title:ja': 'リダイレクトからリンクをクリアする',
'title:pl': 'Wyczyść linki z przekierowań',
run_on_pages: 'watch, channel',
section: 'details',
desc: 'Direct external links',
'desc:zh': '直接链接到外部站点',
'desc:ja': '外部サイトへの直接リンク',
'desc:pl': 'Bezpośrednie łącza zewnętrzne',
_runtime: user_settings => {
document.addEventListener('click', evt => evt.isTrusted && patchLink(evt.target), { capture: true });
document.addEventListener('auxclick', evt => evt.isTrusted && evt.button === 1 && patchLink(evt.target), { capture: true });
const linkSelector = 'a[href*="/redirect?"]';
function patchLink(target = required()) {
if (!target.matches(linkSelector)) {
if (!(target = target.parentElement.matches(linkSelector))) return;
}
if (q = NOVA.queryURL.get('q', target.href)) {
target.href = decodeURIComponent(q);
}
}
},
});
window.nova_plugins.push({
id: 'description-timestamps-scroll',
title: 'Disable scroll to top on click timestamps',
'title:zh': '没有在时间戳上滚动到播放器',
'title:ja': 'タイムスタンプでプレーヤーにスクロールしない',
'title:pl': 'Brak przejścia do odtwarzacza na znacznikach czasu',
run_on_pages: 'watch, -mobile',
section: 'details',
desc: 'Disable scrolling to player when clicking on timestamps',
'desc:pl': 'Wyłącza przewijanie do odtwarzacza podczas klikania znaczników czasu',
_runtime: user_settings => {
if (user_settings['description-popup']) return;
document.addEventListener('click', evt => {
if (!evt.isTrusted || !evt.target.matches('a[href*="&t="]')) return;
if (sec = parseInt(NOVA.queryURL.get('t', evt.target.href))) {
evt.preventDefault();
evt.stopPropagation();
evt.stopImmediatePropagation();
movie_player.seekTo(sec);
}
}, { capture: true });
},
});
window.nova_plugins.push({
id: 'ad-state',
title: 'Show Ads info',
run_on_pages: 'watch, -mobile',
restart_on_location_change: true,
section: 'details',
_runtime: user_settings => {
const SELECTOR_ID = 'nova-monetization';
NOVA.waitSelector('#title h1', { destroy_after_page_leaving: true })
.then(el => {
if (playerResponse = document.getElementById('page-manager')?.getCurrentData()?.playerResponse) {
let text = [];
if (playerResponse?.paidContentOverlay) text.push('Sponsored');
if (adCount = playerResponse?.adPlacements?.length) text.push(`Ads count ${adCount}`);
if (text.length) insertToHTML({ 'text': `「${text.join(', ')}」`, 'container': el });
}
});
function insertToHTML({ text = '', container = required() }) {
if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
(document.getElementById(SELECTOR_ID) || (() => {
const el = document.createElement('span');
el.id = SELECTOR_ID;
el.className = 'style-scope yt-formatted-string bold';
Object.assign(el.style, {
'font-size': '1.35rem',
'line-height': '2rem',
margin: '10px',
});
container.after(el);
return el;
})())
.textContent = text;
}
},
});
window.nova_plugins.push({
id: 'metadata-hide',
title: 'Hide metadata',
run_on_pages: 'watch',
section: 'details',
desc: 'Cover link to games, movies, merch, etc.',
_runtime: user_settings => {
let selectorsList = [
'ytd-watch-metadata > ytd-metadata-row-container-renderer',
'ytd-merch-shelf-renderer, #infocards-section',
];
if (user_settings.description_card_list) {
selectorsList.push('#structured-description ytd-horizontal-card-list-renderer');
}
if (user_settings.description_shorts_remixing) {
selectorsList.push('#structured-description ytd-reel-shelf-renderer');
}
if (user_settings.description_transcript) {
selectorsList.push('#structured-description ytd-video-description-transcript-section-renderer');
}
if (selectorsList.length) {
NOVA.css.push(
selectorsList.join(',\n') + ` {
display: none !important;
background-color: red;
}`);
}
},
options: {
description_card_list: {
_tagName: 'input',
label: 'Chapters/Key moments/Music info',
type: 'checkbox',
},
description_shorts_remixing: {
_tagName: 'input',
label: 'Shorts remixing this video',
type: 'checkbox',
},
description_transcript: {
_tagName: 'input',
label: 'Transcript',
type: 'checkbox',
},
}
});
https://www.youtube.com/watch?v=eB6txyhHFG4 - many dislike count
window.nova_plugins.push({
id: 'return-dislike',
title: 'Show dislike count',
'title:zh': '显示不喜欢计数',
'title:ja': '嫌いな数を表示',
run_on_pages: 'watch, -mobile',
section: 'details-buttons',
desc: 'via by returnyoutubedislike.com',
_runtime: user_settings => {
if (user_settings.details_buttons_label_hide
|| user_settings.details_buttons_hide?.includes('like_dislike')
) {
return;
}
const
CACHE_PREFIX = 'nova-dislikes-count:',
SELECTOR_ID = 'nova-dislikes-count';
NOVA.waitSelector('#actions dislike-button-view-model button', { destroy_after_page_leaving: true })
.then(el => setDislikeCount(el));
NOVA.runOnPageLoad(() => {
if (NOVA.currentPage != 'watch') return;
document.addEventListener('yt-action', dislikeIsUpdated);
});
function dislikeIsUpdated(evt) {
if (NOVA.currentPage != 'watch') return;
switch (evt.detail?.actionName) {
case 'yt-set-active-panel-item-action':
case 'yt-reload-continuation-items-command':
document.removeEventListener('yt-action', dislikeIsUpdated);
NOVA.waitSelector('#actions dislike-button-view-model button', { destroy_after_page_leaving: true })
.then(el => setDislikeCount(el));
break;
}
}
async function setDislikeCount(container = required()) {
const videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id;
if (!videoId) return console.error('return-dislike videoId: empty', videoId);
container.style.width = 'auto';
if (storage = sessionStorage.getItem(CACHE_PREFIX + videoId)) {
insertToHTML({ 'data': JSON.parse(storage), 'container': container });
}
else if (data = await getDislikeCount()) {
insertToHTML({ 'data': data, 'container': container });
}
async function getDislikeCount() {
const videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id;
const fetchAPI = () => fetch(`https://returnyoutubedislikeapi.com/votes?videoId=${videoId}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then(response => response.json())
.then(json => json.dislikes && ({ 'likes': json.likes, 'dislikes': json.dislikes }))
.catch(error => {
});
if (result = await fetchAPI()) {
sessionStorage.setItem(CACHE_PREFIX + videoId, JSON.stringify(result));
return result;
}
}
function insertToHTML({ data = required(), container = required() }) {
if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
const percent = Math.trunc(data.dislikes * 100 / (data.likes + data.dislikes));
const text = `${NOVA.numberFormat.abbr(data.dislikes)} (${percent}%)`;
(document.getElementById(SELECTOR_ID) || (function () {
const el = document.createElement('span');
el.id = SELECTOR_ID;
el.className = 'style-scope yt-formatted-string bold';
el.style.cssText = 'text-overflow:ellipsis; overflow:visible; white-space:nowrap; padding-left:3px;';
return container.appendChild(el);
})())
.textContent = text;
container.title = text;
}
}
},
});
window.nova_plugins.push({
id: 'description-expand',
title: 'Expand description',
'title:zh': '展开说明',
'title:ja': '説明を展開',
'title:pl': 'Rozwiń opis',
run_on_pages: 'watch, -mobile',
section: 'details',
desc: 'on hover',
'plugins-conflict': 'description-popup, comments-sidebar-position-exchange',
_runtime: user_settings => {
if (user_settings['description-popup']) return;
if (user_settings['comments-sidebar-position-exchange']) return;
const SELECTOR_BTN = '[description-collapsed] #description #expand';
switch (user_settings.description_expand_mode) {
case 'onhover':
NOVA.waitSelector(SELECTOR_BTN)
.then(btn => btn.addEventListener('mouseenter', btn.click));
break;
case 'always':
document.addEventListener('yt-page-data-updated', expandSection);
function expandSection() {
if (NOVA.currentPage == 'watch') {
document.body.querySelector(SELECTOR_BTN)?.click();
}
}
break;
}
},
options: {
description_expand_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'always', value: 'always', selected: true,
'label:zh': '每次',
'label:ja': 'いつも',
'label:pl': 'zawsze',
},
{
label: 'on hover', value: 'onhover',
'label:zh': '悬停时',
'label:ja': 'ホバー時に',
'label:pl': 'po najechaniu',
},
],
},
}
});
window.nova_plugins.push({
id: 'subscriptions-home',
title: 'Redirect from home page to subscriptions page',
'title:zh': '从主页重定向到订阅页面',
'title:ja': 'ホーム ページからサブスクリプション ページへのリダイレクト',
'title:pl': 'Przekieruj ze strony głównej na stronę subskrypcji',
run_on_pages: 'home',
restart_on_location_change: true,
section: 'header',
'plugins-conflict': 'page-logo',
_runtime: user_settings => {
location.pathname = '/feed/subscriptions';
},
});
window.nova_plugins.push({
id: 'header-unfixed',
title: 'Header unpinned',
'title:zh': '标题未固定',
'title:ja': 'ヘッダーは固定されていません',
'title:pl': 'Przewijany nagłówek',
run_on_pages: '*, -embed, -mobile, -live_chat',
section: 'header',
desc: 'Prevent header from sticking',
'desc:zh': '防止头部粘连',
'desc:ja': 'ヘッダーがくっつくのを防ぎます',
'desc:pl': 'Nagłówek będzie przewijany wraz ze stroną',
_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 || 'KeyV';
document.addEventListener('keyup', evt => {
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if ((hotkey.length === 1 ? evt.key : evt.code) === hotkey) {
document.documentElement.classList.toggle(CLASS_NAME_TOGGLE);
}
});
}
if (user_settings.header_unfixed_scroll) {
createArrowButton();
document.addEventListener('yt-action', evt => {
switch (evt.detail?.actionName) {
case 'yt-store-grafted-ve-action':
case 'yt-open-popup-action':
scrollAfter();
break;
}
});
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.innerHTML =
`<svg viewBox="0 0 16 16" height="100%" width="100%">
<g fill="currentColor">
<path d="M3.35 4.97 8 9.62 12.65 4.97l.71.71L8 11.03l-5.35-5.35.7-.71z" />
</g>
</svg>`;
scrollDownButton.title = 'Scroll down';
Object.assign(scrollDownButton.style, {
cursor: 'pointer',
'background-color': 'transparent',
color: 'deepskyblue',
border: 'none',
height: '3em',
});
scrollDownButton.addEventListener('click', scrollAfter);
if (endnode = document.getElementById('end')) {
endnode.parentElement.insertBefore(scrollDownButton, endnode);
}
}
}
},
options: {
header_unfixed_scroll: {
_tagName: 'input',
label: 'Scroll after header',
'label:zh': '在标题后滚动',
'label:ja': 'ヘッダーの後にスクロール',
'label:pl': 'Przewiń nagłówek',
type: 'checkbox',
title: 'Makes sense on a small screen',
'title:zh': '在小屏幕上有意义',
'title:ja': '小さな画面で意味があります',
'title:pl': 'Przydatne na małym ekranie',
},
header_unfixed_hotkey: {
_tagName: 'select',
label: 'Hotkey toggle',
options: [
{ label: 'none', value: false },
{ label: 'ShiftL', value: 'ShiftLeft' },
{ label: 'ShiftR', value: 'ShiftRight' },
{ label: 'CtrlL', value: 'ControlLeft' },
{ label: 'CtrlR', value: 'ControlRight' },
{ label: 'AltL', value: 'AltLeft' },
{ label: 'AltR', value: 'AltRight' },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV', selected: true },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
},
},
});
window.nova_plugins.push({
id: 'header-compact',
title: 'Header compact',
'title:zh': '标题紧凑',
'title:ja': 'ヘッダーコンパクト',
'title:pl': 'Kompaktowy nagłówek',
run_on_pages: '*, -embed, -mobile, -live_chat',
section: 'header',
_runtime: user_settings => {
const height = '36px';
NOVA.css.push(
`#masthead #container.ytd-masthead {
max-height: ${height} !important;
}
#masthead #background {
max-height: ${height} !important;
}
#search-form, #search-icon-legacy {
max-height: ${height} !important;
}
body,
html:not(:fullscreen) #page-manager {
--ytd-masthead-height: ${height};
}
#chips-wrapper.ytd-feed-filter-chip-bar-renderer {
--ytd-rich-grid-chips-bar-top: ${height};
}`);
},
});
window.nova_plugins.push({
id: 'search-query',
title: 'Search filter',
'title:zh': '搜索过滤器',
'title:ja': '検索フィルター',
'title:pl': 'Filtry wyszukiwania',
run_on_pages: 'results',
restart_on_location_change: true,
section: 'header',
_runtime: user_settings => {
if (!NOVA.queryURL.has('sp')
&& (sp = user_settings.search_query_date || user_settings.search_query_sort)
) {
location.href = NOVA.queryURL.set({ 'sp': sp });
}
},
options: {
search_query_sort: {
_tagName: 'select',
label: 'Sort by',
'label:zh': '排序方式',
'label:ja': '並び替え',
'label:pl': 'Sortuj według',
options: [
{
label: 'relevance', value: false, selected: true,
},
{
label: 'upload date', value: 'cai%253d',
},
{
label: 'view count', value: 'cam%253d',
},
{
label: 'rating', value: 'cae%253d',
},
],
'data-dependent': { 'search_query_date': false },
},
search_query_date: {
_tagName: 'select',
label: 'Upload date',
'label:zh': '上传日期',
'label:ja': 'アップロード日',
'label:pl': 'Data przesłania',
options: [
{
label: 'all time', value: false, selected: true,
},
{
label: 'last hour', value: 'egiiaq%253d%253d',
},
{
label: 'today', value: 'egiiag%253d%253d',
},
{
label: 'this week', value: 'egiiaw%253d%253d',
},
{
label: 'this month', value: 'egiiba%253d%253d',
},
{
label: 'this year', value: 'egiibq%253d%253d',
},
],
'data-dependent': { 'search_query_sort': false },
},
}
});
window.nova_plugins.push({
id: 'page-logo',
title: 'YouTube logo link',
'title:zh': 'YouTube 徽标',
'title:ja': 'YouTubeロゴ',
run_on_pages: '*, -embed, -mobile, -live_chat',
section: 'header',
_runtime: user_settings => {
NOVA.waitSelector('#masthead a#logo', { destroy_after_page_leaving: true })
.then(async a => {
if (link = new URL(user_settings.page_logo_url_mode)?.href) {
a.href = link;
await NOVA.waitUntil(() => a.data?.commandMetadata?.webCommandMetadata?.url, 1500);
a.data.commandMetadata.webCommandMetadata.url = link;
}
});
},
options: {
page_logo_url_mode: {
_tagName: 'input',
label: 'URL',
type: 'url',
pattern: "https://.*",
placeholder: 'https://youtube.com/...',
value: 'https://youtube.com/feed/subscriptions',
},
}
});
const NOVA = {
waitSelector(selector = required(), limit_data) {
return new Promise((resolve, reject) => {
if (typeof selector !== 'string') {
console.error('wait > selector:', ...arguments);
return reject('wait > selector:', typeof selector);
}
if (limit_data && (!limit_data.hasOwnProperty('destroy_after_page_leaving') && !limit_data.hasOwnProperty('container'))) {
console.error('waitSelector > check format "limit_data":', ...arguments);
return reject('waitSelector > check format "limit_data"');
}
if (limit_data?.container && !(limit_data.container instanceof HTMLElement)) {
console.error('waitSelector > container not HTMLElement:', ...arguments);
return reject('waitSelector > container not HTMLElement');
}
if (selector.includes(':has(') && !CSS.supports('selector(:has(*))')) {
console.warn('CSS ":has()" unsupported');
return reject('CSS ":has()" unsupported');
}
if (element = (limit_data?.container || document.body || document).querySelector(selector)) {
return resolve(element);
}
const observerFactory = 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 = (limit_data?.container || document?.body || document).querySelector(selector))
) {
observer.disconnect();
return resolve(element);
}
});
observerFactory
.observe(limit_data?.container || document.body || document.documentElement || document, {
childList: true,
subtree: true,
attributes: true,
});
if (sec = +limit_data?.destroy_timeout) {
setTimeout(() => {
observerFactory.disconnect();
return reject(`"${selector}" timed out after ${sec} seconds`);
}, sec * 1000);
}
if (limit_data?.destroy_after_page_leaving) {
isURLChange();
window.addEventListener('transitionend', ({ target }) => isURLChange() && observerFactory.disconnect());
function isURLChange() {
return (this.prevURL === document.URL) ? false : this.prevURL = document.URL;
}
}
});
},
waitUntil(condition = required(), timeout = required()) {
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 waitCondition = setInterval(() => {
if (result = condition()) {
clearInterval(waitCondition);
resolve(result);
}
}, +timeout || 500);
}
});
},
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(), 1500);
function process() {
selectors
.forEach(selectorItem => {
if (selectorItem.includes(':has(') && !CSS.supports('selector(:has(*))')) {
return console.warn('CSS ":has()" unsupported');
}
if (attr_mark) selectorItem += `:not([${attr_mark}])`;
document.body.querySelectorAll(selectorItem)
.forEach(el => {
if (attr_mark) el.setAttribute(attr_mark, true);
callback(el);
});
});
}
});
},
runOnPageLoad(callback) {
if (!callback || typeof callback !== 'function') {
return console.error('runOnPageLoad > callback not function:', ...arguments);
}
let prevURL = document.URL;
const isURLChange = () => (prevURL === document.URL) ? false : prevURL = document.URL;
isURLChange() || callback();
document.addEventListener('yt-navigate-finish', () => isURLChange() && callback());
},
css: {
push(css = required(), selector, set_important) {
if (typeof css === 'object') {
if (!selector) return console.error('injectStyle > empty json-selector:', ...arguments);
if (selector.includes(':has(') && !CSS.supports('selector(:has(*))')) {
return console.error('CSS ":has()" unsupported', ...arguments);
}
injectCss(selector + json2css(css));
function json2css(obj) {
let css = '';
Object.entries(obj)
.forEach(([key, value]) => {
css += key + ':' + value + (set_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';
}
},
get(selector = required(), prop_name = required()) {
return (el = (selector instanceof HTMLElement) ? selector : document.body?.querySelector(selector))
? getComputedStyle(el).getPropertyValue(prop_name) : null;
},
},
isInViewport(el = required()) {
if (!(el instanceof HTMLElement)) return console.error('el is not HTMLElement type:', el);
if (distance = el.getBoundingClientRect()) {
return (
distance.top >= 0 &&
distance.left >= 0 &&
distance.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
distance.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
},
collapseElement({ selector = required(), label = required(), remove }) {
const selector_id = `${label.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 ${label}`;
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 = 'inherit';
window.dispatchEvent(new Event('scroll'));
});
el.before(btn);
}
});
},
aspectRatio: {
sizeToFit({
srcWidth = 0, srcHeight = 0,
maxWidth = screen.width, maxHeight = screen.height
}) {
const aspectRatio = Math.min(maxWidth / +srcWidth, maxHeight / +srcHeight, 1);
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),
w = width / divisor,
h = height / divisor;
return (w > 10 && h > 10 && Math.abs(w - h) <= 2)
? '1:1'
: w + ':' + h;
},
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)),
},
openPopup({ url = required(), title = document.title, width = window.innerWidth, height = window.innerHeight, closed_callback }) {
const left = (screen.width / 2) - (width / 2);
const top = (screen.height / 2) - (height / 2);
const win = 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}`);
if (closed_callback && typeof closed_callback === 'function') {
const timer = setInterval(() => {
if (win.closed) {
clearInterval(timer);
closed_callback();
}
}, 500);
}
},
showOSD(text) {
if (!text || !['watch', 'embed'].includes(this.currentPage)) return;
if (typeof this.fadeBezel === 'number') clearTimeout(this.fadeBezel);
const bezelEl = document.body.querySelector('.ytp-bezel-text');
if (!bezelEl) return console.error(`showOSD ${text}=>${bezelEl}`);
const
bezelContainer = bezelEl.parentElement.parentElement,
CLASS_VALUE = 'ytp-text-root',
SELECTOR = '.' + CLASS_VALUE;
if (!this.bezel_css_inited) {
this.bezel_css_inited = true;
this.css.push(
`${SELECTOR} { display: block !important; }
${SELECTOR} .ytp-bezel-text-wrapper {
pointer-events: none;
z-index: 40 !important;
}
${SELECTOR} .ytp-bezel-text { display: inline-block !important; }
${SELECTOR} .ytp-bezel { display: none !important; }`);
}
bezelEl.textContent = text;
bezelContainer.classList.add(CLASS_VALUE);
let ms = 1200;
if ((text = String(text)) && (text.endsWith('%') || text.endsWith('x') || text.startsWith('+'))) {
ms = 600
}
this.fadeBezel = setTimeout(() => {
bezelContainer.classList.remove(CLASS_VALUE);
bezelEl.textContent = '';
}, ms);
},
getChapterList(video_duration = required()) {
if (!['watch', 'embed'].includes(this.currentPage)) return;
switch (NOVA.currentPage) {
case 'embed':
chapsCollect = getFromAPI();
return chapsCollect;
break;
case 'watch':
if ((chapsCollect = getFromDescriptionText() || getFromDescriptionChaptersBlock())
&& chapsCollect.length
) {
return chapsCollect;
}
break;
}
function descriptionExpand() {
document.body.querySelector('#meta [collapsed] #more, [description-collapsed] #description #expand')?.click();
}
function getFromDescriptionText() {
descriptionExpand();
const selectorTimestampLink = 'a[href*="&t="]';
let
timestampsCollect = [],
unreliableSorting;
[
(document.body.querySelector('ytd-watch-flexy')?.playerData?.videoDetails?.shortDescription
|| document.body.querySelector('ytd-watch-metadata #description.ytd-watch-metadata')?.textContent
)
?.split('\n') || [],
[...document.body.querySelectorAll(`#comments #comment #comment-content:has(${selectorTimestampLink})`)]
.map(el => [...el.querySelectorAll(selectorTimestampLink)]
.map(a => ({
'source': 'comment',
'text': `${a.textContent} ${(a.nextSibling || a.previousSibling)?.textContent}`,
}))
)
?.sort((a, b) => b.length - a.length)
?.shift()
|| []
]
?.sort((a, b) => b.length - a.length)
.forEach(chaptersList => {
if (timestampsCollect.length > 1) return;
let prevSec = -1;
chaptersList
.forEach(line => {
unreliableSorting = Boolean(line?.source);
line = (line?.text || line).toString().trim();
if (line.length > 5
&& (timestamp = /((\d?\d:){1,2}\d{2})/g.exec(line))
&& (line.length - timestamp.length) < 200
) {
timestamp = timestamp[0];
const
sec = NOVA.formatTimeOut.hmsToSec(timestamp),
timestampPos = line.indexOf(timestamp);
if (
(unreliableSorting ? true : (sec > prevSec && sec < +video_duration))
&& (timestampPos < 5 || (timestampPos + timestamp.length) === line.length)
) {
if (unreliableSorting) prevSec = sec;
timestampsCollect.push({
'sec': sec,
'time': timestamp,
'title': line
.replace(timestamp, '')
.replace(/\*(.*?)\*/g, '<b>$1</b>')
.trim().replace(/^[\u2011-\u26FF:\-|\[\]]+|[\u2011-\u26FF:\-.;]+$/g, '')
.replace(/[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2580-\u27BF]|\uD83E[\uDD10-\uDDFF]/g, '')
.trim()
});
}
}
});
});
if (timestampsCollect.length == 1 && (timestampsCollect[0].sec < (video_duration / 4))) {
return timestampsCollect;
}
else if (timestampsCollect.length > 1) {
if (unreliableSorting) {
timestampsCollect = timestampsCollect.sort((a, b) => a.sec - b.sec);
}
return timestampsCollect;
}
}
function getFromDescriptionChaptersBlock() {
descriptionExpand();
const selectorTimestampLink = 'a[href*="&t="]';
let timestampsCollect = [];
let prevSec = -1;
document.body.querySelectorAll(`#structured-description ${selectorTimestampLink}`)
.forEach(chapterLink => {
const sec = parseInt(NOVA.queryURL.get('t', chapterLink.href));
if (sec > prevSec) {
prevSec = sec;
timestampsCollect.push({
'time': NOVA.formatTimeOut.HMS.digit(sec),
'sec': sec,
'title': chapterLink.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);
}
if ((ytPubsubPubsubInstance = ytPubsubPubsubInstance.i
|| ytPubsubPubsubInstance.j
|| ytPubsubPubsubInstance.subscriptions_
)
&& Array.isArray(ytPubsubPubsubInstance)
) {
const data = Object.values(
ytPubsubPubsubInstance.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.formatTimeOut.HMS.digit(sec),
'title':
c.chapterRenderer.title.simpleText
|| c.chapterRenderer.title.runs[0].text,
};
});
}
}
}
},
strToArray(str) {
return str
?.trim().split(/[\n,;]/)
.map(e => e.replace(/^(\s+)$/, ''))
.filter(e => e.length);
},
searchFilterHTML({ keyword = required(), filter_selectors = required(), highlight_selector, highlight_class }) {
keyword = keyword.toString().toLowerCase();
document.body.querySelectorAll(filter_selectors)
.forEach(item => {
const
text = item.innerText,
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,
'highlightClass': highlight_class,
});
}
};
(highlight_selector ? item.querySelectorAll(highlight_selector) : [item])
.forEach(highlight);
});
function highlightTerm({ target = required(), keyword = required(), highlightClass }) {
const
content = target.innerText,
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() {
if (!['watch', 'embed'].includes(this.currentPage)) return;
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,
document.URL,
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|RECORDS|Recordings|AMV)$/.test(channelName))
|| (channelName && /(MUSIC|ROCK|SOUNDS|SONGS)/.test(channelName.toUpperCase()))
|| titleWordsList?.length && ['🎵', '♫', 'SONG', 'SONGS', 'SOUNDTRACK', 'LYRIC', 'LYRICS', 'AMBIENT', 'MIX', 'VEVO', 'CLIP', 'KARAOKE', 'OPENING', 'COVER', 'COVERED', 'VOCAL', 'INSTRUMENTAL', 'ORCHESTRAL', 'DUBSTEP', 'DJ', 'DNB', 'BASS', 'BEAT', '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));
}
},
formatTimeOut: {
hmsToSec(str = required()) {
let
parts = str?.split(':'),
t = 0;
switch (parts?.length) {
case 2: t = (parts[0] * 60); break;
case 3: t = (parts[0] * 3600) + (parts[1] * 60); break;
case 4: t = (parts[0] * 86400) + (parts[1] * 3600) + (parts[2] * 60); break;
}
return t + +parts.pop();
},
HMS: {
parseTime(time_sec) {
const ts = Math.abs(+time_sec);
return {
d: Math.trunc(ts / 86400),
h: Math.trunc((ts % 86400) / 3600),
m: Math.trunc((ts % 3600) / 60),
s: Math.trunc(ts % 60),
};
},
digit(time_sec = required()) {
const { d, h, m, s } = this.parseTime(time_sec);
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 { d, h, m, s } = this.parseTime(time_sec);
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', sec: 31536000 },
{ label: 'month', sec: 2592000 },
{ label: 'day', sec: 86400 },
{ label: 'hour', sec: 3600 },
{ label: 'minute', sec: 60 },
{ label: 'second', sec: 1 }
];
const
now = date.getTime(),
seconds = Math.round((Date.now() - Math.abs(now)) / 1000),
interval = samples.find(i => i.sec < seconds),
time = Math.round(seconds / interval.sec);
return `${(now < 0 ? '-' : '') + time} ${interval.label}${time !== 1 ? 's' : ''}`;
},
},
dateFormat(format = 'YYYY/MM/DD') {
if (!(this instanceof Date)) return console.error('dateFormat - is not Date type:', this);
const
twoDigit = n => n.toString().padStart(2, '0'),
date = this.getDate(),
year = this.getFullYear(),
monthIdx = this.getMonth(),
dayWeekIdx = this.getDay(),
hours = this.getHours(),
minutes = this.getMinutes(),
seconds = this.getSeconds();
return format
.replace(/A|Z|S(SS)?|ss?|mm?|HH?|hh?|D{1,4}|M{1,4}|YY(YY)?|'([^']|'')*'/g, partPattern => {
let out;
switch (partPattern) {
case 'YY': out = year.substr(2); break;
case 'YYYY': out = year; break;
case 'M': out = monthIdx + 1; break;
case 'MM': out = twoDigit(monthIdx + 1); break;
case 'MMM': out = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][monthIdx]; break;
case 'MMMM': out = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][monthIdx]; break;
case 'D': out = date; break;
case 'DD': out = twoDigit(date); break;
case 'DDD': out = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'][dayWeekIdx]; break;
case 'DDDD': out = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayWeekIdx]; break;
case 'h': out = (hours % 12) || 12; break;
case 'H': out = hours; break;
case 'HH': out = twoDigit(hours); break;
case 'mm': out = twoDigit(minutes); break;
case 's': out = seconds; break;
case 'ss': out = twoDigit(seconds); break;
case 'SS': out = twoDigit(seconds); break;
case 'A': out = (hours < 12 ? 'AM' : 'PM'); break;
case 'Z': out = ('+' + -this.getTimezoneOffset() / 60)
.replace(/^\D?(\D)/, "$1")
.replace(/^(.)(.)$/, "$10$2") + '00';
break;
}
return out;
});
},
numberFormat: {
abbr(num) {
num = Math.abs(+num);
if (num === 0 || isNaN(num)) return '';
else if (num < 1000) return Math.trunc(num);
else if (num < 1e4) return round(num / 1000) + 'K';
else if (num < 990000) return Math.round(num / 1000) + 'K';
else if (num < 990000000) return Math.round(num / 1e5) / 10 + 'M';
else return Math.round(num / 1e8) / 10 + 'B';
function round(num, sig = 1) {
const prec = Math.pow(10, sig);
return Math.round(num * prec) / prec;
}
},
friendly: num => new Intl.NumberFormat().format(Math.round(num * 10) / 10),
},
extractAsNum: {
float: str => (n = str?.replace(/[^0-9.]/g, '')) && +n,
int: str => (n = str?.replace(/\D+/g, '')) && +n,
},
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();
},
getHashParam: (query = required(), url_string) => location.hash && new URLSearchParams(new URL(url_string || location).hash.substring(1)).get(query.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.trunc(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);
})(),
getChannelId(api_key) {
const isChannelId = id => id && /UC([a-z0-9-_]{22})$/i.test(id);
let result = [
document.head.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.head.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,
((typeof ytcfg === 'object') && (obj = ytcfg.data_?.PLAYER_VARS?.embedded_player_response)
&& NOVA.seachInObjectBy.key({
'obj': JSON.parse(obj),
'keys': 'channelId',
})?.data),
]
.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);
}
},
},
seachInObjectBy: {
key({
obj = required(),
keys = required(),
match_fn = data => data.constructor.name !== 'Object',
multiple = false,
path = ''
}) {
const setPath = d => (path ? path + '.' : '') + d;
let hasKey, results = [];
for (const prop in obj) {
if (obj.hasOwnProperty(prop) && obj[prop]) {
hasKey = keys.constructor.name === 'String' ? (keys === prop) : keys.indexOf(prop) > -1;
if (hasKey && (!match_fn || match_fn(obj[prop]))) {
if (multiple) {
results.push({
'path': setPath(prop),
'data': obj[prop],
});
}
else {
return {
'path': setPath(prop),
'data': obj[prop],
};
}
}
else {
switch (obj[prop].constructor.name) {
case 'Object':
if (result = this.key({
'obj': obj[prop],
'keys': keys,
'path': setPath(prop),
'match_fn': match_fn,
})) {
if (multiple) results.push(result);
else return result;
}
break;
case 'Array':
for (let i = 0; i < obj[prop].length; i++) {
if (result = this.key({
'obj': obj[prop][i],
'keys': keys,
'path': path + `[${i}]`,
'match_fn': match_fn,
})) {
if (multiple) results.push(result);
else return result;
}
}
break;
case 'Function':
if (Object.keys(obj[prop]).length) {
for (const j in obj[prop]) {
if (typeof obj[prop][j] !== 'undefined') {
if (result = this.key({
'obj': obj[prop][j],
'keys': keys,
'path': setPath(prop) + '.' + j,
'match_fn': match_fn,
})) {
if (multiple) results.push(result);
else return result;
}
}
}
}
break;
}
}
}
}
if (multiple) return results;
},
},
log() {
if (this.DEBUG && arguments.length) {
console.groupCollapsed(...arguments);
console.trace();
console.groupEnd();
}
},
};
window.nova_plugins.push({
id: 'page-title-time',
title: 'Show time in tab title',
'title:zh': '在标签标题中显示时间',
'title:ja': 'タブタイトルに時間を表示する',
'title:pl': 'Pokaż czas w tytule karty',
run_on_pages: 'watch',
section: 'other',
_runtime: user_settings => {
NOVA.waitSelector('video')
.then(video => {
document.addEventListener('yt-navigate-start', () => pageTitle.backup = null);
video.addEventListener('playing', pageTitle.save.bind(pageTitle));
video.addEventListener('timeupdate', () => pageTitle.update(video));
video.addEventListener('pause', () => pageTitle.restore(video));
video.addEventListener('ended', () => pageTitle.restore(video));
});
const pageTitle = {
strSplit: ' | ',
saveCheck() {
return (result = (this.backup || document.title).includes(this.strSplit))
? new RegExp(`^((\\d?\\d:){1,2}\\d{2})(${this.strSplit.replace('|', '\\|')})`, '')
.test(document.title)
: result;
},
save() {
if (this.backup
|| movie_player.getVideoData().isLive
|| movie_player.classList.contains('ad-showing')
|| this.saveCheck()
) {
return;
}
this.backup = movie_player.getVideoData().title + ' :: ' + movie_player.getVideoData().author;
},
update(video = NOVA.videoElement) {
if (!this.backup) return;
let newTitleArr = [];
switch (movie_player.getVideoData().isLive ? 'current' : user_settings.page_title_time_mode) {
case 'current':
newTitleArr = [video.currentTime];
break;
case 'current-duration':
if (!isNaN(video.duration)) {
newTitleArr = [video.currentTime, ' / ', video.duration];
}
break;
default:
if (!isNaN(video.duration)) {
newTitleArr = [video.duration - video.currentTime];
}
}
newTitleArr = newTitleArr
.map(t => (typeof t === 'string') ? t : NOVA.formatTimeOut.HMS.digit(t / video.playbackRate))
.join('');
this.set([newTitleArr, this.backup]);
},
restore(video = NOVA.videoElement) {
if (!this.backup) return;
this.set([movie_player.getVideoData().isLive && video.currentTime, this.backup]);
},
set(arr) {
document.title = arr
.filter(Boolean)
.join(this.strSplit);
},
};
},
options: {
page_title_time_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'left', value: 'left', selected: true,
'label:zh': '剩下',
'label:ja': '左',
'label:pl': 'pozostało',
},
{
label: 'current/duration', value: 'current-duration',
'label:zh': '现在/期间',
'label:ja': '現在/期間',
'label:pl': 'bieżący czas',
},
],
},
}
});
window.nova_plugins.push({
id: 'scrollbar-hide',
title: 'Hide scrollbar (for watch page)',
run_on_pages: 'watch, -mobile',
section: 'other',
_runtime: user_settings => {
const HIDE_SCROLL_ATTR = 'nova-scrollbar-hide';
NOVA.css.push(
`html[${HIDE_SCROLL_ATTR}] {
scrollbar-width: none;
}
html[${HIDE_SCROLL_ATTR}] body::-webkit-scrollbar {
width: 0px;
height: 0px;
}`);
NOVA.runOnPageLoad(() => {
const hasAttr = document.documentElement.hasAttribute(HIDE_SCROLL_ATTR);
if ((NOVA.currentPage == 'watch') && !hasAttr) {
document.documentElement.setAttribute(HIDE_SCROLL_ATTR, true);
}
else if ((NOVA.currentPage != 'watch') && hasAttr) {
document.documentElement.removeAttribute(HIDE_SCROLL_ATTR);
}
});
if (user_settings.scrollbar_hide_toggle_on_scroll) {
window.addEventListener('scroll', function blink() {
if (NOVA.currentPage != 'watch') return;
if (document.documentElement.scrollHeight > window.innerHeight) {
if (document.documentElement.hasAttribute(HIDE_SCROLL_ATTR)) {
document.documentElement.removeAttribute(HIDE_SCROLL_ATTR);
}
if (typeof blink.fade === 'number') clearTimeout(blink.fade);
blink.fade = setTimeout(() => {
document.documentElement.setAttribute(HIDE_SCROLL_ATTR, true);
}, 700);
}
});
}
},
options: {
scrollbar_hide_toggle_on_scroll: {
_tagName: 'input',
label: 'Showing on scroll',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'channel-play-all',
title: 'Add "Play All" button',
'title:zh': '在频道页面添加“Play All”按钮',
'title:ja': 'チャンネルページに「Play All」ボタンを追加',
run_on_pages: 'channel, watch, -mobile',
restart_on_location_change: true,
section: 'channel',
_runtime: user_settings => {
const
SELECTOR_ID = 'nova-play-all-channel-btn',
endpoint = '/playlist?list=';
switch (NOVA.currentPage) {
case 'watch':
if (!user_settings.channel_play_all_in_watch) return;
NOVA.waitSelector('#owner.ytd-watch-metadata')
.then(container => {
if (channelId = NOVA.getChannelId()) {
const btnList = user_settings.channel_play_all_mode
? { id: 'UULF', title: 'All' }
: { id: 'UULP', title: 'MOST POPULAR' };
insertToHTML({
'container': container,
'url': endpoint + btnList.id + channelId.substring(2),
});
function insertToHTML({ url = required(), container = required() }) {
console.debug('insertToHTML', ...arguments);
if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
(document.getElementById(SELECTOR_ID) || (function () {
const el = document.createElement('a');
el.id = SELECTOR_ID;
el.className = 'style-scope yt-formatted-string bold yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m';
el.style.cssText = 'margin-left:5px; flex: .6;';
el.textContent = `► Play ${btnList.title}`;
el.title = 'Play all uploads videos from the channel';
return container.appendChild(el);
})())
.href = url;
}
}
});
break;
case 'channel':
let btnList;
switch (NOVA.channelTab) {
case 'videos':
btnList = user_settings.channel_play_all_mode
? { id: 'UULF', title: 'All Videos' }
: { id: 'UULP', title: 'Popular Videos' };
break;
case 'shorts':
btnList = user_settings.channel_play_all_mode
? { id: 'UUSH', title: 'All Shorts' }
: { id: 'UUPS', title: 'Popular Shorts' };
break;
case 'streams':
btnList = user_settings.channel_play_all_mode
? { id: 'UULV', title: 'All Streams' }
: { id: 'UUPV', title: 'Popular Streams' };
break;
}
if (!btnList) return;
NOVA.waitSelector('#header #chips-wrapper')
.then(container => {
container.querySelector(`.${SELECTOR_ID}`)?.remove();
const btn = document.createElement('tp-yt-paper-button');
btn.className = 'style-scope yt-formatted-string bold yt-chip-cloud-chip-renderer 1yt-spec-button-shape-next';
btn.classList.add(SELECTOR_ID);
btn.style.cssText = 'color: wheat; text-wrap: nowrap;';
btn.textContent = `► Play ${btnList.title}`;
btn.addEventListener('click', () => {
if (channelId = NOVA.getChannelId()) {
location.href = endpoint + btnList.id + channelId.substring(2);
}
});
container.append(btn);
});
break;
}
},
options: {
channel_play_all_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'all', value: true,
},
{
label: 'most popular',
},
],
},
channel_play_all_in_watch: {
_tagName: 'input',
label: 'Add in the "watch page" too',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'channel-default-tab',
title: 'Default tab on channel page',
'title:zh': '频道页默认选项卡',
'title:ja': 'チャンネルページのデフォルトタブ',
'title:pl': 'Domyślna karta na stronie kanału',
run_on_pages: 'channel',
restart_on_location_change: true,
section: 'channel',
_runtime: user_settings => {
if (NOVA.channelTab) return;
if (user_settings.channel_default_tab_mode == 'redirect') {
switch (user_settings.channel_default_tab_thumbs_sort) {
case 'popular':
location.assign(`${location.protocol}//${location.hostname}/${location.pathname}/${user_settings.channel_default_tab}?SRT=P`);
return;
break;
}
location.pathname += '/' + user_settings.channel_default_tab;
}
else {
const tabSelectors = '#tabsContent [role="tab"]';
NOVA.waitSelector(tabSelectors, { destroy_after_page_leaving: true })
.then(() => {
let tabActive;
const tabs = [...document.body.querySelectorAll(tabSelectors)];
switch (user_settings.channel_default_tab) {
case 'videos': tabActive = tabs[1]; break;
default:
location.pathname += '/' + user_settings.channel_default_tab;
}
tabActive?.click();
document.addEventListener('yt-navigate-finish', () => window.dispatchEvent(new Event('resize'))
, { capture: true, once: true });
});
}
},
options: {
channel_default_tab: {
_tagName: 'select',
label: 'Default tab',
'label:zh': '默认标签页',
'label:ja': 'デフォルトのタブ',
'label:pl': 'Domyślna karta',
options: [
{
label: 'videos', value: 'videos', selected: true,
'label:pl': 'wideo',
},
{
label: 'shorts', value: 'shorts',
},
{
label: 'live', value: 'streams',
},
{
label: 'podcasts', value: 'podcasts',
},
{
label: 'releases', value: 'releases',
},
{
label: 'playlists', value: 'playlists',
'label:pl': 'playlista',
},
{
label: 'community', value: 'community',
},
],
},
channel_default_tab_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'click',
'label:pl': 'klik',
},
{
label: 'redirect', value: 'redirect',
'label:pl': 'przekierowanie',
},
],
'data-dependent': { 'channel_default_tab': ['videos'] },
},
channel_default_tab_thumbs_sort: {
_tagName: 'select',
label: 'Sort',
options: [
{
label: 'newest', selected: true,
},
{
label: 'popular', value: 'popular',
},
],
'data-dependent': { 'channel_default_tab_mode': ['redirect'] },
},
}
});
window.nova_plugins.push({
id: 'copy-url',
title: 'Copy URL to clipboard',
'title:zh': '将 URL 复制到剪贴板',
'title:ja': 'URLをクリップボードにコピー',
run_on_pages: 'results, channel, playlist, watch, embed',
section: 'other',
_runtime: user_settings => {
const SELECTOR_ID = 'nova-copy-notification';
document.addEventListener('keydown', evt => {
const hotkeyMod = user_settings.copy_url_hotkey || 'ctrlKey';
if (hotkeyMod == 'ctrlKey' && window.getSelection && window.getSelection().toString()) return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt[hotkeyMod] && evt.code === 'KeyC') {
evt.preventDefault();
evt.stopPropagation();
evt.stopImmediatePropagation();
let url;
switch (NOVA.currentPage) {
case 'watch':
case 'embed':
url = 'https://youtu.be/' + (NOVA.queryURL.get('v') || movie_player.getVideoData().video_id);
break;
case 'channel':
url = (channelId = NOVA.getChannelId(user_settings['user-api-key']))
? `https://${location.host}/channel/` + channelId
: location.href;
break
case 'results':
case 'playlist':
url = location.href;
break
}
if (url) {
navigator.clipboard.writeText(url);
showNotification('URL copied');
}
}
});
function showNotification(msg) {
if (typeof showNotification.fade === 'number') {
clearTimeout(showNotification.fade);
clearTimeout(showNotification.hideСompletely);
}
const notify = (document.getElementById(SELECTOR_ID) || (function () {
const el = document.createElement('div');
el.id = SELECTOR_ID;
let initcss = {
position: 'fixed',
'z-index': 9999,
'border-radius': '2px',
'background-color': user_settings.copy_url_color || '#e85717',
'box-shadow': 'rgb(0 0 0 / 50%) 0px 0px 3px',
'border-radius': user_settings['square-avatars'] ? 'inherit' : '12px',
'font-size': `${+user_settings.copy_url_font_size || 1.7}em`,
color: 'var(--yt-spec-text-primary, white)',
padding: '.5em .8em',
cursor: 'pointer',
};
switch (user_settings.copy_url_position) {
case 'top-left':
initcss.top = '60px';
initcss.left = '20px';
break;
case 'bottom-left':
initcss.bottom = '20px';
initcss.left = '20px';
break;
case 'bottom-right':
initcss.bottom = '20px';
initcss.right = '20px';
break;
default:
initcss.top = '60px';
initcss.right = '20px';
break;
}
Object.assign(el.style, initcss);
return document.body.appendChild(el);
})());
notify.textContent = msg;
notify.style.opacity = +user_settings.copy_url_opacity || 1;
notify.style.visibility = 'visible';
showNotification.fade = setTimeout(() => {
notify.style.transition = 'opacity 200ms ease-out';
notify.style.opacity = 0;
showNotification.hideСompletely = setTimeout(() => notify.style.visibility = 'hidden', 5000);
}, 600);
}
},
options: {
copy_url_hotkey: {
_tagName: 'select',
label: 'Hotkey',
'label:zh': '热键',
'label:ja': 'ホットキー',
'label:pl': 'Klawisz skrótu',
options: [
{ label: 'shift+c', value: 'shiftKey', selected: true },
{ label: 'ctrl+c', value: 'ctrlKey' },
],
},
copy_url_position: {
_tagName: 'select',
label: 'Notification position',
options: [
{
label: '↖', value: 'top-left',
},
{
label: '↗', value: 'top-right', selected: true,
},
{
label: '↙', value: 'bottom-left',
},
{
label: '↘', value: 'bottom-right',
},
],
},
copy_url_opacity: {
_tagName: 'input',
label: 'Opacity',
type: 'number',
placeholder: '0.1-1',
step: .1,
min: .1,
max: 1,
value: .8,
},
copy_url_font_size: {
_tagName: 'input',
label: 'Font size',
type: 'number',
title: 'in em',
placeholder: '0.5-3',
step: .1,
min: .5,
max: 3,
value: 1.7,
},
copy_url_color: {
_tagName: 'input',
type: 'color',
value: '#e85717',
label: 'Color',
'label:zh': '颜色',
'label:ja': '色',
'label:pl': 'Kolor',
title: 'default - #e85717',
},
}
});
window.nova_plugins.push({
id: 'rss-link',
title: 'Add RSS feed link',
'title:zh': '添加 RSS 提要链接',
'title:ja': 'RSSフィードリンクを追加',
'title:pl': 'Dodaj kanał RSS',
run_on_pages: 'channel, playlist, -mobile',
restart_on_location_change: true,
section: 'channel',
_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.get('#header div.banner-visible-area', 'height'))) {
container = document.body.querySelector('#channel-header #inner-header-container #buttons');
}
if (url = (document.head.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', { destroy_after_page_leaving: true })
.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.title = 'Nova RSS';
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: 'shorts-redirect',
title: 'Redirect Shorts to regular (watch) URLs',
'title:zh': '将 Shorts 重定向到常规(watch)URL',
'title:ja': 'ショートパンツを通常の(watch)URLにリダイレクトする',
'title:pl': 'Przełączaj Shorts na zwykłe adresy URL',
run_on_pages: 'shorts',
restart_on_location_change: true,
section: 'player',
desc: 'Redirect Shorts video to normal player',
'desc:zh': '将 Shorts 视频重定向到普通播放器',
'desc:ja': 'ショートパンツのビデオを通常のプレーヤーにリダイレクトする',
'desc:pl': 'Przełącza krótkie filmy do normalnego odtwarzacza',
_runtime: user_settings => {
location.href = location.href.replace('shorts/', 'watch?v=');
},
});
window.nova_plugins.push({
id: 'collapse-navigation-panel',
title: 'Collapse navigation panel',
'title:zh': '折叠导航面板',
'title:ja': 'ナビゲーション パネルを折りたたむ',
'title:pl': 'Zwiń panel nawigacyjny',
run_on_pages: '*, -watch, -embed, -live_chat',
section: 'other',
_runtime: user_settings => {
NOVA.waitSelector('#guide[opened]')
.then(el => {
document.getElementById('guide-button').click();
el.removeAttribute('opened');
});
},
});
window.nova_plugins.push({
id: 'channel-trailer-stop-preload',
title: 'Stop play channel trailer',
'title:zh': '停止频道预告片',
'title:ja': 'チャンネルの予告編を停止する',
'title:pl': 'Zatrzymaj zwiastun kanału',
run_on_pages: 'channel, -mobile',
restart_on_location_change: true,
section: 'channel',
_runtime: user_settings => {
NOVA.waitSelector('#c4-player.playing-mode', { destroy_after_page_leaving: true })
.then(player => player.stopVideo());
},
});
window.nova_plugins.push({
id: 'default-miniplayer-disable',
title: 'Disable miniplayer',
run_on_pages: 'results, feed, channel, watch, -mobile',
section: 'other',
desc: 'shown on changeable page when playing playlist',
_runtime: user_settings => {
NOVA.css.push(
`.ytp-right-controls .ytp-miniplayer-button {
display: none !important;
}`);
document.addEventListener('yt-action', evt => {
if (evt.detail?.actionName.includes('miniplayer')) {
document.body.querySelector('ytd-miniplayer[active]')
?.remove();
}
});
document.addEventListener('keydown', evt => {
if (['input', 'textarea'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
if (NOVA.currentPage == 'watch' && evt.code === 'KeyI') {
evt.preventDefault();
}
}, { capture: true });
},
});
window.nova_plugins.push({
id: 'pages-clear',
title: 'Clear pages of junk',
'title:zh': '清除垃圾页面',
'title:ja': 'ジャンクページをクリアする',
'title:pl': 'Wyczyść strony ze śmieci',
run_on_pages: 'results, feed, watch, embed, -mobile',
section: 'other',
desc: 'Remove the annoying stuff',
'desc:zh': '删除烦人的东西',
'desc:ja': '煩わしいものを取り除く',
'desc:pl': 'Usuń irytujące rzeczy',
_runtime: user_settings => {
let selectorsList = [
'.ytp-paid-content-overlay',
'.iv-branding',
'#movie_player:not(:hover) > [class^="ytp-ce-"]',
'.ytp-cards-teaser-text',
'ytm-paid-content-overlay-renderer',
];
switch (NOVA.currentPage) {
case 'embed':
selectorsList.push([
(user_settings['player-quick-buttons'] && user_settings.player_buttons_custom_items?.includes('card-switch')) || '.ytp-pause-overlay',
'.ytp-info-panel-preview',
]);
break;
default:
selectorsList.push([
'ytd-search-pyv-renderer',
'[class^="ytd-promoted-"]',
'ytd-search-pyv-renderer ~ ytd-shelf-renderer',
'ytd-video-renderer + ytd-shelf-renderer',
'#clarify-box',
'ytd-watch-metadata ytd-info-panel-content-renderer',
'.ytd-watch-flexy.attached-message',
'ytd-popup-container tp-yt-paper-dialog ytd-single-option-survey-renderer',
'#donation-shelf ytd-donation-unavailable-renderer',
`#subscribe-button .smartimation__border,
#subscribe-button .smartimation__background,
ytd-watch-metadata #actions .smartimation__border,
ytd-watch-metadata #actions .smartimation__background`,
'[class^="ytp-cultural-moment"]',
'ytd-donation-unavailable-renderer, .ytd-donation-unavailable-renderer',
'.sparkles-light-cta',
'ytd-feed-nudge-renderer',
]);
if (CSS.supports('selector(:has(*))')) {
selectorsList.push([
'ytd-rich-item-renderer:has(ytd-ad-slot-renderer)',
'#chat[collapsed] #message',
'ytd-popup-container:has(yt-tooltip-renderer[position-type="OPEN_POPUP_POSITION_BOTTOM"])',
]);
}
}
if (selectorsList.length) {
NOVA.css.push(
selectorsList.join(',\n') + ` {
display: none !important;
}`);
}
},
});
window.nova_plugins.push({
id: 'scroll-to-top',
title: 'Add "Scroll to top" button',
'title:zh': '滚动到顶部按钮',
'title:ja': 'トップボタンまでスクロール',
'title:pl': 'Przycisk przewijania do góry',
run_on_pages: '*, -embed, -mobile, -live_chat',
section: 'other',
desc: 'Displayed on long pages',
'desc:zh': '出现在长页面上',
'desc:ja': '長いページに表示されます',
'desc:pl': 'Wyświetlaj na długich stronach',
_runtime: user_settings => {
document.addEventListener('scroll', insertButton, { capture: true, once: true });
function insertButton() {
const SELECTOR_ID = 'nova-scrollTop-btn';
const btn = document.createElement('button');
btn.id = SELECTOR_ID;
Object.assign(btn.style, {
position: 'fixed',
cursor: 'pointer',
bottom: 0,
left: '20%',
visibility: 'hidden',
opacity: .5,
width: '40%',
height: '40px',
border: 'none',
outline: 'none',
'z-index': 1,
'border-radius': '100% 100% 0 0',
'font-size': '16px',
'background-color': 'rgba(0,0,0,.3)',
'box-shadow': '0 16px 24px 2px rgba(0, 0, 0, .14), 0 6px 30px 5px rgba(0, 0, 0, .12), 0 8px 10px -5px rgba(0, 0, 0, .4)',
});
btn.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: user_settings.scroll_to_top_smooth ? 'smooth' : 'instant',
});
if (user_settings.scroll_to_top_autoplay && NOVA.currentPage == 'watch'
&& ['UNSTARTED', 'PAUSED'].includes(NOVA.getPlayerState())
) {
movie_player.playVideo();
}
});
const arrow = document.createElement('span');
Object.assign(arrow.style, {
border: 'solid white',
'border-width': '0 3px 3px 0',
display: 'inline-block',
padding: '4px',
'vertical-align': 'middle',
transform: 'rotate(-135deg)',
});
btn.append(arrow);
document.body.append(btn);
NOVA.css.push(
`#${SELECTOR_ID}:hover {
opacity: 1 !important;
background-color: rgba(0,0,0,.6) !important;
}`);
const scrollTop_btn = document.getElementById(SELECTOR_ID);
let sOld;
window.addEventListener('scroll', () => {
const sCurr = document.documentElement.scrollTop > (window.innerHeight / 2);
if (sCurr == sOld) return;
sOld = sCurr;
scrollTop_btn.style.visibility = sCurr ? 'visible' : 'hidden';
});
}
},
options: {
scroll_to_top_smooth: {
_tagName: 'input',
label: 'Smooth',
'label:zh': '光滑的',
'label:ja': 'スムーズ',
'label:pl': 'Płynnie',
type: 'checkbox',
},
scroll_to_top_autoplay: {
_tagName: 'input',
label: 'Unpause a video',
'label:zh': '视频取消暂停',
'label:ja': 'ビデオの一時停止解除',
'label:pl': 'Wyłącz wstrzymanie odtwarzania filmu',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'pause-background-tab',
title: 'Autopause when switching tabs',
'title:zh': '自动暂停除活动选项卡以外的所有选项卡',
'title:ja': 'アクティブなタブを除くすべてのタブを自動一時停止',
'title:pl': 'Zatrzymanie kart w tle oprócz aktywnej',
run_on_pages: 'watch, embed',
section: 'player',
desc: 'Autopause all background tabs except the active one',
_runtime: user_settings => {
if (location.hostname.includes('youtube-nocookie.com')) {
location.hostname = 'youtube.com';
return;
}
if (typeof window === 'undefined') return;
const
storeName = 'nova-playing-instanceIDTab',
instanceID = String(Math.random()),
removeStorage = () => localStorage.removeItem(storeName);
NOVA.waitSelector('video')
.then(video => {
if (user_settings.pause_background_tab_autoplay_onfocus
&& user_settings.pause_background_tab_autopause_unfocus
) {
} else {
video.addEventListener('playing', checkInstance);
['pause', '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()
&& !document.pictureInPictureElement
) {
video.pause();
}
});
function checkInstance() {
if (user_settings.pause_background_tab_autoplay_onfocus !== true
&& localStorage.hasOwnProperty(storeName) && localStorage.getItem(storeName) !== instanceID
&& !document.pictureInPictureElement
) {
video.pause();
}
else {
localStorage.setItem(storeName, instanceID);
}
}
}
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();
}
}, user_settings.pause_background_tab_autoplay_onfocus == 'force' ? false : { capture: true, once: true });
}
if (user_settings.pause_background_tab_autopause_unfocus) {
window.addEventListener('blur', () => {
if ('PLAYING' == NOVA.getPlayerState()
&& !document.pictureInPictureElement
) {
video.pause();
}
});
}
});
},
options: {
pause_background_tab_autoplay_onfocus: {
_tagName: 'select',
label: 'Autoplay on tab focus mode',
'label:zh': '在标签焦点上自动播放',
'label:ja': 'タブフォーカスでの自動再生',
'label:pl': 'Autoodtwarzanie po wybraniu karty',
options: [
{
label: 'disable', selected: true,
},
{
label: 'once for new tab', value: true,
},
{
label: 'always for not started', value: 'force',
},
],
},
pause_background_tab_autopause_unfocus: {
_tagName: 'input',
label: 'Autopause if tab loses focus',
'label:zh': '如果选项卡失去焦点,则自动暂停视频',
'label:ja': 'タブがフォーカスを失った場合にビデオを自動一時停止',
'label:pl': 'Automatycznie wstrzymaj wideo, jeśli karta straci ostrość',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'video-rate',
title: 'Playback speed control',
'title:zh': '播放速度控制',
'title:ja': '再生速度制御',
'title:pl': 'Kontrola prędkości odtwarzania',
run_on_pages: 'home, results, feed, channel, playlist, watch, embed',
section: 'player',
desc: 'With mouse wheel',
'desc:zh': '带鼠标滚轮',
'desc:ja': 'マウスホイール付き',
'desc:pl': 'Za pomocą kółka myszy',
_runtime: user_settings => {
if (user_settings.rate_overlay_time && +user_settings.rate_default !== 1) {
reCalcOverlayTime();
}
NOVA.waitSelector('#movie_player video')
.then(video => {
const sliderContainer = insertSlider.apply(video);
video.addEventListener('ratechange', function () {
NOVA.showOSD(this.playbackRate + 'x');
if (Object.keys(sliderContainer).length) {
sliderContainer.slider.value = this.playbackRate;
sliderContainer.slider.title = `Speed (${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.runOnPageLoad(async () => {
if (NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed') {
if (user_settings['save-channel-state']) {
if (userRate = await NOVA.storage_obj_manager.getParam('speed')) {
video.addEventListener('playing', () => playerRate.set(userRate), { capture: true, once: true });
}
}
expandAvailableRatesMenu();
}
});
});
if (user_settings.rate_hotkey == 'keyboard') {
document.addEventListener('keydown', evt => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
let delta;
switch (user_settings.rate_hotkey_custom_up.length === 1 ? evt.key : evt.code) {
case user_settings.rate_hotkey_custom_up: delta = 1; break;
case user_settings.rate_hotkey_custom_down: delta = -1; break;
}
if (delta) {
evt.preventDefault();
if (step = +user_settings.rate_step * Math.sign(delta)) {
const rate = playerRate.adjust(step);
}
}
}, { capture: true });
}
else 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)
) {
if (step = +user_settings.rate_step * Math.sign(evt.wheelDelta)) {
const rate = playerRate.adjust(step);
}
}
}, { capture: true });
});
}
if (+user_settings.rate_default !== 1 && user_settings.rate_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]', { destroy_after_page_leaving: true })
.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)
&& (NOVA.videoElement?.playbackRate <= 2)
&& ((NOVA.videoElement?.playbackRate % .25) === 0)
&& (typeof movie_player === 'object' && typeof movie_player.getPlaybackRate === 'function'),
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) return false;
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 <= (+user_settings.rate_max || 2)) && +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_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-color: #f00;
}
${SELECTOR} [type="checkbox"]:checked:after {
left: 20px;
background-color: white;
}`);
const slider = document.createElement('input');
slider.className = 'ytp-menuitem-slider';
slider.type = 'range';
slider.min = +user_settings.rate_step;
slider.max = Math.max((+user_settings.rate_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 (Object.keys(_yt_player).length
&& (path = NOVA.seachInObjectBy.key({
'obj': _yt_player,
'keys': 'getAvailablePlaybackRates',
})?.path)) {
setAvailableRates(_yt_player, 0, path.split('.'));
}
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);
}
}
}
function reCalcOverlayTime() {
const
ATTR_MARK = 'nova-thumb-overlay-time-recalc';
document.addEventListener('yt-action', evt => {
switch (evt.detail?.actionName) {
case 'yt-append-continuation-items-action':
case 'ytd-update-grid-state-action':
case 'yt-service-request':
case 'ytd-rich-item-index-update-action':
switch (NOVA.currentPage) {
case 'home':
case 'results':
case 'feed':
case 'channel':
case 'watch':
document.body.querySelectorAll(`#thumbnail #overlays #text:not([${ATTR_MARK}])`)
.forEach(overlay => {
if ((timeLabelEl = overlay.textContent.trim())
) {
overlay.setAttribute(ATTR_MARK, true);
const timeSec = NOVA.formatTimeOut.hmsToSec(timeLabelEl);
overlay.textContent =
NOVA.formatTimeOut.HMS.digit(timeSec / user_settings.rate_default);
}
});
break;
}
break;
}
});
}
},
options: {
rate_default: {
_tagName: 'input',
label: 'Speed at startup',
'label:zh': '启动速度',
'label:ja': '起動時の速度',
'label:pl': 'Prędkość przy uruchamianiu',
type: 'number',
title: '1 - default',
step: 0.05,
min: 1,
max: 5,
value: 1,
},
rate_apply_music: {
_tagName: 'select',
label: 'For music genre',
title: 'Extended detection - may trigger falsely',
'title:zh': '扩展检测 - 可能会错误触发',
'title:ja': '拡張検出-誤ってトリガーされる可能性があります',
'title:pl': 'Rozszerzona detekcja - może działać błędnie',
options: [
{
label: 'skip', value: true, selected: true,
'label:zh': '跳过',
'label:ja': 'スキップ',
'label:pl': 'tęsknić',
},
{
label: 'force apply', value: false,
'label:zh': '施力',
'label:ja': '力を加える',
'label:pl': 'zastosować siłę',
},
],
'data-dependent': { 'rate_default': '!1' },
},
rate_overlay_time: {
_tagName: 'input',
label: 'Recalculate time in thumbnail overlay',
type: 'checkbox',
title: 'by "startup" value',
'data-dependent': { 'rate_default': '!1' },
},
rate_hotkey: {
_tagName: 'select',
label: 'Hotkey',
'label:zh': '热键',
'label:ja': 'ホットキー',
'label:pl': 'Klawisz skrótu',
options: [
{ label: 'none', value: false },
{ label: 'alt+wheel', value: 'altKey', selected: true },
{ label: 'shift+wheel', value: 'shiftKey' },
{ label: 'ctrl+wheel', value: 'ctrlKey' },
{ label: 'wheel', value: 'none' },
{ label: 'keyboard', value: 'keyboard' },
],
},
rate_hotkey_custom_up: {
_tagName: 'select',
label: 'Hotkey up',
options: [
{ label: ']', value: ']', selected: true },
{ label: 'none', },
{ label: 'ShiftL', value: 'ShiftLeft' },
{ label: 'ShiftR', value: 'ShiftRight' },
{ label: 'CtrlL', value: 'ControlLeft' },
{ label: 'CtrlR', value: 'ControlRight' },
{ label: 'AltL', value: 'AltLeft' },
{ label: 'AltR', value: 'AltRight' },
{ label: 'ArrowUp', value: 'ArrowUp' },
{ label: 'ArrowDown', value: 'ArrowDown' },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
'[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
'data-dependent': { 'rate_hotkey': ['keyboard'] },
},
rate_hotkey_custom_down: {
_tagName: 'select',
label: 'Hotkey down',
options: [
{ label: '[', value: '[', selected: true },
{ label: 'none', },
{ label: 'ShiftL', value: 'ShiftLeft' },
{ label: 'ShiftR', value: 'ShiftRight' },
{ label: 'CtrlL', value: 'ControlLeft' },
{ label: 'CtrlR', value: 'ControlRight' },
{ label: 'AltL', value: 'AltLeft' },
{ label: 'AltR', value: 'AltRight' },
{ label: 'ArrowUp', value: 'ArrowUp' },
{ label: 'ArrowDown', value: 'ArrowDown' },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '+', '-', ',', '.', '/', '<', ';', '\\',
],
'data-dependent': { 'rate_hotkey': ['keyboard'] },
},
rate_step: {
_tagName: 'input',
label: 'Hotkey step',
'label:zh': '步',
'label:ja': 'ステップ',
'label:pl': 'Krok',
type: 'number',
title: '0.25 - default',
placeholder: '0.1-1',
step: 0.05,
min: 0.05,
max: 0.5,
value: 0.25,
},
rate_max: {
_tagName: 'input',
label: 'Hotkey Max',
type: 'number',
title: '2 - default',
placeholder: '2-5',
step: .05,
min: 2,
max: 5,
value: 2,
'data-dependent': { 'rate_hotkey': ['!false', '!'] },
},
}
});
window.nova_plugins.push({
id: 'video-volume',
title: 'Volume',
'title:zh': '体积',
'title:ja': '音量',
'title:pl': 'Głośność',
run_on_pages: 'watch, embed, -mobile',
section: 'player',
desc: 'With mouse wheel',
'desc:zh': '带鼠标滚轮',
'desc:ja': 'マウスホイール付き',
'desc:pl': 'Za pomocą kółka myszy',
_runtime: user_settings => {
NOVA.waitSelector('#movie_player video')
.then(video => {
video.addEventListener('volumechange', function () {
NOVA.showOSD(Math.round(this.volume * 100) + '%');
playerVolume.buildVolumeSlider();
if (user_settings.volume_mute_unsave) {
playerVolume.saveInSession(movie_player.getVolume());
}
});
if (user_settings.volume_loudness_normalization) {
const { set } = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'volume');
Object.defineProperty(HTMLMediaElement.prototype, 'volume', {
enumerable: true,
configurable: true,
set(new_value) {
new_value = movie_player.getVolume() / 100;
set.call(this, new_value);
}
});
}
if (user_settings.volume_hotkey == 'keyboard') {
document.addEventListener('keydown', evt => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
let delta;
switch (user_settings.volume_hotkey_custom_up.length === 1 ? evt.key : evt.code) {
case user_settings.volume_hotkey_custom_up: delta = 1; break;
case user_settings.volume_hotkey_custom_down: delta = -1; break;
}
if (delta) {
evt.preventDefault();
if (step = +user_settings.volume_step * Math.sign(delta)) {
const volume = playerVolume.adjust(step);
}
}
}, { capture: true });
}
else if (user_settings.volume_hotkey) {
NOVA.waitSelector('.html5-video-container')
.then(container => {
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)) {
const volume = playerVolume.adjust(step);
}
}
}, { capture: true });
});
}
if (defaultLevel = +user_settings.volume_default) {
video.addEventListener('playing', () => {
(defaultLevel > 100)
? playerVolume.unlimit(defaultLevel)
: playerVolume.set(defaultLevel);
}, { capture: true, once: true });
}
if (user_settings['save-channel-state']) {
NOVA.runOnPageLoad(async () => {
if ((NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed')
&& (userVolume = await NOVA.storage_obj_manager.getParam('volume'))
) {
video.addEventListener('playing', () => 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 !== 'object' || !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()) {
}
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) {
console.debug('unlimit:', level);
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 = Math.trunc(level / 100);
source.connect(this.node);
this.node.connect(this.audioCtx.destination);
}
if (this.node.gain.value < 6) this.node.gain.value += 1;
NOVA.showOSD(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, .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;
return container.appendChild(el);
})())
.textContent = text;
container.title = `${text} %`;
}
}
};
},
options: {
volume_default: {
_tagName: 'input',
label: 'Default level',
'label:zh': '默认音量',
'label:ja': 'デフォルトのボリューム',
'label:pl': 'Poziom domyślny',
type: 'number',
title: '0 - auto',
placeholder: '%',
step: 5,
min: 0,
max: 600,
value: 100,
},
volume_hotkey: {
_tagName: 'select',
label: 'Hotkey',
'label:zh': '热键',
'label:ja': 'ホットキー',
'label:pl': 'Klawisz skrótu',
options: [
{ label: 'none', value: false },
{ label: 'wheel', value: 'none', selected: true },
{ label: 'shift+wheel', value: 'shiftKey' },
{ label: 'ctrl+wheel', value: 'ctrlKey' },
{ label: 'alt+wheel', value: 'altKey' },
{ label: 'keyboard', value: 'keyboard' },
],
},
volume_step: {
_tagName: 'input',
label: 'Hotkey step',
'label:zh': '步',
'label:ja': 'ステップ',
'label:pl': 'Krok',
type: 'number',
title: 'in %',
placeholder: '%',
min: 1,
max: 30,
value: 10,
'data-dependent': { 'volume_hotkey': ['!false'] },
},
volume_hotkey_custom_up: {
_tagName: 'select',
label: 'Hotkey up',
options: [
{ label: 'ShiftL', value: 'ShiftLeft' },
{ label: 'ShiftR', value: 'ShiftRight' },
{ label: 'CtrlL', value: 'ControlLeft' },
{ label: 'CtrlR', value: 'ControlRight' },
{ label: 'AltL', value: 'AltLeft' },
{ label: 'AltR', value: 'AltRight' },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
'[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
'data-dependent': { 'volume_hotkey': ['keyboard'] },
},
volume_hotkey_custom_down: {
_tagName: 'select',
label: 'Hotkey down',
options: [
{ label: 'ShiftL', value: 'ShiftLeft' },
{ label: 'ShiftR', value: 'ShiftRight' },
{ label: 'CtrlL', value: 'ControlLeft' },
{ label: 'CtrlR', value: 'ControlRight' },
{ label: 'AltL', value: 'AltLeft' },
{ label: 'AltR', value: 'AltRight' },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
'[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
'data-dependent': { 'volume_hotkey': ['keyboard'] },
},
volume_mute_unsave: {
_tagName: 'input',
label: 'Not keep muted state',
'label:zh': '不保存静音模式',
'label:ja': 'マナーモードを保存しない',
'label:pl': 'Nie zachowuj wyciszonego stanu',
type: 'checkbox',
title: 'Only affects new tabs',
'title:zh': '只影响新标签',
'title:ja': '新しいタブにのみ影響します',
'title:pl': 'Dotyczy tylko nowych kart',
},
volume_loudness_normalization: {
_tagName: 'input',
label: 'Stable volume level',
type: 'checkbox',
title: 'Boost volume level',
},
volume_unlimit: {
_tagName: 'input',
label: 'Allow above 100%',
'label:zh': '允许超过 100%',
'label:ja': '100%以上を許可する',
'label:pl': 'Zezwól powyżej 100%',
type: 'checkbox',
title: 'With sound distortion',
'data-dependent': { 'volume_hotkey': ['!false'] },
},
}
});
window.nova_plugins.push({
id: 'player-pin-scroll',
title: 'Pin player while scrolling',
'title:zh': '滚动时固定播放器',
'title:ja': 'スクロール中にプレイヤーを固定する',
'title:pl': 'Przypnij odtwarzacz podczas przewijania',
run_on_pages: 'watch, -mobile',
section: 'player',
desc: 'Show mini player when scrolling down',
_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);
makeDraggable.reset();
makeDraggable.disable();
}
else if (!document.fullscreenElement
&& document.documentElement.scrollTop
) {
movie_player.classList.add(CLASS_VALUE);
makeDraggable.init(movie_player);
if (makeDraggable.storePos?.X) makeDraggable.moveByCoordinates(makeDraggable.storePos);
}
window.dispatchEvent(new Event('resize'));
}, {
threshold: .5,
})
.observe(container);
});
}, { capture: true, once: true });
NOVA.waitSelector(PINNED_SELECTOR)
.then(async 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', () =>
document.fullscreenElement && 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, { destroy_after_page_leaving: true })
.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('yt-navigate-start', () => scrollPos = 0);
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement
&& scrollPos
&& makeDraggable.storePos
) {
window.scrollTo({
top: scrollPos,
});
}
}, { capture: false });
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
scrollPos = document.documentElement.scrollTop;
}
}, { capture: true });
}
});
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, .14),' +
'0 6px 30px 5px rgba(0, 0, 0, .12),' +
'0 8px 10px -5px rgba(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(
`html[style*="ytrb-bar"] ${PINNED_SELECTOR} {
--zIndex: 1000;
}
${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);
${user_settings['square-avatars'] ? '' : 'border-radius: 12px;'}
margin: 1em 2em;
--zIndex: ${1 + Math.max(
NOVA.css.get('#chat', 'z-index'),
NOVA.css.get('.ytp-chrome-top .ytp-cards-button', 'z-index'),
NOVA.css.get('#chat', 'z-index'),
NOVA.css.get('ytrb-bar', 'z-index'),
601)};
}
${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;
}
${PINNED_SELECTOR}.ended-mode video {
visibility: hidden;
}`);
}
function insertUnpinButton(player = movie_player) {
NOVA.css.push(
UNPIN_BTN_SELECTOR + ` { display: none; }
${PINNED_SELECTOR} ${UNPIN_BTN_SELECTOR} {
display: inherit !important;
position: absolute;
cursor: pointer;
top: 10px;
left: 10px;
width: 28px;
height: 28px;
color: white;
border: none;
outline: none;
opacity: .1;
${user_settings['square-avatars'] ? '' : 'border-radius: 100%;'}
z-index: var(--zIndex);
font-size: 24px;
font-weight: bold;
background-color: rgba(0, 0, 0, .8);
transition: opacity 100ms linear;
}
${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);
makeDraggable.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);
makeDraggable.reset();
}
});
}
const makeDraggable = {
attrNameMoving: 'nova-el-moving',
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('touchstart', this.dragStart.bind(this));
document.addEventListener('touchend', this.dragEnd.bind(this));
document.addEventListener('touchmove', this.draging.bind(this));
document.addEventListener('mousedown', this.dragStart.bind(this));
document.addEventListener('mouseup', this.dragEnd.bind(this));
document.addEventListener('mousemove', this.draging.bind(this));
},
reset(clear_storePos) {
this.dragTarget?.style.removeProperty('transform');
this.storePos = clear_storePos
? this.xOffset = this.yOffset = 0
: { 'X': this.xOffset, 'Y': this.yOffset };
},
disable() {
this.log('dragDisable', ...arguments);
this.dragTarget = null;
document.removeEventListener('touchstart', this.dragStart);
document.removeEventListener('touchend', this.dragEnd);
document.removeEventListener('touchmove', this.draging);
document.removeEventListener('mousedown', this.dragStart);
document.removeEventListener('mouseup', this.dragEnd);
document.removeEventListener('mousemove', this.draging);
},
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.moving = true;
},
dragEnd(evt) {
if (!this.moving) return;
this.log('dragEnd');
this.initialX = this.currentX;
this.initialY = this.currentY;
this.moving = false;
this.dragTarget.style.pointerEvents = null;
document.body.style.removeProperty('cursor');
this.dragTarget.removeAttribute(this.attrNameMoving);
},
draging(evt) {
if (!this.moving) return;
this.log('draging');
this.dragTarget.style.pointerEvents = 'none';
document.body.style.cursor = 'move';
if (!this.dragTarget.hasAttribute(this.attrNameMoving)) this.dragTarget.setAttribute(this.attrNameMoving, true);
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.moveByCoordinates({ 'X': this.currentX, 'Y': this.currentY });
},
moveByCoordinates({ X = required(), Y = required() }) {
this.log('moveByCoordinates', ...arguments);
this.dragTarget.style.transform = `translate3d(${X}px, ${Y}px, 0)`;
},
log() {
if (this.DEBUG && arguments.length) {
console.groupCollapsed(...arguments);
console.trace();
console.groupEnd();
}
},
};
},
options: {
player_float_scroll_size_ratio: {
_tagName: 'input',
label: 'Player size',
'label:zh': '播放器尺寸',
'label:ja': 'プレーヤーのサイズ',
'label:pl': 'Rozmiar odtwarzacza',
type: 'number',
title: 'Less value - larger size',
'title:zh': '较小的值 - 较大的尺寸',
'title:ja': '小さい値-大きいサイズ',
'title:pl': 'Mniejsza wartość - większy rozmiar',
placeholder: '2-5',
step: 0.1,
min: 1,
max: 5,
value: 2.5,
},
player_float_scroll_position: {
_tagName: 'select',
label: 'Player position',
'label:zh': '球员位置',
'label:ja': 'プレイヤーの位置',
'label:pl': 'Pozycja odtwarzacza',
options: [
{
label: '↖', value: 'top-left',
},
{
label: '↗', value: 'top-right', selected: true,
},
{
label: '↙', value: 'bottom-left',
},
{
label: '↘', value: 'bottom-right',
},
],
},
player_float_scroll_after_fullscreen_restore_srcoll_pos: {
_tagName: 'input',
label: 'Restore scrolling back there after exiting fullscreen',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'video-quality',
title: 'Video quality',
'title:zh': '视频质量',
'title:ja': 'ビデオ品質',
'title:pl': 'Jakość wideo',
run_on_pages: 'watch, embed',
section: 'player',
_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_for_music = false;
user_settings.video_quality_for_fullscreen = false;
}
});
}
if (user_settings['save-channel-state']) {
NOVA.runOnPageLoad(async () => {
if ((NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed')
&& (userQuality = await NOVA.storage_obj_manager.getParam('quality'))
) {
selectedQuality = userQuality;
}
});
}
setQuality();
movie_player.addEventListener('onStateChange', setQuality);
if (user_settings.video_quality_for_fullscreen) {
let selectedQualityBackup = selectedQuality;
document.addEventListener('fullscreenchange', () => {
selectedQuality = document.fullscreenElement
? user_settings.video_quality_for_fullscreen
: selectedQualityBackup;
movie_player.setPlaybackQualityRange(selectedQuality, selectedQuality);
});
}
});
async function setQuality(state) {
if (!selectedQuality) return console.error('selectedQuality unavailable', selectedQuality);
if (user_settings.video_quality_for_music
&& location.search.includes('list=')
&& NOVA.isMusic()
) {
selectedQuality = user_settings.video_quality_for_music;
}
if ((1 == state || 3 == state) && !this.quality_lock) {
this.quality_lock = true;
let availableQualityLevels;
await NOVA.waitUntil(() => (availableQualityLevels = movie_player.getAvailableQualityLevels()) && availableQualityLevels.length, 50);
if (user_settings.video_quality_premium
&& (qualityToSet = [...movie_player.getAvailableQualityData()]
.find(q => //q.quality == selectedQuality
q.isPlayable &&
q.qualityLabel?.toLocaleLowerCase().includes('premium'))?.qualityLabel
)
) {
return setPremium(qualityToSet);
}
const maxWidth = (NOVA.currentPage == 'watch') ? screen.width : window.innerWidth;
const maxQualityIdx = availableQualityLevels.findIndex(i => qualityFormatListWidth[i] <= (maxWidth * 1.3));
availableQualityLevels = availableQualityLevels.slice(maxQualityIdx);
const availableQualityIdx = function () {
let i = availableQualityLevels.indexOf(selectedQuality);
if (i === -1) {
const
availableQuality = Object.keys(qualityFormatListWidth)
.filter(v => availableQualityLevels.includes(v) || (v == selectedQuality)),
nearestQualityIdx = availableQuality.findIndex(q => q === selectedQuality) - 1;
i = availableQualityLevels[nearestQualityIdx] ? nearestQualityIdx : 0;
}
return i;
}();
const newQuality = availableQualityLevels[availableQualityIdx];
if (typeof movie_player.setPlaybackQuality === 'function') {
movie_player.setPlaybackQuality(newQuality);
}
if (typeof movie_player.setPlaybackQualityRange === 'function') {
movie_player.setPlaybackQualityRange(newQuality, newQuality);
}
}
else if (state <= 0) {
this.quality_lock = false;
}
}
async function setPremium(qualityLabel = required()) {
const SELECTOR_CONTAINER = '#movie_player';
const settingsButton = await NOVA.waitSelector(`${SELECTOR_CONTAINER} .ytp-chrome-bottom button.ytp-settings-button[aria-expanded="false"]`);
settingsButton.click();
//const qualityMenuButton = await NOVA.waitSelector(`${SELECTOR_CONTAINER} .ytp-settings-menu [role="menuitem"]:last-child`);
const qualityMenuButton = [...document.body.querySelectorAll(`${SELECTOR_CONTAINER} .ytp-settings-menu [role="menuitem"] .ytp-menuitem-content`)]
.find(menuItem => menuItem.textContent.toLocaleLowerCase().includes('auto') || (NOVA.extractAsNum.int(menuItem.textContent) >= 144));
qualityMenuButton.click();
const qualityItem = [...document.body.querySelectorAll('.ytp-quality-menu [role="menuitemradio"]')]
.find(menuItem => menuItem.textContent.includes(qualityLabel));
await NOVA.delay(1500);
qualityItem.click();
document.body.click();
document.body.querySelector('video').focus();
setQuality.quality_lock = true;
}
NOVA.waitSelector('.ytp-error [class*="reason"]', { destroy_after_page_leaving: true })
.then(error_reason_el => {
if (alertText = error_reason_el.textContent) {
throw alertText;
}
});
},
options: {
video_quality: {
_tagName: 'select',
label: 'Default',
'label:zh': '默认视频质量',
'label:ja': 'デフォルトのビデオ品質',
'label:pl': 'Domyślna jakość',
options: [
{ label: '8K/4320p', value: 'highres' },
{ label: '5K/2880p', value: 'hd2880' },
{ label: '4K/2160p', value: 'hd2160' },
{ label: 'QHD/1440p', value: 'hd1440' },
{ label: 'FHD/1080p', value: 'hd1080', selected: true },
{ label: 'HD/720p', value: 'hd720' },
{ label: '480p', value: 'large' },
{ label: '360p', value: 'medium' },
{ label: 'SD/240p', value: 'small' },
{ label: '144p', value: 'tiny' },
],
},
video_quality_premium: {
_tagName: 'input',
label: 'Use Premium bitrate if available',
type: 'checkbox',
},
video_quality_manual_save_in_tab: {
_tagName: 'input',
label: 'Save manual selection for next video',
'label:zh': '手动选择的质量保存在当前选项卡中',
'label:ja': '手動で選択した品質が現在のタブに保存されます',
'label:pl': 'Właściwości dla obecnej karty',
type: 'checkbox',
title: 'Affects to next videos',
'title:zh': '对下一个视频的影响',
'title:ja': '次の動画への影響',
'title:pl': 'Zmiany w następnych filmach',
},
video_quality_for_music: {
_tagName: 'select',
label: 'For music (in playlists)',
title: 'to save traffic / increase speed',
'title:zh': '节省流量/提高速度',
'title:ja': 'トラフィックを節約/速度を上げる',
'title:pl': 'aby zaoszczędzić ruch / zwiększyć prędkość',
options: [
{ label: 'QHD/1440p', value: 'hd1440' },
{ label: 'FHD/1080p', value: 'hd1080' },
{ label: 'HD/720p', value: 'hd720' },
{ label: 'SD/480p', value: 'large' },
{ label: 'SD/360p', value: 'medium' },
{ label: 'SD/240p', value: 'small' },
{ label: 'SD/144p', value: 'tiny' },
{ label: 'Auto', value: 'auto' },
{ label: 'default', selected: true },
],
},
video_quality_for_fullscreen: {
_tagName: 'select',
label: 'For fullscreen',
options: [
{ label: '8K/4320p', value: 'highres' },
{ label: '4K/2160p', value: 'hd2160' },
{ label: 'QHD/1440p', value: 'hd1440' },
{ label: 'FHD/1080p', value: 'hd1080' },
{ label: 'HD/720p', value: 'hd720' },
{ label: 'SD/480p', value: 'large' },
{ label: 'SD/360p', value: 'medium' },
{ label: 'default', selected: true },
],
},
}
});
window.nova_plugins.push({
id: 'player-resume-playback',
title: 'Remember playback time',
'title:zh': '恢复播放时间状态',
'title:ja': '再生時間の位置を再開します',
'title:pl': 'Powrót do pozycji czasowej odtwarzania',
run_on_pages: 'watch, embed',
section: 'player',
desc: 'On page reload - resume playback',
'desc:zh': '在页面重新加载 - 恢复播放',
'desc:ja': 'ページがリロードされると、再生が復元されます',
'desc:pl': 'Przy ponownym załadowaniu strony - wznawiaj odtwarzanie',
_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('#movie_player 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') || NOVA.queryURL.getHashParam('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, Math.trunc(this.currentTime));
}
}
async function resumePlayback() {
if (NOVA.queryURL.has('t') || NOVA.queryURL.getHashParam('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': Math.trunc(this.currentTime) + 's' }));
}, 100);
}
});
this.addEventListener('playing', () => {
if (typeof delaySaveOnPauseURL === 'number') clearTimeout(delaySaveOnPauseURL);
if (NOVA.queryURL.has('t')) NOVA.updateUrl(NOVA.queryURL.remove('t'));
});
}
},
options: {
player_resume_playback_url_mark: {
_tagName: 'input',
label: 'Mark time in URL when paused',
'label:zh': '暂停时在 URL 中节省时间',
'label:ja': '一時停止したときにURLで時間を節約する',
'label:pl': 'Zaznacz czas w adresie URL po wstrzymaniu',
type: 'checkbox',
title: 'Makes sense when saving bookmarks',
'title:zh': '保存书签时有意义',
'title:ja': 'ブックマークを保存するときに意味があります',
'title:pl': 'Ma sens podczas zapisywania zakładek',
},
}
});
window.nova_plugins.push({
id: 'video-autostop',
title: 'Stop video preload',
'title:zh': '停止视频预加载',
'title:ja': 'ビデオのプリロードを停止します',
'title:pl': 'Zatrzymaj ładowanie wideo',
run_on_pages: 'watch, embed',
section: 'player',
desc: 'Prevent auto-buffering',
_runtime: user_settings => {
if (user_settings.video_autostop_embed && NOVA.currentPage != 'embed') return;
if (location.hostname.includes('youtube.googleapis.com')) return;
if (NOVA.queryURL.has('popup')) return;
if (NOVA.currentPage == 'embed'
&& window.self !== window.top
&& ['0', 'false'].includes(NOVA.queryURL.get('autoplay'))
) {
return;
}
if (user_settings.video_autostop_peview_thumbnail && NOVA.currentPage == 'watch') {
NOVA.css.push(
`.unstarted-mode {
background: url("https://i.ytimg.com/vi/${NOVA.queryURL.get('v')}/maxresdefault.jpg") center center / contain no-repeat content-box;
}
.unstarted-mode video {
opacity: 0 !important;
}`);
}
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', 100);
movie_player.stopVideo();
movie_player.addEventListener('onStateChange', onPlayerStateChange.bind(this));
function onPlayerStateChange(state) {
if (user_settings.video_autostop_ignore_playlist && location.search.includes('list=')) return;
if (user_settings.video_autostop_ignore_live && movie_player.getVideoData().isLive) return;
if (!disableStop && state > 0 && state < 5) {
movie_player.stopVideo();
}
}
document.addEventListener('keyup', evt => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
switch (evt.code) {
case 'KeyK':
case 'Space':
case 'MediaPlay':
case 'MediaPlayPause':
disableHoldStop();
break;
}
});
navigator.mediaSession.setActionHandler('play', disableHoldStop);
document.addEventListener('click', evt => {
if (evt.isTrusted
&& evt.target.closest('#movie_player')
&& !disableStop
) {
evt.preventDefault();
evt.stopImmediatePropagation();
disableHoldStop();
}
}, { capture: true });
function disableHoldStop() {
if (!disableStop) {
disableStop = true;
movie_player.playVideo();
}
}
});
},
options: {
video_autostop_embed: {
_tagName: 'select',
label: 'Apply to video type',
options: [
{
label: 'all', value: false, selected: true,
},
{
label: 'embed', value: 'on',
},
],
},
video_autostop_ignore_playlist: {
_tagName: 'input',
label: 'Ignore playlist',
'label:zh': '忽略播放列表',
'label:ja': 'プレイリストを無視する',
'label:pl': 'Zignoruj listę odtwarzania',
type: 'checkbox',
'data-dependent': { 'video_autostop_embed': false },
},
video_autostop_ignore_live: {
_tagName: 'input',
label: 'Ignore live',
type: 'checkbox',
'data-dependent': { 'video_autostop_embed': false },
},
video_autostop_peview_thumbnail: {
_tagName: 'input',
label: 'Display preview thumbnail',
type: 'checkbox',
title: 'Instead black-screen',
'data-dependent': { 'video_autostop_embed': false },
},
}
});
window.nova_plugins.push({
id: 'subtitle-style',
title: 'Subtitles (captions) style',
'title:zh': '字幕样式',
'title:ja': '字幕スタイル',
'title:pl': 'Styl napisów',
run_on_pages: 'watch, embed, -mobile',
section: 'player',
_runtime: async user_settings => {
const SELECTOR = '.ytp-caption-segment';
let cssObj = {};
if (user_settings.subtitle_transparent) {
cssObj = {
'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) cssObj['font-weight'] = 'bold';
if (Object.keys(cssObj).length) {
NOVA.css.push(cssObj, SELECTOR, 'important');
}
if (user_settings.subtitle_fixed) {
NOVA.css.push(
`${CSS.supports('selector(:has(*))') ? '#ytp-caption-window-container:has(~ .ytp-chrome-bottom:not(:hover))' : ''} .caption-window:not(:hover) {
margin-bottom: 1px !important;
bottom: 1% !important;
}`);
}
if (user_settings.subtitle_selectable) {
NOVA.watchElements({
selectors: [SELECTOR],
callback: el => {
el.addEventListener('mousedown', evt => evt.stopPropagation(), { capture: true });
el.setAttribute('draggable', 'false');
el.setAttribute('selectable', 'true');
el.style.userSelect = 'text';
el.style.WebkitUserSelect = 'text';
el.style.cursor = 'text';
}
});
}
if (user_settings.subtitle_color != '#ffffff') {
NOVA.css.push(
`${SELECTOR} { color: ${user_settings.subtitle_color} !important;  }`);
}
if (+user_settings.subtitle_font_size) {
NOVA.css.push(
`${SELECTOR} { font-size: calc(32px * ${+user_settings.subtitle_font_size || 1}) !important; }`);
}
if (user_settings.subtitle) {
await NOVA.waitUntil(() => typeof movie_player === 'object' && typeof movie_player.toggleSubtitlesOn === 'function', 500);
movie_player.toggleSubtitlesOn();
}
},
options: {
subtitle: {
_tagName: 'input',
label: 'Subtitles show by default',
type: 'checkbox',
},
subtitle_fixed: {
_tagName: 'input',
label: 'Fixed from below',
'label:zh': '从下方固定',
'label:ja': '下から固定',
'label:pl': 'Przyklejone na dole',
type: 'checkbox',
title: 'Preventing captions jumping up/down when pause/resume',
'title:zh': '暂停/恢复时防止字幕跳上/跳下',
'title:ja': '一時停止/再開時にキャプションが上下にジャンプしないようにする',
'title:pl': 'Zapobieganie przeskakiwaniu napisów w górę/w dół podczas pauzy/wznowienia',
},
subtitle_selectable: {
_tagName: 'input',
label: 'Make selectable',
'label:zh': '使字幕可选',
'label:ja': '字幕を選択可能にする',
'label:pl': 'Ustaw napisy do wyboru',
type: 'checkbox',
},
subtitle_font_size: {
_tagName: 'input',
label: 'Font size',
'label:zh': '字体大小',
'label:ja': 'フォントサイズ',
'label:pl': 'Rozmiar czcionki',
type: 'number',
title: '0 - default',
placeholder: '0-5',
step: 1,
min: 0,
max: 5,
value: 0,
},
subtitle_transparent: {
_tagName: 'input',
label: 'Transparent',
'label:zh': '透明的',
'label:ja': '透明',
'label:pl': 'Przezroczyste',
type: 'checkbox',
},
subtitle_bold: {
_tagName: 'input',
label: 'Bold text',
'label:zh': '粗体',
'label:ja': '太字',
'label:pl': 'Tekst pogrubiony',
type: 'checkbox',
},
subtitle_color: {
_tagName: 'input',
type: 'color',
value: '#ffffff',
label: 'Color',
'label:zh': '颜色',
'label:ja': '色',
'label:pl': 'Kolor',
title: 'default - #FFF',
},
}
});
window.nova_plugins.push({
id: 'video-unblock-region',
title: 'Redirect video not available in your country',
'title:zh': '尝试解锁您所在地区的视频',
'title:ja': 'お住まいの地域の動画のブロックを解除してみてください',
'title:pl': 'Spróbuj odblokować, jeśli film nie jest dostępny w Twoim kraju',
run_on_pages: 'watch, embed, -mobile',
section: 'player',
opt_api_key_warn: true,
desc: 'Some mirrors will partially replace VPNs',
_runtime: user_settings => {
const SELECTOR_EMBED = '#movie_player.ytp-embed-error .ytp-error[role="alert"] .ytp-error-content-wrap-subreason:not(:empty)';
const SELECTOR = `ytd-watch-flexy[player-unavailable] #player-error-message-container #info, ${SELECTOR_EMBED}`;
NOVA.waitSelector(SELECTOR, { destroy_after_page_leaving: true })
.then(async container => {
if (container.querySelector('button')) return;
const videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id;
insertLinks(container, videoId);
function insertLinks(container = required(), video_id = required()) {
if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
NOVA.css.push(
`${SELECTOR} ul {
border-radius: 12px;
background-color: var(--yt-spec-badge-chip-background);
font-size: 1.4rem;
line-height: 2rem;
padding: 10px;
}
${SELECTOR} li {
color: var(--yt-spec-text-primary);
}
${SELECTOR} a:not(:hover) {
color: var(--yt-spec-text-primary);
text-decoration: none;
}`);
const ul = document.createElement('ul');
[
{ label: 'hooktube.com', value: 'hooktube.com' },
{ label: 'clipzag.com', value: 'clipzag.com' },
{ label: 'piped.video', value: 'piped.video' },
{ label: 'yewtu.be', value: 'yewtu.be' },
{ label: 'nsfwyoutube.com', value: 'nsfwyoutube.com' },
{ label: 'yout-ube.com', value: 'yout-ube.com' },
{ label: 'riservato-xyz.frama.io', value: 'riservato-xyz.frama.io' },
]
.forEach(domain => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `${location.protocol}//${domain.value}${location.port ? ':' + location.port : ''}/watch?v=${video_id}`;
a.target = '_blank';
a.textContent = '→ Open with ' + domain.label;
a.title = 'Open with ' + domain.label;
li.append(a);
ul.append(li);
});
const liAtention = document.createElement('li');
liAtention.className = 'bold style-scope yt-formatted-string';
liAtention.textContent = 'Enable map select allowed country in your VPN';
ul.append(liAtention);
container.append(ul);
}
});
NOVA.waitSelector(`ytd-watch-flexy[player-unavailable], ${SELECTOR_EMBED}`, { destroy_after_page_leaving: true })
.then(el => {
if (user_settings.video_unblock_region_domain
&& el.querySelector('yt-player-error-message-renderer #button.yt-player-error-message-renderer button')
&& confirm('Nova [video-unblock-region]\nThe video is not available in your region, open a in mirror?')
) {
redirect();
}
if (user_settings.video_unblock_region_open_map) {
NOVA.request.API({
request: 'videos',
params: {
'id': NOVA.queryURL.get('v') || movie_player.getVideoData().video_id,
'part': 'contentDetails',
},
api_key: user_settings['user-api-key'],
})
.then(res => {
if (res?.error) return alert(`Error [${res.code}]: ${res?.message}`);
res?.items?.forEach(item => {
if (data = item.contentDetails?.regionRestriction) {
const mapLink = NOVA.queryURL.set(data, 'https://raingart.github.io/region_map/');
NOVA.openPopup({ url: mapLink, width: '1200px', height: '600px' });
}
});
});
}
});
function redirect(new_tab) {
const videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id;
if (new_tab) {
window.open(`${location.protocol}//${user_settings.video_unblock_region_domain || 'hooktube.com'}${location.port ? ':' + location.port : ''}/watch?v=${videoId}`);
}
else {
location.hostname = user_settings.video_unblock_region_domain || 'hooktube.com';
}
}
},
options: {
video_unblock_region_domain: {
_tagName: 'input',
label: 'Redirect to URL',
type: 'text',
list: 'video_unblock_region_domain_help_list',
pattern: "^[a-zA-Z0-9-]{2,20}\.[a-zA-Z]{2,5}$",
title: 'without "https://"',
'title:zh': '没有“https://”',
'title:ja': '「https://」なし',
'title:pl': 'bez „https://”',
placeholder: 'domain.com',
minlength: 5,
maxlength: 20,
},
video_unblock_region_domain_help_list: {
_tagName: 'datalist',
options: [
{ label: 'hooktube.com', value: 'hooktube.com' },
{ label: 'clipzag.com', value: 'clipzag.com' },
{ label: 'piped.video', value: 'piped.video' },
{ label: 'yewtu.be', value: 'yewtu.be' },
{ label: 'nsfwyoutube.com', value: 'nsfwyoutube.com' },
{ label: 'yout-ube.com', value: 'yout-ube.com' },
{ label: 'riservato-xyz.frama.io', value: 'riservato-xyz.frama.io' },
],
},
video_unblock_region_open_map: {
_tagName: 'input',
label: 'Open the map',
'label:zh': '打开可用区域的地图',
'label:ja': '利用可能な地域の地図を開く',
'label:pl': 'Otwórz mapę z dostępnością w regionach',
type: 'checkbox',
title: 'which regions is available',
},
}
});
window.nova_plugins.push({
id: 'video-autopause',
title: 'Video autopause',
'title:zh': '视频自动暂停',
'title:ja': 'ビデオの自動一時停止',
'title:ko': '비디오 자동 일시 중지',
'title:id': 'Jeda otomatis video',
'title:es': 'Pausa automática de video',
'title:it': 'Pausa automatica del video',
'title:pl': 'Automatyczne zatrzymanie wideo',
run_on_pages: 'watch, embed',
section: 'player',
desc: 'Disable autoplay',
'desc:zh': '禁用自动播放',
'desc:ja': '自動再生を無効にする',
'desc:ko': '자동 재생 비활성화',
'desc:it': 'Nonaktifkan putar otomatis',
'desc:es': 'Deshabilitar reproducción automática',
'desc:it': 'Disabilita la riproduzione automatica',
'desc:pl': 'Wyłącz autoodtwarzanie',
'data-conflict': 'video-autostop',
_runtime: user_settings => {
if (user_settings['video-stop-preload'] && !user_settings.stop_preload_embed) return;
if (NOVA.queryURL.has('popup')) 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;
pauseVideo.apply(video);
NOVA.runOnPageLoad(async () => {
if (!location.search.includes('list=') && NOVA.currentPage == 'watch') {
video.addEventListener('playing', pauseVideo, { capture: true, once: true });
}
});
const backupFn = HTMLVideoElement.prototype.play;
HTMLVideoElement.prototype.play = pauseVideo;
document.addEventListener('keyup', evt => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
switch (evt.code) {
case 'KeyK':
case 'Space':
case 'MediaPlay':
case 'MediaPlayPause':
restorePlayFn();
break;
}
});
navigator.mediaSession.setActionHandler('play', restorePlayFn);
document.addEventListener('click', evt => {
if (evt.isTrusted
&& evt.target.closest('#movie_player')
) {
restorePlayFn();
}
}, { capture: true });
function pauseVideo() {
movie_player.pauseVideo();
this.paused || this.pause();
};
function restorePlayFn() {
restorePlayFn = function () { }
HTMLVideoElement.prototype.play = backupFn;
movie_player.playVideo();
}
});
},
options: {
video_autopause_ignore_playlist: {
_tagName: 'input',
label: 'Ignore playlist',
'label:zh': '忽略播放列表',
'label:ja': 'プレイリストを無視する',
'label:ko': '재생목록 무시',
'label:id': 'Abaikan daftar putar',
'label:es': 'Ignorar lista de reproducción',
'label:it': 'Ignora playlist',
'label:pl': 'Zignoruj listę odtwarzania',
type: 'checkbox',
'data-dependent': { 'video_autopause_embed': false },
},
video_autopause_ignore_live: {
_tagName: 'input',
label: 'Ignore live',
type: 'checkbox',
'data-dependent': { 'video_autopause_embed': false },
},
video_autopause_embed: {
_tagName: 'select',
label: 'Apply to video type',
options: [
{
label: 'all', value: false, selected: true,
},
{
label: 'embed', value: 'on',
},
],
},
}
});
window.nova_plugins.push({
id: 'video-unblock-warn-content',
title: 'Skip inappropriate/offensive content warn',
run_on_pages: 'watch, embed, -mobile',
section: 'player',
desc: "skip 'The following content may contain suicide or self-harm topics.'",
_runtime: user_settings => {
NOVA.waitSelector('ytd-watch-flexy[player-unavailable] #player-error-message-container #info button', { destroy_after_page_leaving: true })
.then(btn => btn.click());
},
});
window.nova_plugins.push({
id: 'player-disable-fullscreen-scroll',
title: 'Disable scrolling for fullscreen player',
'title:zh': '禁用全屏滚动',
'title:ja': 'フルスクリーンスクロールを無効にする',
'title:pl': 'Wyłącz przewijanie w trybie pełnoekranowym',
run_on_pages: 'watch, -mobile',
section: 'player',
_runtime: user_settings => {
NOVA.css.push(`.ytp-chrome-controls button.ytp-fullerscreen-edu-button { display: none !important; }`);
document.addEventListener('fullscreenchange', () => {
document.fullscreenElement
? document.addEventListener('wheel', lockscroll, { passive: false })
: document.removeEventListener('wheel', lockscroll);
});
function lockscroll(evt) {
evt.preventDefault();
}
},
});
window.nova_plugins.push({
id: 'sponsor-block',
title: 'SponsorBlock',
run_on_pages: 'watch, embed',
section: 'player',
_runtime: user_settings => {
NOVA.waitSelector('#movie_player video')
.then(video => {
const categoryNameLabel = {
sponsor: 'Sponsor',
selfpromo: 'Self Promotion',
interaction: 'Reminder Subscribe',
intro: 'Intro',
outro: 'Credits (Outro)',
preview: 'Preview/Recap',
music_offtopic: 'Non-Music Section',
exclusive_access: 'Full Video Label Only',
};
let segmentsList = [];
let muteState;
let videoId;
video.addEventListener('loadeddata', init.bind(video));
async function init() {
videoId = NOVA.queryURL.get('v') || movie_player.getVideoData().video_id;
segmentsList = await getSkipSegments(videoId) || [];
if (user_settings['player-float-progress-bar'] && segmentsList.length) {
const SELECTOR = '#nova-player-float-progress-bar-chapters > span[time]';
const deflectionSec = 5;
await NOVA.waitSelector(SELECTOR, { destroy_after_page_leaving: true });
document.body.querySelectorAll(SELECTOR)
.forEach((chapterEl, idx, chaptersEls) => {
if (idx === chaptersEls.length - 1) return;
const
chapterStart = Math.trunc(NOVA.formatTimeOut.hmsToSec(chapterEl.getAttribute('time'))),
chapterNextStart = Math.trunc(NOVA.formatTimeOut.hmsToSec(chaptersEls[idx + 1].getAttribute('time')));
for (const [i, value] of segmentsList.entries()) {
const [segmentStart, segmentEnd, category] = value;
if (((Math.trunc(segmentStart) + deflectionSec) <= chapterNextStart)
&& ((Math.trunc(segmentEnd) - deflectionSec) >= chapterStart)
) {
let color;
switch (category) {
case 'sponsor': color = '255, 231, 0'; break;
case 'interaction': color = '255, 127, 80'; break;
case 'selfpromo': color = '255, 99, 71'; break;
case 'intro': color = '255, 165, 0'; break;
case 'outro': color = '255, 165, 0'; break;
default: color = '0, 255, 107'; break;
}
const
newChapter = document.createElement('span'),
startPoint = Math.max(segmentStart, chapterStart),
sizeChapter = chapterNextStart - chapterStart,
getPt = d => (d * 100 / sizeChapter) + '%';
newChapter.title = category;
Object.assign(newChapter.style, {
width: getPt(Math.min(segmentEnd, chapterNextStart) - startPoint),
left: getPt(startPoint - chapterStart),
'background-color': `rgb(${color}, .4`,
});
chapterEl.append(newChapter);
}
}
});
}
}
video.addEventListener('timeupdate', function () {
let segmentStart, segmentEnd, category;
for (let i = 0; i < segmentsList.length; i++) {
[segmentStart, segmentEnd, category] = segmentsList[i];
segmentStart = Math.trunc(segmentStart);
segmentEnd = Math.ceil(segmentEnd);
const inSegment = (this.currentTime > segmentStart && this.currentTime < segmentEnd);
switch (user_settings.sponsor_block_action) {
case 'mute':
if (inSegment && !muteState && !this.muted) {
muteState = true;
movie_player.mute(true);
return novaNotification('muted');
}
else if (!inSegment && muteState && this.muted) {
muteState = false;
movie_player.unMute();
segmentsList.splice(i, 1);
return novaNotification('unMuted');
}
break;
case 'skip':
if (inSegment) {
this.currentTime = segmentEnd;
segmentsList.splice(i, 1);
return novaNotification();
}
break;
}
}
function novaNotification(prefix = '') {
if (!user_settings.sponsor_block_notification) return;
const msg = `${prefix} ${NOVA.formatTimeOut.HMS.digit(segmentEnd - segmentStart)} [${categoryNameLabel[category]}] • ${NOVA.formatTimeOut.HMS.digit(segmentStart)} - ${NOVA.formatTimeOut.HMS.digit(segmentEnd)}`;
console.info(videoId, msg);
NOVA.showOSD(msg);
}
});
});
async function getSkipSegments(videoId = required()) {
const CACHE_PREFIX = 'nova-videos-sponsor-block:';
if (
navigator.cookieEnabled
&& (storage = sessionStorage.getItem(CACHE_PREFIX + videoId))
) {
return JSON.parse(storage);
}
else {
const
actionTypes = (Array.isArray(user_settings.sponsor_block_action)
? user_settings.sponsor_block_action : [user_settings.sponsor_block_action])
|| ['skip', 'mute'],
categories = user_settings.sponsor_block_category || [
'sponsor',
'interaction',
'selfpromo',
'intro',
'outro',
],
params = {
'videoID': videoId,
'actionTypes': JSON.stringify(actionTypes),
'categories': JSON.stringify(categories),
},
query = Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
const fetchAPI = () => fetch((user_settings.sponsor_block_url || 'https://sponsor.ajay.app')
+ `/api/skipSegments?${query}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then(response => response.json())
.then(json => json
.map(a => [...a.segment, a.category])
)
.catch(error => {
});
if (result = await fetchAPI()) {
if (navigator.cookieEnabled) {
sessionStorage.setItem(CACHE_PREFIX + videoId, JSON.stringify(result));
}
return result;
}
}
}
},
options: {
sponsor_block_category: {
_tagName: 'select',
label: 'Category',
title: '[Ctrl+Click] to select several',
'title:zh': '[Ctrl+Click] 选择多个',
'title:ja': '「Ctrl+Click」して、いくつかを選択します',
'title:pl': 'Ctrl+kliknięcie, aby zaznaczyć kilka',
multiple: null,
required: true,
size: 7,
options: [
{
label: 'Ads/Sponsor', value: 'sponsor',
},
{
label: 'Unpaid/Self Promotion', value: 'selfpromo',
},
{
label: 'Reminder Subscribe', value: 'interaction',
},
{
label: 'Intro', value: 'intro',
},
{
label: 'Endcards/Credits (Outro)', value: 'outro',
},
{
label: 'Preview/Recap', value: 'preview',
},
{
label: 'Music: Non-Music Section', value: 'music_offtopic',
},
{
label: 'Full Video Label Only', value: 'exclusive_access',
},
],
},
sponsor_block_action: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'skip', value: 'skip', selected: true,
},
{
label: 'mute', value: 'mute',
},
],
},
sponsor_block_url: {
_tagName: 'input',
label: 'URL',
type: 'url',
pattern: "https://.*",
placeholder: 'https://domain.com',
value: 'https://sponsor.ajay.app',
required: true,
},
sponsor_block_notification: {
_tagName: 'input',
label: 'Show OSD notification',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'embed-popup',
title: 'Open small embedded in popup',
'title:zh': '将嵌入式视频重定向到弹出窗口',
'title:ja': '埋め込まれたビデオをポップアップにリダイレクトします',
'title:pl': 'Przekieruj osadzone wideo do wyskakującego okienka',
run_on_pages: 'embed, -mobile',
section: 'player',
desc: 'if iframe width is less than 720p',
'plugins-conflict': 'player-fullscreen-mode',
_runtime: user_settings => {
if (window.top === window.self
|| location.hostname.includes('googleapis.com')
|| NOVA.queryURL.has('popup')
) {
return;
}
if (user_settings.player_full_viewport_mode == 'redirect_watch_to_embed') return;
if (user_settings['player-fullscreen-mode']) return;
if (window.innerWidth > 720 && window.innerHeight > 480) return;
NOVA.waitSelector('#movie_player video')
.then(video => {
video.addEventListener('playing', createPopup.bind(video), { capture: true, once: true });
});
function createPopup() {
if (this.videoHeight < window.innerWidth && this.videoHeight < window.innerHeight) return;
const { width, height } = NOVA.aspectRatio.sizeToFit({
'srcWidth': this.videoWidth,
'srcHeight': this.videoHeight,
});
movie_player.stopVideo();
const url = new URL(
document.head.querySelector('link[itemprop="embedUrl"][href]')?.href
|| (location.origin + '/embed/' + movie_player.getVideoData().video_id)
);
url.searchParams.set('autoplay', 1);
url.searchParams.set('popup', true);
NOVA.openPopup({ 'url': url.href, 'width': width, 'height': height });
}
},
});
window.nova_plugins.push({
id: 'theater-mode',
title: 'Auto wide player (Theater mode)',
'title:pl': 'Tryb kinowy',
run_on_pages: 'watch, -mobile',
section: 'player',
_runtime: user_settings => {
if (user_settings.player_full_viewport_mode == 'redirect_watch_to_embed') {
return location.assign(`https://www.youtube.com/embed/` + NOVA.queryURL.get('v'));
}
if (user_settings.theater_mode_ignore_playlist && location.search.includes('list=')) return;
NOVA.waitSelector('ytd-watch-flexy:not([player-unavailable])')
.then(el => {
if (isTheaterMode()) return;
NOVA.waitUntil(() => isTheaterMode() ? true : toggleTheater(), 500);
function isTheaterMode() {
return (el.hasAttribute('theater')
|| (typeof el.isTheater_ === 'function' && el.isTheater_())
);
}
function toggleTheater() {
(typeof movie_player === 'object' ? movie_player : document)
.dispatchEvent(
new KeyboardEvent(
'keydown',
{
keyCode: 84,
key: 't',
code: 'KeyT',
which: 84,
bubbles: true,
cancelable: false,
}
)
);
}
if (!user_settings['video-unblock-warn-content']) {
NOVA.waitSelector('ytd-watch-flexy[player-unavailable] yt-player-error-message-renderer #button.yt-player-error-message-renderer button', { destroy_after_page_leaving: true })
.then(btn => btn.click());
}
});
if (user_settings.player_full_viewport_mode == '') return;
if (user_settings['player-fullscreen-mode']
&& !user_settings.player_fullscreen_mode_embed
&& user_settings.player_full_viewport_mode != 'cinema_mode'
) {
return;
}
NOVA.waitSelector('#movie_player')
.then(movie_player => {
const
PLAYER_CONTAINER_SELECTOR = 'ytd-watch-flexy[theater]:not([fullscreen]) #ytd-player',
PINNED_SELECTOR = '.nova-player-pin',
PLAYER_SCROLL_LOCK_CLASS_NAME = 'nova-lock-scroll',
PLAYER_SELECTOR = `${PLAYER_CONTAINER_SELECTOR} #movie_player:not(${PINNED_SELECTOR}):not(.${PLAYER_SCROLL_LOCK_CLASS_NAME})`,
zIndex = Math.max(getComputedStyle(movie_player)['z-index'], 2020);
addScrollDownBehavior();
switch (user_settings.player_full_viewport_mode) {
case 'offset':
NOVA.css.push(
PLAYER_CONTAINER_SELECTOR + ` {
min-height: calc(100vh - ${user_settings['header-compact']
? '36px'
: NOVA.css.get('#masthead-container', 'height') || '56px'
}) !important;
}
ytd-watch-flexy[theater]:not([fullscreen]) #columns {
position: absolute;
top: 100vh;
}
${PLAYER_SELECTOR} {
background-color: black;
}`);
break;
case 'force':
setPlayerFullViewport(user_settings.player_full_viewport_mode_exit);
break;
case 'smart':
if (user_settings.player_full_viewport_mode_exclude_shorts && NOVA.currentPage == 'shorts') {
return;
}
NOVA.waitSelector('video')
.then(video => {
video.addEventListener('loadeddata', function () {
if (user_settings.player_full_viewport_mode_exclude_shorts && this.videoWidth < this.videoHeight) {
return;
}
const miniSize = NOVA.aspectRatio.sizeToFit({
'srcWidth': this.videoWidth,
'srcHeight': this.videoHeight,
'maxWidth': window.innerWidth,
'maxHeight': window.innerHeight,
});
if (miniSize.width < window.innerWidth) {
setPlayerFullViewport('player_full_viewport_mode_exit');
}
});
});
break;
case 'cinema_mode':
NOVA.css.push(
PLAYER_SELECTOR + ` {
z-index: ${zIndex};
}
${PLAYER_SELECTOR}:before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, ${+user_settings.cinema_mode_opacity});
opacity: 0;
transition: opacity 400ms ease-in-out;
pointer-events: none;
}
${PLAYER_SELECTOR}.playing-mode:before {
opacity: 1;
}
.ytp-ad-player-overlay,
#playlist:hover,
#masthead-container:hover,
iframe, 
#guide,
[class*="popup"],
[role="navigation"],
[role="dialog"] {
z-index: ${zIndex + 1};
}
#playlist:hover {
position: relative;
}`);
addHideScrollbarCSS();
break;
}
function setPlayerFullViewport(exclude_pause) {
const CLASS_OVER_PAUSED = 'nova-player-fullviewport';
NOVA.css.push(
`${PLAYER_SELECTOR}.playing-mode,
${exclude_pause ? '' : `${PLAYER_SELECTOR}.paused-mode,`}
${PLAYER_SELECTOR}.${CLASS_OVER_PAUSED} {
width: 100vw;
height: 100vh;
position: fixed;
bottom: 0 !important;
z-index: ${zIndex};
background-color: black;
}`);
if (CSS.supports('selector(:has(*))')) {
NOVA.css.push(
`#masthead-container:has( ~ #page-manager ytd-watch-flexy[theater]) {
position: fixed;
z-index: ${zIndex + 1};
opacity: 0;
}
#masthead-container:has( ~ #page-manager ytd-watch-flexy[theater]):hover,
#masthead-container:has( ~ #page-manager ytd-watch-flexy[theater]):focus {
opacity: 1;
}`);
}
addHideScrollbarCSS();
if (user_settings.player_full_viewport_mode_exit) {
NOVA.waitSelector('video')
.then(video => {
video.addEventListener('pause', () => {
if (!document.body.querySelector('.ytp-progress-bar')?.contains(document.activeElement)) {
window.dispatchEvent(new Event('resize'));
}
});
video.addEventListener('play', () => window.dispatchEvent(new Event('resize')));
});
NOVA.waitSelector('.ytp-progress-bar')
.then(progress_bar => {
['mousedown', 'mouseup'].forEach(evt => {
progress_bar.addEventListener(evt, () => {
movie_player.classList.add(CLASS_OVER_PAUSED);
});
});
});
}
}
function addScrollDownBehavior() {
if (activateScrollElement = document.body.querySelector('.ytp-chrome-controls')) {
activateScrollElement.addEventListener('wheel', evt => {
switch (Math.sign(evt.wheelDelta)) {
case -1:
movie_player.classList.add(PLAYER_SCROLL_LOCK_CLASS_NAME);
break;
}
});
document.addEventListener('scroll', evt => {
if (window.scrollY === 0 && movie_player.classList.contains(PLAYER_SCROLL_LOCK_CLASS_NAME)) {
movie_player.classList.remove(PLAYER_SCROLL_LOCK_CLASS_NAME);
}
});
}
}
function addHideScrollbarCSS() {
if (user_settings['scrollbar-hide']) return;
NOVA.css.push(`html body:has(${PLAYER_SELECTOR})::-webkit-scrollbar{ display: none; }`);
}
});
},
options: {
player_full_viewport_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'default', selected: true,
},
{
label: 'cinema', value: 'cinema_mode',
},
{
label: 'full-viewport', value: 'force',
},
{
label: 'full-viewport (auto)', value: 'smart',
},
{
label: 'full-viewport (offset)', value: 'offset',
},
{
label: 'redirect to embedded', value: 'redirect_watch_to_embed',
},
],
},
player_full_viewport_mode_exit: {
_tagName: 'input',
label: 'Exit Fullscreen on video end/pause',
'label:zh': '视频结束/暂停时退出',
'label:ja': 'ビデオが終了/一時停止したら終了します',
'label:pl': 'Wyjdź, gdy film się kończy/pauzuje',
type: 'checkbox',
'data-dependent': { 'player_full_viewport_mode': ['force', 'smart'] },
},
player_full_viewport_mode_exclude_shorts: {
_tagName: 'input',
label: 'Full-viewport exclude shorts',
'label:zh': '全视口不包括短裤',
'label:ja': 'フルビューポートはショートパンツを除外します',
'label:pl': 'Pełny ekran wyklucza krótkie filmy',
type: 'checkbox',
'data-dependent': { 'player_full_viewport_mode': 'smart' },
},
cinema_mode_opacity: {
_tagName: 'input',
label: 'Opacity',
'label:zh': '不透明度',
'label:ja': '不透明度',
'label:pl': 'Przezroczystość',
type: 'number',
title: '0-1',
placeholder: '0-1',
step: .05,
min: 0,
max: 1,
value: .75,
'data-dependent': { 'player_full_viewport_mode': 'cinema_mode' },
},
theater_mode_ignore_playlist: {
_tagName: 'input',
label: 'Ignore playlist',
'label:zh': '忽略播放列表',
'label:ja': 'プレイリストを無視する',
'label:pl': 'Zignoruj listę odtwarzania',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'player-indicator',
title: 'Custom On-Screen Display (OSD)',
'title:zh': '替换默认指示器',
'title:ja': 'デフォルトのインジケーターを置き換える',
run_on_pages: 'watch, embed, -mobile',
section: 'player',
_runtime: user_settings => {
const
SELECTOR_ID = 'nova-player-indicator-info',
COLOR_OSD = user_settings.player_indicator_color || '#ff0000';
NOVA.waitSelector('#movie_player video')
.then(video => {
video.addEventListener('volumechange', function () {
OSD.show({
'pt': Math.round(movie_player.getVolume()),
'suffix': '%',
'clear_previous_text': true,
});
});
video.addEventListener('ratechange', () => OSD.show({
'pt': video.playbackRate,
'suffix': 'x',
'clear_previous_text': true,
}));
if (user_settings.player_indicator_chapter) {
NOVA.waitSelector('ytd-watch-metadata #description.ytd-watch-metadata')
.then(() => {
const getNextChapterIdx = () => chapterList?.findIndex(c => c.sec > video.currentTime);
let chapterList, lastChapTime = 0;
video.addEventListener('loadeddata', () => chapterList = []);
video.addEventListener('timeupdate', function () {
if (chapterList !== null && !chapterList?.length) {
chapterList = NOVA.getChapterList(movie_player.getDuration()) || null;
}
if (chapterList?.length
&& this.currentTime > lastChapTime
) {
let nextChapterIdx = getNextChapterIdx();
if (nextChapterIdx === -1) nextChapterIdx = chapterList.length;
lastChapTime = chapterList[nextChapterIdx]?.sec;
if (chapterData = chapterList[nextChapterIdx - 1]) {
const separator = ' • ';
const msg = chapterData.title + separator + chapterData.time;
NOVA.showOSD(msg);
}
}
});
video.addEventListener('seeking', () => {
if (chapterList?.length && (nexChapterData = chapterList[getNextChapterIdx()])) {
lastChapTime = nexChapterData.sec;
}
});
});
}
});
NOVA.waitSelector('.ytp-bezel-text')
.then(target => {
new MutationObserver(mutationRecordsArray => {
if (target.textContent) {
let unlimitVol;
if ((target.textContent?.endsWith('%')
&& !(unlimitVol = (user_settings.volume_unlimit && parseInt(target.textContent) > 100))
)
|| ((user_settings['video-rate'] || user_settings.player_buttons_custom_items?.includes('range-speed')) && (target.textContent?.length < 6) && target.textContent?.endsWith('x'))
|| (user_settings['time-jump'] && target.textContent?.startsWith(`+${user_settings.time_jump_step}`))
) {
return;
}
OSD.show({
'pt': target.textContent,
'timeout_ms': (user_settings.player_indicator_chapter_time || 1.8) * 1000,
'clear_previous_text': unlimitVol,
});
}
})
.observe(target, { attributes: true, childList: true });
});
const OSD = {
create() {
NOVA.css.push(
`.ytp-bezel-text-wrapper,
.ytp-doubletap-ui-legacy.ytp-time-seeking,
.ytp-chapter-seek {
display:none !important;
}`);
NOVA.css.push(
`#${SELECTOR_ID} {
--color: white;
--bg-color: rgba(0, 0, 0, ${user_settings.player_indicator_opacity || .3});
--zindex: ${1 + Math.max(NOVA.css.get('.ytp-chrome-top', 'z-index'), 60)};
position: absolute;
right: 0;
z-index: calc(var(--zindex) + 1);
margin: 0 auto;
text-align: center;
opacity: 0;
background-color: var(--bg-color);
color: var(--color);
}
#${SELECTOR_ID} span {
text-overflow: ellipsis;
word-wrap: break-word;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 5; 
line-clamp: 5;
-webkit-box-orient: vertical;
}`);
const template = document.createElement('div');
template.id = SELECTOR_ID;
template.innerHTML = '<span></span>';
movie_player.append(template);
this.container = document.getElementById(SELECTOR_ID);
this.spanOSD = this.container.querySelector('span');
switch (user_settings.player_indicator_type) {
case 'bar-center':
Object.assign(this.container.style, {
left: 0,
bottom: '20%',
width: '30%',
'font-size': '1.2em',
});
Object.assign(this.spanOSD.style, {
'background-color': COLOR_OSD,
transition: 'width 100ms ease-out',
display: 'inline-block',
});
break;
case 'bar-vertical':
Object.assign(this.container.style, {
top: 0,
height: '100%',
width: '25px',
'font-size': '1.2em',
});
Object.assign(this.spanOSD.style, {
position: 'absolute',
bottom: 0,
right: 0,
'background-color': COLOR_OSD,
transition: 'height 100ms ease-out 0s',
display: 'inline-block',
width: '100%',
'font-weight': 'bold',
});
break;
default:
Object.assign(this.container.style, {
top: 0,
width: '100%',
padding: '.2em',
'font-size': '1.55em',
});
}
return this.container;
},
show({ pt = 100, suffix = '', timeout_ms = 800, clear_previous_text }) {
if (typeof this.fade === 'number') clearTimeout(this.fade);
const notify = this.container || this.create();
if (this.oldMsg) {
this.spanOSD.innerText += '\n' + pt + suffix;
}
else {
this.spanOSD.innerHTML = pt + suffix;
}
if (!clear_previous_text) {
this.oldMsg = this.spanOSD.innerText;
clearTimeout(this.timeoutMultiLine);
this.timeoutMultiLine = setTimeout(() => this.oldMsg = null, 600);
}
if (suffix == 'x') {
const maxPercent = (+user_settings.rate_step % .25) === 0 ? 2 : 3;
pt = +pt * 100 / maxPercent;
}
pt = Math.round(pt);
switch (user_settings.player_indicator_type) {
case 'bar-center':
this.spanOSD.style.width = pt + '%';
break;
case 'bar-vertical':
this.spanOSD.style.height = pt + '%';
break;
case 'bar-top':
notify.style.background = `linear-gradient(to right, ${COLOR_OSD}50 ${pt}%, rgba(0,0,0,.8) ${pt}%)`;
this.spanOSD.style.width = pt + '%';
break;
}
notify.style.transition = 'none';
notify.style.opacity = 1;
notify.style.visibility = 'visible';
this.fade = setTimeout(() => {
notify.style.transition = 'opacity 200ms ease-in';
notify.style.opacity = 0;
setTimeout(() => notify.style.visibility = 'hidden', 1000);
}, timeout_ms);
}
};
},
options: {
player_indicator_type: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'text-top', value: 'text-top', selected: true,
},
{
label: 'bar-top', value: 'bar-top',
},
{
label: 'bar-center', value: 'bar-center',
},
{
label: 'bar-vertical', value: 'bar-vertical',
},
],
},
player_indicator_opacity: {
_tagName: 'input',
label: 'Opacity',
'label:zh': '不透明度',
'label:ja': '不透明度',
'label:tr': 'opaklık',
'label:pl': 'Przezroczystość',
type: 'number',
title: 'less value - more transparency',
placeholder: '0-1',
step: .1,
min: .1,
max: .9,
value: .3,
},
player_indicator_color: {
_tagName: 'input',
type: 'color',
value: '#ff0000',
label: 'Color',
'label:zh': '颜色',
'label:ja': '色',
'label:pl': 'Kolor',
'data-dependent': { 'player_indicator_type': '!text-top' },
},
player_indicator_chapter: {
_tagName: 'input',
label: 'Show info at start chapter',
'label:zh': '在开始章节显示信息',
'label:ja': '章の開始時に情報を表示',
'label:pl': 'Pokaż informacje na początku rozdziału',
type: 'checkbox',
},
player_indicator_chapter_time: {
_tagName: 'input',
label: 'Chapter timeout',
type: 'number',
'label:zh': '章节超时',
'label:ja': 'チャプターのタイムアウト',
title: 'in sec',
placeholder: '0-10',
step: .1,
min: .1,
max: 10,
value: 1.8,
'data-dependent': { 'player_indicator_chapter': true },
},
}
});
window.nova_plugins.push({
id: 'disable-player-sleep-mode',
title: 'Disable the "Continue watching?" popup',
'title:zh': '玩家永远保持活跃',
'title:ja': 'プレーヤーは永遠にアクティブなままです',
'title:pl': 'Wyłącz tryb uśpienia odtwarzacza',
run_on_pages: 'watch, -mobile',
section: 'player',
_runtime: user_settings => {
setInterval(() => {
if (!document.hasFocus()) {
document.dispatchEvent(
new KeyboardEvent(
'keyup',
{
keyCode: 143,
which: 143,
bubbles: true,
cancelable: true,
}
)
);
}
}, 1000 * 60 * 5);
},
});
window.nova_plugins.push({
id: 'player-resize-ratio',
title: 'Player force resize 16:9',
run_on_pages: 'watch',
section: 'player',
desc: 'only for 4:3 video',
_runtime: user_settings => {
NOVA.waitSelector('ytd-watch-flexy:not([theater])')
.then(ytd_watch => {
NOVA.waitSelector('#movie_player video', { container: ytd_watch })
.then(video => {
console.assert(ytd_watch.calculateCurrentPlayerSize_, '"ytd_watch" does not have fn "calculateCurrentPlayerSize_"');
const
heightRatio = .5625,
squareAspectRatio = () => {
const aspectRatio = NOVA.aspectRatio.getAspectRatio({
'width': video.videoWidth,
'height': video.videoHeight,
});
return (
(video.videoWidth / video.videoHeight) > 2.3
|| '4:3' == aspectRatio || '1:1' == aspectRatio
);
};
if (ytd_watch.calculateCurrentPlayerSize_ && ytd_watch.updateStyles) {
const backupFn = ytd_watch.calculateCurrentPlayerSize_;
patchYtCalculateFn()
video.addEventListener('loadeddata', () => {
(NOVA.currentPage == 'watch') && patchYtCalculateFn();
});
function sizeBypass() {
let width = height = NaN;
if (!ytd_watch.theater) {
width = movie_player.offsetWidth;
height = Math.ceil(movie_player.offsetWidth / (16 / 9));
if (ytd_watch.updateStyles) {
ytd_watch.updateStyles({
'--ytd-watch-flexy-width-ratio': 1,
'--ytd-watch-flexy-height-ratio': heightRatio,
});
window.dispatchEvent(new Event('resize'));
}
}
return {
'width': width,
'height': height,
};
}
function patchYtCalculateFn() {
ytd_watch.calculateCurrentPlayerSize_ = squareAspectRatio() ? sizeBypass : backupFn;
ytd_watch.calculateCurrentPlayerSize_();
}
}
else {
new MutationObserver(mutationRecordsArray => {
if (!ytd_watch.theater && heightRatio != ytd_watch.style.getPropertyValue('--ytd-watch-flexy-height-ratio')) {
updateRatio();
}
})
.observe(ytd_watch, { attributes: true, attributeFilter: ['style'] });
}
window.addEventListener('resize', updateRatio);
function updateRatio() {
if (squareAspectRatio()) {
ytd_watch.style.setProperty('--ytd-watch-flexy-width-ratio', 1);
ytd_watch.style.setProperty('--ytd-watch-flexy-height-ratio', heightRatio);
}
}
});
});
},
});
window.nova_plugins.push({
id: 'auto-buffer',
title: 'Video preloading/buffering',
run_on_pages: 'watch, embed',
section: 'player',
desc: 'Working while video is paused',
_runtime: user_settings => {
const maxBufferSec = (+user_settings.auto_buffer_sec || 60);
const SELECTOR_CLASS_NAME = 'buffered';
NOVA.css.push(
`.${SELECTOR_CLASS_NAME} .ytp-swatch-background-color {
background-color: ${user_settings.auto_buffer_color || '#ffa000'} !important;
}`);
let stopPreload = true;
let saveCurrentTime = false;
NOVA.waitSelector('#movie_player video')
.then(video => {
let isLive;
video.addEventListener('loadeddata', () => {
saveCurrentTime = false;
isLive = movie_player.getVideoData().isLive;
});
video.addEventListener('playing', function () {
if (!this.paused && saveCurrentTime !== false) {
this.currentTime = saveCurrentTime;
saveCurrentTime = false;
movie_player.classList.remove(SELECTOR_CLASS_NAME);
}
});
document.addEventListener('keydown', evt => {
if (!video.paused || !saveCurrentTime) return;
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.code == 'ArrowLeft' || evt.code == 'ArrowRight') reSaveTime();
});
document.addEventListener('click', evt => {
if (evt.isTrusted
&& video.paused && saveCurrentTime
&& evt.target.closest('.ytp-progress-bar')
) {
reSaveTime();
}
});
function reSaveTime() {
movie_player.classList.add(SELECTOR_CLASS_NAME);
saveCurrentTime = video.currentTime;
}
video.addEventListener('pause', recordBuffer.bind(video));
video.addEventListener('progress', recordBuffer.bind(video));
function recordBuffer() {
if (!this.paused || !this.buffered?.length) return;
if (stopPreload) return;
const bufferedSeconds = this.buffered.end(this.buffered.length - 1);
if (saveCurrentTime === false) {
movie_player.classList.add(SELECTOR_CLASS_NAME);
saveCurrentTime = this.currentTime;
}
if (saveCurrentTime && ((bufferedSeconds - saveCurrentTime) > maxBufferSec)) {
this.currentTime = saveCurrentTime;
movie_player.classList.remove(SELECTOR_CLASS_NAME);
return;
}
if (!isLive || !isNaN(this.duration)) {
const bufferedPercent = bufferedSeconds / this.duration;
if (bufferedPercent > .9) {
movie_player.classList.remove(SELECTOR_CLASS_NAME); return;
}
}
this.currentTime = bufferedSeconds;
}
});
NOVA.waitSelector('#movie_player .ytp-left-controls .ytp-play-button')
.then(container => {
const
SELECTOR_CLASS = 'nova-right-custom-button',
btn = document.createElement('button');
btn.className = `ytp-button ${SELECTOR_CLASS}`;
Object.assign(btn.style, {
padding: '0 12px',
opacity: .5,
'min-width': getComputedStyle(container).width || '48px',
});
btn.title = 'Preload video';
btn.innerHTML =
`<svg viewBox="0 0 465 465" height="100%" width="100%">
<g fill="currentColor">
<path d="M279.591,423.714c-3.836,0.956-7.747,1.805-11.629,2.52c-10.148,1.887-16.857,11.647-14.98,21.804 c0.927,4.997,3.765,9.159,7.618,11.876c3.971,2.795,9.025,4.057,14.175,3.099c4.623-0.858,9.282-1.867,13.854-3.008 c10.021-2.494,16.126-12.646,13.626-22.662C299.761,427.318,289.618,421.218,279.591,423.714z"/>
<path d="M417.887,173.047c1.31,3.948,3.811,7.171,6.97,9.398c4.684,3.299,10.813,4.409,16.662,2.475 c9.806-3.256,15.119-13.83,11.875-23.631c-1.478-4.468-3.118-8.95-4.865-13.314c-3.836-9.59-14.714-14.259-24.309-10.423 c-9.585,3.834-14.256,14.715-10.417,24.308C415.271,165.528,416.646,169.293,417.887,173.047z"/>
<path d="M340.36,397.013c-3.299,2.178-6.704,4.286-10.134,6.261c-8.949,5.162-12.014,16.601-6.854,25.546 c1.401,2.433,3.267,4.422,5.416,5.942c5.769,4.059,13.604,4.667,20.127,0.909c4.078-2.352,8.133-4.854,12.062-7.452 c8.614-5.691,10.985-17.294,5.291-25.912C360.575,393.686,348.977,391.318,340.36,397.013z"/>
<path d="M465.022,225.279c-0.407-10.322-9.101-18.356-19.426-17.953c-10.312,0.407-18.352,9.104-17.947,19.422 c0.155,3.945,0.195,7.949,0.104,11.89c-0.145,6.473,3.021,12.243,7.941,15.711c2.931,2.064,6.488,3.313,10.345,3.401 c10.322,0.229,18.876-7.958,19.105-18.285C465.247,234.756,465.208,229.985,465.022,225.279z"/>
<path d="M414.835,347.816c-8.277-6.21-19.987-4.524-26.186,3.738c-2.374,3.164-4.874,6.289-7.434,9.298 c-6.69,7.86-5.745,19.666,2.115,26.361c0.448,0.38,0.901,0.729,1.371,1.057c7.814,5.509,18.674,4.243,24.992-3.171 c3.057-3.59,6.037-7.323,8.874-11.102C424.767,365.735,423.089,354.017,414.835,347.816z"/>
<path d="M442.325,280.213c-9.855-3.09-20.35,2.396-23.438,12.251c-1.182,3.765-2.492,7.548-3.906,11.253 c-3.105,8.156-0.13,17.13,6.69,21.939c1.251,0.879,2.629,1.624,4.126,2.19c9.649,3.682,20.454-1.159,24.132-10.812 c1.679-4.405,3.237-8.906,4.646-13.382C457.66,293.795,452.178,283.303,442.325,280.213z"/>
<path d="M197.999,426.402c-16.72-3.002-32.759-8.114-47.968-15.244c-0.18-0.094-0.341-0.201-0.53-0.287 c-3.584-1.687-7.162-3.494-10.63-5.382c-0.012-0.014-0.034-0.023-0.053-0.031c-6.363-3.504-12.573-7.381-18.606-11.628 C32.24,331.86,11.088,209.872,73.062,121.901c13.476-19.122,29.784-35.075,47.965-47.719c0.224-0.156,0.448-0.311,0.67-0.468 c64.067-44.144,151.06-47.119,219.089-1.757l-14.611,21.111c-4.062,5.876-1.563,10.158,5.548,9.518l63.467-5.682 c7.12-0.64,11.378-6.799,9.463-13.675L387.61,21.823c-1.908-6.884-6.793-7.708-10.859-1.833l-14.645,21.161 C312.182,7.638,252.303-5.141,192.87,5.165c-5.986,1.036-11.888,2.304-17.709,3.78c-0.045,0.008-0.081,0.013-0.117,0.021 c-0.225,0.055-0.453,0.128-0.672,0.189C123.122,22.316,78.407,52.207,46.5,94.855c-0.269,0.319-0.546,0.631-0.8,0.978 c-1.061,1.429-2.114,2.891-3.145,4.353c-1.686,2.396-3.348,4.852-4.938,7.308c-0.199,0.296-0.351,0.597-0.525,0.896 C10.762,149.191-1.938,196.361,0.24,244.383c0.005,0.158-0.004,0.317,0,0.479c0.211,4.691,0.583,9.447,1.088,14.129 c0.027,0.302,0.094,0.588,0.145,0.89c0.522,4.708,1.177,9.427,1.998,14.145c8.344,48.138,31.052,91.455,65.079,125.16 c0.079,0.079,0.161,0.165,0.241,0.247c0.028,0.031,0.059,0.047,0.086,0.076c9.142,9.017,19.086,17.357,29.793,24.898 c28.02,19.744,59.221,32.795,92.729,38.808c10.167,1.827,19.879-4.941,21.703-15.103 C214.925,437.943,208.163,428.223,197.999,426.402z"/>
<path d="M221.124,83.198c-8.363,0-15.137,6.78-15.137,15.131v150.747l137.87,71.271c2.219,1.149,4.595,1.69,6.933,1.69 c5.476,0,10.765-2.982,13.454-8.185c3.835-7.426,0.933-16.549-6.493-20.384l-121.507-62.818V98.329 C236.243,89.978,229.477,83.198,221.124,83.198z"/>
</g>
</svg>`;
btn.addEventListener('click', toggleLoop);
container.after(btn);
NOVA.waitSelector('#movie_player video')
.then(video => {
video.addEventListener('loadeddata', ({ target }) => {
stopPreload = movie_player.classList.contains('ad-showing') || !Boolean(user_settings.auto_buffer_default);
btn.style.opacity = stopPreload ? .5 : 1;
});
});
function toggleLoop() {
stopPreload = !stopPreload;
btn.style.opacity = stopPreload ? .5 : 1;
NOVA.showOSD('Preload is ' + Boolean(stopPreload));
if (stopPreload) {
NOVA.videoElement.currentTime = saveCurrentTime;
movie_player.classList.remove(SELECTOR_CLASS_NAME);
}
}
});
},
options: {
auto_buffer_sec: {
_tagName: 'input',
label: 'Sec',
type: 'number',
title: 'buffer time',
placeholder: '10-300',
step: 5,
min: 30,
max: 300,
value: 60,
},
auto_buffer_default: {
_tagName: 'select',
label: 'Default state',
'label:zh': '默认状态',
'label:ja': 'デフォルト状態',
'label:pl': 'Stan domyślny',
options: [
{
label: 'on', value: true, selected: true,
},
{
label: 'off', value: false,
},
],
},
auto_buffer_color: {
_tagName: 'input',
type: 'color',
value: '#ffa000',
label: 'Color',
'label:zh': '颜色',
'label:ja': '色',
'label:pl': 'Kolor',
},
}
});
window.nova_plugins.push({
id: 'video-zoom',
title: 'Zoom video',
'title:zh': '缩放视频',
'title:ja': 'ズームビデオ',
run_on_pages: 'watch, embed, -mobile',
section: 'player',
desc: 'Remove horizontal black bars',
_runtime: user_settings => {
const ZOOM_CLASS_NAME = 'nova-zoom';
NOVA.waitSelector('.html5-video-container')
.then(container => {
let zoomPercent = 100;
if (user_settings.zoom_hotkey == 'keyboard') {
document.addEventListener('keydown', evt => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
let delta;
switch (user_settings.zoom_hotkey_custom_in.length === 1 ? evt.key : evt.code) {
case user_settings.zoom_hotkey_custom_in: delta = 1; break;
case user_settings.zoom_hotkey_custom_out: delta = -1; break;
}
if (delta) {
evt.preventDefault();
evt.stopPropagation();
evt.stopImmediatePropagation();
if (step = +user_settings.zoom_step * Math.sign(delta)) {
setScale(zoomPercent + step);
}
}
}, { capture: true });
}
else if (user_settings.zoom_hotkey) {
container.addEventListener('wheel', evt => {
evt.preventDefault();
evt.stopPropagation();
if (evt[user_settings.zoom_hotkey] || (user_settings.zoom_hotkey == 'none'
&& !evt.ctrlKey && !evt.altKey && !evt.shiftKey && !evt.metaKey)
) {
if (step = +user_settings.zoom_step * Math.sign(evt.wheelDelta)) {
setScale(zoomPercent + step);
}
}
}, { capture: true });
}
if (hotkey = user_settings.zoom_auto_max_width_hotkey_toggle) {
document.addEventListener('keyup', evt => {
if (NOVA.currentPage != 'watch' && NOVA.currentPage != 'embed') return;
if (['input', 'textarea', 'select'].includes(evt.target.localName) || evt.target.isContentEditable) return;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) return;
if ((hotkey.length === 1 ? evt.key : evt.code) === hotkey
&& (maxZoomPercent = geVideoMaxWidthPercent())
) {
setScale(zoomPercent === maxZoomPercent ? 100 : maxZoomPercent);
}
});
}
if (user_settings['save-channel-state']) {
NOVA.runOnPageLoad(async () => {
if ((NOVA.currentPage == 'watch' || NOVA.currentPage == 'embed')
&& (userZoom = await NOVA.storage_obj_manager.getParam('zoom'))
) {
setScale(userZoom * 100);
}
});
}
if (user_settings.zoom_auto_max_width) {
NOVA.waitSelector('video')
.then(video => {
video.addEventListener('loadeddata', () => {
const squareAspectRatio = () => {
const aspectRatio = NOVA.aspectRatio.getAspectRatio({
'width': video.videoWidth,
'height': video.videoHeight,
});
return ('4:3' == aspectRatio || '1:1' == aspectRatio);
};
if (!squareAspectRatio()
&& (maxZoomPercent = geVideoMaxWidthPercent())
&& (Math.trunc(maxZoomPercent) !== 100)
&& (Math.trunc(maxZoomPercent) < 175)
) {
setScale(maxZoomPercent);
}
});
});
}
function setScale(zoom_pt = 100) {
zoom_pt = Math.max(100, Math.min(250, Math.trunc(zoom_pt)));
if (zoom_pt === 100 && container.classList.contains(ZOOM_CLASS_NAME)) {
container.classList.remove(ZOOM_CLASS_NAME);
container.style.removeProperty('transform');
}
else if (zoom_pt !== 100 && !container.classList.contains(ZOOM_CLASS_NAME)) {
container.classList.add(ZOOM_CLASS_NAME);
}
NOVA.showOSD(`Zoom: ${zoom_pt}%`);
if (zoom_pt === zoomPercent) return;
zoomPercent = zoom_pt;
container.style.setProperty('transform', `scale(${zoom_pt / 100})`);
}
function geVideoMaxWidthPercent() {
return Math.trunc(movie_player.clientWidth / NOVA.videoElement.videoHeight * 100);
}
NOVA.css.push(
`.${ZOOM_CLASS_NAME} {
transition: transform 100ms linear;
transform-origin: center;
}
.${ZOOM_CLASS_NAME} video {
position: relative !important;
}`);
});
},
options: {
zoom_hotkey: {
_tagName: 'select',
label: 'Hotkey',
'label:zh': '热键',
'label:ja': 'ホットキー',
'label:pl': 'Klawisz skrótu',
options: [
{ label: 'none', },
{ label: 'wheel', value: 'none' },
{ label: 'shift+wheel', value: 'shiftKey' },
{ label: 'ctrl+wheel', value: 'ctrlKey' },
{ label: 'alt+wheel', value: 'altKey' },
{ label: 'keyboard', value: 'keyboard', selected: true },
],
},
zoom_hotkey_custom_in: {
_tagName: 'select',
label: 'Hotkey zoom in',
options: [
{ label: '+', value: '+', selected: true },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '[', '-', ',', '.', '/', '<', ';', '\\',
],
'data-dependent': { 'zoom_hotkey': ['keyboard'] },
},
zoom_hotkey_custom_out: {
_tagName: 'select',
label: 'Hotkey zoom out',
options: [
{ label: '-', value: '-', selected: true },
{ label: 'ShiftL', value: 'ShiftLeft' },
{ label: 'ShiftR', value: 'ShiftRight' },
{ label: 'CtrlL', value: 'ControlLeft' },
{ label: 'CtrlR', value: 'ControlRight' },
{ label: 'AltL', value: 'AltLeft' },
{ label: 'AltR', value: 'AltRight' },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ' },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '[', '+', ',', '.', '/', '<', ';', '\\',
],
'data-dependent': { 'zoom_hotkey': ['keyboard'] },
},
zoom_step: {
_tagName: 'input',
label: 'Hotkey step',
'label:zh': '步',
'label:ja': 'ステップ',
'label:pl': 'Krok',
type: 'number',
title: 'in %',
placeholder: '%',
step: 5,
min: 5,
max: 50,
value: 10,
},
zoom_auto_max_width: {
_tagName: 'input',
label: 'Auto fit to width',
type: 'checkbox',
},
zoom_auto_max_width_hotkey_toggle: {
_tagName: 'select',
label: 'Hotkey toggle fit to width',
title: 'exception square video',
options: [
{ label: 'none', value: false },
{ label: 'ShiftL', value: 'ShiftLeft' },
{ label: 'ShiftR', value: 'ShiftRight' },
{ label: 'CtrlL', value: 'ControlLeft' },
{ label: 'CtrlR', value: 'ControlRight' },
{ label: 'AltL', value: 'AltLeft' },
{ label: 'AltR', value: 'AltRight' },
{ label: 'A', value: 'KeyA' },
{ label: 'B', value: 'KeyB' },
{ label: 'C', value: 'KeyC' },
{ label: 'D', value: 'KeyD' },
{ label: 'E', value: 'KeyE' },
{ label: 'F', value: 'KeyF' },
{ label: 'G', value: 'KeyG' },
{ label: 'H', value: 'KeyH' },
{ label: 'I', value: 'KeyI' },
{ label: 'J', value: 'KeyJ' },
{ label: 'K', value: 'KeyK' },
{ label: 'L', value: 'KeyL' },
{ label: 'M', value: 'KeyM' },
{ label: 'N', value: 'KeyN' },
{ label: 'O', value: 'KeyO' },
{ label: 'P', value: 'KeyP' },
{ label: 'Q', value: 'KeyQ', selected: true },
{ label: 'R', value: 'KeyR' },
{ label: 'S', value: 'KeyS' },
{ label: 'T', value: 'KeyT' },
{ label: 'U', value: 'KeyU' },
{ label: 'V', value: 'KeyV' },
{ label: 'W', value: 'KeyW' },
{ label: 'X', value: 'KeyX' },
{ label: 'Y', value: 'KeyY' },
{ label: 'Z', value: 'KeyZ' },
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
']', '[', '+', '-', ',', '.', '/', '<', ';', '\\',
],
},
}
});
window.nova_plugins.push({
id: 'playlist-collapse',
title: 'Collapse playlist',
'title:zh': '播放列表自动折叠',
'title:ja': 'プレイリストの自動折りたたみ',
'title:pl': 'Automatyczne zwijanie listy odtwarzania',
run_on_pages: 'watch, -mobile',
section: 'playlist',
_runtime: user_settings => {
if (!location.search.includes('list=')) return;
NOVA.waitSelector('#secondary #playlist:not([collapsed]) #expand-button button')
.then(btn => {
btn.click();
});
},
});
window.nova_plugins.push({
id: 'playlist-duration',
title: 'Show playlist duration',
'title:zh': '显示播放列表持续时间',
'title:ja': 'プレイリストの期間を表示',
'title:pl': 'Pokaż czas trwania playlisty',
run_on_pages: 'watch, playlist, -mobile',
restart_on_location_change: true,
section: 'playlist',
_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('#primary #thumbnail #overlays #text: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
|| document.body.querySelector('ytd-watch-flexy')?.__data.playlistData?.contents
|| document.body.querySelector('ytd-watch-flexy')?.data?.playlist?.playlist?.contents;
const duration = vids_list?.reduce((acc, vid) => acc + (+vid.playlistVideoRenderer?.lengthSeconds || 0), 0);
if (duration) {
return outFormat(duration);
}
}
});
break;
case 'watch':
NOVA.waitSelector('#secondary .index-message-wrapper', { destroy_after_page_leaving: true })
.then(el => {
const waitPlaylist = setInterval(() => {
const
playlistLength = movie_player.getPlaylist()?.length,
playlistList = document.body.querySelector('yt-playlist-manager')?.currentPlaylistData_?.contents
.filter(e => e.playlistPanelVideoRenderer?.lengthText?.simpleText)
.map(e => NOVA.formatTimeOut.hmsToSec(e.playlistPanelVideoRenderer.lengthText.simpleText));
console.assert(playlistList?.length === playlistLength, 'playlist loading:', playlistList?.length + '/' + playlistLength);
if (playlistLength && (playlistList?.length === playlistLength)) {
clearInterval(waitPlaylist);
if (duration = getPlaylistDuration(playlistList)) {
insertToHTML({ 'container': el, 'text': duration });
NOVA.waitSelector('#movie_player video', { destroy_after_page_leaving: true })
.then(video => {
video.addEventListener('ratechange', () => {
insertToHTML({ 'container': el, 'text': getPlaylistDuration(playlistList) });
});
});
}
else if (!user_settings.playlist_duration_progress_type) {
getPlaylistDurationFromThumbnails('#playlist #playlist-items #unplayableText[hidden]')
?.then(duration => insertToHTML({ 'container': el, 'text': duration }));
}
}
}, 2000);
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()) {
if (container && !(container instanceof HTMLElement)) {
return console.error('container not HTMLElement:', container);
}
return new Promise(resolve => {
let forcePlaylistRun = false;
const waitThumbnails = setInterval(() => {
const
timeStampList = document.body.querySelectorAll(items_selector),
playlistLength = movie_player.getPlaylist()?.length
|| document.body.querySelector('ytd-player')?.player_?.getPlaylist()?.length
|| timeStampList.length,
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.formatTimeOut.hmsToSec(e.textContent))
.filter(Number);
return arr.length && arr.reduce((acc, time) => acc + +time, 0);
}
}
function outFormat(duration = 0, total) {
let outArr = [
NOVA.formatTimeOut.HMS.digit(
(NOVA.currentPage == 'watch' && NOVA.videoElement?.playbackRate)
? (duration / NOVA.videoElement.playbackRate) : duration
)
];
if (total) {
outArr.push(`(${Math.trunc(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;
}
},
options: {
playlist_duration_progress_type: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
label: 'Time display mode',
'title:zh': '时间显示方式',
'title:ja': '時間表示モード',
'title:pl': 'Tryb wyświetlania czasu',
options: [
{
label: 'done', value: 'done',
'label:zh': '结束',
'label:ja': '終わり',
'label:pl': 'zakończone',
},
{
label: 'left', value: 'left',
'label:zh': '剩下',
'label:ja': '残り',
'label:pl': 'pozostało',
},
{
label: 'total', value: false, selected: true,
'label:zh': '全部的',
'label:ja': '全て',
'label:pl': 'w sumie',
},
],
},
playlist_duration_percentage: {
_tagName: 'input',
label: 'Add %',
'label:zh': '显示百分比',
'label:ja': 'パーセンテージを表示',
'label:pl': 'Pokaż procenty',
type: 'checkbox',
'data-dependent': { 'playlist_duration_progress_type': ['done', 'left'] },
},
}
});
window.nova_plugins.push({
id: 'playlist-extended',
title: 'Playlist extended section',
'title:zh': '播放列表扩展部分',
'title:ja': 'プレイリスト拡張セクション',
run_on_pages: 'watch, -mobile',
section: 'playlist',
_runtime: user_settings => {
let height = 90;
if (user_settings['move-to-sidebar']) {
switch (user_settings.move_to_sidebar_target) {
case 'info': height = 84; break;
}
}
NOVA.css.push(
`ytd-watch-flexy:not([theater]) #secondary #playlist {
--ytd-watch-flexy-panel-max-height: ${height}vh !important;
}`);
},
});
window.nova_plugins.push({
id: 'playlist-reverse',
title: 'Add playlist reverse order button',
'title:zh': '添加按钮反向播放列表顺序',
'title:ja': 'ボタンの逆プレイリストの順序を追加',
'title:pl': 'Dodaj przycisk odtwarzania w odwrotnej kolejności',
run_on_pages: 'watch, -mobile',
section: 'playlist',
_runtime: user_settings => {
const
SELECTOR_ID = 'nova-playlist-reverse-btn',
SELECTOR = '#' + SELECTOR_ID,
CLASS_NAME_ACTIVE = 'nova-playlist-reverse-on';
window.nova_playlistReversed;
NOVA.css.push(
SELECTOR + ` {
background: none;
border: 0;
}
yt-icon-button {
width: 40px;
height: 40px;
padding: 10px;
}
${SELECTOR} svg {
fill: white;
fill: var(--yt-spec-text-secondary);
}
${SELECTOR}:hover svg { fill: #66afe9; }
${SELECTOR}:active svg,
${SELECTOR}.${CLASS_NAME_ACTIVE} svg { fill: #2196f3; }`);
if (user_settings.playlist_reverse_auto_enabled && !window.nova_playlistReversed) {
window.nova_playlistReversed = true;
}
NOVA.runOnPageLoad(async () => {
if (location.search.includes('list=') && NOVA.currentPage == 'watch') {
reverseControl();
document.addEventListener('yt-page-data-updated', insertButton, { capture: true, once: true });
}
});
function insertButton() {
NOVA.waitSelector('ytd-watch-flexy.ytd-page-manager:not([hidden]) ytd-playlist-panel-renderer:not([collapsed]) #playlist-action-menu .top-level-buttons:not([hidden]), #secondary #playlist #playlist-action-menu #top-level-buttons-computed', { destroy_after_page_leaving: true })
.then(el => createButton(el));
function createButton(container = required()) {
if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
document.getElementById(SELECTOR_ID)?.remove();
const
reverseBtn = document.createElement('div'),
renderTitle = () => reverseBtn.title = `Reverse playlist order is ${window.nova_playlistReversed ? 'ON' : 'OFF'}`;
if (window.nova_playlistReversed) reverseBtn.className = CLASS_NAME_ACTIVE;
reverseBtn.id = SELECTOR_ID;
renderTitle();
reverseBtn.innerHTML =
`<yt-icon-button>
<svg viewBox="0 0 381.399 381.399" height="100%" width="100%">
<g>
<path d="M233.757,134.901l-63.649-25.147v266.551c0,2.816-2.286,5.094-5.104,5.094h-51.013c-2.82,0-5.099-2.277-5.099-5.094 V109.754l-63.658,25.147c-2.138,0.834-4.564,0.15-5.946-1.669c-1.389-1.839-1.379-4.36,0.028-6.187L135.452,1.991 C136.417,0.736,137.91,0,139.502,0c1.576,0,3.075,0.741,4.041,1.991l96.137,125.061c0.71,0.919,1.061,2.017,1.061,3.109 c0,1.063-0.346,2.158-1.035,3.078C238.333,135.052,235.891,135.735,233.757,134.901z M197.689,378.887h145.456v-33.62H197.689 V378.887z M197.689,314.444h145.456v-33.622H197.689V314.444z M197.689,218.251v33.619h145.456v-33.619H197.689z"/>
</g>
</svg>
</yt-icon-button>`;
reverseBtn.addEventListener('click', () => {
reverseBtn.classList.toggle(CLASS_NAME_ACTIVE);
window.nova_playlistReversed = !window.nova_playlistReversed;
if (window.nova_playlistReversed) {
reverseControl();
renderTitle();
fixConflictPlugins();
}
else location.reload();
});
container.append(reverseBtn);
}
}
function fixConflictPlugins() {
document.getElementById('nova-playlist-duration').innerHTML = '&nbsp; [out of reach] &nbsp;';
if (autoplayBtn = document.getElementById('nova-playlist-autoplay-btn')) {
autoplayBtn.disabled = true;
autoplayBtn.title = 'out of reach';
}
}
async function reverseControl() {
if (!window.nova_playlistReversed) return;
if ((ytdWatch = await NOVA.waitSelector('ytd-watch-flexy', { destroy_after_page_leaving: true }))
&& (data = await NOVA.waitUntil(() => ytdWatch.data?.contents?.twoColumnWatchNextResults, 100))
&& (playlist = data.playlist?.playlist)
&& (autoplay = data.autoplay?.autoplay)
) {
playlist.contents.reverse();
playlist.currentIndex = (playlist.totalVideos - playlist.currentIndex) - 1;
playlist.localCurrentIndex = (playlist.contents.length - playlist.localCurrentIndex) - 1;
for (const i of autoplay.sets) {
i.autoplayVideo = i.previousButtonVideo;
i.previousButtonVideo = i.nextButtonVideo;
i.nextButtonVideo = i.autoplayVideo;
}
ytdWatch.updatePageData_(data);
if ((manager = document.body.querySelector('yt-playlist-manager'))
&& (ytdPlayer = document.getElementById('ytd-player'))
) {
ytdPlayer.updatePlayerComponents(null, autoplay, null, playlist);
manager.autoplayData = autoplay;
manager.setPlaylistData(playlist);
ytdPlayer.updatePlayerPlaylist_(playlist);
}
}
scrollToElement(document.body.querySelector('#secondary #playlist-items[selected], ytm-playlist .item[selected=true]'));
}
function scrollToElement(targetEl = required()) {
if (!(targetEl instanceof HTMLElement)) return console.error('targetEl not HTMLElement:', targetEl);
const container = targetEl.parentElement;
container.scrollTop = targetEl.offsetTop - container.offsetTop;
}
},
options: {
playlist_reverse_auto_enabled: {
_tagName: 'input',
label: 'Default enabled state',
'label:zh': '默认启用',
'label:ja': 'デフォルトで有効になっています',
'label:pl': 'Domyślnie włączone',
type: 'checkbox',
},
},
});
window.nova_plugins.push({
id: 'playlist-toggle-autoplay',
title: 'Add playlist autoplay control button',
'title:zh': '播放列表自动播放控制',
'title:ja': 'プレイリストの自動再生コントロール',
'title:pl': 'Kontrola autoodtwarzania listy odtwarzania',
run_on_pages: 'watch, -mobile',
section: 'playlist',
_runtime: user_settings => {
const
SELECTOR_ID = 'nova-playlist-autoplay-btn',
SELECTOR = '#' + SELECTOR_ID;
let sesionAutoplayState = user_settings.playlist_autoplay;
NOVA.css.push(
`#playlist-action-menu .top-level-buttons {
align-items: center;
}
${SELECTOR}[type=checkbox] {
--height: 1em;
width: 2.2em;
}
${SELECTOR}[type=checkbox]:after {
transform: scale(1.5);
}
${SELECTOR}[type=checkbox] {
--opacity: .7;
--color: white;
height: var(--height);
line-height: 1.6em;
border-radius: 3em;
background-color: var(--paper-toggle-button-unchecked-bar-color, black);
appearance: none;
-webkit-appearance: none;
position: relative;
cursor: pointer;
outline: 0;
border: none;
}
${SELECTOR}[type=checkbox]:after {
position: absolute;
top: 0;
left: 0;
content: '';
width: var(--height);
height: var(--height);
border-radius: 50%;
background-color: var(--color);
box-shadow: 0 0 .25em rgba(0, 0, 0, .3);
}
${SELECTOR}[type=checkbox]:checked:after {
left: calc(100% - var(--height));
--color: var(--paper-toggle-button-checked-button-color, var(--primary-color));
}
${SELECTOR}[type=checkbox]:focus, input[type=checkbox]:focus:after {
transition: all 200ms ease-in-out;
}
${SELECTOR}[type=checkbox]:disabled {
opacity: .3;
}`);
NOVA.runOnPageLoad(() => {
if (location.search.includes('list=') && NOVA.currentPage == 'watch') {
insertButton();
}
});
function insertButton() {
NOVA.waitSelector('ytd-watch-flexy.ytd-page-manager:not([hidden]) ytd-playlist-panel-renderer:not([collapsed]) #playlist-action-menu .top-level-buttons:not([hidden]), #secondary #playlist #playlist-action-menu #top-level-buttons-computed', { destroy_after_page_leaving: true })
.then(el => renderCheckbox(el));
function renderCheckbox(container = required()) {
if (!(container instanceof HTMLElement)) return console.error('container not HTMLElement:', container);
document.getElementById(SELECTOR_ID)?.remove();
const checkboxBtn = document.createElement('input');
checkboxBtn.id = SELECTOR_ID;
checkboxBtn.type = 'checkbox';
checkboxBtn.title = 'Playlist toggle autoplay';
checkboxBtn.addEventListener('change', ({ target }) => {
sesionAutoplayState = target.checked;
setAssociatedAutoplay();
});
container.append(checkboxBtn);
checkboxBtn.checked = sesionAutoplayState;
setAssociatedAutoplay();
function setAssociatedAutoplay() {
if (manager = document.body.querySelector('yt-playlist-manager')) {
manager.interceptedForAutoplay = true;
manager.canAutoAdvance_ = checkboxBtn.checked;
checkboxBtn.checked = manager?.canAutoAdvance_;
checkboxBtn.title = `Playlist Autoplay is ${manager?.canAutoAdvance_ ? 'ON' : 'OFF'}`;
if (checkboxBtn.checked) checkHiddenVideo();
}
else console.error('Error playlist-autoplay. Playlist manager is', manager);
async function checkHiddenVideo() {
const ytdWatch = document.body.querySelector('ytd-watch-flexy');
let vids_list;
await NOVA.waitUntil(() => {
if ((vids_list =
ytdWatch?.data?.contents?.twoColumnWatchNextResults?.playlist?.playlist?.contents
|| ytdWatch?.data?.playlist?.playlist?.contents
)
&& vids_list.length) return true;
}, 1000);
const
currentIndex = movie_player.getPlaylistIndex(),
lastAvailableIdx = vids_list.findIndex(i => i.hasOwnProperty('messageRenderer')) - 1;
if (currentIndex === lastAvailableIdx) {
manager.canAutoAdvance_ = false;
alert('Nova [playlist-toggle-autoplay]:\nPlaylist has hide video. Playlist autoplay disabled');
checkboxBtn.checked = false;
}
}
}
}
}
},
options: {
playlist_autoplay: {
_tagName: 'select',
label: 'Default state',
'label:zh': '默认状态',
'label:ja': 'デフォルト状態',
'label:pl': 'Stan domyślny',
options: [
{
label: 'play', value: true, selected: true,
},
{
label: 'stop', value: false,
},
],
},
}
});
window.nova_plugins.push({
id: 'move-to-sidebar',
title: 'Move to sidebar',
'title:zh': '转移到侧边栏',
'title:ja': 'サイドバーに転送',
'title:pl': 'Przenieś na pasek boczny',
run_on_pages: 'watch, -mobile',
section: 'sidebar',
'plugins-conflict': 'description-popup',
_runtime: user_settings => {
if (user_settings.move_to_sidebar_target != 'info' && location.search.includes('list=')) return;
const
SELECTOR_CONTAINER = 'ytd-watch-flexy:not([fullscreen])',
SELECTOR_BELOW = `${SELECTOR_CONTAINER} #below`,
SELECTOR_SECONDARY = `${SELECTOR_CONTAINER} #secondary`;
switch (user_settings.move_to_sidebar_target) {
case 'info':
moveChannelInfo();
break;
case 'description':
if (user_settings['description-popup']) return;
NOVA.waitSelector(`${SELECTOR_BELOW} #description.ytd-watch-metadata`, { destroy_after_page_leaving: true })
.then(description => {
NOVA.waitSelector(`${SELECTOR_SECONDARY}-inner`, { destroy_after_page_leaving: true })
.then(async secondary => {
if (document.body.querySelector('#chat:not([collapsed])')) return;
secondary.prepend(description);
moveChannelInfo();
if (!user_settings['description-popup'] && !user_settings['video-date-format']) {
document.body.querySelector(`${SELECTOR_BELOW} ytd-watch-metadata #title`)
?.append(document.body.querySelector(`${SELECTOR_SECONDARY} #info-container`));
}
else {
document.body.querySelector(`${SELECTOR_SECONDARY} #info-container`)?.remove();
}
NOVA.css.push(
SELECTOR_SECONDARY + ` #owner {
margin: 0;
}
${SELECTOR_SECONDARY} #description.ytd-watch-metadata {
height: fit-content !important;
max-height: 80vh !important;
overflow-y: auto;
}
${SELECTOR_SECONDARY} #description #collapse {
display: none;
}
#ytd-watch-info-text, #info-container a {
display: none;
}`);
document.body.querySelector(`${SELECTOR_SECONDARY} #description #expand`)?.click();
});
});
moveSidebar();
break;
case 'comments':
if (user_settings.comments_visibility_mode == 'disable'
|| user_settings['comments-popup']
) {
return;
}
NOVA.waitSelector(`${SELECTOR_BELOW} #comments`, { destroy_after_page_leaving: true })
.then(comments => {
if (document.body.querySelector('#chat:not([collapsed])')) return;
document.body.querySelector(`${SELECTOR_SECONDARY}`)?.appendChild(comments);
comments.style.cssText = 'height:100vh; overflow-y:auto;';
});
moveSidebar();
break;
}
function moveSidebar() {
NOVA.waitSelector(`${SELECTOR_SECONDARY} #related`, { destroy_after_page_leaving: true })
.then(related => {
if (document.body.querySelector('#chat:not([collapsed])')) return;
document.body.querySelector('#below')?.appendChild(related);
});
}
function moveChannelInfo() {
NOVA.waitSelector(`${SELECTOR_SECONDARY}-inner`, { destroy_after_page_leaving: true })
.then(secondary => {
NOVA.waitSelector(`${SELECTOR_BELOW} ytd-watch-metadata #owner`, { destroy_after_page_leaving: true })
.then(channelInfo => {
secondary.prepend(channelInfo);
});
});
}
},
options: {
move_to_sidebar_target: {
_tagName: 'select',
label: 'Target of movement',
'label:zh': '运动目标',
'label:ja': '移動の対象',
options: [
{ label: 'info', value: 'info' },
{ label: 'info + description', value: 'description', selected: true },
{ label: 'comments', value: 'comments' },
],
},
},
});
window.nova_plugins.push({
id: 'related-visibility',
title: 'Collapse related section',
'title:zh': '收起相关栏目',
'title:ja': '関連セクションを折りたたむ',
'title:pl': 'Zwiń powiązaną sekcję',
run_on_pages: 'watch, -mobile',
section: 'sidebar',
_runtime: user_settings => {
NOVA.collapseElement({
selector: '#secondary #related',
label: 'related',
remove: (user_settings.related_visibility_mode == 'disable') ? true : false,
});
},
options: {
related_visibility_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'collapse', value: 'hide', selected: true,
'label:pl': 'zwiń',
},
{
label: 'remove', value: 'disable',
'label:zh': '消除',
'label:ja': '削除',
'label:pl': 'usunąć',
},
],
},
}
});
window.nova_plugins.push({
id: 'livechat-toggle-mode',
title: '"Livechat" mode instead of "Top chat"',
run_on_pages: 'live_chat, -mobile',
restart_on_location_change: true,
section: 'sidebar',
_runtime: user_settings => {
NOVA.waitSelector('#chat-messages #menu a[aria-selected="false"]')
.then(async btn => {
await btn.click();
});
},
});
window.nova_plugins.push({
id: 'sidebar-thumbs-channel-link-patch',
title: 'Fix channel links in sidebar',
'title:zh': '修复侧边栏中的频道链接',
'title:ja': 'サイドバーのチャネルリンクを修正',
'title:pl': 'Napraw linki do kanałów na pasku bocznym',
run_on_pages: 'watch, -mobile',
section: 'sidebar',
_runtime: user_settings => {
document.addEventListener('click', evt => patchLink(evt), { capture: true });
document.addEventListener('auxclick', evt => evt.button === 1 && patchLink(evt), { capture: true });
function patchLink(evt) {
if (evt.isTrusted
&& NOVA.currentPage == 'watch'
&& evt.target.closest('#channel-name')
&& (link = evt.target.closest('a'))
) {
if ((data = evt.target.closest('ytd-compact-video-renderer, ytd-video-meta-block')?.data)
&& (res = NOVA.seachInObjectBy.key({
'obj': data,
'keys': 'navigationEndpoint',
'match_fn': val => {
return val?.commandMetadata?.webCommandMetadata?.webPageType == 'WEB_PAGE_TYPE_CHANNEL';
},
})?.data)
) {
const
urlOrigData = link.data,
urlOrig = link.href;
link.data = res;
link.href = link.data.commandMetadata.webCommandMetadata.url += (user_settings['channel-default-tab'] && user_settings.channel_default_tab) || '/videos';
evt.target.addEventListener('mouseout', ({ target }) => {
link.data = urlOrigData;
link.href = urlOrig;
}, { capture: true, once: true });
}
}
}
},
});
window.nova_plugins.push({
id: 'livechat-visibility',
title: 'Collapse livechat',
'title:zh': '隐藏实时聊天',
'title:ja': 'ライブチャットを非表示',
'title:pl': 'Ukryj czat na żywo',
run_on_pages: 'watch, -mobile',
restart_on_location_change: true,
section: 'sidebar',
_runtime: user_settings => {
if (user_settings.livechat_visibility_mode == 'disable') {
NOVA.waitSelector('#chat', { destroy_after_page_leaving: true })
.then(chat => {
chat.remove();
});
}
else {
NOVA.waitSelector('#chat:not([collapsed]) #show-hide-button button', { destroy_after_page_leaving: true })
.then(btn => {
btn.click();
});
}
},
options: {
livechat_visibility_mode: {
_tagName: 'select',
label: 'Mode',
'label:zh': '模式',
'label:ja': 'モード',
'label:pl': 'Tryb',
options: [
{
label: 'collapse', value: 'hide', selected: true,
'label:pl': 'zwiń',
},
{
label: 'remove', value: 'disable',
'label:zh': '消除',
'label:ja': '削除',
'label:pl': 'usunąć',
},
],
},
}
});
window.nova_plugins.push({
id: 'thumbs-hide',
title: 'Thumbnails filter',
'title:zh': '缩略图过滤',
'title:ja': 'サムネイルのフィルタリング',
'title:pl': 'Ukryj kilka miniatur',
run_on_pages: 'home, results, feed, channel, watch, -mobile',
section: 'thumbs',
_runtime: user_settings => {
const
SELECTOR_THUMBS_HIDE_CLASS_NAME = 'nova-thumbs-hide',
thumbsSelectors = [
'ytd-rich-item-renderer',
'ytd-video-renderer',
'ytd-playlist-renderer',
'ytd-compact-video-renderer',
'yt-append-continuation-items-action',
'ytm-compact-video-renderer',
'ytm-item-section-renderer'
]
.map(i => `${i}:not(.${SELECTOR_THUMBS_HIDE_CLASS_NAME})`)
.join(',');
document.addEventListener('yt-action', evt => {
switch (evt.detail?.actionName) {
case 'yt-append-continuation-items-action':
case 'ytd-update-grid-state-action':
case 'yt-rich-grid-layout-refreshed':
case 'yt-store-grafted-ve-action':
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 'watch':
thumbRemove.live();
thumbRemove.mix();
thumbRemove.watched();
break;
}
break;
}
});
document.addEventListener('yt-navigate-finish', () => NOVA.queryURL.has('flow') && insertButton());
insertButton();
function insertButton() {
NOVA.waitSelector('#filter-button, ytd-shelf-renderer #title-container a[href="/feed/channels"]', { destroy_after_page_leaving: true })
.then(container => {
const filterBtn = document.createElement('button');
filterBtn.className = 'style-scope yt-formatted-string bold yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--text';
filterBtn.innerHTML =
`<span class="yt-spec-button-shape-next__icon" style="height:100%">
<svg viewBox="-50 -50 400 400" height="100%" width="100%">
<g fill="currentColor">
<path d="M128.25,175.6c1.7,1.8,2.7,4.1,2.7,6.6v139.7l60-51.3v-88.4c0-2.5,1-4.8,2.7-6.6L295.15,65H26.75L128.25,175.6z" />
</g>
</svg>
</span>`;
filterBtn.title = 'Toggle NOVA plugin [thumbs-hide]';
Object.assign(filterBtn.style, {
border: 0,
cursor: 'pointer',
scale: .7,
});
filterBtn.addEventListener('click', () => {
document.body.classList.toggle('nova-thumbs-unhide');
});
container.after(filterBtn);
});
}
NOVA.css.push(
`body.nova-thumbs-unhide .${SELECTOR_THUMBS_HIDE_CLASS_NAME} {
border: 2px dashed orange;
}
body:not(.nova-thumbs-unhide) .${SELECTOR_THUMBS_HIDE_CLASS_NAME} {
display: none
}`);
if (user_settings.thumbs_hide_shorts) {
const stylesList = [
'ytd-reel-shelf-renderer',
'ytd-rich-grid-row + ytd-rich-section-renderer',
'[is-shorts]',
];
if (CSS.supports('selector(:has(*))')) {
stylesList.push('ytd-guide-entry-renderer:has(path[d^="M10 14.65v-5.3L15"])');
}
NOVA.css.push(stylesList.join(',\n') + `{ display: none !important; }`);
}
const thumbRemove = {
shorts() {
if (!user_settings.thumbs_hide_shorts) return;
if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'shorts') return;
document.body.querySelectorAll('a#thumbnail[href*="shorts/"]')
.forEach(el => {
if (thumb = el.closest(thumbsSelectors)) {
thumb.classList.add(SELECTOR_THUMBS_HIDE_CLASS_NAME);
}
});
},
durationLimits() {
if (!+user_settings.thumbs_hide_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))
&& (timeSec = NOVA.formatTimeOut.hmsToSec(el.textContent.trim()))
&& (timeSec * (user_settings.rate_default || 1)) < (+user_settings.thumbs_hide_min_duration || 60)
) {
thumb.classList.add(SELECTOR_THUMBS_HIDE_CLASS_NAME);
}
});
});
},
premieres() {
if (!user_settings.thumbs_hide_premieres) return;
document.body.querySelectorAll(
`#thumbnail #overlays [aria-label="Premiere"],
#thumbnail #overlays [aria-label="Upcoming"]`
)
.forEach(el => {
if (thumb = el.closest(thumbsSelectors)) {
thumb.classList.add(SELECTOR_THUMBS_HIDE_CLASS_NAME);
}
});
document.body.querySelectorAll('[class*="badge"] [class*="live-now"]')
.forEach(el => {
if (thumb = el.closest(thumbsSelectors)) {
thumb.classList.add(SELECTOR_THUMBS_HIDE_CLASS_NAME);
}
});
},
live() {
if (!user_settings.thumbs_hide_live) return;
if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'streams') return;
const BLOCK_KEYWORDS = NOVA.strToArray(user_settings.thumbs_hide_live_channels_exception?.toLowerCase());
document.body.querySelectorAll('#thumbnail img[src*="_live.jpg"]')
.forEach(el => {
if (thumb = el.closest(thumbsSelectors)) {
if (BLOCK_KEYWORDS?.includes(thumb.querySelector('#channel-name a')?.textContent.trim().toLowerCase())) {
if (user_settings['search-filter']) {
thumb.style.display = 'block';
}
return;
}
thumb.classList.add(SELECTOR_THUMBS_HIDE_CLASS_NAME);
}
});
},
streamed() {
if (!user_settings.thumbs_hide_streamed) return;
if (NOVA.currentPage == 'channel' && NOVA.channelTab == 'streams') return;
const BLOCK_KEYWORDS = NOVA.strToArray(user_settings.thumbs_hide_live_channels_exception?.toLowerCase());
document.body.querySelectorAll('#metadata')
.forEach(el => {
if (el.querySelector('#metadata-line > span:last-of-type')?.textContent?.split(' ').length === 4
&& (thumb = el.closest(thumbsSelectors))
) {
if (BLOCK_KEYWORDS?.includes(thumb.querySelector('#channel-name a')?.textContent.trim().toLowerCase())) {
if (user_settings['search-filter']) {
thumb.style.display = 'block';
}
return;
}
thumb.classList.add(SELECTOR_THUMBS_HIDE_CLASS_NAME);
}
});
},
mix() {
if (!user_settings.thumbs_hide_mix) return;
document.body.querySelectorAll(
`a[href*="list="][href*="start_radio="]:not([hidden]),
#video-title[title^="Mix -"]:not([hidden])`
)
.forEach(el => {
if (thumb = el.closest('ytd-radio-renderer, ytd-compact-radio-renderer,' + thumbsSelectors)) {
thumb.classList.add(SELECTOR_THUMBS_HIDE_CLASS_NAME);
}
});
},
watched() {
if (!user_settings.thumbs_hide_watched) return;
if (!user_settings['thumbs-watched']) return;
const PERCENT_COMPLETE = +user_settings.thumbs_hide_watched_percent_complete || 90;
document.body.querySelectorAll('#thumbnail #overlays #progress[style*="width"]')
.forEach(el => {
if ((parseInt(el.style.width) > PERCENT_COMPLETE)
&& (thumb = el.closest(thumbsSelectors))
) {
thumb.classList.add(SELECTOR_THUMBS_HIDE_CLASS_NAME);
}
});
},
};
if (user_settings.thumbs_hide_mix) {
NOVA.css.push(
`ytd-radio-renderer {
display: none !important;
}`);
}
},
options: {
thumbs_hide_shorts: {
_tagName: 'input',
label: 'Hide Shorts',
'label:zh': '隐藏短裤',
'label:ja': 'ショーツを隠す',
'label:pl': 'Ukryj YouTube Shorts',
type: 'checkbox',
},
thumbs_hide_min_duration: {
_tagName: 'input',
label: 'Min duration in sec (for regular video)',
'label:zh': '最短持续时间(以秒为单位)',
'label:ja': '秒単位の最小期間',
'label:pl': 'Poniżej czasu trwania w sekundach',
type: 'number',
title: 'in sec / 0 - disable',
placeholder: '60-3600',
step: 1,
min: 0,
max: 3600,
value: 0,
},
thumbs_hide_premieres: {
_tagName: 'input',
label: 'Hide Premieres/Upcoming',
'label:zh': '隐藏首映/即将上映',
'label:ja': 'プレミア公開/近日公開を非表示',
'label:pl': 'Ukrywaj premiery',
type: 'checkbox',
title: 'Premiere Announcements',
},
thumbs_hide_live: {
_tagName: 'input',
label: 'Hide Live now streams',
'label:zh': '隐藏直播',
'label:ja': 'ライブ ストリームを非表示にする',
'label:pl': 'Ukryj strumień (na żywo)',
type: 'checkbox',
title: 'Now airing',
'title:zh': '正在播出',
'title:ja': '放映中',
'title:pl': 'Teraz wietrzenie',
},
thumbs_hide_live_channels_exception: {
_tagName: 'textarea',
label: 'Live channels exception',
'label:zh': '异常通道列表',
'label:ja': '例外チャネルのリスト',
title: 'separator: "," or ";" or "new line"',
'title:zh': '分隔器: "," 或 ";" 或 "新队"',
'title:ja': 'セパレータ: "," または ";" または "改行"',
'title:pl': 'separator: "," lub ";" lub "now linia"',
placeholder: 'channel1\nchannel2',
'data-dependent': { 'thumbs_hide_live': true },
},
thumbs_hide_streamed: {
_tagName: 'input',
label: 'Hide finished streams',
'label:zh': '隐藏完成的流',
'label:ja': '終了したストリームを非表示にする',
'label:pl': 'Ukryj po streamie',
type: 'checkbox',
'data-dependent': { 'thumbs_hide_live': true },
},
thumbs_hide_mix: {
_tagName: 'input',
label: "Hide 'Mix' thumbnails",
'label:zh': '隐藏[混合]缩略图',
'label:ja': '「Mix」サムネイルを非表示',
'label:pl': 'Ukryj miniaturki "Mix"',
type: 'checkbox',
title: '[Mix] offers to rewatch what has already saw',
'title:zh': '[混合]提供重新观看已经看过的内容',
'title:ja': '「Mix」は、すでに見たものを再視聴することを提案します',
'title:pl': '[Mix] proponuje ponowne obejrzenie już obejrzanych filmów',
},
thumbs_hide_watched: {
_tagName: 'input',
label: 'Hide watched',
'label:zh': '隐藏观看',
'label:ja': '監視対象を非表示',
'label:pl': 'Ukryj oglądane',
type: 'checkbox',
title: 'Need to Turn on [YouTube History]',
},
thumbs_hide_watched_percent_complete: {
_tagName: 'input',
label: 'Threshold percent',
type: 'number',
title: 'in %',
placeholder: '%',
step: 5,
min: 5,
max: 100,
value: 90,
'data-dependent': { 'thumbs_hide_watched': true },
},
}
});
window.nova_plugins.push({
id: 'thumbs-clear',
title: 'Thumbnails preview image',
'title:zh': '清除缩略图',
'title:ja': 'サムネイルをクリアする',
'title:pl': 'Wyczyść miniatury',
run_on_pages: 'home, feed, channel, watch',
section: 'thumbs',
desc: 'Replaces the predefined clickbait thumbnails',
'desc:zh': '替换预定义的缩略图',
'desc:ja': '事前定義されたサムネイルを置き換えます',
'desc:pl': 'Zastępuje predefiniowaną miniaturkę',
_runtime: user_settings => {
const
ATTR_MARK = 'nova-thumb-preview-cleared',
thumbsSelectors = [
'ytd-rich-item-renderer',
'yt-append-continuation-items-action',
'ytm-compact-video-renderer',
'ytm-item-section-renderer'
];
let DISABLE_YT_IMG_DELAY_LOADING_default = false;
NOVA.watchElements({
selectors: [
'#thumbnail:not(.ytd-playlist-thumbnail):not([class*=markers]):not([href*="/shorts/"]) img[src]:not([src*="_live.jpg"])',
'a:not([href*="/shorts/"]) img.video-thumbnail-img[src]:not([src*="_live.jpg"])'
],
attr_mark: ATTR_MARK,
callback: async img => {
if (NOVA.currentPage == 'results') return;
if (window.yt?.config_?.DISABLE_YT_IMG_DELAY_LOADING
&& DISABLE_YT_IMG_DELAY_LOADING_default !== window.yt?.config_?.DISABLE_YT_IMG_DELAY_LOADING
) {
DISABLE_YT_IMG_DELAY_LOADING_default = window.yt?.config_?.DISABLE_YT_IMG_DELAY_LOADING;
await NOVA.delay(100);
document.body.querySelectorAll(`[${ATTR_MARK}]`).forEach(e => e.removeAttribute(ATTR_MARK));
}
if ((thumb = img.closest(thumbsSelectors))
&& thumb.querySelector(
`#badges [class*="live-now"],
#overlays [aria-label="PREMIERE"],
#overlays [overlay-style="UPCOMING"]`)
) {
return;
}
if (src = patchImg(img.src)) img.src = patchImg(src);
},
});
if (user_settings.thumbs_clear_overlay) {
NOVA.css.push(
`#hover-overlays {
visibility: hidden !important;
}`);
}
function patchImg(str) {
if ((re = /(\w{2}default|hq\d+)./i) && re.test(str)) {
return str.replace(re, (user_settings.thumbs_clear_preview_timestamp || 'hq2') + '.');
}
}
},
options: {
thumbs_clear_preview_timestamp: {
_tagName: 'select',
label: 'Timestamps moment',
'label:zh': '缩略图时间戳',
'label:ja': 'サムネイルのタイムスタンプ',
'label:pl': 'Znaczniki czasowe miniatur',
title: 'Show thumbnail from video time position',
'title:zh': '从视频时间位置显示缩略图',
'title:ja': 'ビデオの時間位置からサムネイルを表示',
'title:pl': 'Pokaż miniaturkę z pozycji czasu wideo',
options: [
{
label: 'start', value: 'hq1',
'label:zh': '开始',
'label:ja': '始まり',
'label:pl': 'początek',
},
{
label: 'middle', value: 'hq2', selected: true,
'label:zh': '中间',
'label:ja': '真ん中',
'label:pl': 'środek',
},
{
label: 'end', value: 'hq3',
'label:zh': '结尾',
'label:ja': '終わり',
'label:pl': 'koniec',
}
],
},
thumbs_clear_overlay: {
_tagName: 'input',
label: 'Hide overlay buttons on a thumbnail',
'label:zh': '隐藏覆盖在缩略图上的按钮',
'label:ja': 'サムネイルにオーバーレイされたボタンを非表示にする',
'label:pl': 'Ukryj przyciski nakładki na miniaturce',
type: 'checkbox',
title: 'Hide [ADD TO QUEUE] [WATCH LATER]',
},
}
});
window.nova_plugins.push({
id: 'thumbs-title-normalize',
title: 'Decapitalize thumbnails title',
'title:zh': '从大写中删除缩略图标题',
'title:ja': 'サムネイルのタイトルを大文字から外す',
'title:pl': 'Zmniejsz czcionkę w tytule miniatur',
run_on_pages: 'home, feed, channel, watch',
section: 'thumbs',
desc: 'Upper Case thumbnails title back to normal',
'plugins-conflict': 'thumbs-title-lang',
_runtime: user_settings => {
if (user_settings['thumbs-title-lang']) return;
const
VIDEO_TITLE_SELECTOR = [
'#video-title',
'a > [class*="media-item-headline"]',
]
.map(i => i + ':not(:empty)'),
MAX_CAPS_LETTERS = +user_settings.thumbs_title_normalize_smart_max_words || 2,
ATTR_MARK = 'nova-thumb-title-normalized',
clearOfSymbols = str => str.replace(/[\u2011-\u26FF]/g, ' ').replace(/\s{2,}/g, ' '),
clearOfEmoji = str => str.replace(/[^<>=\p{L}\p{N}\p{P}\p{Z}{\^\$}]/gu, ' ').replace(/\s{2,}/g, ' ');
if (user_settings.thumbs_title_show_full) {
NOVA.css.push(
VIDEO_TITLE_SELECTOR.join(',') + `{
display: block !important;
max-height: unset !important;
}`);
}
const UpperCaseLetterRegex = new RegExp("([\-0-9A-ZÀ-ÖØ-ÞĀĂĄĆĈĊČĎĐĒĔĖĘĚĜĞĠĢĤĦĨĪĬĮİIJĴĶĹĻĽĿŁŃŅŇŊŌŎŐŒŔŖŘŚŜŞŠŢŤŦŨŪŬŮŰŲŴŶŸ-ŹŻŽƁ-ƂƄƆ-ƇƉ-ƋƎ-ƑƓ-ƔƖ-ƘƜ-ƝƟ-ƠƢƤƦ-ƧƩƬƮ-ƯƱ-ƳƵƷ-ƸƼDŽLJNJǍǏǑǓǕǗǙǛǞǠǢǤǦǨǪǬǮDZǴǶ-ǸǺǼǾȀȂȄȆȈȊȌȎȐȒȔȖȘȚȜȞȠȢȤȦȨȪȬȮȰȲȺ-ȻȽ-ȾɁɃ-ɆɈɊɌɎͰͲͶΆΈ-ΊΌΎ-ΏΑ-ΡΣ-ΫϏϒ-ϔϘϚϜϞϠϢϤϦϨϪϬϮϴϷϹ-ϺϽ-ЯѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎҐҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾӀ-ӁӃӅӇӉӋӍӐӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӶӸӺӼӾԀԂԄԆԈԊԌԎԐԒԔԖԘԚԜԞԠԢԱ-Ֆ֊־٠-٩۰-۹߀-߉०-९০-৯੦-੯૦-૯୦-୯௦-௯౦-౯೦-೯൦-൯๐-๙໐-໙༠-༩၀-၉႐-႙Ⴀ-Ⴥ០-៩᠆᠐-᠙᥆-᥏᧐-᧙᭐-᭙᮰-᮹᱀-᱉᱐-᱙ḀḂḄḆḈḊḌḎḐḒḔḖḘḚḜḞḠḢḤḦḨḪḬḮḰḲḴḶḸḺḼḾṀṂṄṆṈṊṌṎṐṒṔṖṘṚṜṞṠṢṤṦṨṪṬṮṰṲṴṶṸṺṼṾẀẂẄẆẈẊẌẎẐẒẔẞẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼẾỀỂỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪỬỮỰỲỴỶỸỺỼỾἈ-ἏἘ-ἝἨ-ἯἸ-ἿὈ-ὍὙὛὝὟὨ-ὯᾸ-ΆῈ-ΉῘ-ΊῨ-ῬῸ-Ώ‐-―ℂℇℋ-ℍℐ-ℒℕℙ-ℝℤΩℨK-ℭℰ-ℳℾ-ℿⅅↃⰀ-ⰮⱠⱢ-ⱤⱧⱩⱫⱭ-ⱯⱲⱵⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢ⸗⸚〜〰゠꘠-꘩ꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝽ-ꝾꞀꞂꞄꞆꞋ꣐-꣙꤀-꤉꩐-꩙︱-︲﹘﹣-0-9A-Z]|\ud801[\udc00-\udc27\udca0-\udca9]|\ud835[\udc00-\udc19\udc34-\udc4d\udc68-\udc81\udc9c\udc9e-\udc9f\udca2\udca5-\udca6\udca9-\udcac\udcae-\udcb5\udcd0-\udce9\udd04-\udd05\udd07-\udd0a\udd0d-\udd14\udd16-\udd1c\udd38-\udd39\udd3b-\udd3e\udd40-\udd44\udd46\udd4a-\udd50\udd6c-\udd85\udda0-\uddb9\uddd4-\udded\ude08-\ude21\ude3c-\ude55\ude70-\ude89\udea8-\udec0\udee2-\udefa\udf1c-\udf34\udf56-\udf6e\udf90-\udfa8\udfca\udfce-\udfff]){2,}", 'g');
NOVA.css.push({
'text-transform': 'uppercase',
}, VIDEO_TITLE_SELECTOR.map(e => `${e}[${ATTR_MARK}]::first-letter`), 'important');
NOVA.watchElements({
selectors: VIDEO_TITLE_SELECTOR,
attr_mark: ATTR_MARK,
callback: async videoTitleEl => {
if (NOVA.currentPage == 'results') return;
let countCaps = 0;
if (user_settings.thumbs_title_clear_emoji) {
videoTitleEl.textContent = clearOfEmoji(videoTitleEl.innerText).trim();
}
if (user_settings.thumbs_title_clear_symbols) {
videoTitleEl.textContent = clearOfSymbols(videoTitleEl.innerText).trim();
}
const normalizedText = videoTitleEl.innerText.replace(UpperCaseLetterRegex, match => {
++countCaps;
return (
/\d/.test(match)
|| (match.length === 1 && /[A-Z]/.test(match))
|| (match.length < 5 && match.length > 1 && ['HD', 'UHD', 'USB', 'TV', 'CPU', 'GPU', 'APU', 'AMD', 'XT', 'RX', 'GTX', 'RTX', 'GT', 'FX', 'SE', 'HP', 'SSD', 'RAM', 'PC', 'FPS', 'RDNA', 'FSR', 'DLSS', 'MSI', 'VR', 'GOTY', 'AAA', 'UI', 'BBC', 'WWE', 'OS', 'OP', 'ED', 'MV', 'PV', 'OST', 'NCS', 'BGM', 'EDM', 'GMV', 'AMV', 'MMD', 'MAD', 'SQL', 'CAPS'].includes(match))
|| (match.length < 5 && /(M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3}))/i.test(match))
) ? match : match.toLowerCase();
});
if (countCaps > MAX_CAPS_LETTERS
|| (countCaps > 1 && normalizedText.split(/\s+/).length === countCaps)
) {
videoTitleEl.innerText = normalizedText;
}
}
});
document.addEventListener('yt-action', evt => {
if (evt.detail?.actionName == 'yt-chip-cloud-chip-select-action') {
window.addEventListener('transitionend', restoreTitle, { capture: true, once: true });
}
});
function restoreTitle() {
const selectorOldTitle = '#video-title-link[title]';
if (NOVA.channelTab == 'videos') {
document.body.querySelectorAll(`${selectorOldTitle} ${VIDEO_TITLE_SELECTOR}[${ATTR_MARK}]`)
.forEach(el => {
if (oldTitle = el.closest(selectorOldTitle)?.title) {
el.innerText = oldTitle;
el.removeAttribute(ATTR_MARK);
}
});
}
}
},
options: {
thumbs_title_show_full: {
_tagName: 'input',
label: 'Show full title',
'label:zh': '显示完整标题',
'label:ja': '完全なタイトルを表示',
'label:pl': 'Pokaż pełny tytuł',
type: 'checkbox'
},
thumbs_title_normalize_smart_max_words: {
_tagName: 'input',
label: 'Max words in uppercase',
'label:zh': '大写字数上限',
'label:ja': '大文字の最大単語数',
'label:pl': 'Maksymalna liczba słów pisanych wielkimi literami',
type: 'number',
placeholder: '1-10',
min: 1,
max: 10,
value: 2,
},
thumbs_title_clear_emoji: {
_tagName: 'input',
label: 'Remove emoji',
'label:zh': '从表情符号中清除标题',
'label:ja': 'クリア絵文字',
'label:pl': 'Usuń emoji',
type: 'checkbox',
},
thumbs_title_clear_symbols: {
_tagName: 'input',
label: 'Remove symbols',
type: 'checkbox',
},
}
});
window.nova_plugins.push({
id: 'thumbs-grid-count',
title: 'Thumbnails count in row',
run_on_pages: 'feed, channel, -mobile',
section: 'thumbs',
_runtime: user_settings => {
const
MathMin_orig = Math.min,
addRowCount = +user_settings.thumbs_grid_count || 1;
Math.min = function () {
return MathMin_orig.apply(Math, arguments)
+ (/calcElementsPerRow/img.test(Error().stack || '') ? addRowCount - 1 : 0);
};
},
options: {
thumbs_grid_count: {
_tagName: 'input',
label: 'Add to row',
type: 'number',
placeholder: '1-10',
step: 1,
min: 1,
max: 10,
value: 1,
},
}
});
window.nova_plugins.push({
id: 'thumbs-watch-later',
title: 'Add "Watch Later" button on thumbnails (for feed page)',
run_on_pages: 'feed, -mobile',
section: 'thumbs',
desc: 'You must be logged in',
_runtime: user_settings => {
const
SELECTOR_OVERLAY_ID_NAME = 'nova-thumb-overlay',
SELECTOR_CLASS_NAME = 'nova-thumbs-watch-later-btn',
thumbsSelectors = [
'ytd-rich-item-renderer',
'ytd-compact-video-renderer',
'yt-append-continuation-items-action',
'ytm-compact-video-renderer',
'ytm-item-section-renderer'
]
.map(i => `${i}:not(.${SELECTOR_CLASS_NAME})`)
.join(',');
document.addEventListener('yt-action', evt => {
switch (evt.detail?.actionName) {
case 'yt-append-continuation-items-action':
case 'ytd-update-grid-state-action':
case 'yt-rich-grid-layout-refreshed':
case 'yt-store-grafted-ve-action':
switch (NOVA.currentPage) {
case 'feed':
document.body.querySelectorAll(thumbsSelectors)
.forEach(thumb => {
thumb.classList.add(SELECTOR_CLASS_NAME);
if (container = thumb.querySelector('a#thumbnail.ytd-thumbnail')) {
const div = document.createElement('div');
div.id = SELECTOR_OVERLAY_ID_NAME;
div.append(renderButton(thumb));
container.append(div);
}
});
break;
}
break;
}
});
NOVA.css.push(
`#${SELECTOR_OVERLAY_ID_NAME} {
position: absolute;
top: 0;
left: 0;
z-index: 999;
}
button.${SELECTOR_CLASS_NAME} {
border: 0;
cursor: pointer;
height: 1.3em;
font-size: 2em;
background-color: transparent;
background-color: var(--yt-spec-static-overlay-background-heavy);
color: var(--yt-spec-static-overlay-text-primary);
}`);
function renderButton(thumb = required()) {
const btn = document.createElement('button');
btn.className = SELECTOR_CLASS_NAME;
btn.innerHTML =
`<svg viewBox="0 0 24 24" height="100%" width="100%">
<g fill="currentColor">
<path d="M14.97 16.95 10 13.87V7h2v5.76l4.03 2.49-1.06 1.7zM12 3c-4.96 0-9 4.04-9 9s4.04 9 9 9 9-4.04 9-9-4.04-9-9-9m0-1c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12 6.48 2 12 2z" />
</g>
</svg>`;
btn.title = 'Watch Later';
btn.addEventListener('click', async evt => {
evt.preventDefault();
evt.stopPropagation();
evt.stopImmediatePropagation();
if (menu = thumb.querySelector('#menu button')) {
menu.click();
await NOVA.waitSelector('#menu [menu-active]', { container: thumb, destroy_after_page_leaving: true });
if (menuItemEl = document.body.querySelector('tp-yt-iron-dropdown [role="menuitem"]:has(path[d^="M14.97"])')) {
menuItemEl.style.backgroundColor = 'red';
await menuItemEl.click();
menuItemEl.style.removeProperty('backgroundColor');
}
document.body.click();
}
});
return btn;
}
},
});
window.nova_plugins.push({
id: 'thumbs-watched',
title: 'Mark watched thumbnails',
'title:zh': '标记您观看的缩略图',
'title:ja': '視聴したサムネイルにマークを付ける',
'title:pl': 'Oznacz obejrzane miniaturki',
run_on_pages: 'home, results, feed, channel, playlist, watch, -mobile',
section: 'thumbs',
_runtime: user_settings => {
NOVA.css.push(
`a#thumbnail,
a[class*="thumbnail"] {
outline: 1px solid var(--yt-spec-general-background-a);
}
a#thumbnail:visited,
a[class*="thumbnail"]:visited {
outline: 1px solid ${user_settings.thumbs_watched_frame_color || 'red'} !important;
}
ytd-playlist-panel-video-renderer a:visited #meta * {
color: ${user_settings.thumbs_watched_title_color || '#ff4500'} !important;
}`);
if (user_settings.thumbs_watched_title) {
NOVA.css.push(
`a#video-title:visited:not(:hover),
#description a:visited {
color: ${user_settings.thumbs_watched_title_color} !important;
}`);
}
},
options: {
thumbs_watched_frame_color: {
_tagName: 'input',
label: 'Frame color',
'label:zh': '框架颜色',
'label:ja': 'フレームカラー',
'label:pl': 'Kolor ramki',
type: 'color',
value: '#FF0000',
},
thumbs_watched_title: {
_tagName: 'input',
label: 'Set title color',
'label:zh': '您要更改标题颜色吗?',
'label:ja': 'タイトルの色を変更しますか?',
'label:pl': 'Ustaw kolor tytułu',
type: 'checkbox',
},
thumbs_watched_title_color: {
_tagName: 'input',
label: 'Choose title color',
'label:zh': '选择标题颜色',
'label:ja': 'タイトルの色を選択',
'label:pl': 'Wybierz kolor tytułu',
type: 'color',
value: '#ff4500',
'data-dependent': { 'thumbs_watched_title': true },
},
}
});
window.nova_plugins.push({
id: 'search-filter',
title: 'Blocked channels',
'title:zh': '屏蔽频道列表',
'title:ja': 'ブロックされたチャネルのリスト',
'title:pl': 'Zablokowane kanały',
run_on_pages: 'results, feed, -mobile',
section: 'thumbs',
desc: 'Hide channels on the search page',
'desc:zh': '在搜索页面上隐藏频道',
'desc:ja': '検索ページでチャンネルを非表示にする',
'desc:pl': 'Ukryj kanały na stronie wyszukiwania',
_runtime: user_settings => {
const BLOCK_KEYWORDS = NOVA.strToArray(user_settings.search_filter_channels_blocklist?.toLowerCase());
const thumbsSelectors = [
'ytd-rich-item-renderer',
'ytd-video-renderer',
'ytd-playlist-renderer',
'ytm-compact-video-renderer',
]
.join(',');
if (NOVA.isMobile) {
NOVA.watchElements({
selectors: ['#channel-name'],
attr_mark: 'nova-thumb-channel-filtered',
callback: channel_name => {
if (BLOCK_KEYWORDS.includes(channel_name.textContent.trim().toLowerCase())
&& (thumb = channel_name.closest(thumbsSelectors))
) {
thumb.remove();
}
}
});
}
else {
document.addEventListener('yt-action', evt => {
switch (evt.detail?.actionName) {
case 'yt-append-continuation-items-action':
case 'ytd-update-grid-state-action':
case 'yt-rich-grid-layout-refreshed':
case 'yt-store-grafted-ve-action':
document.body.querySelectorAll(
'#channel-name a[href]:first-child'
)
.forEach(channel_name => {
BLOCK_KEYWORDS.forEach(keyword => {
if (keyword.startsWith('@')
&& channel_name.href.includes(keyword)
&& (thumb = channel_name.closest(thumbsSelectors))
) {
thumb.remove();
}
else if ((channel_name.textContent.trim().toLowerCase() == keyword)
&& (thumb = channel_name.closest(thumbsSelectors))
) {
thumb.style.display = 'none';
}
});
});
break;
}
});
if (typeof GM_info === 'object') {
NOVA.waitSelector('tp-yt-iron-dropdown:not([aria-hidden="true"]) ytd-menu-popup-renderer[slot="dropdown-content"] [role="menuitem"]')
.then(container => {
const btn = document.createElement('div');
btn.classList = 'style-scope ytd-menu-service-item-renderer';
Object.assign(btn.style, {
'font-size': '14px',
padding: '9px 15px 9px 56px',
cursor: 'pointer',
});
btn.innerHTML = '<b>Nova block channel</b>';
btn.title = 'Nova block channel';
btn.addEventListener('click', () => {
const currentCannelName = document.querySelector('#menu [menu-active]')
.closest('#details, #meta')
.querySelector('#channel-name a')?.textContent;
if (currentCannelName && confirm(`Add channel [${currentCannelName}] to the blacklist?`)) {
user_settings.search_filter_channels_blocklist += '\n' + currentCannelName;
GM_setValue(configStoreName, user_settings);
}
});
container.after(btn);
});
}
}
},
options: {
search_filter_channels_blocklist: {
_tagName: 'textarea',
label: 'List',
'label:zh': '频道列表',
'label:ja': 'チャンネルリスト',
'label:pl': 'Lista',
title: 'separator: "," or ";" or "new line"',
'title:zh': '分隔器: "," 或 ";" 或 "新队"',
'title:ja': 'セパレータ: "," または ";" または "改行"',
'title:pl': 'separator: "," lub ";" lub "now linia"',
placeholder: 'channel1\nchannel2',
required: true,
},
}
});
window.nova_plugins.push({
id: 'thumbs-title-filter',
title: 'Block thumbnails by title',
'title:zh': '按标题阻止缩略图',
'title:ja': 'タイトルでサムネイルをブロックする',
'title:pl': 'Blokuj miniatury według tytułu',
run_on_pages: '*, -embed, -mobile, -live_chat',
section: 'thumbs',
_runtime: user_settings => {
const BLOCK_KEYWORDS = NOVA.strToArray(user_settings.thumbs_filter_title_blocklist?.toLowerCase());
const thumbsSelectors = [
'ytd-rich-item-renderer',
'ytd-video-renderer',
'ytd-playlist-renderer',
'ytd-compact-video-renderer',
'yt-append-continuation-items-action',
'ytm-compact-video-renderer',
'ytm-item-section-renderer'
]
.join(',');
if (NOVA.isMobile) {
NOVA.watchElements({
selectors: ['#video-title:not(:empty)'],
attr_mark: 'nova-thumb-title-filtered',
callback: video_title => {
BLOCK_KEYWORDS.forEach(keyword => {
if (video_title.textContent.trim().toLowerCase().includes(keyword)
&& (thumb = channel_name.closest(thumbsSelectors))
) {
}
});
}
});
}
else {
document.addEventListener('yt-action', evt => {
switch (evt.detail?.actionName) {
case 'yt-append-continuation-items-action':
case 'ytd-update-grid-state-action':
case 'yt-rich-grid-layout-refreshed':
case 'yt-store-grafted-ve-action':
hideThumb();
break;
}
});
function hideThumb() {
document.body.querySelectorAll('#video-title')
.forEach(titleEl => {
BLOCK_KEYWORDS.forEach(keyword => {
if (titleEl.textContent.toLowerCase().includes(keyword)
&& (thumb = titleEl.closest(thumbsSelectors))
) {
thumb.remove();
}
});
});
}
}
},
options: {
thumbs_filter_title_blocklist: {
_tagName: 'textarea',
label: 'Words list',
'label:zh': '单词列表',
'label:ja': '単語リスト',
'label:pl': 'Lista słów',
title: 'separator: "," or ";" or "new line"',
'title:zh': '分隔器: "," 或 ";" 或 "新队"',
'title:ja': 'セパレータ: "," または ";" または "改行"',
'title:pl': 'separator: "," lub ";" lub "now linia"',
placeholder: 'text1\ntext2',
required: true,
},
}
});
window.nova_plugins.push({
id: 'thumbs-not-interested',
title: 'Add "Not Interested" button on thumbnails',
run_on_pages: 'feed, channel, watch, -mobile',
section: 'thumbs',
desc: 'You must be logged in',
_runtime: user_settings => {
const
SELECTOR_OVERLAY_ID_NAME = 'nova-thumb-overlay',
SELECTOR_CLASS_NAME = 'nova-thumbs-not-interested-btn',
thumbsSelectors = [
'ytd-rich-item-renderer',
'ytd-compact-video-renderer',
'yt-append-continuation-items-action',
'ytm-compact-video-renderer',
'ytm-item-section-renderer'
]
.map(i => `${i}:not(.${SELECTOR_CLASS_NAME})`)
.join(',');
document.addEventListener('yt-action', evt => {
switch (evt.detail?.actionName) {
case 'yt-append-continuation-items-action':
case 'ytd-update-grid-state-action':
case 'yt-rich-grid-layout-refreshed':
case 'yt-store-grafted-ve-action':
case 'yt-forward-redux-action-to-live-chat-iframe':
switch (NOVA.currentPage) {
case 'feed':
case 'watch':
document.body.querySelectorAll(thumbsSelectors)
.forEach(thumb => {
thumb.classList.add(SELECTOR_CLASS_NAME);
if (container = thumb.querySelector('a#thumbnail.ytd-thumbnail')) {
if (user_settings['thumbs-watch-later']) {
NOVA.waitSelector(`#${SELECTOR_OVERLAY_ID_NAME}`, { 'container': container })
.then(container => {
container.append(renderButton(thumb));
});
}
else {
const div = document.createElement('div');
div.id = SELECTOR_OVERLAY_ID_NAME;
div.append(renderButton(thumb));
container.append(div);
}
}
});
break;
}
break;
}
});
if (!user_settings['thumbs-watch-later']) {
NOVA.css.push(
`#${SELECTOR_OVERLAY_ID_NAME} {
position: absolute;
top: 0;
left: 0;
z-index: 999;
}`);
}
NOVA.css.push(
`button.${SELECTOR_CLASS_NAME} {
border: 0;
cursor: pointer;
height: 1.3em;
font-size: 2em;
background-color: transparent;
background-color: var(--yt-spec-static-overlay-background-heavy);
color: var(--yt-spec-static-overlay-text-primary);
}`);
function renderButton(thumb = required()) {
const btn = document.createElement('button');
btn.className = SELECTOR_CLASS_NAME;
btn.innerHTML =
`<svg viewBox="0 0 24 24" height="100%" width="100%">
<g fill="currentColor">
<path d="M12 2c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12 6.48 2 12 2zM3 12c0 2.31.87 4.41 2.29 6L18 5.29C16.41 3.87 14.31 3 12 3c-4.97 0-9 4.03-9 9zm15.71-6L6 18.71C7.59 20.13 9.69 21 12 21c4.97 0 9-4.03 9-9 0-2.31-.87-4.41-2.29-6z" />
</g>
</svg>`;
btn.title = 'Not Interested';
btn.addEventListener('click', async evt => {
evt.preventDefault();
evt.stopPropagation();
evt.stopImmediatePropagation();
if (menu = thumb.querySelector('#menu button')) {
menu.click();
await NOVA.waitSelector('#menu [menu-active]', { container: thumb, destroy_after_page_leaving: true });
if (menuItemEl = document.body.querySelector('tp-yt-iron-dropdown [role="menuitem"]:has(path[d^="M12 2c5.52"])')) {
menuItemEl.style.backgroundColor = 'red';
await menuItemEl.click();
menuItemEl.style.removeProperty('backgroundColor');
}
}
});
return btn;
}
},
});
window.nova_plugins.push({
id: 'thumbs-title-lang',
title: "Show titles original language",
'title:zh': '显示缩略图标题原始语言',
'title:ja': 'サムネイルのタイトルを元の言語で表示する',
run_on_pages: 'feed, channel, watch',
section: 'thumbs',
opt_api_key_warn: true,
'plugins-conflict': 'thumbs-title-normalize',
_runtime: user_settings => {
const
CACHE_NAME = 'thumbs-title',
SELECTOR_THUMBS_PATCHED_ATTR = 'nova-thumbs-title-lang',
thumbsSelectors = [
'ytd-rich-item-renderer',
'ytd-compact-video-renderer',
'yt-append-continuation-items-action',
'ytm-compact-video-renderer',
'ytm-item-section-renderer'
]
.map(i => `${i}:has(a#thumbnail[${SELECTOR_THUMBS_PATCHED_ATTR}][href*="%id%"]) #video-title`)
.join(',');
NOVA.css.push(
`#video-title[${SELECTOR_THUMBS_PATCHED_ATTR}] {
color: #86d2ed
}
*:hover > #video-title[${SELECTOR_THUMBS_PATCHED_ATTR}],
*:not(:hover) > #video-title[${SELECTOR_THUMBS_PATCHED_ATTR}] + #video-title {
display: none !important;
}`);
let
idsToProcess = [],
newCacheItem = {},
timeout;
NOVA.watchElements({
selectors: 'a#thumbnail[href].ytd-thumbnail',
attr_mark: SELECTOR_THUMBS_PATCHED_ATTR,
callback: thumbnail => {
if (id = NOVA.queryURL.get('v', thumbnail.href)) {
idsToProcess.push(id);
run_process();
}
},
});
function run_process(sec = 1) {
clearTimeout(timeout);
timeout = setTimeout(() => {
refreshCache(newCacheItem);
patchThumbs(idsToProcess);
}, 1000 * sec);
}
function patchThumbs(ids = []) {
if (!ids.length) return;
idsToProcess = [];
const cacheData = JSON.parse(sessionStorage.getItem(CACHE_NAME));
const newIds = ids
.filter(id => {
if (cacheData?.hasOwnProperty(id)) {
if (cacheItem = cacheData[id]) {
patchTitle({ 'id': id, 'text': cacheItem.text });
return false;
}
}
return true;
});
requestTitle(newIds);
}
function refreshCache(new_cache = {}) {
newCacheItem = {};
const cacheData = JSON.parse(sessionStorage.getItem(CACHE_NAME)) || {};
sessionStorage.setItem(CACHE_NAME, JSON.stringify(Object.assign(new_cache, cacheData)));
}
function requestTitle(ids = []) {
const YOUTUBE_API_MAX_IDS_PER_CALL = 50;
chunkArray(ids, YOUTUBE_API_MAX_IDS_PER_CALL)
.forEach(id_part => {
NOVA.request.API({
request: 'videos',
params: { 'id': id_part.join(','), 'part': 'snippet' },
api_key: user_settings['user-api-key'],
})
.then(res => {
res?.items?.forEach(item => {
patchTitle({ 'id': item.id, 'text': item.snippet.title });
newCacheItem[item.id] = { 'text': item.snippet.title };
});
run_process(3);
});
});
function chunkArray(array = [], size = 0) {
let chunked = [];
while (array.length) chunked.push(array.splice(0, +size));
return chunked;
}
}
function patchTitle({ id = required(), text = required() }) {
document.querySelectorAll(thumbsSelectors.replaceAll('%id%', id))
.forEach(videoTitleEl => {
if (videoTitleEl.textContent?.trim() == text) return;
const newTitleEl = videoTitleEl.cloneNode(true);
videoTitleEl.before(newTitleEl);
newTitleEl.setAttribute(SELECTOR_THUMBS_PATCHED_ATTR, true);
newTitleEl.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
pathnameArray = location.pathname.split('/').filter(Boolean),
{ page, channelTab } = identifyCurrentPage(pathnameArray[0], pathnameArray.pop());
NOVA.channelTab = channelTab;
return page;
})();
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('*') && !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)) + '&labels=bug&template=bug_report.md&title=pluginRunErr';
if (user_settings.report_issues) {
_pluginsCaptureException({
'trace_name': plugin.id,
'err_stack': err.stack,
'app_ver': app_ver,
'confirm_msg': `ERROR in Nova YouTube™\n\nCrash plugin: "${plugin.title || plugin.id}"\nPlease report the bug or disable the plugin\n\nSend the bug report 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 identifyCurrentPage(page = 'home', channel_tab) {
switch (page) {
case '': page = 'home'; break;
case 'live_chat':
case 'live_chat_replay':
page = 'live_chat'; break;
case 'channel':
case 'c':
case 'user':
page = 'channel';
break;
case 'watch':
case 'clip':
page = 'watch';
break;
default:
if (page?.startsWith('@')
|| /[A-Z\d_]/.test(page)
) {
page = 'channel';
}
break;
}
switch (channel_tab) {
case 'featured':
case 'videos':
case 'shorts':
case 'streams':
case 'podcasts':
case 'releases':
case 'playlists':
case 'community':
case 'channels':
case 'about':
case 'search':
page = 'channel';
channel_tab = channel_tab;
break;
default:
if (channel_tab?.startsWith('UC')) page = 'channel';
channel_tab = false;
break;
}
return {
'page': page,
'channelTab': channel_tab,
};
}
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 /• %s •/', '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.self !== window.top)) {
return console.warn(GM_info.script.name + ': processed in the iframe disable');
}
registerMenuCommand();
if (location.hostname === new URL(configPage).hostname) setupConfigPage();
else {
if ((window.self !== window.top)
&& (!location.pathname.startsWith('/embed') && !location.pathname.startsWith('/live_chat'))
) {
return console.warn('iframe skiped:', location.pathname);
}
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?')) window.open(configPage, '_blank');
user_settings['report_issues'] = 'on';
GM_setValue(configStoreName, user_settings);
}
else {
appLander();
const exportedSettings = Object.assign({}, user_settings);
delete exportedSettings['user-api-key'];
delete exportedSettings['sponsor_block'];
delete exportedSettings['sponsor_block_category'];
delete exportedSettings['sponsor_block_url'];
delete exportedSettings['thumbs_filter_title_blocklist'];
delete exportedSettings['search_filter_channels_blocklist'];
delete exportedSettings['thumbs_hide_live_channels_exception'];
delete exportedSettings['comments_sort_blocklist'];
delete exportedSettings['download_video_mode'];
delete exportedSettings['video_unblock_region_domain'];
unsafeWindow.window.nova_settings = exportedSettings;
}
}
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': delete obj[key]; break;
default: obj[key] = value;
}
};
}
console.debug(`update ${configStoreName}:`, obj);
GM_setValue(configStoreName, obj);
}, { capture: true });
window.addEventListener('DOMContentLoaded', () => {
localizePage(user_settings?.lang_code);
storeData = user_settings;
unsafeWindow.window.nova_plugins = window.nova_plugins;
});
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) + '&labels=bug&template=bug_report.md';
});
});
}
function appLander() {
if (document.readyState == 'loading') {
document.addEventListener('DOMContentLoaded', appRun);
}
else {
appRun();
}
let prevURL = document.URL;
const isURLChanged = () => prevURL == document.URL ? false : prevURL = document.URL;
if (isMobile = (location.host == 'm.youtube.com')) {
window.addEventListener('transitionend', ({ target }) => target.id == 'progress' && isURLChanged() && appRun());
}
else {
document.addEventListener('yt-navigate-start', () => isURLChanged() && appRun());
document.addEventListener('yt-action', reloadAfterMiniplayer);
function reloadAfterMiniplayer(evt) {
if (location.pathname == '/watch'
&& (evt.detail?.actionName == 'yt-cache-miniplayer-page-action')
&& isURLChanged()
) {
document.removeEventListener('yt-action', reloadAfterMiniplayer);
appRun();
}
}
}
function appRun() {
console.groupCollapsed('plugins status');
Plugins.run({
'user_settings': user_settings,
'app_ver': GM_info.script.version,
});
}
}
function registerMenuCommand() {
GM_registerMenuCommand('Settings', () => window.open(configPage, '_blank'));
GM_registerMenuCommand('Import settings', () => {
if (json = JSON.parse(prompt('Enter json file context'))) {
saveImportSettings(json);
}
else if (confirm('Import via file?')) {
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 {
saveImportSettings(JSON.parse(rdr.result));
}
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();
}
function saveImportSettings(json) {
GM_setValue(configStoreName, json);
renameStorageKeys({
'disable_in_frame': 'exclude_iframe',
'custom-api-key': 'user-api-key',
'shorts-disable': 'thumbs_hide_shorts',
'shorts_disable': 'thumbs_hide_shorts',
'premiere-disable': 'thumbs_hide_premieres',
'premieres-disable': 'thumbs_hide_premieres',
'premieres_disable': 'thumbs_hide_premieres',
'thumbs_min_duration': 'thumbs_hide_min_duration',
'shorts_disable_min_duration': 'thumbs_hide_min_duration',
'streams-disable': 'thumbs_hide_live',
'streams_disable': 'thumbs_hide_live',
'live_disable': 'thumbs_hide_live',
'thumbnails-mix-hide': 'thumbs_hide_mix',
'thumb_mix_disable': 'thumbs_hide_mix',
'mix_disable': 'thumbs_hide_mix',
'player_fullscreen_mode_exit': 'player_fullscreen_mode_onpause',
'subtitle-transparent': 'subtitle_transparent',
'video-description-expand': 'description-expand',
'video_quality_in_music': 'video_quality_in_music_playlist',
'player_float_progress_bar_color': 'player_progress_bar_color',
'header-short': 'header-compact',
'player-buttons-custom': 'player-quick-buttons',
'shorts_thumbnails_time': 'shorts-thumbnails-time',
'comments-sidebar-position-exchange': 'move-in-sidebar',
'comments_sidebar_position_exchange_target': 'move_in_sidebar_target',
'streamed_disable_channel_exception': 'thumbs_hide_live_channels_exception',
'streamed_disable_channels_exception': 'thumbs_hide_live_channels_exception',
'video_quality_in_music_quality': 'video_quality_for_music',
'volume_normalization': 'volume_loudness_normalization',
'button_no_labels_opacity': 'details_buttons_opacity',
'details_button_no_labels_opacity': 'details_buttons_opacity',
'button-no-labels': 'details_buttons_label_hide',
'details_button_no_labels': 'details_buttons_label_hide',
'volume-wheel': 'video-volume',
'rate-wheel': 'video-rate',
'video-stop-preload': 'video-autostop',
'stop_preload_ignore_playlist': 'video_autostop_ignore_playlist',
'stop_preload_ignore_live': 'video_autostop_ignore_live',
'stop_preload_embed': 'video_autostop_embed',
'disable-video-cards': 'pages-clear',
'volume_level_default': 'volume_default',
'thumb_filter_title_blocklist': 'thumbs_filter_title_blocklist',
'search_filter_channel_blocklist': 'search_filter_channels_blocklist',
'streamed_disable': 'thumbs_hide_streamed',
'watched_disable': 'thumbs_hide_watched',
'watched_disable_percent_complete': 'thumbs_hide_watched_percent_complete',
'sidebar-channel-links-patch': 'sidebar-thumbs-channel-link-patch',
'move-in-sidebar': 'move-to-sidebar',
'move_in_sidebar_target': 'move_to_sidebar_target',
'skip_into_step': 'skip_into_sec',
'miniplayer-disable': 'default-miniplayer-disable',
'thumbnails_title_normalize_show_full': 'thumbs_title_show_full',
'thumbnails_title_normalize_smart_max_words': 'thumbs_title_normalize_smart_max_words',
'thumbnails_title_clear_emoji': 'thumbs_title_clear_emoji',
'thumbnails_title_clear_symbols': 'thumbs_title_clear_symbols',
'thumbnails-clear': 'thumbs-clear',
'thumbnails_clear_preview_timestamp': 'thumbs_clear_preview_timestamp',
'thumbnails_clear_overlay': 'thumbs_clear_overlay',
'thumbnails-grid-count': 'thumbs-grid-count',
'thumbnails_grid_count': 'thumbs_grid_count',
'thumbnails-watched': 'thumbs-watched',
'thumbnails_watched_frame_color': 'thumbs_watched_frame_color',
'thumbnails_watched_title': 'thumbs_watched_title',
'thumbnails_watched_title_color': 'thumbs_watched_title_color',
'details-buttons': 'details-buttons-visibility',
'comments_sort_words_blocklist': 'comments_sort_blocklist',
'thumbnails-title-normalize': 'thumbs-title-normalize',
'time_remaining_mode': 'time_remaining_format',
});
alert('Settings imported!');
location.reload();
}
});
GM_registerMenuCommand('Export settings', () => {
const d = document.createElement('a');
d.style.display = 'none';
d.download = 'nova_backup.json';
d.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(user_settings));
document.body.append(d);
d.click();
d.remove();
});
}
function renameStorageKeys(key_template_obj = required()) {
let needSave;
for (const oldKey in user_settings) {
if (newKey = key_template_obj[oldKey]) {
console.log(oldKey, '=>', newKey);
needSave = true;
delete Object.assign(user_settings, { [newKey]: user_settings[oldKey] })[oldKey];
}
if (needSave) GM_setValue(configStoreName, user_settings);
}
}
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;
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', null, { capture: true });
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);
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: .6ch;
background-color: #616161;
box-shadow: 0 1em 2em -0.5em rgb(0 0 0 / 35%);
color: white;
z-index: 1000;
}
#${SETTING_BTN_ID} {
position: relative;
opacity: .3;
transition: opacity 300ms 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: 600ms;
stop-color: #7a7cbd;
}
#${SETTING_BTN_ID}:hover .nova-gradient-start {
stop-color: #0ff;
}
#${SETTING_BTN_ID}:hover .nova-gradient-stop {
stop-color: #0095ff;
}`);
menu.prepend(a);
});
}
function _pluginsCaptureException({ trace_name, err_stack, confirm_msg, app_ver }) {
if (confirm(confirm_msg || `Error in ${GM_info.script.name}. Send the bug raport to developer?`)) {
openBugReport();
}
function openBugReport() {
window.open(
'https://docs.google.com/forms/u/0/d/e/1FAIpQLScfpAvLoqWlD5fO3g-fRmj4aCeJP9ZkdzarWB8ge8oLpE5Cpg/viewform' +
'?entry.35504208=' + encodeURIComponent(trace_name) +
'&entry.151125768=' + encodeURIComponent(err_stack) +
'&entry.744404568=' + encodeURIComponent(document.URL) +
'&entry.1416921320=' + encodeURIComponent(app_ver + ' | ' + navigator.userAgent + ' [' + window.navigator.language + ']')
, '_blank');
}
}
user_settings.report_issues && window.addEventListener('unhandledrejection', err => {
if ((err.reason?.stack || err.stack)?.includes('Nova')
&& !((err.reason?.stack || err.stack)?.includes('movie_player is not defined'))
) {
console.error('[ERROR PROMISE]\n', err.reason, '\nPlease report the bug: https://github.com/raingart/Nova-YouTube-extension/issues/new?body=' + encodeURIComponent(GM_info.script.version + ' | ' + navigator.userAgent)) + '&labels=bug&template=bug_report.md&title=unhandledrejection';
_pluginsCaptureException({
'trace_name': 'unhandledRejection',
'err_stack': err.reason.stack || err.stack,
'app_ver': GM_info.script.version,
'confirm_msg': `Failure when async-call of one "${GM_info.script.name}" plugin.\nDetails in the console\n\nOpen tab to report the bug?`,
});
}
});