scattering clouds

Coding practice 4: Make YouTube great again with bookmarklets.

There is a lot of things to like about YouTube, but there is also a lot of things to hate. So I decided to create a collection of bookmarklets to tailor YouTube to my liking.

See my previous blog post to learn how you can make use of bookmarklets by copying and pasting JavaScript code into your browser bookmarks or favourites.

Bulk delete videos from a YouTube playlist.

Adding videos to a playlist (e.g. “Watch later”) is fairly simple and straightforward. But clearing out old or unwanted videos from a playlist? Not so much. For some reason, there is no official way to bulk-delete videos from a YouTube playlist.

Apparently, Google intends for us to to delete each video by hand, one by one.

So if your Watch later playlist is filled with up to ~5000 videos, it will amount to ~10,000 clicks at 2 clicks per video removed. Even if you can click up to 3 times per second, you would still need to sit down and click away for at least an hour or two.

It is an excruciatingly slow and painful process.

To deal with this oversight, a GitHub user by the name of JanTheDeveloper has created a script that automates all the clicking for you at a fixed interval. Jan had since changed or closed their GitHub account, and the script has become outdated.

Fortunately, another GitHub user by the name of BryanHaley has created an updated version that works on all YouTube playlists.

The script does still come with several limitations or caveats:

  • It is unable to delete playlist videos that are no longer available or have been made private. For those videos you still have to delete them by hand.
  • It only works if you use the English version of YouTube. For other languages, the text for “Action menu” and “Remove from” needs to be changed to whatever words YouTube uses for their corresponding language and locale.
  • If you delete videos too quickly, YouTube might stop responding. In cases like this it is best to wait about 5 minutes, refresh the playlist page, and then try again. By default it is set to remove a video every 500 milliseconds or 0.5 seconds, but you can increase or decrease the frequency as need be.

I have made some modifications to Bryan’s script to make it a wee bit more user friendly, and converted it into a bookmarklet. Use it at your own risk.

javascript: ((timeInMillisecond) => {
	const domainIsValid = () => {
		const hostnames = [
			"youtube.com",
			"www.youtube.com",
			"m.youtube.com"
		];
		return hostnames.includes(window.location.host);
	};
	const isPlaylist = () => {
		const pathname = window.location.pathname.split("/");
		if(pathname.length > 1 && pathname[1] === "playlist") return true;
		return false;
	};
	const deleteVideos = () => {
		let videos = document.getElementsByTagName("ytd-playlist-video-renderer");
		if(videos.length < 1){
			alert("Unable to find any more videos to delete. Please refresh the page to see if the playlist has been cleared.");
			clearInterval(repeatOnInterval);
			return;
		}
		videos[0].querySelector('#primary button[aria-label="Action menu"]').click();
		let things = document.evaluate(
			'//span[contains(text(),"Remove from")]',
			document,
			null,
			XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
			null
		);
		for (var i = 0; i < things.snapshotLength; i++) things.snapshotItem(i).click();
	};
	if(!domainIsValid()){
		alert("This bookmarklet only works on the YouTube website!");
		return;
	}
	if(!isPlaylist() && !confirm("Unable to determine if this page contains a YouTube playlist. Do you want to try and proceed?")) return;
	if(!confirm("WARNING: This bookmarklet will attempt to delete all videos from this playlist. Please confirm to continue.")) return;
	let repeatOnInterval = setInterval(deleteVideos, timeInMillisecond);
})(500)

Force YouTube to play a video using its regular video interface.

YouTube wants you to keep watching videos. To keep watching as many videos as you can stomach, because it gives them more opportunity to shower you with ads. And one of their tactics is to force videos to autoplay or loop indefinitely.

Because the more time you spend glued to their screen, the better it is for revenue!

I have written an entire blog post bemoaning the issues I have with YouTube Shorts and its incessantly looping videos, horrible design, intent, and user interface. I also very much dislike the lack of options to turn off autoplay in a YouTube playlist.

When I finish watching a video, I like to scroll down and look at the comments.

But when I watch a video in my “Watch later” playlist, it will not stop when the video ends. YouTube will automatically bring up the next video in the playlist, forcing me to either intervene or keep watching. Probably because it makes for an opportune moment to shove two more advertisements into my face.

If I fail to pause a video before it ends, the next video will begin running, and I have to hit the “back” button and wait for the comment section to reload.

And this pisses me off to no end.

So I have coded a bookmarklet that reloads Shorts and playlist videos using the regular YouTube video interface. One pre-emptive click at the start of each video, and I get to browse the comment section in relative peace when a video ends.

It’s still not a very convenient way to browse YouTube, but is nevertheless a vast improvement over YouTube’s default behaviour.

I call the bookmarklet “Cleanse YouTube”.

javascript: (() => {
	const domainIsValid = () => {
		const hostnames = [
			"youtube.com",
			"www.youtube.com",
			"m.youtube.com"
		];
		return hostnames.includes(window.location.host);
	};
	const identifyVideoID = () => {
		const pathname = window.location.pathname.split("/");
		if(pathname[1] === "shorts") return pathname.length >= 3 ? pathname[2] : null;
		return new URLSearchParams(window.location.search).get("v");
	};
	const redirectOrReload = (videoID) => {
		if(videoID) window.location.href = window.location.origin + "/watch?v=" + videoID;
		else window.location.reload();
	};
	if(domainIsValid()) redirectOrReload(identifyVideoID());
	else alert("This bookmarklet only works on the YouTube website!");
})()

Rotate a YouTube video by 90 degrees.

On YouTube you would sometimes find a vertical video that is filmed horizontally. Examples of this would be videos of Korean race queen models, like the ones you can find on 4kOTAKU’s YouTube channel.

It is easy to watch such videos on a mobile phone. You just need to get the video to go full screen, and it would play vertically when your phone is held upright.

No such luck on a computer though. These videos are nigh unwatchable on my laptop unless I keep my neck tilted sideways. So I created a bookmarklet that rotates YouTube videos by 90 degrees clockwise each time it is clicked.

Once the bookmarklet has been activated, it will also listen for button presses on your keyboard, so you can still trigger rotations by pressing “r” when the video is in full screen. Pressing “SHIFT + r” will rotate the video counter-clockwise.

This bookmarklet needs to be reactivated each time you refresh or reload the webpage. And no, it doesn’t work with YouTube Shorts, and I don’t really care why.

If you need to rotate Shorts videos (such as these animated videos), you have to first get YouTube to play the Shorts video with its regular video interface. This can be done by utilising the “Cleanse YouTube” bookmarklet mentioned previously.

javascript: (() => {
	const domainIsValid = () => {
		const hostnames = [
			"youtube.com",
			"www.youtube.com",
			"m.youtube.com"
		];
		return hostnames.includes(window.location.host);
	};
	const rotateVid = (degrees) => {
		if (!document.body.dataset.scYTVideoOrientation) document.body.dataset.scYTVideoOrientation = degrees;
		else document.body.dataset.scYTVideoOrientation = (+document.body.dataset.scYTVideoOrientation + degrees) % 360;
		let v = document.getElementsByTagName("video")[0];
		let ratio = +document.body.dataset.scYTVideoOrientation % 180 ? v.videoHeight / v.videoWidth : 1;
		v.style.transform = `rotate(${+document.body.dataset.scYTVideoOrientation}deg) scale(${ratio})`;
	};
	if(domainIsValid()){
		if (!document.body.dataset.scYTVideoOrientation) window.addEventListener("keydown", (e) => {
			if(e.key === "r") rotateVid(90);
			else if (e.key === "R") rotateVid(-90);
		});
		rotateVid(90);
	}else alert("This bookmarklet only works on the YouTube website!");
})()

Update 10 Jun 2023: The above code can get a little buggy when you enable or disable full screen mode, so I have made some changes to check for an existing rotation before actually performing one.

javascript: (() => {
	const domainIsValid = () => {
		const hostnames = [
			"youtube.com",
			"www.youtube.com",
			"m.youtube.com"
		];
		return hostnames.includes(window.location.host);
	};
	const getTransformRotation = (v) => {
		let s = v.style.transform;
		if(s.length < 1) return 0;
		let arr = s.split(/[\s\)]{2}|\(/);
		let index = arr.findIndex((e) => e === "rotate");
		if(++index == 0) return 0;
		if(index < arr.length && arr[index].includes("deg")) return parseInt(arr[index]);
		return 0;
	};
	const rotateVid = (degrees) => {
		let v = document.getElementsByTagName("video");
		if(v.length < 1){
			alert("Unable to find video!");
			return;
		}
		v = v[0];
		let rotate = getTransformRotation(v) + degrees;
		let ratio = rotate % 180 ? v.videoHeight / v.videoWidth : 1;
		v.style.transform = `rotate(${rotate}deg) scale(${ratio})`;
	};
	if(!domainIsValid()){
		alert("This bookmarklet only works on the YouTube website!");
		return;
	}
	if (!document.body.dataset.scYTVideoOrientation){
		window.addEventListener("keydown", (e) => {
			if(e.key === "r") rotateVid(90);
			else if (e.key === "R") rotateVid(-90);
		});
		document.body.dataset.scYTVideoOrientation = "true";
	}
	rotateVid(90);
})()

Update 12 Jun 2023: The above code applies inline styles to the <video> element, which is not ideal. This is particularly apparent when you enable and disable full screen mode, because YouTube appears to create a new <video> element every time full screen is toggled on or off, and you have to readjust its orientation again.

So I changed the code to set a global style that targets all <video> elements directly, allowing us to update the orientation of the video using CSS variables.

The advantage to this approach is that YouTube videos will always “remember” its previous orientation – so long as you don’t refresh or reload the page, or open a video in a new tab or window.

Unfortunately, using the “Cleanse YouTube” bookmarklet will always force a page reload, so you would need to reactivate this bookmarklet again.

javascript: (() => {
	const domainIsValid = () => {
		const hostnames = [
			"youtube.com",
			"www.youtube.com",
			"m.youtube.com"
		];
		return hostnames.includes(window.location.host);
	};
	const getCurrentRotation = () => {
		const r = document.querySelector(':root');
		let s = getComputedStyle(r).getPropertyValue("--scYTVideoRotation");
		if(s.length > 0 && s.includes("deg")) return parseInt(s);
		return 0;
	};
	const rotateVid = (degrees) => {
		let v = document.getElementsByTagName("video");
		if(v.length < 1){
			alert("Unable to find video!");
			return;
		}
		v = v[0];
		const rotate = (getCurrentRotation() + degrees) % 360;
		const ratio = rotate % 180 ? v.videoHeight / v.videoWidth : 1;
		const r = document.querySelector(':root'); 
		r.style.setProperty('--scYTVideoRotation', `${rotate}deg`);
		r.style.setProperty('--scYTVideoScale', `${ratio}`);
	};
	if(!domainIsValid()){
		alert("This bookmarklet only works on the YouTube website!");
		return;
	}
	if (!document.body.dataset.scYTVideoOrientation){
		window.addEventListener("keydown", (e) => {
			if(e.key === "r") rotateVid(90);
			else if (e.key === "R") rotateVid(-90);
		});
		const ss = document.createElement("style");
		document.head.appendChild(ss);
		ss.sheet.insertRule("video {transform: rotate(var(--scYTVideoRotation)) scale(var(--scYTVideoScale)) !important;}");
		document.body.dataset.scYTVideoOrientation = "true";
	}
	rotateVid(90);
})()

Skip YouTube ads.

A YouTuber by the name of ZeSardine has created an awesome bookmarklet that allows you to skip ads on YouTube, which is quite useful if you do not already have an adblocker running on your computer.

His original code is available on GitHub, and here is a reformatted copy for the sake of posterity. I have tested the bookmarklet on Chrome but did not bother to modify it because I prefer to stick with an adblocker.

Update 02 Jul 2023: Since Google has been trialling ways to prevent the use of adblockers on YouTube for the past two months, this bookmarklet may see some use after all. At least until Google catches wind of it and decide to patch the exploit.

javascript: (() => {
	if(document.getElementsByClassName("video-ads")[0].innerHTML !== ""){ 
		let banner = false; 
		for(let i = 0; i < document.getElementsByClassName("ytp-ad-overlay-close-button").length; i++){
			document.getElementsByClassName("ytp-ad-overlay-close-button")[i].click();
			banner = true;
		} 
		if(banner === false){
			document.getElementsByClassName("html5-main-video")[0].currentTime = document.getElementsByClassName("html5-main-video")[0].duration;
			document.getElementsByClassName("ytp-ad-skip-button")[0].click();
		}
	}
})()

Update 18 Dec 2023: Since I have finally run into Google’s anti-adblock measures on YouTube, I dug up this code and made a few changes to better suit my use case.

Once the following bookmarklet has been activated, you can press “s” on your keyboard to continue skipping ads. This is useful in times where YouTube decides to show you an ad in the middle of a fullscreen video – you don’t have to close fullscreen, click on the bookmarklet, and then reenable fullscreen again.

Unfortunately, the bookmarklet will need to be reactivated if you refresh the page or open a video in a new tab.

javascript: (() => {

const skipYTAds = () => {
if(document.getElementsByClassName("video-ads")[0].innerHTML !== ""){
let banner = false;
for(let i = 0; i < document.getElementsByClassName("ytp-ad-overlay-close-button").length; i++){
document.getElementsByClassName("ytp-ad-overlay-close-button")[i].click();
banner = true;
}
if(banner === false){
document.getElementsByClassName("html5-main-video")[0].currentTime = document.getElementsByClassName("html5-main-video")[0].duration;
}
}
const allTheButtonClasses = [
"ytp-ad-skip-button",
"ytp-ad-skip-button-modern"
];
for(let i = 0; i < allTheButtonClasses.length; i++){
let buttons = document.getElementsByClassName(allTheButtonClasses[i]);
if(buttons.length === 0) continue;
for(let j = 0; j < buttons.length; j++){
buttons[j].click();
}
}
};
if (!document.body.dataset.scYTAdSkip){
window.addEventListener("keydown", (e) => {
if(e.key === "s" || e.key === "S") skipYTAds();
});
document.body.dataset.scYTAdSkip = "true";
}
skipYTAds();
})()

back to top