// ==UserScript==
// @name Youtube Speed By Channel
// @namespace Alpe
// @version 0.1.4
// @description Allow to choose the default speed for specific YT channel
// @author Alpe
// @include https://www.youtube.com/*
// @grant GM.setValue
// @grant GM.getValue
// @require https://code.jquery.com/jquery-3.4.1.min.js
// ==/UserScript==
const COLOR_SELECTED = "red",
COLOR_NORMAL = "#dcdcdc",
DEFAULT_SPEED = 1.0;
const BUTTON_TEMPLATES = [
["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]
];
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(el){
let id = el.target.parentNode.id.match(/\d+$/)[0];
el = $(el.target);
el.parent().children(":not([title])").css("color", COLOR_NORMAL);
el.css("color", COLOR_SELECTED);
$('video[vsb-video=' + id + ']')[0].playbackRate = parseFloat(el.attr('speed'));
}
function setchanneldefault(el){
let id = el.target.parentNode.id.match(/\d+$/)[0];
let channel = $('#channel-name[vsb-channel=' + id + ']').find('#container #text').text().trim();
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.innerHTML += '<span style="margin-right: 10px; font-weight: bold; font-size: 80%; 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; font-weight: bold; font-size: 80%; color: ' + (curspeed === button[1] ? 'red' : 'rgb(220, 220, 220)') + '; 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 ob_youtube_movieplayer (mutationsList, observer){
for(let mutation of mutationsList) {
if (mutation.attributeName === 'video-id'){
let 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", "");
setTimeout(async function(){
let channelspeed = await GM.getValue(channeldiv.find('#container #text').text().trim());
$('span[speed="' + channelspeed + '"]', el).css("text-decoration", "underline")
let speed = getspeed({
channelspeed: channelspeed,
defspeed: (channeldiv.find('.badge-style-type-verified-artist').length === 1 ? 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;
},2000);
}
}
}
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({
channelspeed: await GM.getValue(channeldiv.find('#container #text').text().trim()),
defspeed: (channeldiv.find('.badge-style-type-verified-artist').length === 1 ? 1 : null)
});;
},2000);
}
}
}
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"){
let channeldiv = el.closest('ytd-watch-flexy').find('#upload-info #channel-name');
if (!channeldiv.length) return;
let channelname = channeldiv.find('#container #text').text().trim();
if (channelname === '') return;
let appendto = el.find("div.ytp-right-controls");
if (!appendto.length) return;
let videodiv = el.find('video')
if (!videodiv.length) return;
el.attr('monitored', '');
let id = getid();
channeldiv.attr('vsb-channel', id);
videodiv.attr('vsb-video', id);
$('#ytp-id-20 .ytp-menuitem-label:contains(Playback speed)', el).parent().css('display', 'none');
console.log("Adding video-id observer");
(new MutationObserver(ob_youtube_movieplayer)).observe(el.closest('ytd-watch-flexy')[0], { attributes: true });
channelspeed = await GM.getValue(channelname);
speed = getspeed({
channelspeed: channelspeed,
defspeed: (channeldiv.find('.badge-style-type-verified-artist').length === 1 ? 1 : null),
force1x: (el.find('.ytp-live').length === 1)
});
let div = createcontainer(speed, id);
$('span[speed="' + channelspeed + '"]', div).css("text-decoration", "underline");
appendto.append(div);
videodiv[0].playbackRate = speed;
} else if (this.id === "c4-player"){
let channeldiv = el.closest('ytd-browse').find('#header #channel-name');
if (!channeldiv.length) return;
let channelname = channeldiv.find('#container #text').text().trim();
if (channelname === '') return;
let videodiv = el.find('video')
if (!videodiv.length) return;
el.attr('monitored', '');
let id = getid();
channeldiv.attr('vsb-channel', id);
videodiv.attr('vsb-video', id);
console.log("Adding c4 observer");
(new MutationObserver(ob_youtube_c4player)).observe(el.find('video')[0], { attributes: true, subtree: true });
videodiv[0].playbackRate = getspeed({
channelspeed: await GM.getValue(channelname),
defspeed: (channeldiv.find('.badge-style-type-verified-artist').length === 1 ? 1 : null)
});
}
});
}
function mark_loop(){
if (location.host.endsWith('youtube.com')){
youtube();
if (window.vsbid < 2){
setTimeout(mark_loop, ((window.vsbid === 0 ? 500 : 2500) * (vis() ? 1 : 2)));
} else {
console.log('stopping loop');
}
} else {
setTimeout(mark_loop, 1500 * (vis() ? 1 : 4));
}
}
window.addEventListener('load', mark_loop);