An auto-like script for Douyin Live.
// ==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();
})();