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();
});
})();
@KohGeek
Copy link
Author

KohGeek commented Jun 2, 2023

Did Youtube break something again? I'll look into this as soon as I can

@KohGeek
Copy link
Author

KohGeek commented Jun 2, 2023

@paperclone22 I looked into it, and wasn't able to identify it on my side. When you scroll down manually, is there the spinning circle for loading?

@paperclone22
Copy link

paperclone22 commented Jun 2, 2023

there's no spinning circle when I scroll down. maybe I've done something in my account settings to mess this up

@paperclone22
Copy link

I tried in a new install of Brave with no other extensions, and I get the same result. pretty weird

@KohGeek
Copy link
Author

KohGeek commented Jun 3, 2023

Hmm, that's pretty weird. You're sure you have more than 100 videos, right? Can you send a screenshot of your watch later list? You can censor out all video information if necessary, I just want to know if we're using the same version

@paperclone22
Copy link

Ah well the watch later is down to 45 now. Here's WL and also another one that is above 100 videos
WL
DO

@Linoud
Copy link

Linoud commented Jul 18, 2023

This is gonna sound crazy, but I've 3.7k videos in my WL and the list doesn't like sorting once it goes over 500. Am I just gonna have to bite the bullet and cull the list down manually and try sort em individually?

@KohGeek
Copy link
Author

KohGeek commented Jul 19, 2023

This is gonna sound crazy, but I've 3.7k videos in my WL and the list doesn't like sorting once it goes over 500. Am I just gonna have to bite the bullet and cull the list down manually and try sort em individually?

That is unfortunately gonna be the case, the issue is with each additional 100 video you need to sort, the time increases rather disproportionately. I did say in the script that anything above 600 is likely to break.

I was gonna recommend this site, but even they have 2000 videos limit, and they don't work with WL playlist in the first place.

@Linoud
Copy link

Linoud commented Jul 26, 2023

Turns out my main issue was trying it in Firefox, it kept dying even trying to do 300. Swapped to Chrome to let it run and it's ticking away at 1600 and sorting no problem.

I reckon it probably could do all 3700 at the moment but it'd be an incredibly long time and I'm mostly using it to put all the short videos to the front for my commute for now, so I'll do it in chunks.

Thanks for the nice script! :)

@KohGeek
Copy link
Author

KohGeek commented Jul 31, 2023

Turns out my main issue was trying it in Firefox, it kept dying even trying to do 300. Swapped to Chrome to let it run and it's ticking away at 1600 and sorting no problem.

I reckon it probably could do all 3700 at the moment but it'd be an incredibly long time and I'm mostly using it to put all the short videos to the front for my commute for now, so I'll do it in chunks.

Thanks for the nice script! :)

Interesting, I will look into this if I ever had the time or interest to pick up and development again. So far dying in a depressive rut, so yeah. Ideally speaking Firefox really should not have different behaviour than when compared to Chromium

@tempo660
Copy link

This script doesn't appear to function in Firefox or LibreFox. The window that is supposed to appear in Watch Later is simply absent.

Also I would appreciate userscript suggestions in how to interact with Watch Later and Liked Videos pages. Especially how to mass delete the bookmarked videos.

@TerrinX
Copy link

TerrinX commented Oct 3, 2023

Was working on Chrome as long as I refreshed, but now it doesn't.

In a playlist of 9 it says "Loading more videos - 9 videos loaded" but won't sort as it seems to think there are more videos to load?
When switching to 'Sort only loaded' it gives "Playlist is not fully loaded, waiting..." even though it clearly already loaded the total 9

@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