Scrolller.com Autoplay Feed

Autoplay Videos in Feed on Scrolller.com

目前為 2025-06-21 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name            Scrolller.com Autoplay Feed
// @name:de         Scrolller.com Automatische Wiedergabe im Feed
// @version         1.0.4
// @description     Autoplay Videos in Feed on Scrolller.com
// @description:de  Spiele Videos im Feed automatisch ab auf Scrolller.com
// @icon            https://scrolller.com/assets/favicon-16x16.png
// @author          TalkLounge (https://github.com/TalkLounge)
// @namespace       https://github.com/TalkLounge/scrolller.com-autoplay-feed
// @license         MIT
// @match           https://scrolller.com/*
// @grant           none
// @run-at          document-start
// ==/UserScript==

(function () {
    'use strict';

    let interval, muted = true;
    let cooldown = Date.now();

    function video2SVGParent(video) {
        const parent = video.parentNode.parentNode.parentNode;
        //console.log(`video2SVGParent():`, video, "Return:", parent);
        return parent;
    }

    function insertSound(parent, mute) {
        if (!parent || [...parent.classList].includes("noaudio")) {
            return;
        }
        //console.log(`insertSound():`, parent, mute);

        parent.querySelector(".sound")?.remove();

        let html;
        if (mute) {
            html = `
            <svg class="sound muted" viewBox="0 0 512 512" style="fill: grey; position: absolute; z-index: 1; width: 1.5em; cursor: pointer; margin-left: 0.2em">
                <path d="M232 416a23.88 23.88 0 01-14.2-4.68 8.27 8.27 0 01-.66-.51L125.76 336H56a24 24 0 01-24-24V200a24 24 0 0124-24h69.75l91.37-74.81a8.27 8.27 0 01.66-.51A24 24 0 01256 120v272a24 24 0 01-24 24zm-106.18-80zm-.27-159.86zM320 336a16 16 0 01-14.29-23.19c9.49-18.87 14.3-38 14.3-56.81 0-19.38-4.66-37.94-14.25-56.73a16 16 0 0128.5-14.54C346.19 208.12 352 231.44 352 256c0 23.86-6 47.81-17.7 71.19A16 16 0 01320 336z"></path>
                <path d="M368 384a16 16 0 01-13.86-24C373.05 327.09 384 299.51 384 256c0-44.17-10.93-71.56-29.82-103.94a16 16 0 0127.64-16.12C402.92 172.11 416 204.81 416 256c0 50.43-13.06 83.29-34.13 120a16 16 0 01-13.87 8z"></path>
                <path d="M416 432a16 16 0 01-13.39-24.74C429.85 365.47 448 323.76 448 256c0-66.5-18.18-108.62-45.49-151.39a16 16 0 1127-17.22C459.81 134.89 480 181.74 480 256c0 64.75-14.66 113.63-50.6 168.74A16 16 0 01416 432z"></path>
            </svg>`;
        } else {
            html = `
            <svg class="sound volume" viewBox="0 0 512 512" style="fill: white; position: absolute; z-index: 1; width: 1.5em; cursor: pointer; margin-left: 0.2em">
                <path d="M232 416a23.88 23.88 0 01-14.2-4.68 8.27 8.27 0 01-.66-.51L125.76 336H56a24 24 0 01-24-24V200a24 24 0 0124-24h69.75l91.37-74.81a8.27 8.27 0 01.66-.51A24 24 0 01256 120v272a24 24 0 01-24 24zm-106.18-80zm-.27-159.86zM320 336a16 16 0 01-14.29-23.19c9.49-18.87 14.3-38 14.3-56.81 0-19.38-4.66-37.94-14.25-56.73a16 16 0 0128.5-14.54C346.19 208.12 352 231.44 352 256c0 23.86-6 47.81-17.7 71.19A16 16 0 01320 336z"></path>
                <path d="M368 384a16 16 0 01-13.86-24C373.05 327.09 384 299.51 384 256c0-44.17-10.93-71.56-29.82-103.94a16 16 0 0127.64-16.12C402.92 172.11 416 204.81 416 256c0 50.43-13.06 83.29-34.13 120a16 16 0 01-13.87 8z"></path>
                <path d="M416 432a16 16 0 01-13.39-24.74C429.85 365.47 448 323.76 448 256c0-66.5-18.18-108.62-45.49-151.39a16 16 0 1127-17.22C459.81 134.89 480 181.74 480 256c0 64.75-14.66 113.63-50.6 168.74A16 16 0 01416 432z"></path>
            </svg>`;
        }
        html = html.replace(/>\s+</g, '><').trim(); // Clean up formatted html, Thanks to https://stackoverflow.com/a/27841683
        const child = new DOMParser().parseFromString(html, "text/html");

        child.body.firstChild.addEventListener("click", (e) => {
            e.stopPropagation();

            const svg = e.target.closest(".sound");
            //console.log("Clicked Sound Button", svg);

            document.querySelectorAll("video").forEach(video => {
                if (!video.muted) {
                    insertSound(video2SVGParent(video), true);
                }

                video.muted = true;
            });

            if ([...svg.classList].includes("muted")) {
                //console.log("Muted");
                muted = false;

                parent.querySelector("video").muted = false;

                insertSound(parent);
            } else {
                //console.log("Unmuted");
                muted = true;

                insertSound(parent, true);
            }
        });

        parent.insertBefore(child.body.firstChild, parent.firstChild);
    }

    function parseJSON(data) {
        try {
            return JSON.parse(data);
        } catch (e) {
            const possibleEnds = [...data.matchAll(/\]\]/g)].map(match => match.index);
            for (let i = 0; i < possibleEnds.length; i++) { // Try possible ends of the Array
                try {
                    return JSON.parse(data.slice(0, possibleEnds[i] + 2));
                } catch (e) { }
            }

            throw e;
        }
    }

    async function loadVideo(parent) {
        if ([...parent.classList].includes("loaded")) {
            return;
        }
        //console.log(`loadVideo():`, parent);

        parent.classList.add("loaded");
        parent.querySelector("div>svg").parentNode.remove();

        let response = await fetch(parent.querySelector("a").href);
        response = await response.text();
        response = new DOMParser().parseFromString(response, "text/html");

        let data;
        try { // Try old first
            data = [...response.querySelectorAll("head script")];
            data = data.find(item => item.innerText.includes("window.scrolllerConfig"));
            data = data.textContent;
            data = data.replace("window.scrolllerConfig=", "");
            data = data.replace(/\\'/g, "'");
            data = JSON.parse(JSON.parse(data));
            data = data.item.mediaSources;
        } catch (e) { // Then try new
            data = [...response.querySelectorAll("script")];
            data = data.find(item => item.innerText.includes("mediaSources") && item.innerText.includes("blurredMediaSources"));

            if (!data) {
                console.log("----------");
                console.error("Could not load url scripts");
                console.log(parent.querySelector("a").href);
                return;
            }

            data = data.innerText;
            const dataIntial = data;


            // Base
            data = data.replace('self.__next_f.push([1,"', "");


            if (data.indexOf('],\"default\"]') != -1) { // Delete static/chunks Array
                data = data.slice(data.indexOf('],\"default\"]'));

                console.log(data.indexOf(':[\"$undefined\"'));
                if (data.indexOf(':[\"$undefined\"') != -1) {
                    data = data.slice(data.indexOf(':[\"$undefined\"') + 15);
                    console.log(data);
                }
            }

            if (data.indexOf('dangerouslySetInnerHTML\":{\"__html\":\"$') != -1) { // Delete empty dangerouslySetInnerHTML
                data = data.slice(data.indexOf('dangerouslySetInnerHTML\":{\"__html\":\"$') + 36);
            }

            // Delete parts of another JSON / Array
            while (data.indexOf("}") < data.indexOf("{")) {
                data = data.slice(data.indexOf("}") + 1);
            }

            while (data.indexOf("]") < data.indexOf("[")) {
                data = data.slice(data.indexOf("]") + 1);
            }

            data = data.replace(/^,/, "");
            data = data.trim();


            // Detect Type
            let type;
            if (/^\d+:\[\[/.test(data)) { // 8:[[
                console.log('Test Case is 8:[[');
                type = 0;
            } else if (/^\[\"\$\",/.test(data)) { // ["$",
                console.log('Test Case is ["$",');
                type = 1;
            } else if (/^\[\\"\$\\",/.test(data)) { // ["$",
                console.log('Test Case is ["$",');
                type = 2;
            } else if (/^[A-Za-z]+,/.test(data)) { // ,null
                console.log('Test Case is ,null');
                type = 3;
            }


            // Start
            if (type == 0) {
                data = data.replace(/^\d+:\[\[/, "[[");
            } else if (type == 1 || type == 2) {
                data = data.replace(/^\[/, "[[], [");
            } else if (type == 3) {
                data = data.replace(/^[A-Za-z]+,/, '[[], ["", "", null,');
            }


            // De-Escape
            if (type == 0) {
                data = data.replace(/:"{/g, ":{");
                data = data.replace(/}"}/g, "}}");

                data = data.replace(/:\\"\{/g, ":{");
                data = data.replace(/}\\"\}/g, "}}");
            }

            data = data.replace(/\\"/g, '"');
            data = data.replace(/\\"/g, '"');
            data = data.replace(/\\"/g, '"');

            if (type == 0) {
                data = data.replace(/\"\"/g, '"');
                data = data.replaceAll('":",', '":"",');
            }


            // End
            data = data.replace(/\"\]\)$/, "");
            data = data.replace(/\n/g, "");


            // Parse JSON
            try {
                const dataBefore = data;
                data = parseJSON(data);

                if (typeof (data) != "object") {
                    console.log("----------");
                    console.error("JSON is not a JSON");
                    console.log("Data Initial\n", dataIntial);
                    console.log("Data Before\n", dataBefore);
                    console.log("Data After\n", data);
                    return;
                }
            } catch (e) {
                console.log("----------");
                console.error("Unable to parse JSON");
                console.error(e);
                console.log("Data Initial\n", dataIntial);
                console.log("Data Before\n", data);
                return;
            }

            data = data[1][3].post.mediaSources;
        }

        data = data.filter(item => (item.url.endsWith(".webm") || item.url.endsWith(".mp4")) && !item.url.endsWith("_thumb."));
        data = data.map(item => item.url);

        const video = document.createElement("video");
        video.autoplay = true;
        video.muted = true;
        video.loop = true;
        video.style.height = "100%";
        video.style.position = "absolute";
        video.addEventListener("loadeddata", () => {
            parent.querySelector("picture").remove();

            const hasAudio = video.mozHasAudio || Boolean(video.webkitAudioDecodedByteCount) || Boolean(video.audioTracks && video.audioTracks.length);

            if (!hasAudio) {
                parent.classList.add("noaudio");
            }

            insertSound(parent, true);
        });

        for (const src of data) {
            const source = document.createElement("source");
            source.src = src;
            video.append(source);
        }

        parent.querySelector("a>div").insertBefore(video, parent.querySelector("a>div").firstChild);
    }

    function loadVideos() {
        const items = document.querySelectorAll("main>div>div>div a:has(div>svg), [class^=verticalView_container]>div>div a:has(div>svg)");

        for (const item of items) {
            loadVideo(item.parentNode);
        }
    }

    async function init() {
        if (!document.querySelector("main>div>div>div picture, [class^=verticalView_container]>div>div picture")) {
            return;
        }

        window.clearInterval(interval);

        window.setInterval(loadVideos, 500);

        document.body.onscroll = function () {
            if (muted) {
                return;
            }

            if (Date.now() - cooldown < 250) {
                return;
            }
            cooldown = Date.now();

            //console.log("Scroll");
            let diffMin = 1000000;
            let nearest;
            let middle = window.innerHeight / 2;

            document.querySelectorAll("video").forEach(video => {
                const loud = video2SVGParent(video).querySelector(".sound:not(.muted)");
                if (loud) {
                    insertSound(video2SVGParent(video), true);
                }
                video.muted = true;
                const rect = video.getBoundingClientRect();
                const elemMiddle = rect.y + (rect.height / 2);
                const diff = Math.abs(middle - elemMiddle);
                if (diff < diffMin) {
                    diffMin = diff;
                    nearest = video;
                }
            });

            if (!nearest || diffMin > middle || video2SVGParent(nearest).querySelector(".sound:not(.muted)")) {
                return;
            }

            nearest.muted = false;

            insertSound(video2SVGParent(nearest));
        }
    }

    interval = window.setInterval(init, 500);
})();