8chan External Sounds

Plays audio associated with images on 8chan.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name 8chan External Sounds
// @namespace lig
// @description Plays audio associated with images on 8chan.
// @author Bakugo + MFG
// @version 1.7.2
// @match *://8chan.cc/*
// @match *://8chan.moe/*
// @match *://8chan.se/*
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/UserScript==

const debug_GM_fetch = false

function parseHeaders(responseHeaders) {
	let head = new Headers()
	let pairs = responseHeaders.trim().split('\n')
	pairs.forEach(function(header) {
		let split = header.trim().split(':')
		let key = split.shift().trim()
		let value = split.join(':').trim()
		try {
			head.append(key, value)
		} catch(e) {
			console.error(e);
		}
	})
	return head
}

function GM_fetch(url, options = {}) {
	return new Promise((res, rej) => {
		const host = new URL(url).hostname;
		options.url = url;
		options.method = options.method || 'GET';
		options.responseType = options.responseType || 'text';
		options.headers = options.headers || {};
		//options.headers.userAgent
		Object.assign(options.headers, {
			accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,audio/mpeg,*/*;q=0.8",
			"accept-encoding": "gzip, deflate, br",
			"accept-language": "en-US,en;q=0.5",
			"alt-used": host,
			"cache-control": "no-cache",
			connection: "keep-alive",
			host: host,
			pragma: "no-cache",
			"sec-fetch-dest": "document",
			"sec-fetch-mode": "navigate",
			"sec-fetch-site": "none",
			"sec-fetch-user": "?1",
			"upgrade-insecure-requests": "1",
		})
		options.onload = _res => {
			const parsedHeaders = parseHeaders(_res.responseHeaders);
			if(debug_GM_fetch) {
				console.log('parsedHeaders', parsedHeaders);
				console.log('response', _res);
			}
			const response = new Response(_res.response, {
				status: _res.status,
				statusText: _res.statusText,
				headers: parsedHeaders
			})
			Object.defineProperty(response, "url", { value: url });
			res(response);
		};

		options.onerror = function() {
			setTimeout(function() {
				rej(new TypeError('Network request failed'))
			}, 0)
		}

		options.ontimeout = function() {
			setTimeout(function() {
				rej(new TypeError('Network request timed out'))
			}, 0)
		}

		options.onabort = function() {
			setTimeout(function() {
				rej(new DOMException('Aborted', 'AbortError'))
			}, 0)
		}

		GM_xmlhttpRequest(options);
	});
}

function arrayBufferToBase64(buffer) {
	const bytes = new Uint8Array(buffer);
	const len = buffer.byteLength;
	let binary = "";
	for (let i = 0; i < len; i++) {
		binary += String.fromCharCode(bytes[i]);
	}
	return window.btoa(binary);
}

async function fetchSound(url) {
	const response = await GM_fetch(url, { responseType: "arraybuffer" })
    const arrayBuffer = await response.arrayBuffer()
    const type = response.headers.get('Content-Type')
	const b64 = arrayBufferToBase64(arrayBuffer)
    
    const src = `data:${type};base64,${b64}`
	return [src, type]
}

(function() {
	let doInit;
	let doParseFile;
	let doParseFiles;
	let doPlayFile;
	let doMakeKey;
	
	let allow;
	let players;
	
	allow = [
		"4cdn.org",
		"catbox.moe",
		"dmca.gripe",
		"lewd.se",
		"pomf.cat",
		"zz.ht"
	];
	
	document.addEventListener(
		"DOMContentLoaded",
		function (event) {
			setTimeout(
				function () {
					doInit();
				},
				(1)
			);
		}
	);
	
	doInit = function () {
		let observer;
		
		if (players) {
			return;
		}
		
		players = {};
		
		doParseFiles(document.body);
		
		observer =
			new MutationObserver(
				function (mutations) {
					mutations.forEach(
						function (mutation) {
							if (mutation.type === "childList") {
								mutation.addedNodes.forEach(
									function (node) {
										if (node.nodeType === Node.ELEMENT_NODE) {
											doParseFiles(node);
											doPlayFile(node);
										}
									}
								);
							}
						}
					);
				}
			);
		
		observer
			.observe(
				document.body,
				{
					childList: true,
					subtree: true
				}
			);
	};
	
	doParseFile = function (file) {
		let fileLink;
		let fileName;
		let key;
		let match;
		let player;
		let link;
		
		if (!file.classList.contains("uploadCell")) {
			return;
		}
		
		fileLink = file.querySelector(".originalNameLink");
		
		if (!fileLink) {
			return;
		}
		
		if (!fileLink.href) {
			return;
		}
		
		fileName = fileLink.textContent;
		
		if (!fileName) {
			return;
		}
		
		fileName = fileName.replace(/\-/, "/");
		
		key = doMakeKey(fileLink.href);
		
		if (!key) {
			return;
		}
		
		if (players[key]) {
			return;
		}
		
		match = fileName.match(/[\[\(\{](?:audio|sound)[ \=\:\|\$](.*?)[\]\)\}]/i);
		
		if (!match) {
			return;
		}
		
		link = match[1];
		
		if (link.includes("%")) {
			try {
				link = decodeURIComponent(link);
			} catch (error) {
				return;
			}
		}
		
		
		if (link.match(/^(https?\:)?\/\//) === null) {
			link = (location.protocol + "//" + link);
		}
		
		try {
			link = new URL(link);
		} catch (error) {
			return;
		}
		
		if (
			allow.some(
				function (item) {
					return (
						link.hostname.toLowerCase() === item ||
						link.hostname.toLowerCase().endsWith("." + item)
					);
				}
			) == false
		) {
			return;
		}

		if(key.endsWith('mp4') || key.endsWith('webm')) {
			const video = file.querySelector('video')
			const imgLink = file.querySelector('.imgLink')
			console.log('binding video soundpost', key, video, imgLink)
			imgLink.addEventListener('click', e => {
				doPlayFile(video)
			})
		}
		
		player = new Audio();
		
		player.fetched = false
		player.crossOrigin = 'anonymous';
		player.preload = "none";
		player.volume = 0.80;
		player.loop = true;
		
		player.src = link.href;
		
		players[key] = player;
	};
	
	doParseFiles = function (target) {
		target.querySelectorAll(".innerPost, .innerOP")
			.forEach(
				function (post) {
					if (post.parentElement.classList.contains("quoteTooltip")) {
						return;
					}
					
					if (!post.querySelector('.uploadCell')) {
						return;
					}
					
					post.querySelectorAll(".uploadCell")
						.forEach(
							function (file) {
								doParseFile(file);
							}
						);
				}
			);
	};
	
	doPlayFile = async function (target) {
		let key;
		let player;
		let interval;

		if (!(
			target.matches('video[controls="true"]') && target.parentElement.parentElement.matches('.uploadCell') ||
			target.matches(".imgExpanded") ||
			target.matches('img') && target.parentElement.matches('body') ||
			target.matches('video') && target.parentElement.matches('body')
		)) {
			return;
		}

		if (!target.src && !target.currentSrc) {
			return;
		}
		
		key = doMakeKey(target.src || target.currentSrc);
		
		if (!key) {
			return;
		}
		
		player = players[key];

		if (!player) {
			return;
		}

		if(target.matches('video[controls="true"]')) {
			console.log('found video soundpost')
		}

		console.log('players', players)

		if(target.matches(`.imgExpanded`)) {
			target.addEventListener('click', e => {
				let parent = target.parentElement
				setTimeout(() => {
					target.remove()
					parent.querySelector('img').removeAttribute('style')
				}, 50)
			})
		}

		if(!player.fetched) {
			if(!player.response)
				player.response = fetchSound(player.src)
			let [src, type] = await player.response
            player = new Audio()
            player.fetched = true
            player.response = true
            player.type = type
            player.src = src
			players[key] = player
		}

		if (!player.paused) {
			if (player.dataset.play == 1) {
				player.dataset.again = 1;
			} else {
				player.pause();
			}
		}
		
		if (player.dataset.play != 1) {
			player.dataset.play = 1;
			player.dataset.again = 0;
			player.dataset.moveTime = 0;
			player.dataset.moveLast = 0;
		}
		
		switch (target.tagName) {
			case "IMG":
				player.loop = true;
				
				if (player.dataset.again != 1) {
					player.currentTime = 0;
					player.play();
				}
				
				break;
			
			case "VIDEO":
				player.loop = false;
				player.currentTime = target.currentTime;
				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) && !target.matches('[style$="display: none;"]')) {
						if (target.tagName === "VIDEO") {
							if (target.currentTime != (+player.dataset.moveLast)) {
								player.dataset.moveTime = Date.now();
								player.dataset.moveLast = target.currentTime;
							}
							
							if (player.duration != NaN) {
								if (
									target.paused == true ||
									target.duration == NaN ||
									target.currentTime > player.duration ||
									((Date.now() - (+player.dataset.moveTime)) > 300)
								) {
									if (!player.paused) {
										player.pause();
									}
								} else {
									if (
										player.paused ||
										Math.abs(target.currentTime - player.currentTime) > 0.100
									) {
										player.currentTime = target.currentTime;
									}
									
									if (player.paused) {
										player.play();
									}
								}
							}
						}
					} else {
						clearInterval(interval);
						
						if (player.dataset.again == 1) {
							player.dataset.again = 0;
						} else {
							player.pause();
							player.dataset.play = 0;
						}
					}
				},
				(1000/30)
			);
	};
	
	doMakeKey = function (link) {
		let match;
		match = link.match(/https\:\/\/8chan\.(?:cc|moe|se)\/\.media\/(.+?)\.(.+)$/);
		
		if (match) {
			return (match[1] + "." + match[2]);
		}
		
		return null;
	};
})();