4chan External Sounds

Plays audio associated with images on 4chan.

  1. // ==UserScript==
  2. // @name 4chan External Sounds
  3. // @namespace b4k
  4. // @description Plays audio associated with images on 4chan.
  5. // @author Bakugo
  6. // @version 1.7.0
  7. // @match *://boards.4chan.org/*
  8. // @match *://boards.4channel.org/*
  9. // @run-at document-start
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. var doInit;
  14. var doParseFile;
  15. var doParseFiles;
  16. var doPlayFile;
  17. var doMakeKey;
  18. var allow;
  19. var players;
  20. var isChanX;
  21. allow = [
  22. "4cdn.org",
  23. "catbox.moe",
  24. "dmca.gripe",
  25. "lewd.se",
  26. "pomf.cat",
  27. "zz.ht"
  28. ];
  29. document.addEventListener(
  30. "DOMContentLoaded",
  31. function (event) {
  32. setTimeout(
  33. function () {
  34. if (
  35. document.body.classList.contains("ws") ||
  36. document.body.classList.contains("nws")
  37. ) {
  38. isChanX = false;
  39. doInit();
  40. }
  41. },
  42. (1)
  43. );
  44. }
  45. );
  46. document.addEventListener(
  47. "4chanXInitFinished",
  48. function (event) {
  49. if (
  50. document.documentElement.classList.contains("fourchan-x") &&
  51. document.documentElement.classList.contains("sw-yotsuba")
  52. ) {
  53. isChanX = true;
  54. doInit();
  55. }
  56. }
  57. );
  58. doInit = function () {
  59. var observer;
  60. if (players) {
  61. return;
  62. }
  63. players = {};
  64. doParseFiles(document.body);
  65. observer =
  66. new MutationObserver(
  67. function (mutations) {
  68. mutations.forEach(
  69. function (mutation) {
  70. if (mutation.type === "childList") {
  71. mutation.addedNodes.forEach(
  72. function (node) {
  73. if (node.nodeType === Node.ELEMENT_NODE) {
  74. doParseFiles(node);
  75. doPlayFile(node);
  76. }
  77. }
  78. );
  79. }
  80. }
  81. );
  82. }
  83. );
  84. observer
  85. .observe(
  86. document.body,
  87. {
  88. childList: true,
  89. subtree: true
  90. }
  91. );
  92. };
  93. doParseFile = function (file) {
  94. var fileLink;
  95. var fileName;
  96. var key;
  97. var match;
  98. var player;
  99. var link;
  100. if (!file.classList.contains("file")) {
  101. return;
  102. }
  103. if (isChanX) {
  104. fileLink = file.querySelector(".fileText .file-info > a");
  105. } else {
  106. fileLink = file.querySelector(".fileText > a");
  107. }
  108. if (!fileLink) {
  109. return;
  110. }
  111. if (!fileLink.href) {
  112. return;
  113. }
  114. fileName = null;
  115. if (isChanX) {
  116. [
  117. file.querySelector(".fileText .file-info .fnfull"),
  118. file.querySelector(".fileText .file-info > a")
  119. ].some(
  120. function (node) {
  121. if (node) {
  122. if (node.textContent) {
  123. fileName = node.textContent;
  124. return true;
  125. }
  126. }
  127. return false;
  128. }
  129. );
  130. } else {
  131. [
  132. file.querySelector(".fileText"),
  133. file.querySelector(".fileText > a")
  134. ].some(
  135. function (node) {
  136. if (node) {
  137. if (node.title) {
  138. fileName = node.title;
  139. return true;
  140. }
  141. if (
  142. node.tagName === "A" &&
  143. node.textContent
  144. ) {
  145. fileName = node.textContent;
  146. return true;
  147. }
  148. }
  149. return false;
  150. }
  151. );
  152. }
  153. if (!fileName) {
  154. return;
  155. }
  156. fileName = fileName.replace(/\-/, "/");
  157. key = doMakeKey(fileLink.href);
  158. if (!key) {
  159. return;
  160. }
  161. if (players[key]) {
  162. return;
  163. }
  164. match = fileName.match(/[\[\(\{](?:audio|sound)[ \=\:\|\$](.*?)[\]\)\}]/i);
  165. if (!match) {
  166. return;
  167. }
  168. link = match[1];
  169. if (link.includes("%")) {
  170. try {
  171. link = decodeURIComponent(link);
  172. } catch (error) {
  173. return;
  174. }
  175. }
  176. if (link.match(/^(https?\:)?\/\//) === null) {
  177. link = (location.protocol + "//" + link);
  178. }
  179. try {
  180. link = new URL(link);
  181. } catch (error) {
  182. return;
  183. }
  184. if (
  185. allow.some(
  186. function (item) {
  187. return (
  188. link.hostname.toLowerCase() === item ||
  189. link.hostname.toLowerCase().endsWith("." + item)
  190. );
  191. }
  192. ) == false
  193. ) {
  194. return;
  195. }
  196. player = new Audio();
  197. player.preload = "none";
  198. player.volume = 0.80;
  199. player.loop = true;
  200. player.src = link.href;
  201. players[key] = player;
  202. };
  203. doParseFiles = function (target) {
  204. target.querySelectorAll(".post")
  205. .forEach(
  206. function (post) {
  207. if (post.parentElement.parentElement.id === "qp") {
  208. return;
  209. }
  210. if (post.parentElement.classList.contains("noFile")) {
  211. return;
  212. }
  213. post.querySelectorAll(".file")
  214. .forEach(
  215. function (file) {
  216. doParseFile(file);
  217. }
  218. );
  219. }
  220. );
  221. };
  222. doPlayFile = function (target) {
  223. var key;
  224. var player;
  225. var interval;
  226. if (isChanX) {
  227. if (!(
  228. target.id === "ihover" ||
  229. target.className === "full-image"
  230. )) {
  231. return;
  232. }
  233. } else {
  234. if (!(
  235. target.id === "image-hover" ||
  236. target.className === "expanded-thumb" ||
  237. target.className === "expandedWebm"
  238. )) {
  239. return;
  240. }
  241. }
  242. if (!target.src) {
  243. return;
  244. }
  245. key = doMakeKey(target.src);
  246. if (!key) {
  247. return;
  248. }
  249. player = players[key];
  250. if (!player) {
  251. return;
  252. }
  253. if (!player.paused) {
  254. if (player.dataset.play == 1) {
  255. if (isChanX) {
  256. return;
  257. } else {
  258. player.dataset.again = 1;
  259. }
  260. } else {
  261. player.pause();
  262. }
  263. }
  264. if (player.dataset.play != 1){
  265. player.dataset.play = 1;
  266. player.dataset.again = 0;
  267. player.dataset.moveTime = 0;
  268. player.dataset.moveLast = 0;
  269. }
  270. switch (target.tagName) {
  271. case "IMG":
  272. player.loop = true;
  273. if (player.dataset.again != 1) {
  274. player.currentTime = 0;
  275. player.play();
  276. }
  277. break;
  278. case "VIDEO":
  279. player.loop = false;
  280. player.currentTime = target.currentTime;
  281. player.play();
  282. break;
  283. default:
  284. return;
  285. }
  286. if (player.paused) {
  287. document.dispatchEvent(
  288. new CustomEvent(
  289. "CreateNotification",
  290. {
  291. bubbles: true,
  292. detail: {
  293. type: "warning",
  294. content: "Your browser blocked autoplay, click anywhere on the page to activate it and try again.",
  295. lifetime: 5
  296. }
  297. }
  298. )
  299. );
  300. }
  301. interval =
  302. setInterval(
  303. function () {
  304. if (document.body.contains(target)) {
  305. if (target.tagName === "VIDEO") {
  306. if (target.currentTime != (+player.dataset.moveLast)) {
  307. player.dataset.moveTime = Date.now();
  308. player.dataset.moveLast = target.currentTime;
  309. }
  310. if (player.duration != NaN) {
  311. if (
  312. target.paused == true ||
  313. target.duration == NaN ||
  314. target.currentTime > player.duration ||
  315. ((Date.now() - (+player.dataset.moveTime)) > 300)
  316. ) {
  317. if (!player.paused) {
  318. player.pause();
  319. }
  320. } else {
  321. if (
  322. player.paused ||
  323. Math.abs(target.currentTime - player.currentTime) > 0.100
  324. ) {
  325. player.currentTime = target.currentTime;
  326. }
  327. if (player.paused) {
  328. player.play();
  329. }
  330. }
  331. }
  332. }
  333. } else {
  334. clearInterval(interval);
  335. if (player.dataset.again == 1) {
  336. player.dataset.again = 0;
  337. } else {
  338. player.pause();
  339. player.dataset.play = 0;
  340. }
  341. }
  342. },
  343. (1000/30)
  344. );
  345. };
  346. doMakeKey = function (link) {
  347. var match;
  348. match = link.match(/\.(?:4cdn|4chan)\.org\/(.+?)\/(\d+?)\.(.+?)$/);
  349. if (match) {
  350. return (match[1] + "." + match[2]);
  351. }
  352. return null;
  353. };
  354. })();