Douyin Tools - Quick Profile & Smart Block

Q keyboard opens author profile + Auto block author and close page (with auto_block parameter)|Q键盘打开当前推荐视频作者主页 + 支持自动拉黑作者并关闭页面(带auto_block参数)|关键词:抖音拉黑,推荐页面一键拉黑

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

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

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

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

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

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Douyin Tools - Quick Profile & Smart Block
// @namespace    http://tampermonkey.net/
// @version      2.7
// @description  Q keyboard opens author profile + Auto block author and close page (with auto_block parameter)|Q键盘打开当前推荐视频作者主页 + 支持自动拉黑作者并关闭页面(带auto_block参数)|关键词:抖音拉黑,推荐页面一键拉黑
// @author       Bela Proinsias
// @match        *://www.douyin.com/*
// @icon         https://www.douyin.com/favicon.ico
// @grant        GM_openInTab
// @grant        GM_notification
// @license      GPLv3
// ==/UserScript==

(function() {
    "use strict";

    const BLOCK_TIMEOUT = 15000;
    const BASE_DELAY = 1000;
    const MIN_DELAY = 200;
    const WAIT_TIMEOUT = 8000;
    const CLICK_DELAY = 200;
    const CONFIRM_DELAY = 600;

    let g_taskQueue = [];
    let g_isProcessing = false;
    let g_statusDiv = null;
    let g_messageDiv = null;
    let g_messageTimer = null;

    let g_statusUserIdSpan = null;
    let g_statusCountSpan = null;

    if (!document.getElementById('block-task-style')) {
        const style = document.createElement('style');
        style.id = 'block-task-style';
        style.textContent = `
            .block-task-status, .block-task-message {
                position: fixed;
                top: 10px;
                background: rgba(0,0,0,0.8);
                color: white;
                padding: 8px 12px;
                border-radius: 6px;
                font-size: 12px;
                z-index: 10000;
                backdrop-filter: blur(10px);
                border: 1px solid rgba(255,255,255,0.1);
                display: none;
                opacity: 0;
                transition: opacity 0.3s ease;
            }
            .block-task-status {
                left: 10px;
                width: 200px;
                cursor: pointer;
            }
            .block-task-status.block-task-show, .block-task-message.block-task-show {
                display: flex !important;
                opacity: 1 !important;
            }
            .block-task-status {
                align-items: center;
                gap: 4px;
            }
            .block-task-userid {
                flex: 1 1 auto;
                min-width: 0;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            .block-task-count {
                flex-shrink: 0;
                white-space: nowrap;
            }
            .block-task-message {
                left: 220px;
                width: 200px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
        `;
        document.head.appendChild(style);
    }

    function createFloatingDiv(className, clickHandler) {
        const div = document.createElement('div');
        div.className = className;
        if (clickHandler) div.addEventListener('click', clickHandler);
        document.body.appendChild(div);
        return div;
    }

    function initUI() {
        if (!g_statusDiv) {
            g_statusDiv = createFloatingDiv('block-task-status', () => {
                if (g_taskQueue.length) {
                    const url = `https://www.douyin.com/user/${g_taskQueue[0].user_id}`;
                    navigator.clipboard.writeText(url)
                        .then(() => showMessage('Copy Success'))
                        .catch(() => showMessage('Copy Failed'));
                }
            });
            g_statusUserIdSpan = document.createElement('span');
            g_statusUserIdSpan.className = 'block-task-userid';
            g_statusCountSpan = document.createElement('span');
            g_statusCountSpan.className = 'block-task-count';
            g_statusDiv.appendChild(g_statusUserIdSpan);
            g_statusDiv.appendChild(g_statusCountSpan);
        }
        if (!g_messageDiv) {
            g_messageDiv = createFloatingDiv('block-task-message');
        }
    }

    function updateStatus() {
        if (!g_statusDiv) initUI();
        if (g_taskQueue.length === 0) {
            g_statusDiv.classList.remove('block-task-show');
            return;
        }
        const current = g_taskQueue[0];
        g_statusUserIdSpan.textContent = current.user_id;
        g_statusUserIdSpan.title = current.user_id;
        g_statusCountSpan.textContent = g_taskQueue.length > 1 ? ` (+${g_taskQueue.length - 1})` : '';
        g_statusDiv.classList.add('block-task-show');
    }

    function showMessage(text, duration = 2000) {
        if (!g_messageDiv) initUI();
        g_messageDiv.textContent = text;
        g_messageDiv.classList.add('block-task-show');
        if (g_messageTimer) clearTimeout(g_messageTimer);
        g_messageTimer = setTimeout(() => {
            g_messageDiv.classList.remove('block-task-show');
        }, duration);
    }

    function addToQueue(userId) {
        if (g_taskQueue.some(t => t.user_id === userId)) {
            showMessage(`User ${userId} already in queue`);
            return false;
        }
        g_taskQueue.push({ user_id: userId, timestamp: Date.now() });
        updateStatus();
        showMessage(`Added block task: ${userId}, ${g_taskQueue.length} tasks in queue`);
        if (!g_isProcessing) processQueue();
        return true;
    }

    function getNextDelay() {
        return Math.max(MIN_DELAY, BASE_DELAY - g_taskQueue.length * 100);
    }

    async function processQueue() {
        if (g_isProcessing || g_taskQueue.length === 0) return;
        g_isProcessing = true;

        while (g_taskQueue.length) {
            const task = g_taskQueue[0];
            updateStatus();
            try {
                await executeBlockTask(task);
            } catch (err) {
                console.error('Block task failed:', err);
                showMessage(`Task failed: ${task.user_id}`, 3000);
            }
            g_taskQueue.shift();
            updateStatus();
            if (g_taskQueue.length) {
                await new Promise(r => setTimeout(r, getNextDelay()));
            }
        }
        g_isProcessing = false;
        showMessage('All block tasks completed');
    }

    function executeBlockTask(task) {
        return new Promise((resolve, reject) => {
            const tab = GM_openInTab(
                `https://www.douyin.com/user/${task.user_id}?auto_block=true`,
                { active: false, insert: true, setParent: true }
            );

            let resolved = false;
            const cleanup = () => {
                resolved = true;
                clearTimeout(timeout);
            };

            try {
                if (tab.contentWindow) {
                    tab.contentWindow.addEventListener('beforeunload', () => {
                        if (!resolved) {
                            cleanup();
                            resolve();
                        }
                    });
                }
            } catch (e) {}

            const interval = setInterval(() => {
                if (resolved) return;
                if (tab.closed) {
                    clearInterval(interval);
                    cleanup();
                    resolve();
                }
            }, 300);

            const timeout = setTimeout(() => {
                if (!resolved && !tab.closed) {
                    clearInterval(interval);
                    tab.close();
                    reject(new Error('Task timeout'));
                }
            }, BLOCK_TIMEOUT);
        });
    }

    let currentVideo = null;
    const intersectionObserver = new IntersectionObserver(
        entries => {
            entries.forEach(e => {
                if (e.isIntersecting) currentVideo = e.target;
            });
        },
        { threshold: 0.5 }
    );

    new MutationObserver(mutations => {
        mutations.forEach(m => {
            m.addedNodes.forEach(node => {
                if (node.nodeType === 1 && node.matches('[data-e2e*="feed"]')) {
                    intersectionObserver.observe(node);
                }
            });
        });
    }).observe(document.body, { childList: true, subtree: true });

    document.addEventListener('keydown', e => {
        if (e.key.toLowerCase() === 'q' && !e.ctrlKey && !e.altKey && !e.metaKey) {
            const userId = currentVideo
                ?.querySelector('a[href*="/user/"]')
                ?.href.match(/user\/([\w-]+)/)?.[1];
            if (userId) addToQueue(userId);
        }
    });

    if (location.pathname.includes('/user/') && new URLSearchParams(location.search).get('auto_block')) {
        window.addEventListener('load', () => setTimeout(runAutoBlock, 1200));
    }

    async function clickElement(el, delay = CLICK_DELAY) {
        if (!el) return;
        el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
        const rect = el.getBoundingClientRect();
        const x = rect.left + rect.width / 2;
        const y = rect.top + rect.height / 2;
        ['mouseover', 'mousedown', 'click', 'mouseup'].forEach(type => {
            el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, clientX: x, clientY: y }));
        });
        await new Promise(r => setTimeout(r, delay));
    }

    function waitForElement(selector, timeout = WAIT_TIMEOUT) {
        const start = Date.now();
        return new Promise((resolve, reject) => {
            const check = () => {
                const el = document.querySelector(selector);
                if (el) return resolve(el);
                if (Date.now() - start >= timeout) {
                    reject(new Error(`Element ${selector} timeout`));
                } else {
                    setTimeout(check, 200);
                }
            };
            check();
        });
    }

    async function runAutoBlock() {
        try {
            const menuBtn = await waitForElement('#tooltip button', 8000);
            await clickElement(menuBtn);

            await waitForElement('.semi-dropdown-item, [role="menuitem"]', 4000);

            const blockBtn = Array.from(
                document.querySelectorAll('.semi-dropdown-item, [role="menuitem"], [class*="dropdown-item"]')
            ).find(el => el.textContent.includes('拉黑'));

            if (!blockBtn) {
                return finishAndClose();
            }

            await clickElement(blockBtn);
            await new Promise(r => setTimeout(r, CONFIRM_DELAY));

            const confirmBtn = Array.from(document.querySelectorAll('button'))
                .find(el => el.textContent.includes('确认拉黑'));

            if (confirmBtn) {
                await clickElement(confirmBtn);
                await new Promise(r => setTimeout(r, 500));
            }

            finishAndClose();
        } catch {
            finishAndClose();
        }
    }

    function finishAndClose() {
        window.open('about:blank', '_self');
        window.close();
    }

    initUI();
})();