Skip to content

Instantly share code, notes, and snippets.

@danybeam
Created October 16, 2021 03:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danybeam/be6a191f68d7213ec7ea31fd5d8862f3 to your computer and use it in GitHub Desktop.
Save danybeam/be6a191f68d7213ec7ea31fd5d8862f3 to your computer and use it in GitHub Desktop.
Sort YouTube lists by length
/**
Copyright 2021 Daniel Gerardo Orozco Hernandez
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Utility variables
const trimregex = /\n| /ig; // regex to locate and delete empty spaces in legth string
var currentMin = 2160000; // legth of the longest video in youtube in seconds, if you need more than this... please seek profesional help
var currentCandidate = null; // element to be sent to the bottom
// Utility function to simulate mouse clicks
function simulateClick(element) {
const event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
const cancelled = !element.dispatchEvent(event);
if (cancelled) {
alert("cancelled "+ candidateIndex); // alert in case of error, so you know where it encountered an issue
}
}
// Main function, made async to be able to pause the execution after clicks
// If clicks are done too quickly chrome will not detect them correctly
// This is a selection sort
async function sortList(elementsNbr, verbose = false){
if(verbose) console.log("sorting " + elementsNbr + " elements")
var dynamicLimit = 0; // number of sorted elements
// Iterate until all elements are sorted
while(dynamicLimit < elementsNbr){
// scroll to the bottom
var continuation = document.querySelector("ytd-continuation-item-renderer");
if(continuation && verbose) console.log("continuationTokenFound")
while(continuation){
if(verbose) console.log("Scrolling down")
continuation.scrollIntoView()
await new Promise(r => setTimeout(r,250)) // sleeping so the browser has time to react
continuation = document.querySelector("ytd-continuation-item-renderer");
if(continuation && verbose) console.log("continuationTokenFound")
}
if(verbose) console.log("at the bottom")
// grab all the elements in the playlist
var playlist = document.querySelectorAll("ytd-playlist-video-renderer")
if(verbose) console.log("Elements found: " + playlist.length + " elements examined " + (playlist.length - dynamicLimit))
// iterate only through unsorted items
for(i = 0; i < playlist.length-dynamicLimit; i++){
// parse and convert length to seconds
const video = playlist[i];
var thumbnail = video.querySelectorAll(".ytd-thumbnail-overlay-time-status-renderer#text")
var lengthString = thumbnail[0].textContent.replaceAll(trimregex,"").split(':');
var length = 0
for(j = lengthString.length - 1; j>=0; j-- ){
length += lengthString[j] * (60**(lengthString.length-j-1))
}
if(verbose) console.log("currentMin: "+ currentMin + " candidate length: " + length)
// if shortest video send to the bottom
if(length < currentMin){
if(verbose) console.log("new best candidate")
candidateIndex = i
currentCandidate = video.querySelector("button.yt-icon-button")
currentMin = length
if(verbose) console.log("candidate index: "+candidateIndex)
}
}
// open video dot menu to open it
// if improvements are to be done they'll surely be done here
// I can not tell from the HTML alone how YouTube knows that foo or bar video is to be sent to the bottom
// if this can be made a one-click operation it would cut sorting time by ~half
if(verbose) console.log("clicking candidate menu and sleeping one sec")
simulateClick(currentCandidate)
await new Promise(r => setTimeout(r,500))
var menuOptions = document.querySelectorAll("tp-yt-paper-item.ytd-menu-service-item-renderer")
if(verbose) console.log("seeking for bottom button")
for(i = 0; i<menuOptions.length; i++){
if(menuOptions[i].innerText == "Move to bottom"){
if(verbose) console.log("moving " + candidateIndex + " to bottom and sleeping one second")
simulateClick(menuOptions[i])
await new Promise(r => setTimeout(r,500))
}
}
// Mark video as sorted and scroll to the top to reduce inconsistencies with the dynamic list loading time
dynamicLimit++
if(verbose) console.log("new limit: "+dynamicLimit)
currentMin = 2160000; // Always reset your reference variables, no one likes an infinite loop :)
if(verbose) console.log("sleeping")
window.scrollTo(0,0)
await new Promise(r => setTimeout(r,100))
}
if(verbose) console.log("done")
}
// Get size of the playlist and lunch the script
var videoNbr = parseInt(document.querySelectorAll("div#stats")[0].children[0].children[0].innerText)
sortList(videoNbr)
@danybeam
Copy link
Author

danybeam commented Oct 16, 2021

This script was developed and tested in chrome on 2021-10-15 against a 304 video long list with lengths ranging from 49 seconds to slightly above 4 hrs (DnD one shot don't @ me)

This script is still very fragile, there is a wierd 1-off difference error were some random video gets stuck above the shortest video (This might or might not still be true, the only 1-off error after it's last full run test was that the longest video never got sorted)
also if you do ANYTHING to the window while running you risk that some property or other does not get loaded because of responsive website 🙄
if YouTube naming scheme changes even by one letter this script is as good as dead and will need reparing

As it is now it will take you very very far in sorting... kinda like wonky heaps everything is sorted correctly except for the longest video that can get stuck at the top
I cannot promise I'll continue working on this, but it is interesting enough that I won't let it die immediately

Finally this is anything BUT performant
the time formula is (in milliseconds)

T(v) = 800*v + ceiling(v/100)*250 + loading time
800 ms per video and 250 aditional ms per page + whatever it takes you to load

and it does not have sort detection so I hope you don't run it in a sorted list 🙃

@danybeam
Copy link
Author

To use it
open the playlist you want to sort
press ctrl+I on google chrome
Go to the sources tab on the window that opened
press the >> and click "Snippets"
Make a new snippet and copypaste this
right click the snippet and click run

@burnsaga
Copy link

burnsaga commented Aug 6, 2023

Do you know if this still works? It's not working for me.

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