SturdyChan External Sounds

Plays 4chan-style soundposts on SturdyChan

// ==UserScript==
// @name         SturdyChan External Sounds
// @namespace    Sturdychan
// @description  Plays 4chan-style soundposts on SturdyChan
// @author       anonVNscripts
// @version      1.0.0
// @match        *://sturdychan.help/*
// @run-at       document-start
// ==/UserScript==

(function() {
    var doInit;
    var doParseFile;
    var doParseFiles;
    var doPlayFile;
    var doMakeKey;

    var allow;
    var players;

    // Allowed domains for sound files
    allow = [
        "4cdn.org",
        "catbox.moe",
        "dmca.gripe",
        "lewd.se",
        "pomf.cat",
        "zz.ht"
    ];

    // Initialize when DOM is ready.
    document.addEventListener("DOMContentLoaded", function () {
        setTimeout(doInit, 1);
    });

    doInit = function () {
        var 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
        });
    };

    // Modified doParseFile: accepts either an anchor (<a>) that contains the sound marker
    // or a container element from which we try to extract an anchor.
    doParseFile = function (elem) {
        var fileLink, fileName, key, match, player, link;

        // If the element is an anchor, use it directly.
        if (elem.tagName === "A") {
            fileLink = elem;
            // Prefer the download attribute if available; otherwise, use the text content.
            fileName = fileLink.download || fileLink.textContent;
        } else if (elem.classList.contains("file")) {
            // For backward compatibility: search inside a container with class "file"
            fileLink = elem.querySelector("a");
            if (fileLink) {
                fileName = fileLink.title || fileLink.textContent;
            }
        } else {
            return;
        }

        if (!fileLink || !fileLink.href) {
            return;
        }

        if (!fileName) {
            return;
        }

        // Replace a hyphen with a slash (mirroring original behavior)
        fileName = fileName.replace(/\-/, "/");

        key = doMakeKey(fileLink);
        if (!key) {
            return;
        }

        if (players[key]) {
            return;
        }

        // Look for an audio marker such as [sound=...]
        match = fileName.match(/[\[\(\{](?:audio|sound)[ \=\:\|\$](.*?)[\]\)\}]/i);
        if (!match) {
            return;
        }

        link = match[1];

        // Decode percent-encoded characters if needed.
        if (link.includes("%")) {
            try {
                link = decodeURIComponent(link);
            } catch (error) {
                return;
            }
        }

        // Ensure the link is a full URL; if not, prepend the page protocol.
        if (link.match(/^(https?\:)?\/\//) === null) {
            link = (location.protocol + "//" + link);
        }

        try {
            link = new URL(link);
        } catch (error) {
            return;
        }

        // Make sure the host for the sound file is allowed.
        if (allow.some(function (item) {
            return link.hostname.toLowerCase() === item ||
                   link.hostname.toLowerCase().endsWith("." + item);
        }) === false) {
            return;
        }

        // Create and configure the audio player.
        player = new Audio();
        player.preload = "none";
        player.volume = 0.80;
        player.loop = true;
        player.src = link.href;

        players[key] = player;
    };

    // Parse all potential file containers or anchor elements within the target node.
    doParseFiles = function (target) {
        // Look for elements with class "file"
        target.querySelectorAll(".file").forEach(function (node) {
            doParseFile(node);
        });
        // Additionally, check for anchor elements that might be direct links with sound markers.
        target.querySelectorAll("a").forEach(function (node) {
            // Check if the download attribute or text contains "[sound"
            if ((node.download && node.download.includes("[sound")) ||
                (node.textContent && node.textContent.includes("[sound"))) {
                doParseFile(node);
            }
        });
    };

    // Modified doPlayFile remains similar, relying on the key generated via doMakeKey.
    doPlayFile = function (target) {
        var key, player, interval;

        // Adjust the selectors based on what sturdychan.help uses for preview images/videos.
        if (!(
            target.id === "image-hover" ||
            target.className === "expanded-thumb" ||
            target.className === "expandedWebm" ||
            target.tagName === "IMG"  // also consider standalone images
        )) {
            return;
        }

        if (!target.src) {
            return;
        }

        key = doMakeKey(target);
        if (!key) {
            return;
        }

        player = players[key];
        if (!player) {
            return;
        }

        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)) {
                if (target.tagName === "VIDEO") {
                    if (target.currentTime != (+player.dataset.moveLast)) {
                        player.dataset.moveTime = Date.now();
                        player.dataset.moveLast = target.currentTime;
                    }
                    if (!isNaN(player.duration) && (
                        target.paused === true ||
                        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);
    };

    // Modified doMakeKey:
    // If the file element (or anchor) has a data-hash attribute, use that.
    // Otherwise, try to extract an identifier from the href.
    doMakeKey = function(elem) {
        // If the element has a dataset with a hash, use it.
        if (elem.dataset && elem.dataset.hash) {
            return elem.dataset.hash;
        }

        // Otherwise, try to use the href.
        // If elem is an image (or has a src), use that; else use href.
        var urlString = (elem.src) ? elem.src : elem.href;
        if (!urlString) {
            return null;
        }

        // Try to match URLs of the form:
        // https://sturdychan.help/assets/images/src/<hash>.<ext>
        var match = urlString.match(/sturdychan\.help\/assets\/images\/src\/([a-f0-9]+)\.(?:jpg|png|gif|webm|m4a)/i);
        if (match) {
            return match[1];
        }
        return null;
    };

})();