-
-
Save KohGeek/65ad9e0118ee5f5ee484676731bcd092 to your computer and use it in GitHub Desktop.
/** 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(); | |
}); | |
})(); |
Did Youtube break something again? I'll look into this as soon as I can
@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?
there's no spinning circle when I scroll down. maybe I've done something in my account settings to mess this up
I tried in a new install of Brave with no other extensions, and I get the same result. pretty weird
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
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?
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.
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! :)
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
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
Mine is doing the same as TerrinX's
it was working perfectly, but now it gets stuck in loading the videos.
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.
@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.
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.
I'm using the same as @kastru with the same issue, sorting in non-'Watch Later' playlists
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
oh wait.. does this ONLY work with specifically watch later? :( what about other playlists?
@Scribblecloud it used to work on other playlists too, but the code seems to be broken now
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.
and wait @kastru I saw that you reported this a while ago on Chrome and TamperMonkey, let me see if I can replicate anything
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?
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).
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:
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
@KohGeek cool, i'd like to assist but i'm not much of a coder. happy new year!
@KohGeek Do you review pull requests, or would be interested in seeing some enhancements?
@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
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.
what can be done to sort if the query for
ytd-continuation-item-renderer
turns up null?it shows as null on both firefox and chrome both using violent monkey
debug says the below (plus some console.log lines I added to check)