// ==UserScript==
// @name YouTube Playlist Duration Sort
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Add duration sorting options to YouTube playlists.
// @author Surf Archer
// @icon https://www.youtube.com/favicon.ico
// @match https://www.youtube.com/playlist?*
// @grant unsafeWindow
// ==/UserScript==
(function() {
'use strict';
window.addEventListener("load", () => {
addSortMenuItem("Duration (shortest)", "durationSortClickShortest");
addSortMenuItem("Duration (longest)", "durationSortClickLongest");
injectJS();
});
function addSortMenuItem(text, handler) {
var sfm = document.querySelector("#sort-filter-menu");
var submenu = sfm.querySelector("#menu");
var d1=document.createElement("DIV");
d1.setAttribute("class", "item style-scope yt-dropdown-menu");
d1.textContent=text;
var d2=document.createElement("DIV");
d2.setAttribute("secondary", "");
d2.setAttribute("id", "subtitle");
d2.setAttribute("class", "style-scope yt-dropdown-menu");
d2.setAttribute("hidden", "");
var pib=document.createElement("paper-item-body");
pib.setAttribute("class", "style-scope yt-dropdown-menu");
pib.appendChild(d1);
pib.appendChild(d2);
var pi=document.createElement("paper-item");
pi.setAttribute("class", "style-scope yt-dropdown-menu");
pi.setAttribute("role", "option");
pi.tabindex="0";
pi.appendChild(pib);
var e=document.createElement("A");
e.setAttribute("class", "yt-simple-endpoint style-scope yt-dropdown-menu");
e.setAttribute("id", handler);
e.setAttribute("tabindex", "-1");
e.appendChild(pi);
submenu.appendChild(e);
}
function injectJS() {
logMsg("Injecting Javascript...");
var script = document.createElement("script");
script.type = "application/javascript";
script.textContent = "(" + injectScript + ")();";
document.body.appendChild(script);
logMsg("Javascript injected!");
}
function injectScript() {
logMsg("Initialising durationSort...");
const debug = false;
var duratonSortRunning = false;
document.getElementById('durationSortClickShortest').onclick = durationSortClickShortest;
document.getElementById('durationSortClickLongest').onclick = durationSortClickLongest;
document.addEventListener('ytplDurationSortEvent', durationSortEvent);
function durationSortClickShortest() {
logMsg('durationSortClickShortest')
setTimeout(function(){
var event = new CustomEvent('ytplDurationSortEvent', {
detail: {
operation: "init", order: 0
}
});
document.dispatchEvent(event);
}, 50);
}
function durationSortClickLongest() {
logMsg('durationSortClickLongest')
setTimeout(function(){
var event = new CustomEvent('ytplDurationSortEvent', {
detail: {
operation: "init", order: 1
}
});
document.dispatchEvent(event);
}, 50);
}
function durationSortEvent(e) {
logDebug("durationSortEvent("+e+")");
switch(e.detail.operation) {
case "init":
// De-click the menu.
setTimeout(function(){
var elemMenu = document.querySelector("#sort-filter-menu > yt-sort-filter-sub-menu-renderer > yt-dropdown-menu > paper-menu-button");
eventFire(elemMenu, 'click')
}, 64);
// Then initiate the sort.
e.detail.operation = "sort";
setTimeout(function(){
var event = new CustomEvent('ytplDurationSortEvent', {
detail: e.detail
});
document.dispatchEvent(event);
}, 128);
break;
case "scroll-to-end":
loadPlaylist(e);
break;
case "sort":
sortPlaylist(e);
break;
}
}
function loadPlaylist(e) {
logMsg(" - loadPlaylist() starting");
var playList = document.querySelector("#contents > ytd-playlist-video-renderer:nth-child(1)").parentNode;
var numChildren = playList.children.length;
logDebug(" - Videos in list: "+numChildren);
if(numChildren > 0) {
logDebug(" - Scrolling to last video...");
document.querySelector("#contents > ytd-playlist-video-renderer:nth-child("+numChildren+")").scrollIntoView(true);
// Then initiate the sort.
setTimeout(function(){
var event = new CustomEvent('ytplDurationSortEvent', {
detail: { operation: "sort", order: e.detail.order }
});
document.dispatchEvent(event);
}, 256);
}
logMsg(" - loadPlaylist() finished");
}
function sortPlaylist(e) {
logDebug(" - sortPlaylist("+JSON.stringify(e.detail)+") starting");
var numInList=document.querySelector("#contents > ytd-playlist-video-renderer:nth-child(1)").parentNode.children.length;
for (var i = 1; numInList > 1 && i < numInList; i++) {
var nInd=i + 1;
var nSecs=parseInt(getRowVal(i, "lengthSeconds"));
var nPrevInd=0;
var nMoveTo=-1;
while(nPrevInd < nInd && nMoveTo == -1) {
var nPrevSecs=parseInt(getRowVal(nPrevInd, "lengthSeconds"));
if(nPrevSecs !== undefined) {
if(e.detail.order == 0 && nPrevSecs > nSecs) {
nMoveTo=nPrevInd;
}
if(e.detail.order == 1 && nPrevSecs < nSecs) {
nMoveTo=nPrevInd;
}
}
nPrevInd++;
}
if(nMoveTo > -1) {
nMoveTo++;
logDebug(" - MOVING row "+nInd+" ("+nSecs+" secs) to row "+nMoveTo+" ("+nPrevSecs+" secs)", true);
moveRowTo(nInd, nMoveTo);
if(i < (numInList - 1)) {
// Then send the next sort.
setTimeout(function(){
var event = new CustomEvent('ytplDurationSortEvent', {
detail: e.detail
});
document.dispatchEvent(event);
}, 512);
return;
}
} else {
logDebug(" - Ignoring row "+nInd+" (duration: "+nSecs+", prevDuration: "+nPrevSecs+")");
}
}
logDebug(" - sortPlaylist("+e.detail.order+") finished");
}
function getRowVal(index, key) {
var row=document.querySelector("#contents > ytd-playlist-video-renderer:nth-child(1)").parentNode.children[index];
if((key in row.data)) {
return row.data[key];
}
}
function moveRowTo(src, dest) {
triggerDragAndDrop("#contents > ytd-playlist-video-renderer:nth-child("+src+")", "#contents > ytd-playlist-video-renderer:nth-child("+dest+")");
}
function fireMouseEvent(type, elem, centerX, centerY) {
var evt = document.createEvent('MouseEvents');
evt.initMouseEvent(type, true, true, window, 1, 1, 1, centerX, centerY, false, false, false, false, 0, elem);
elem.dispatchEvent(evt);
//document.elem.dispatchEvent(evt);
};
function eventFire(el, etype){
if (el.fireEvent) {
el.fireEvent('on' + etype);
} else {
var evObj = document.createEvent('Events');
evObj.initEvent(etype, true, false);
el.dispatchEvent(evObj);
}
};
function triggerDragAndDrop(selectorDrag, selectorDrop) {
// fetch target elements
var elemDrag = document.querySelector(selectorDrag);
var elemDrop = document.querySelector(selectorDrop);
if (!elemDrag || !elemDrop) return false;
// calculate positions
var pos = elemDrag.getBoundingClientRect();
var center1X = Math.floor((pos.left + pos.right) / 2);
var center1Y = Math.floor((pos.top + pos.bottom) / 2);
pos = elemDrop.getBoundingClientRect();
var center2X = Math.floor((pos.left + pos.right) / 2);
var center2Y = Math.floor((pos.top + pos.bottom) / 2);
// mouse over dragged element and mousedown
fireMouseEvent('mousemove', elemDrag, center1X, center1Y);
wait(10);
fireMouseEvent('mouseenter', elemDrag, center1X, center1Y);
wait(10);
fireMouseEvent('mouseover', elemDrag, center1X, center1Y);
wait(10);
fireMouseEvent('mousedown', elemDrag, center1X, center1Y);
wait(30);
// start dragging process over to drop target
fireMouseEvent('dragstart', elemDrag, center1X, center1Y);
wait(10);
fireMouseEvent('drag', elemDrag, center1X, center1Y);
wait(10);
fireMouseEvent('mousemove', elemDrag, center1X, center1Y);
wait(10);
fireMouseEvent('drag', elemDrag, center2X, center2Y);
wait(10);
fireMouseEvent('mousemove', elemDrop, center2X, center2Y);
wait(30);
// trigger dragging process on top of drop target
fireMouseEvent('mouseenter', elemDrop, center2X, center2Y);
wait(10);
fireMouseEvent('dragenter', elemDrop, center2X, center2Y);
wait(10);
fireMouseEvent('mouseover', elemDrop, center2X, center2Y);
wait(10);
fireMouseEvent('dragover', elemDrop, center2X, center2Y);
wait(30);
// release dragged element on top of drop target
fireMouseEvent('drop', elemDrop, center2X, center2Y);
wait(10);
fireMouseEvent('dragend', elemDrag, center2X, center2Y);
wait(10);
fireMouseEvent('mouseup', elemDrag, center2X, center2Y);
wait(150);
return true;
}
function logMsg(msg) {
console.log("[yt-pl-duration-sort] "+msg);
}
function logDebug(msg, force=false) {
if(debug || force) {
console.debug("[yt-pl-duration-sort] "+msg);
}
}
function wait(ms){
var start = new Date().getTime();
var end = start;
while(end < start + ms) {
end = new Date().getTime();
}
}
logMsg("Initialisation finished.");
}
function logMsg(msg) {
console.log("[yt-pl-duration-sort] "+msg);
}
function simulateKey (keyCode, type, modifiers) {
var evtName = (typeof(type) === "string") ? "key" + type : "keydown";
var modifier = (typeof(modifiers) === "object") ? modifier : {};
var event = document.createEvent("HTMLEvents");
event.initEvent(evtName, true, false);
event.keyCode = keyCode;
for (var i in modifiers) {
event[i] = modifiers[i];
}
document.dispatchEvent(event);
}
})();