fourier-like

An auto-like script for Douyin Live.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Вам потребуется установить расширение, например Tampermonkey или Violentmonkey, чтобы установить этот скрипт.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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