// ==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;
};
})();