fourier-like

An auto-like script for Douyin Live.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         fourier-like
// @name:zh-CN   傅里叶点赞    
// @namespace    https://github.com/ryu-dayo/fourier-like
// @version      1.0.0
// @description  An auto-like script for Douyin Live.
// @description:zh-CN   自动为抖音直播间持续点赞。
// @author       Zhuorui
// @match        https://live.douyin.com/*
// @match        https://www.douyin.com/*/live/*
// @exclude      https://live.douyin.com/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=douyin.com
// @grant        none
// @license      GPL-3.0
// ==/UserScript==

(function () {
    'use strict';

    const ICONS = {
        enableAutoLike: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81 86"><path d="M0 58.2c0 12.6 8.3 23.4 18.4 23.4H31q7.6 4.2 18 4.3h5.2a52 52 0 0 0 11.6-1c5.4-1.2 8.9-5 8.9-9.9q0-1.3-.4-2.5c2.6-2 4-5 4-8.1q0-2.3-.8-4.2 2.6-3 2.7-7.2 0-2.7-1.1-5 1.5-2.5 1.6-6c0-5.9-4.6-10.5-10.5-10.5H55.8q-.8 0-.9-.7c0-4 6.4-13.6 6.4-21.3C61.3 4 57.5 0 52.2 0c-4 0-6.5 2-9 6.9a156 156 0 0 1-19 27.6H17c-9.4 0-17 10.7-17 23.7m22.8-.3c0-8.1 1.8-13.3 6.9-20.1 5.7-7.6 13.6-16.7 19.2-28 1.4-2.8 2.3-3.4 3.6-3.4q2.3 0 2.4 3c0 6.1-6.4 15.4-6.4 21.4 0 4.4 3.6 7 8.2 7h13.5a4 4 0 0 1 4 4.2c0 1.8-.6 2.9-2.1 4.5q-1 1-.2 2c1.3 2 1.8 3 1.8 4.5q0 2.5-2.6 4.5-1.4 1.1-.7 2.9c1 1.9 1.4 2.6 1.4 4q.1 2.8-4 4.9-1.3.8-.7 1.9c1 2.6 1 2.9 1 3.8q0 2.6-3.8 3.7-3.5.8-10 .8h-4.8c-16 0-26.7-9-26.7-21.6m-16.4.3c0-9.5 5-17.3 10.6-17.3h3q-3.7 7.4-3.6 16.8.1 10.3 6.4 17.5h-4.4c-6.3 0-12-7.9-12-17"/></svg>',
        disableAutoLike: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 78 81"><path d="M0 55.3c0 11.6 7.3 21.4 17 21.4h6.6c-7-5.3-9.7-13-9.7-21.8 0-9.7 3.8-16.9 7.2-21.1h-5.5C6.8 33.8 0 43.2 0 55.3m19.6-.3c0 14.3 11.2 25.4 29.5 25.4h5.3q7.7 0 11-1c3-.7 6-2.5 6-6.3q0-2.2-.9-3.4-.4-.9.3-1.1a7 7 0 0 0 4.4-6.4 7 7 0 0 0-1.4-4.4q-.7-1 .4-1.6c1.7-1 3-3.2 3-5.8 0-1.8-.6-3.7-1.7-4.6q-.8-.8.1-1.6 2-1.6 2-5c0-3.8-2.8-7-6.8-7H57c-3.5 0-5.8-1.7-5.8-4.6 0-5.2 6.6-14.9 6.6-21.8 0-3.7-2.4-5.8-5.5-5.8-2.8 0-4.2 2-5.7 4.9C40.8 16.2 33 25.4 27.1 33.3c-5 6.7-7.5 12.5-7.5 21.7"/></svg>',
    };

    const STYLE = `
        .fl-container {
            position: absolute;
            bottom: 64px;
            right: 56px;
            z-index: 99;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .fl-btn {
            display: inline-flex;
            background-color: transparent;
            border: none;
            cursor: pointer;
        }

        .fl-picture {
            width: 36px;
            height: 36px;
            background-color: #fff;
            mix-blend-mode: plus-lighter;
            mask-image: var(--icon);
            mask-size: 100% 100%;
            mask-repeat: no-repeat;
            transition: transform 0.15s;
            pointer-events: none;
        }

        .fl-picture { --icon: url('${ICONS.enableAutoLike}'); }
        .fl-picture[data-active="true"] { --icon: url('${ICONS.disableAutoLike}'); }

        .fl-btn:active picture { transform: scale(0.89); }
    `;

    class UIManager {
        constructor(onToggle) {
            this.container = this.createContainer();
            this.onToggle = onToggle;
            this.init();
        }

        init() {
            if (!document.getElementById('fl-style')) {
                const style = document.createElement('style');
                style.id = 'fl-style';
                style.textContent = STYLE;
                document.head.appendChild(style);
            }

            const btn = this.createToggleButton();
            this.container.appendChild(btn);
        }

        createContainer() {
            const container = document.createElement('div');
            container.classList.add('fl-container');
            return container;
        }

        createToggleButton() {
            const btn = this.createButton('fl-picture', () => this.onToggle());
            return btn;
        }

        createButton(cls, onClick) {
            const picture = document.createElement('picture');
            picture.classList.add(cls);

            const btn = document.createElement('button');
            btn.classList.add('fl-btn');
            btn.appendChild(picture);

            btn.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                onClick();
            });

            return btn;
        };

        setActive(active) {
            const picture = this.container.querySelector('.fl-picture');
            if (picture) picture.setAttribute('data-active', active);
        };

        attachToVideo(video) {
            if (!video || !video.parentElement) return false;
            if (this.container.parentElement !== video.parentElement) {
                video.parentElement.appendChild(this.container);
            }
            return true;
        }
    }

    class App {
        constructor() {
            this.ui = new UIManager(() => this.toggleAutoLike());
            this.MIN_CLICK_DELAY_MS = 200;
            this.MAX_CLICK_DELAY_MS = 300;
            this.LIMIT_COOLDOWN_MS = 600000;

            this.STATE_CHECK_INTERVAL_MS = 5000;
            this.lastStateCheckTime = 0;
            this.cachedRoomState = 'live';

            this.likeCount = 0;
            this.pressTimer = null;
            this.observer = null;
            this.observerTimer = null;

            this.isAutoPressing = false;
            this.resumeAfterAttach = false;
            this.waitForVideo(false);
        }

        toggleAutoLike() {
            this.isAutoPressing ? this.stopAutoLike() : this.startAutoLike();
        };

        stopWaitingForVideo() {
            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }
            if (this.observerTimer) {
                clearTimeout(this.observerTimer);
                this.observerTimer = null;
            }
        }

        waitForVideo(resumeAfterAttach) {
            this.stopWaitingForVideo();
            this.resumeAfterAttach = Boolean(resumeAfterAttach);

            const video = document.querySelector('video');
            if (video) {
                this.handleVideoReady(video);
                return;
            }

            this.observer = new MutationObserver(() => {
                const video = document.querySelector('video');
                if (video) {
                    this.handleVideoReady(video);
                }
            });

            this.observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            this.observerTimer = setTimeout(() => {
                this.stopWaitingForVideo();
            }, 10000);
        };

        handleVideoReady(video) {
            if (!this.ui.attachToVideo(video)) return;
            this.stopWaitingForVideo();
            if (this.resumeAfterAttach) {
                this.resumeAfterAttach = false;
                this.resumeAutoLike();
            }
        };

        startAutoLike() {
            this.likeCount = 0;
            console.log('Auto-like started');
            this.activateAutoLike();
        };

        resumeAutoLike() {
            if (this.isAutoPressing) return;
            console.log('Auto-like resumed');
            this.activateAutoLike();
        }

        activateAutoLike() {
            this.clearPressTimer();
            this.isAutoPressing = true;
            this.lastStateCheckTime = 0;
            this.runAutoLikeLoop();
            this.ui.setActive(true);
        }

        stopAutoLike() {
            this.isAutoPressing = false;
            this.resumeAfterAttach = false;
            this.clearPressTimer();
            console.log('Auto-like stopped, total likes: ' + this.likeCount);
            this.ui.setActive(false);
        };

        clearPressTimer() {
            if (this.pressTimer) {
                clearTimeout(this.pressTimer);
                this.pressTimer = null;
            }
        }

        sendLikeKey() {
            const event = new KeyboardEvent('keydown', {
                key: 'z',
                code: 'KeyZ',
                keyCode: 90,
                which: 90,
                bubbles: true,
                cancelable: true
            });

            document.dispatchEvent(event);
            this.likeCount++;
        }

        getRoomStateThrottled() {
            const now = Date.now();
            if (now - this.lastStateCheckTime > this.STATE_CHECK_INTERVAL_MS) {
                this.cachedRoomState = this.checkRoomState();
                this.lastStateCheckTime = now;
            }
            return this.cachedRoomState;
        }

        runAutoLikeLoop() {
            if (!this.isAutoPressing) return;

            if (!document.body.contains(this.ui.container)) {
                this.clearPressTimer();
                this.isAutoPressing = false;
                this.waitForVideo(true);
                return;
            }

            const state = this.getRoomStateThrottled();
            let delay = 0;

            if (state === 'ended') { this.stopAutoLike(); return; }
            if (state === 'cooldown') { console.log('Auto-like on cooldown'); delay = this.LIMIT_COOLDOWN_MS; }
            else { this.sendLikeKey(); delay = this.getNextDelay(); }

            this.pressTimer = setTimeout(() => {
                this.pressTimer = null;
                this.runAutoLikeLoop();
            }, delay);
        };

        getNextDelay() {
            return Math.floor(Math.random() * (this.MAX_CLICK_DELAY_MS - this.MIN_CLICK_DELAY_MS + 1)) + this.MIN_CLICK_DELAY_MS;
        };

        checkRoomState() {
            if (document.querySelector('.LGWzuUuN')) return 'ended';

            const popup = document.querySelector('.VBIRbGZt');
            if (popup) {
                const text = popup.textContent || '';
                if (text.includes('直播已结束')) return 'ended';
                else return 'cooldown';
            }

            return 'live';
        }
    }

    new App();
})();