4chan External Sounds

Plays audio associated with images on 4chan.

As of 2019-08-03. See the latest version.

// ==UserScript==
// @name 4chan External Sounds
// @namespace b4k
// @description Plays audio associated with images on 4chan.
// @author Bakugo
// @version 1.1.0
// @match *://boards.4chan.org/*
// @match *://boards.4channel.org/*
// @run-at document-start
// ==/UserScript==

(function() {
	var doInit;
	var doProcessFile;
	var doProcessFiles;
	var doPlayFile;
	var doMakeKey;
	var players;
	
	document.addEventListener(
		"4chanXInitFinished",
		function (event) {
			doInit();
		}
	);
	
	doInit = function () {
		if (document.documentElement.classList.contains("fourchan-x") == false) {
			return;
		}
		
		players = {};
		
		doProcessFiles(document.body);
		
		(
			new MutationObserver(
				function (mutations) {
					mutations.forEach(
						function (mutation) {
							if (mutation.type === "childList") {
								mutation.addedNodes.forEach(
									function (node) {
										if (node.nodeType === Node.ELEMENT_NODE) {
											doProcessFiles(node);
											doPlayFile(node);
										}
									}
								);
							}
						}
					);
				}
			)
		).observe(
			document.body,
			{
				childList: true,
				subtree: true
			}
		);
	};
	
	doProcessFile = function (file) {
		var fileLink;
		var fileName;
		var key;
		var match;
		var player;
		var playerSrc;
		
		if (!file.classList.contains("file")) {
			return;
		}
		
		fileLink = file.querySelector(".file-info > a");
		
		if (!fileLink) {
			return;
		}
		
		fileName = null;
		
		file.querySelectorAll(".file-info .fnfull, .file-info > a")
			.forEach(
				function (node) {
					if (fileName) {
						return;
					}
					
					if (node && node.textContent) {
						fileName = node.textContent;
					}
				}
			);
		
		if (!fileName) {
			return;
		}
		
		fileName = fileName.replace(/\-/, "/");
		
		key = doMakeKey(fileLink.href);
		
		if (!key) {
			return;
		}
		
		if (players[key]) {
			return;
		}
		
		match = fileName.match(/[\[\(\{]audio[ \=\:\|\$](.*?)[\]\)\}]/i);
		
		if (!match) {
			return;
		}
		
		playerSrc = match[1];
		playerSrc = decodeURIComponent(playerSrc);
		playerSrc = (playerSrc.match(/^(https?\:)?\/\//) ? playerSrc : ("//" + playerSrc));
		
		player = document.createElement("audio");
		
		player.preload = "none";
		player.volume = 0.8;
		player.loop = true;
		
		player.src = playerSrc;
		
		players[key] = player;
	};
	
	doProcessFiles = function (target) {
		target.querySelectorAll(".post")
			.forEach(
				function (post) {
					if (post.parentElement.parentElement.id === "qp") {
						return;
					}
					
					if (post.parentElement.classList.contains("noFile")) {
						return;
					}
					
					post.querySelectorAll(".file")
						.forEach(
							function (file) {
								doProcessFile(file);
							}
						);
				}
			);
	};
	
	doPlayFile = function (target) {
		var key;
		var player;
		var interval;
		
		if (!(
			target.id === "ihover" ||
			target.className === "full-image"
		)) {
			return;
		}
		
		key = doMakeKey(target.src);
		
		if (!key) {
			return;
		}
		
		player = players[key];
		
		if (!player) {
			return;
		}
		
		if (!player.paused) {
			return;
		}
		
		switch (target.tagName) {
			case "IMG":
				player.currentTime = 0;
				player.loop = true;
				player.play();
				
				break;
			
			case "VIDEO":
				player.currentTime = target.currentTime;
				player.loop = false;
				player.play();
				
				break;
			
			default:
				return;
		}
		
		if (player.paused) {
			document.dispatchEvent(
				new CustomEvent("CreateNotification", {
					bubbles: true,
					detail: {
						type: "warning",
						content: "Your browser blocked autoplay, click anywhere on the page to activate it and try again.",
						lifetime: 5
					}
				})
			);
		}
		
		interval =
			setInterval(
				function () {
					if (document.body.contains(target)) {
						if (target.tagName === "VIDEO") {
							if (player.duration) {
								if (target.currentTime > player.duration) {
									player.pause();
								} else {
									if (Math.abs(target.currentTime - player.currentTime) > 0.1) {
										player.currentTime = target.currentTime;
									}
									
									if (!target.paused && player.paused) {
										player.play();
									}
									
									if (target.paused && !player.paused) {
										player.pause();
									}
								}
							}
						}
					} else {
						clearInterval(interval);
						player.pause();
					}
				}, 
				(10)
			);
	};
	
	doMakeKey = function (link) {
		var match;
		
		match = link.match(/\.(?:4cdn|4chan)\.org\/(.+?)\/(\d+?)\.(.+?)$/);
		
		if (match) {
			return (match[1] + "." + match[2]);
		}
		
		return null;
	};
})();