fourier-like

An auto-like script for Douyin Live.

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();