Allow to choose the default speed for specific YT channel
// ==UserScript== // @name Youtube Speed By Channel // @namespace Alpe // @version 0.2.15 // @description Allow to choose the default speed for specific YT channel // @author Alpe // @include https://www.youtube.com/* // @grant GM.setValue // @grant GM.getValue // @grant GM.deleteValue // @grant GM_registerMenuCommand // @grant GM_getResourceText // @run-at document-start // @resource jquery https://code.jquery.com/jquery-3.7.1.min.js // ==/UserScript== (async () => { if (window.trustedTypes && window.trustedTypes.createPolicy){ try { trustedTypes?.createPolicy?.('default', {createScriptURL: s => s, createScript: s => s, createHTML: s => s}); } catch {} } eval(GM_getResourceText('jquery')); const defaults = { DEFAULT_SPEED: 1.0, SHOW_RELATIVE_TIME: true, COLOR_SELECTED: "red", COLOR_NORMAL: "rgb(220,220,220)", BUTTON_TEMPLATES: JSON.stringify([ ["50%", 0.5], ["75%", 0.75], ["Normal", 1], ["1.25x", 1.25], ["1.5x", 1.5], ["1.75x", 1.75], ["2x", 2], ["2.25x", 2.25], ["2.5x", 2.5], ["3x", 3], ["3.5x", 3.5] ]), AUDIO_BOOST: 1, SAVE_R###ME_TIME: false, SHOW_ON_PLAYER: false } for (let name in defaults) { let value = defaults[name]; window[name] = (name === "BUTTON_TEMPLATES" ? JSON.parse(await GM.getValue("_YSC-" + name,value)) : await GM.getValue("_YSC-" + name,value)); } async function toggleconfig(name,e){ e = e||!(await GM.getValue("_YSC-" + name,defaults[name])); GM.setValue("_YSC-" + name,e); alert(name + ': ' + e); } if (typeof GM_registerMenuCommand == 'undefined') { this.GM_registerMenuCommand = (caption, commandFunc, accessKey) => { if (!document.body) { if (document.readyState === 'loading' && document.documentElement && document.documentElement.localName === 'html') { new MutationObserver((mutations, observer) => { if (document.body) { observer.disconnect(); GM_registerMenuCommand(caption, commandFunc, accessKey); } }).observe(document.documentElement, {childList: true}); } else { console.error('GM_registerMenuCommand got no body.'); } return; } let contextMenu = document.body.getAttribute('contextmenu'); let menu = (contextMenu ? document.querySelector('menu#' + contextMenu) : null); if (!menu) { menu = document.createElement('menu'); menu.setAttribute('id', 'gm-registered-menu'); menu.setAttribute('type', 'context'); document.body.appendChild(menu); document.body.setAttribute('contextmenu', 'gm-registered-menu'); } let menuItem = document.createElement('menuitem'); menuItem.textContent = caption; menuItem.addEventListener('click', commandFunc, true); menu.appendChild(menuItem); }; } $.each([ ["List current settings", async function(){ var set = []; for (let name in defaults) { let value = defaults[name]; set.push(name + ' = ' + await GM.getValue('_YSC-' + name,value) + (( await GM.getValue('_YSC-' + name,value)!=defaults[name])?" [default is " + defaults[name] + "]":"")); } alert(set.join('\n')); }], ["Configure default speed", async function(){ var temp = prompt("Default: " + defaults['DEFAULT_SPEED'], await GM.getValue('_YSC-DEFAULT_SPEED',DEFAULT_SPEED)); if (temp === null) return; if (temp.length === 0){ GM.deleteValue('_YSC-DEFAULT_SPEED'); alert("default restored"); return; } temp = parseFloat(temp); if (!isNaN(temp)) toggleconfig('DEFAULT_SPEED',temp); }], ["Show time relative to speed", async function(){ var temp = prompt("Default: " + defaults['SHOW_RELATIVE_TIME'], await GM.getValue('_YSC-SHOW_RELATIVE_TIME',SHOW_RELATIVE_TIME)); if (temp === null) return; if (temp.length === 0){ GM.deleteValue('_YSC-SHOW_RELATIVE_TIME'); alert("default restored"); return; } if (temp === "true" || temp === "false") toggleconfig('SHOW_RELATIVE_TIME', (temp === "true")); }], ["Configure Color for the selected speed", async function(){ var temp = prompt("Default: " + defaults['COLOR_SELECTED'], await GM.getValue('_YSC-COLOR_SELECTED',COLOR_SELECTED)); if (temp === null) return; if (temp.length === 0){ GM.deleteValue('_YSC-COLOR_SELECTED'); alert("default restored"); return; } toggleconfig('COLOR_SELECTED',temp); }], ["Configure color for unselected speed", async function(){ var temp = prompt("Default: " + defaults['COLOR_NORMAL'], await GM.getValue('_YSC-COLOR_NORMAL',COLOR_NORMAL)); if (temp === null) return; if (temp.length === 0){ GM.deleteValue('_YSC-COLOR_NORMAL'); alert("default restored"); return; } toggleconfig('COLOR_NORMAL',temp); }], ["Configure Buttons", async function(){ var temp = prompt("What buttons should be displayed.\nformat: [text,speed]\neg: [half,0.5][normal,1][double,2]", '[' + JSON.parse(await GM.getValue('_YSC-BUTTON_TEMPLATES',JSON.stringify(BUTTON_TEMPLATES))).join('],[') + ']'); if (temp === null) return; if (temp.length === 0){ GM.deleteValue('_YSC-BUTTON_TEMPLATES'); alert("default restored"); return; } var match = temp.match(/\[[^,]+,[^\]]+\]/g); if (!match){ alert("invalid option"); } else { var array = []; for (let i=0; i < match.length; i++){ let match2 = match[i].match(/\[([^,]+),([^\]]+)/); array.push([match2[1], parseFloat(match2[2])]) } toggleconfig('BUTTON_TEMPLATES',JSON.stringify(array)); } }], ["Configure audio boost", async function(){ var temp = prompt("Can be any number bigger than 1.\n\n1 = function disabled\n1.5 = 50% boost\n2 = 100% boost\n\n\nDefault: " + defaults['AUDIO_BOOST'], await GM.getValue('_YSC-AUDIO_BOOST',AUDIO_BOOST)); if (temp === null) return; if (temp.length === 0){ GM.deleteValue('_YSC-AUDIO_BOOST'); alert("default restored"); return; } temp = parseFloat(temp); if (!isNaN(temp) && temp >= 1){ toggleconfig('AUDIO_BOOST',temp); window['AUDIO_BOOST'] = temp; $(temp === 1 ? 'video[vsb-audioboost]' : 'video').each(function(){ audioboost(this, true); }); } }], ["Save r###me time to url", async function(){ var temp = prompt("Can be true or false\nIf true, it updates the url every 5 seconds with &t= parameter. So if you close the browser or tab and reopen it, the video will start playing close to where you stopped it.\n\nDefault: " + defaults['SAVE_R###ME_TIME'], await GM.getValue('_YSC-SAVE_R###ME_TIME',SAVE_R###ME_TIME)); if (temp === null) return; if (temp.length === 0){ GM.deleteValue('_YSC-SAVE_R###ME_TIME'); alert("default restored"); return; } if (temp === "true" || temp === "false") toggleconfig('SAVE_R###ME_TIME', (temp === "true")); }], ["Show speed controls on top of the player instead of below the progress bar", async function(){ var temp = prompt("Can be true or false\n\nDefault: " + defaults['SHOW_ON_PLAYER'], await GM.getValue('_YSC-SHOW_ON_PLAYER',SHOW_ON_PLAYER)); if (temp === null) return; if (temp.length === 0){ GM.deleteValue('_YSC-SHOW_ON_PLAYER'); alert("default restored"); return; } if (temp === "true" || temp === "false") toggleconfig('SHOW_ON_PLAYER', (temp === "true")); }], ["Restore default",function(){ for (let name in defaults) { GM.deleteValue('_YSC-' + name); } alert("Default restored"); }] ], function(a,b){ GM_registerMenuCommand(b[0],b[1]); }); var stateKey, eventKey; { let keys = { hidden: "visibilitychange", webkitHidden: "webkitvisibilitychange", mozHidden: "mozvisibilitychange", msHidden: "msvisibilitychange" } for (stateKey in keys) { if (stateKey in document) { eventKey = keys[stateKey]; break; } } } function vis (c) { if (c) document.addEventListener(eventKey, c); return !document[stateKey]; } function getspeed(params = {}){ let speed, reason; if (params.hasOwnProperty('force1x') && params.force1x){ speed = 1; reason = "forcing 1x (live?)"; } else if (params.hasOwnProperty('channelspeed') && typeof params.channelspeed === "number"){ speed = params.channelspeed; reason = "channelspeed"; } else if (params.hasOwnProperty('defspeed') && Number.isInteger(params.defspeed)){ speed = params.defspeed; reason = "overwritten default (music?)"; } else { speed = DEFAULT_SPEED; reason = "default"; } if (params.channelspeed === undefined) delete params.channelspeed; if (params.defspeed === null) delete params.defspeed; if (params.force1x === false) delete params.force1x; params['chosenspeed'] = speed; params['chosenreason'] = reason; console.log(params); return speed; } function buttonclick(evt){ let id = evt.target.parentNode.id.match(/\d+$/)[0], el = $(evt.target); el.parent().children(":not([title])").css("color", COLOR_NORMAL); el.css("color", COLOR_SELECTED); if ($('video[vsb-video="' + id + '"]').length === 0){ youtubefix(); } try { let video = $('video[vsb-video=' + id + ']')[0]; video.playbackRate = parseFloat(el.attr('speed')); if (SHOW_RELATIVE_TIME || SAVE_R###ME_TIME) changetime(video); } catch (err){ console.log('error on buttonclick()', evt, err); setTimeout(function(){ buttonclick(evt); }, 1000); } } function getchannelname (id, div = null){ try { if (div === null) div = $('#channel-name[vsb-channel=' + id + ']'); let channel = div.find('#container #text:visible:first').text().trim(); if (!channel){ channel = div.find('.ytd-channel-name').find('a').text().trim(); } if (!channel){ let metavid = document.querySelector('#watch7-content > meta[itemprop=videoId]'); if (metavid !== null && (new URL(div.closest('ytd-watch-flexy, ytd-browse').find('video')[0].baseURI)).searchParams.get('v') === metavid.getAttribute('content')){ let metaname = document.querySelector('#watch7-content > span[itemprop=author] > link[itemprop=name]'); if (metaname !== null) channel = metaname.getAttribute('content'); if (!channel) channel = ''; } } return channel; } catch (e) { console.log("error", e); return ''; } } function setchanneldefault(el){ let id = el.target.parentNode.id.match(/\d+$/)[0]; let channel = getchannelname(id); changebuttontitle(id, channel); let currentspeed = $('video[vsb-video=' + id + ']')[0].playbackRate; el = $(el.target).parent(); el.children().css("text-decoration", "").filter('span[speed="' + currentspeed + '"]').css("text-decoration", "underline"); GM.setValue(channel, currentspeed); console.log('changing default for (' + channel + ') to (' + currentspeed + ')'); } function createcontainer(curspeed, id){ let div = document.createElement("div"); let prev_node = null; div.id = "vsb-container" + id; div.style.marginBottom = "0px"; div.style.paddingBottom = "0px"; div.style.float = "left"; div.style.fontWeight = "bold"; div.style.fontSize = "80%"; div.innerHTML += '<span style="margin-right: 10px; color: white; cursor: pointer;" title="Set current speed as default for this channel">setdefault</span>'; BUTTON_TEMPLATES.forEach(function(button){ div.innerHTML += '<span style="margin-right: 10px; color: ' + (curspeed === button[1] ? COLOR_SELECTED : COLOR_NORMAL) + '; cursor: pointer;" speed="' + button[1] + '">' + button[0] + '</span>'; }); $('span:not([title])', div).on( "click", buttonclick); $('span[title]', div).on( "click", setchanneldefault); return div; } window.vsbid = 0; function getid(){ let id = window.vsbid; window.vsbid++; return id; } function changebuttontitle(id, channelname = ''){ let container = $('#vsb-container' + id + ' > span[title]'); if (container.length > 0){ container[0].title = container[0].title.split(' [')[0] + (channelname !== '' ? ' [' + channelname + ']' : ''); } } function ob_youtube_movieplayer (mutationsList, observer){ for(let mutation of mutationsList) { if (mutation.attributeName === 'video-id'){ let el = $('[id^=vsb-container]', mutation.target); if (el.length === 0){ alert('fixing'); console.log('fixing this'); youtube(); el = $('[id^=vsb-container]', mutation.target); } let id = el[0].id.match(/\d+$/)[0]; let channeldiv = $('#channel-name[vsb-channel="' + id + '"]'); if($('span[speed="1"]', el).click().length === 0) $('video[vsb-video="' + id + '"]')[0].playbackRate = 1; $('span', el).css("text-decoration", ""); changebuttontitle(id); setTimeout(async function(){ let channelspeed, channelname = getchannelname(id, channeldiv); let tries = 1; while(channelname === '' && tries <= 8){ if (tries === 1){ alert('sleeping'); console.log("id", id); console.log("channeldiv", channeldiv); console.log("channelname", channelname); } console.log('sleeping ' + tries, channeldiv); await (new Promise(resolve => setTimeout(resolve, 200))); channelname = getchannelname(id, channeldiv); tries++; } if (channelname !== ''){ channelspeed = await GM.getValue(channelname); } else { channelspeed = undefined; } changebuttontitle(id, channelname); $('span[speed="' + channelspeed + '"]', el).css("text-decoration", "underline"); let speed = getspeed({ channelname: channelname, channelspeed: channelspeed, defspeed: (channeldiv.find('.badge-style-type-verified-artist').length > 0 || channelname.match(/VEVO$/) ? 1 : null), force1x: (el.closest('#movie_player').find('.ytp-live').length === 1) }); if($('span[speed="' + speed + '"]', el).click().length === 0) $('video[vsb-video="' + id + '"]')[0].playbackRate = speed; },500); } } } function ob_youtube_c4player (mutationsList, observer){ for(let mutation of mutationsList) { if (mutation.attributeName === 'src' && mutation.target.src !== ''){ let id = mutation.target.getAttribute("vsb-video"); let channeldiv = $('#channel-name[vsb-channel="' + id + '"]'); $('video[vsb-video="' + id + '"]')[0].playbackRate = 1; setTimeout(async function(){ $('video[vsb-video="' + id + '"]')[0].playbackRate = getspeed({ channelname: getchannelname(id, channeldiv), channelspeed: await GM.getValue(getchannelname(id, channeldiv)), defspeed: (channeldiv.find('.badge-style-type-verified-artist').length === 1 ? 1 : null) }); },1000); } } } function youtubefix(){ $('#movie_player[monitored], #c4-player[monitored]').each( function(){ let video = $('video', this); if (video.attr('vsb-video') === undefined){ let el = $(this); let id = el.attr('monitored'); console.log('fixing', video); setTimeout(function(){ audioboost(video, true); video.attr('vsb-video', id); youtubefix2('#vsb-container' + id); if (SHOW_RELATIVE_TIME || SAVE_R###ME_TIME) ['timeupdate','seeked', 'pause'].forEach( function(evt) { video[0].addEventListener(evt, changetime,false) }); },750); } else { console.log('fixing2', video); youtubefix2('#vsb-container' + $(this).attr('monitored')); } } ); } function youtubefix2(el, log = false){ try { $('span:not([title]):visible', el).filter(function() { return ( this.style.color == COLOR_SELECTED ); }).click(); if (log) console.log('fixing3', video); } catch { if (log) console.log('fixing3 failed', video); } } function fancyTimeFormat(duration){ var hrs = ~~(duration / 3600); var mins = ~~((duration % 3600) / 60); var secs = ~~duration % 60; var ret = ""; if (hrs > 0) { ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); } ret += "" + mins + ":" + (secs < 10 ? "0" : ""); ret += "" + secs; return ret; } function changetime (event){ let video = (typeof event.target === "object" ? event.target : event); if (SHOW_RELATIVE_TIME){ let id = video.getAttribute('vsb-video'); let timediv = $('#movie_player[monitored="' + id + '"]:visible .ytp-time-display:visible'); if (timediv.length === 0) return; let reltimespan = timediv[0].getElementsByClassName('vsb-reltime'); if (reltimespan.length === 0){ timediv[0].insertAdjacentHTML('beforeend', '<span class="vsb-reltime"></span>'); reltimespan = timediv[0].getElementsByClassName('vsb-reltime'); } reltimespan[0].innerHTML = (video.playbackRate === 1 || isNaN(video.duration) ? '' : '<span> (</span>' + fancyTimeFormat(video.currentTime / video.playbackRate) + ' / ' + fancyTimeFormat(video.duration / video.playbackRate) + '<span>)</span>'); } if (SAVE_R###ME_TIME){ const time = Math.floor(video.currentTime), url = new URL(location); if (url.pathname === "/watch" && time >= 10){ if (typeof event.target !== "object" || event.type !== "timeupdate" || Number.isInteger(time/5)){ url.searchParams.set('t', (time - 5) + 's'); history.replaceState({}, document.title, url.toString()) } } else if (url.searchParams.has('t')){ url.searchParams.delete('t'); history.replaceState({}, document.title, url.toString()) } } } function audioboost(el = null, force = false){ if (el === null || typeof el !== 'object') el = this; if (el.tagName !== "VIDEO") return; if (el.getAttribute('vsb-audioboost') === null){ el.setAttribute('vsb-audioboost', getid()); } else if (!force){ return; } const audioCtx = "YTSBC_audioCtx_" + el.getAttribute('vsb-audioboost'); try { window[audioCtx].close(); } catch {} if (AUDIO_BOOST === 1 && !force) return; window[audioCtx] = new AudioContext(); const source = window[audioCtx].createMediaElementSource(el), gainNode = window[audioCtx].createGain(); gainNode.gain.value = AUDIO_BOOST; source.connect(gainNode); if (AUDIO_BOOST > 1){ const limiterNode = window[audioCtx].createDynamicsCompressor(); limiterNode.threshold.value = -5.0; limiterNode.knee.value = 0; limiterNode.ratio.value = 40.0; limiterNode.attack.value = 0.001; limiterNode.release.value = 0.1; limiterNode.connect(window[audioCtx].destination); gainNode.connect(limiterNode); } else { gainNode.connect(window[audioCtx].destination); } } function observevideo(el){ const observer = new MutationObserver((mutationsList, observer) => { for(const mutation of mutationsList) { if (mutation.target.src === ''){ youtubefix(); } else { const id = mutation.target.getAttribute('vsb-video'); youtubefix2('#vsb-container' + id); } } }); observer.observe(el, {attributes: true, attributeFilter: ['src']}); } function youtube(){ $('#movie_player:visible:not([monitored]), #c4-player:visible:not([monitored])').each(async function( index ) { let el = $(this); let speed, channelspeed; if (this.id === "movie_player" && !this.classList.contains('ytp-player-minimized')){ let channeldiv = el.closest('ytd-watch-flexy').find('#upload-info #channel-name'); if (!channeldiv.length) channeldiv = $('ytd-watch-metadata #upload-info #channel-name'); if (!channeldiv.length) return; let channelname = getchannelname(-1, channeldiv); if (channelname === '') return; let appendto = (SHOW_ON_PLAYER ? el.find("div.ytp-iv-video-content") : el.find("div.ytp-right-controls")); if (!appendto.length) return; let videodiv = el.find('video') if (!videodiv.length) return; let id = getid(); el.attr('monitored', id); channeldiv.attr('vsb-channel', id); videodiv.attr('vsb-video', id); videodiv.each(audioboost); $('#ytp-id-20 .ytp-menuitem-label:contains(Playback speed)', el).parent().css('display', 'none'); console.log("Adding video-id observer"); let el2 = el.closest('ytd-watch-flexy'); if (!el2.length) el2 = $('ytd-watch-flexy:visible'); (new MutationObserver(ob_youtube_movieplayer)).observe(el2[0], { attributes: true }); el2 = el.find('video'); if (!el2.length) el2 = $('video:visible'); observevideo(el2[0]); channelspeed = await GM.getValue(channelname); speed = getspeed({ channelname: channelname, channelspeed: channelspeed, defspeed: (channeldiv.find('.badge-style-type-verified-artist').length > 0 || channelname.match(/VEVO$/) ? 1 : null), force1x: (el.find('.ytp-live').length === 1) }); let div = createcontainer(speed, id); $('span[speed="' + channelspeed + '"]', div).css("text-decoration", "underline"); if (SHOW_ON_PLAYER){ div.style.position = "absolute"; div.style.zIndex = 10; div.style.textShadow = "-1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000"; } appendto.append(div); changebuttontitle(id, channelname); videodiv[0].playbackRate = speed; if (SHOW_RELATIVE_TIME || SAVE_R###ME_TIME) ['timeupdate','seeked', 'pause'].forEach( function(evt) { videodiv[0].addEventListener(evt, changetime,false) }); } else if (this.id === "c4-player"){ let channeldiv = el.closest('ytd-browse').find('#header #channel-name'); if (!channeldiv.length) return; let channelname = getchannelname(-1, channeldiv); if (channelname === '') return; let videodiv = el.find('video') if (!videodiv.length) return; el.attr('monitored', id); let id = getid(); channeldiv.attr('vsb-channel', id); videodiv.attr('vsb-video', id); videodiv.each(audioboost); console.log("Adding c4 observer"); (new MutationObserver(ob_youtube_c4player)).observe(el.find('video')[0], { attributes: true, subtree: true }); videodiv[0].playbackRate = getspeed({ channelname: channelname, channelspeed: await GM.getValue(channelname), defspeed: (channeldiv.find('.badge-style-type-verified-artist').length === 1 ? 1 : null) }); } }); if (AUDIO_BOOST !== 1){ $('video:not([vsb-audioboost])').each(audioboost); } } function mark_loop(){ if (location.host.endsWith('youtube.com')){ youtube(); let test = document.querySelectorAll('[monitored]'); if (test.length < 2){ setTimeout(mark_loop, ((test.length === 0 || test[0].id !== "movie_player" ? 250 : 2000) * (vis() ? 1 : 2))); } else { console.log('stopping loop'); } } else { setTimeout(mark_loop, 1500 * (vis() ? 1 : 4)); } } if (AUDIO_BOOST !== 1){ mark_loop(); } else { window.addEventListener('load', mark_loop); } })();