Skip to content

Instantly share code, notes, and snippets.

@KohGeek
Forked from sunnywiz/sortWL.js
Last active April 2, 2024 06:04
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save KohGeek/65ad9e0118ee5f5ee484676731bcd092 to your computer and use it in GitHub Desktop.
Save KohGeek/65ad9e0118ee5f5ee484676731bcd092 to your computer and use it in GitHub Desktop.
SortYoutubePlaylistByDuration
/** THE SCRIPT HAS BEEN MIGRATED TO A PROPER REPOSITORY **/
/** https://github.com/KohGeek/SortYoutubePlaylistByDuration/ **/
/** The script is left here just for legacy purpose, please direct all downloads and questions to the new repository **/
/** Changelog 24/12/2023
* - Fixed an issue where recommended videos at the end of the list breaks sorting (due to the lack of reorder anchors)
* - Attempted fix for "Upcoming" or any other non-timestamped based videos, sorting to bottom (operating on principle that split(':') will produce at least 2 elements on timestamps)
* - Renaming the script to more accurately reflects its capability
* - Change license to fit SPDX license list
* - Minor code cleanups
*/
/* jshint esversion: 8 */
// ==UserScript==
// @name Sort Youtube Playlist by Duration
// @namespace https://gist.github.com/KohGeek/65ad9e0118ee5f5ee484676731bcd092
// @version 3.0.0
// @description As the name implies, sorts youtube playlist by duration
// @author KohGeek
// @license GPL-2.0-only
// @match http://*.youtube.com/playlist*
// @match https://*.youtube.com/playlist*
// @require https://greasyfork.org/scripts/374849-library-onelementready-es7/code/Library%20%7C%20onElementReady%20ES7.js
// @supportURL https://gist.github.com/KohGeek/65ad9e0118ee5f5ee484676731bcd092/
// @grant none
// @run-at document-start
// ==/UserScript==
/**
* Variables and constants
*/
const css =
`
.sort-playlist-div {
font-size: 12px;
padding: 3px 1px;
}
.sort-button-wl {
border: 1px #a0a0a0;
border-radius: 2px;
padding: 3px;
cursor: pointer;
}
.sort-button-wl-default {
background-color: #30d030;
}
.sort-button-wl-stop {
background-color: #d03030;
}
.sort-button-wl-default:active {
background-color: #209020;
}
.sort-button-wl-stop:active {
background-color: #902020;
}
.sort-log {
padding: 3px;
margin-top: 3px;
border-radius: 2px;
background-color: #202020;
color: #e0e0e0;
}
.sort-margin-right-3px {
margin-right: 3px;
}
`
const modeAvailable = [
{ value: 'asc', label: 'Shortest First' },
{ value: 'desc', label: 'Longest First' }
];
const autoScrollOptions = [
{ value: true, label: 'Sort all' },
{ value: false, label: 'Sort only loaded' }
]
const debug = false;
var scrollLoopTime = 500;
var waitTimeAfterDrag = 1800;
let sortMode = 'asc';
let autoScrollInitialVideoList = true;
let log = document.createElement('div');
let stopSort = false;
/**
* Fire a mouse event on an element
* @param {string=} type
* @param {Element} elem
* @param {number} centerX
* @param {number} centerY
*/
let fireMouseEvent = (type, elem, centerX, centerY) => {
const event = new MouseEvent(type, {
view: window,
bubbles: true,
cancelable: true,
clientX: centerX,
clientY: centerY
});
elem.dispatchEvent(event);
};
/**
* Simulate drag and drop
* @see: https://ghostinspector.com/blog/simulate-drag-and-drop-javascript-casperjs/
* @param {Element} elemDrag - Element to drag
* @param {Element} elemDrop - Element to drop
*/
let simulateDrag = (elemDrag, elemDrop) => {
// calculate positions
let pos = elemDrag.getBoundingClientRect();
let center1X = Math.floor((pos.left + pos.right) / 2);
let center1Y = Math.floor((pos.top + pos.bottom) / 2);
pos = elemDrop.getBoundingClientRect();
let center2X = Math.floor((pos.left + pos.right) / 2);
let center2Y = Math.floor((pos.top + pos.bottom) / 2);
// mouse over dragged element and mousedown
fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
fireMouseEvent("mouseenter", elemDrag, center1X, center1Y);
fireMouseEvent("mouseover", elemDrag, center1X, center1Y);
fireMouseEvent("mousedown", elemDrag, center1X, center1Y);
// start dragging process over to drop target
fireMouseEvent("dragstart", elemDrag, center1X, center1Y);
fireMouseEvent("drag", elemDrag, center1X, center1Y);
fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
fireMouseEvent("drag", elemDrag, center2X, center2Y);
fireMouseEvent("mousemove", elemDrop, center2X, center2Y);
// trigger dragging process on top of drop target
fireMouseEvent("mouseenter", elemDrop, center2X, center2Y);
fireMouseEvent("dragenter", elemDrop, center2X, center2Y);
fireMouseEvent("mouseover", elemDrop, center2X, center2Y);
fireMouseEvent("dragover", elemDrop, center2X, center2Y);
// release dragged element on top of drop target
fireMouseEvent("drop", elemDrop, center2X, center2Y);
fireMouseEvent("dragend", elemDrag, center2X, center2Y);
fireMouseEvent("mouseup", elemDrag, center2X, center2Y);
};
/**
* Scroll automatically to the bottom of the page
*/
let autoScroll = async () => {
let element = document.scrollingElement;
let currentScroll = element.scrollTop;
do {
currentScroll = element.scrollTop;
element.scrollTop = element.scrollHeight;
await new Promise((r) => setTimeout(r, scrollLoopTime));
} while (currentScroll != element.scrollTop);
};
/**
* Log activities
* @param {string=} message
*/
let logActivity = (message) => {
log.innerText = message;
if (debug) {
console.log(message);
}
};
/**
* Generate menu container element
*/
let renderContainerElement = () => {
const element = document.createElement('div')
element.className = 'sort-playlist sort-playlist-div'
element.style.paddingBottom = '16px'
// Add buttonChild container
const buttonChild = document.createElement('div')
buttonChild.className = 'sort-playlist-div sort-playlist-button'
element.appendChild(buttonChild)
// Add selectChild container
const selectChild = document.createElement('div')
selectChild.className = 'sort-playlist-div sort-playlist-select'
element.appendChild(selectChild)
document.querySelector('div.thumbnail-and-metadata-wrapper').append(element)
}
/**
* Generate button element
* @param {function} click - OnClick handler
* @param {string=} label - Button Label
*/
let renderButtonElement = (click = () => { }, label = '', red = false) => {
// Create button
const element = document.createElement('button')
if (red) {
element.className = 'style-scope sort-button-wl sort-button-wl-stop sort-margin-right-3px'
} else {
element.className = 'style-scope sort-button-wl sort-button-wl-default sort-margin-right-3px'
}
element.innerText = label
element.onclick = click
// Render button
document.querySelector('.sort-playlist-button').appendChild(element)
};
/**
* Generate select element
* @param {number} variable - Variable to update
* @param {Object[]} options - Options to render
* @param {string=} label - Select Label
*/
let renderSelectElement = (variable = 0, options = [], label = '') => {
// Create select
const element = document.createElement('select');
element.className = 'style-scope sort-margin-right-3px';
element.onchange = (e) => {
if (variable === 0) {
sortMode = e.target.value;
} else if (variable === 1) {
autoScrollInitialVideoList = e.target.value;
}
};
// Create options
options.forEach((option) => {
const optionElement = document.createElement('option')
optionElement.value = option.value
optionElement.innerText = option.label
element.appendChild(optionElement)
});
// Render select
document.querySelector('.sort-playlist-select').appendChild(element);
};
/**
* Generate number element
* @param {number} variable
* @param {number} defaultValue
* @param {string=} label
*/
let renderNumberElement = (variable = 0, defaultValue = 0, label = '') => {
// Create div
const elementDiv = document.createElement('div');
elementDiv.className = 'sort-playlist-div sort-margin-right-3px';
elementDiv.innerText = label;
// Create input
const element = document.createElement('input');
element.type = 'number';
element.value = defaultValue;
element.className = 'style-scope';
element.oninput = (e) => {
if (variable === 0) {
scrollLoopTime = +(e.target.value);
} else if (variable === 1) {
waitTimeAfterDrag = +(e.target.value);
}
};
// Render input
elementDiv.appendChild(element);
document.querySelector('div.sort-playlist').appendChild(elementDiv);
};
/**
* Generate log element
*/
let renderLogElement = () => {
// Populate div
log.className = 'style-scope sort-log';
log.innerText = 'Logging...';
// Render input
document.querySelector('div.sort-playlist').appendChild(log);
};
/**
* Add CSS styling
*/
let addCssStyle = () => {
const element = document.createElement('style');
element.innerHTML = css;
document.head.appendChild(element);
};
/**
* Sort videos by time
* @param {Element[]} allAnchors - Array of anchors
* @param {Element[]} allDragPoints - Array of draggable elements
* @return {number} - Number of videos sorted
*/
let sortVideos = (allAnchors, allDragPoints) => {
let videos = [];
let sorted = 0;
let dragged = false;
// Sometimes after dragging, the page is not fully loaded yet
// This can be seen by the number of anchors not being a multiple of 100
if (allAnchors.length % 100 !== 0 && document.querySelector(".ytd-continuation-item-renderer") !== null) {
logActivity("Playlist is not fully loaded, waiting...");
return 0;
}
for (let j = 0; j < allAnchors.length; j++) {
let thumb = allAnchors[j];
let drag = allDragPoints[j];
let timeSpan = thumb.querySelector("#text");
let timeDigits = timeSpan.innerText.trim().split(":").reverse();
let time;
if (timeDigits.length == 1) {
sortMode == "asc" ? time = 999999999999999999 : time = -1;
} else {
time = parseInt(timeDigits[0]);
if (timeDigits[1]) time += parseInt(timeDigits[1]) * 60;
if (timeDigits[2]) time += parseInt(timeDigits[2]) * 3600;
}
videos.push({ anchor: drag, time: time, originalIndex: j });
}
if (sortMode == "asc") {
videos.sort((a, b) => a.time - b.time);
} else {
videos.sort((a, b) => b.time - a.time);
}
for (let j = 0; j < videos.length; j++) {
let originalIndex = videos[j].originalIndex;
if (debug) {
console.log("Loaded: " + videos.length + ". Current: " + j + ". Original: " + originalIndex + ".");
}
if (originalIndex !== j) {
let elemDrag = videos[j].anchor;
let elemDrop = videos.find((v) => v.originalIndex === j).anchor;
logActivity("Drag " + originalIndex + " to " + j);
simulateDrag(elemDrag, elemDrop);
dragged = true;
}
sorted = j;
if (stopSort || dragged) {
break;
}
}
return sorted;
};
/**
* There is an inherent limit in how fast you can sort the videos, due to Youtube refreshing
* This limit also applies if you do it manually
* It is also much worse if you have a lot of videos, for every 100 videos, it's about an extra 2-4 seconds, maybe longer
*/
let activateSort = async () => {
let allAnchors = document.querySelectorAll("ytd-item-section-renderer:first-of-type div#content a#thumbnail.inline-block.ytd-thumbnail");
let allDragPoints;
let sortedCount = 0;
let initialVideoCount = allAnchors.length;
stopSort = false;
while (document.querySelector(".ytd-continuation-item-renderer") !== null
&& document.URL.includes("playlist?list=")
&& stopSort === false
&& autoScrollInitialVideoList === true) {
logActivity("Loading more videos - " + allAnchors.length + " videos loaded");
if (allAnchors.length > 300) {
logActivity(log.innerText + "\nNumber of videos loaded is high, sorting may take a long time");
} else if (allAnchors.length > 600) {
logActivity(log.innerText + "\nSorting may take extremely long time/is likely to bug out");
}
await autoScroll();
allAnchors = document.querySelectorAll("ytd-item-section-renderer:first-of-type div#content a#thumbnail.inline-block.ytd-thumbnail");
initialVideoCount = allAnchors.length;
}
logActivity(initialVideoCount + " videos loaded.");
while (sortedCount < initialVideoCount && stopSort === false) {
allAnchors = document.querySelectorAll("ytd-item-section-renderer:first-of-type div#content a#thumbnail.inline-block.ytd-thumbnail");
allDragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
let anchorListLength = allAnchors.length;
if (!allAnchors[anchorListLength - 1].querySelector("#text")) {
logActivity("Video " + anchorListLength + " is not loaded yet, waiting " + waitTimeAfterDrag + "ms");
await new Promise((r) => setTimeout(r, waitTimeAfterDrag));
continue;
}
sortedCount = sortVideos(allAnchors, allDragPoints) + 1;
await new Promise((r) => setTimeout(r, waitTimeAfterDrag));
}
if (stopSort === true) {
logActivity("Sort cancelled");
stopSort = false;
} else {
logActivity("Sort complete. Video sorted: " + sortedCount);
}
};
/**
* Initialise script - IIFE
*/
(() => {
onElementReady('div.thumbnail-and-metadata-wrapper', false, () => {
renderContainerElement();
addCssStyle();
renderButtonElement(activateSort, 'Sort Videos', false);
renderButtonElement(() => { stopSort = true }, 'Stop Sort', true);
renderSelectElement(0, modeAvailable, 'Sort Mode');
renderSelectElement(1, autoScrollOptions, 'Auto Scroll');
renderNumberElement(1, 1800, 'Wait Time After Drag (ms)');
renderLogElement();
});
})();
@Grayonic123
Copy link

Mine is doing the same as TerrinX's

@kastru
Copy link

kastru commented Oct 18, 2023

it was working perfectly, but now it gets stuck in loading the videos.

@davidwolfpaw
Copy link

The script works great for me!

I did notice an issue, using the latest version of Firefox on Mac, but I haven't looked to see if it is browser specific:
If I have a video that is Upcoming (no time associated with it), the script stops as complete when it gets to that video.

So say that I have three videos, an upcoming video on the playlist, and three more videos. Running the script will sort the first three, then stop at the upcoming video and not process the final three that would otherwise be sorted.

@KohGeek
Copy link
Author

KohGeek commented Oct 27, 2023

@davidwolfpaw good to hear! Upcoming is broken because the code assumes all timecode are present, when timecode is not present/not in numerical form, it just breaks. I will fix at a much later date.

@kastru @Grayonic123 @TerrinX @tempo660
Sorry for the late reply, I could not duplicate any of your issues. Please let me know what script runner you're using, as well as the browser version. Things are fine for me on ViolentMonket.

@kastru
Copy link

kastru commented Oct 27, 2023

Hey @KohGeek, thanks for the follow-up. I'm using Tampermonkey 4.19.0 on Chrome 118.0.5993.118.
Great news, the sorting issue on the 'watch later' playlist sorted itself out. However, attempting to sort other playlists leads to an endless "Loading more videos" message.

@Grayonic123
Copy link

I'm using the same as @kastru with the same issue, sorting in non-'Watch Later' playlists

@Scribblecloud
Copy link

Hi, i have a relatively small playlist of just 30-40 something videos but it just keeps saying "logging" forever and ever and ever and trying to click "sort" doesnt do anything? :(
https://www.youtube.com/playlist?list=PLorIEMvQiHluLnQyVU7zuLKWBUsyQWHEA
image

@Scribblecloud
Copy link

oh wait.. does this ONLY work with specifically watch later? :( what about other playlists?

@kastru
Copy link

kastru commented Dec 22, 2023

@Scribblecloud it used to work on other playlists too, but the code seems to be broken now

@KohGeek
Copy link
Author

KohGeek commented Dec 24, 2023

Working fine on Edge for me, can you let me know what script runner you're using and on what browser? Specifically I'm using ViolentMonkey on Edge 120.0.2210.77.

@Scribblecloud

and wait @kastru I saw that you reported this a while ago on Chrome and TamperMonkey, let me see if I can replicate anything

@KohGeek
Copy link
Author

KohGeek commented Dec 24, 2023

I seem to have found the issue, but not exactly. For me, Chrome + Tamper doesn't get stuck on logging, but it gets stuck when it tries to sort the recommended videos (which is obviously invalid). Is everyone else getting the same thing, or it's a different issue?

@KohGeek
Copy link
Author

KohGeek commented Dec 24, 2023

New version v3 has been released. Please update and check if it works now, especially under such situations:

  • Videos that are live or upcoming are now sorted to bottom (please let me know if there are demands to make this adjustable)
  • Generic playlists with recommendations at the bottom are now sortable again
  • Tamper/ViolentMonkey in Chromium browser should be working

I finally found the issue with Firefox, and it requires quite a bit of logic rewrite but it should lead to improvement in sorting time for all browsers (technically).

@kastru
Copy link

kastru commented Dec 25, 2023

New version v3 has been released. Please update and check if it works now, especially under such situations:

  • Videos that are live or upcoming are now sorted to bottom (please let me know if there are demands to make this adjustable)
  • Generic playlists with recommendations at the bottom are now sortable again
  • Tamper/ViolentMonkey in Chromium browser should be working

I finally found the issue with Firefox, and it requires quite a bit of logic rewrite but it should lead to improvement in sorting time for all browsers (technically).

Awesome, this is so useful. On the watch later it's working even better, like a charm.

On the other playlists, however, it still got stuck on loading:
image

@KohGeek
Copy link
Author

KohGeek commented Dec 25, 2023

ah sounds like it could be an oversight in programming, i'll look later, but I won't be available until new year's eve

@kastru
Copy link

kastru commented Jan 6, 2024

@KohGeek cool, i'd like to assist but i'm not much of a coder. happy new year!

@theacademe
Copy link

theacademe commented Feb 10, 2024

@KohGeek Do you review pull requests, or would be interested in seeing some enhancements?

@KohGeek
Copy link
Author

KohGeek commented Feb 11, 2024

@KohGeek Do you review pull requests, or would be interested in seeing some enhancements?

I'm very much interested! But unfortunately gist isn't setup for these kind of work, I might consider moving it to an actual repo if there's demand for this. It's a little hard for me to spare time to implement all the small details I would love to

@KohGeek
Copy link
Author

KohGeek commented Feb 11, 2024

I have moved the script to the new repo, here:
https://github.com/KohGeek/SortYoutubePlaylistByDuration/

The script will be frozen here just for reference purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment