Scrolller.com Autoplay Feed

Autoplay Videos in Feed on Scrolller.com

当前为 2025-06-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
})();