Greasy Fork

Greasy Fork is available in English.

B站动态批量删除助手

这是一个帮助B站用户高效管理个人动态的脚本,支持多种类型动态的批量删除操作。

目前为 2024-12-14 提交的版本。查看 最新版本

// ==UserScript==
// @name         B站动态批量删除助手
// @version      0.25
// @description  这是一个帮助B站用户高效管理个人动态的脚本,支持多种类型动态的批量删除操作。
// @author       梦把我
// @match        https://space.bilibili.com/*
// @match        http://space.bilibili.com/*
// @require      http://greasyfork.icu/scripts/38220-mscststs-tools/code/MSCSTSTS-TOOLS.js?version=713767
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// @icon         https://static.hdslb.com/images/favicon.ico
// @namespace http://greasyfork.icu/users/1383389
// @license      MIT
// @grant        none
// ==/UserScript==
 
(function () {
    'use strict';
 
    const uid = window.location.pathname.split("/")[1];
 
    function getUserCSRF() {
        return document.cookie.split("; ").find(row => row.startsWith("bili_jct="))?.split("=")[1];
    }
 
    const csrfToken = getUserCSRF();
 
    class Api {
        constructor() { }
 
        async spaceHistory(offset = 0) { // 获取个人动态
            return this.retryOn429(() => this._api(
                `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history?visitor_uid=${uid}&host_uid=${uid}&offset_dynamic_id=${offset}`,
                {}, "get"
            ));
        }
 
        async removeDynamic(id) { // 删除动态
            return this._api(
                "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/rm_dynamic",
                { dynamic_id: id, csrf_token: csrfToken }
            );
        }
 
        async _api(url, data, method = "post") { // 通用请求
            return axios({
                url,
                method,
                data: this.transformRequest(data),
                withCredentials: true,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                }
            }).then(res => res.data);
        }
 
        transformRequest(data) { // 转换请求参数
            return Object.entries(data).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
        }
 
        async fetchJsonp(url) { // jsonp请求
            return fetchJsonp(url).then(res => res.json());
        }
 
        async retryOn429(func, retries = 5, delay = 100) { // 出现429错误时冷却100ms重试,出现412错误时提示并退出
            while (retries > 0) {
                try {
                    return await func();
                } catch (err) {
                    if (err.response && err.response.status === 429) {
                        await this.sleep(delay);
                        retries--;
                    } else if (err.response && err.response.status === 412) {
                        alert('由于请求过于频繁,IP暂时被ban,请更换IP或稍后再试。');
                        throw new Error('IP blocked, please retry later.');
                    } else {
                        throw err;
                    }
                }
            }
            throw new Error('Too many retries, request failed.');
        }
 
        sleep(ms) { // 睡眠
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    }
 
    const api = new Api();
    const buttons = [".onlyDeleteRepost", ".deleteVideo", ".deleteImage", ".deleteText", ".deleteCustomType"];
    let logNode;
 
    // 添加确认状态管理
    const confirmStates = {
        deleteStates: {},
        resetTimer: null
    };
 
    async function init() {
        try {
            // 等待页面加载完成
            await new Promise(resolve => setTimeout(resolve, 500));

            // 检查是否为新版界面的个人空间
            const announcementText = document.querySelector("#app > main > div.space-dynamic > div.space-dynamic__right > div:nth-child(6) > div > div.ann-section__content > div > div.show-wrap > div > p");
            const isNewVersionMySpace = announcementText && announcementText.textContent === "编辑我的公告";

            // 检查是否为旧版界面的个人空间
            const oldVersionText = await mscststs.wait(".h-version-state", true, 100);
            const isOldVersionMySpace = oldVersionText && oldVersionText.innerText === "我自己";

            // 如果既不是新版也不是旧版的个人空间,则退出
            if (!isNewVersionMySpace && !isOldVersionMySpace) {
                console.log('当前不是自己的个人动态页面,脚本未启用');
                return;
            }

            // 创建控制面板节点
            const node = createControlPanel();
            
            // 尝试插入到新版或旧版界面
            try {
                if (isNewVersionMySpace) {
                    // 新版界面插入位置
                    const newVersionContainer = document.querySelector("#app > main > div.space-dynamic > div.space-dynamic__right");
                    if (newVersionContainer) {
                        const firstChild = newVersionContainer.querySelector("div:nth-child(1)");
                        if (firstChild) {
                            newVersionContainer.insertBefore(node, firstChild);
                        } else {
                            newVersionContainer.appendChild(node);
                        }
                        console.log('成功插入到新版界面');
                    } else {
                        console.error('无法找到新版界面插入位置');
                        return;
                    }
                } else {
                    // 旧版界面插入位置
                    const oldVersionContainer = document.querySelector("#page-dynamic .col-2");
                    if (oldVersionContainer) {
                        oldVersionContainer.appendChild(node);
                        console.log('成功插入到旧版界面');
                    } else {
                        console.error('无法找到旧版界面插入位置');
                        return;
                    }
                }

                // 设置事件监听
                setEventListeners();
                
                // 设置教程链接
                document.querySelector('.tutorial-btn').href = '占位符URL';
                
                // 添加样式
                addConfirmationStyles();
                
            } catch (error) {
                console.error('插入控制面板失败:', error);
            }

        } catch (error) {
            console.error('验证用户身份失败:', error);
        }
    }
 
    function createControlPanel() {
        const node = document.createElement("div");
        node.className = "msc_panel";
        node.innerHTML = `
            <div class="inner">
                <div class="panel-section type-table">
                    <h3>动态类型对照表</h3>
                    <table border="1">
                        <tr><th>类型值</th><th>含义</th></tr>
                        <tr><td>1</td><td>转发</td></tr>
                        <tr><td>2</td><td>图片动态</td></tr>
                        <tr><td>4</td><td>文字动态</td></tr>
                        <tr><td>8</td><td>视频动态</td></tr>
                        <tr><td>16</td><td>小视频</td></tr>
                        <tr><td>64</td><td>专栏</td></tr>
                    </table>
                </div>

                <div class="panel-section custom-delete">
                    <h3>自定义删除</h3>
                    <div class="input-group">
                        <input type="number" class="type-input" placeholder="输入要删除的动态类型值">
                        <button class="deleteCustomType primary-btn">删除</button>
                    </div>
                </div>

                <div class="panel-section quick-actions">
                    <h3>快捷操作</h3>
                    <div class="button-group">
                        <button class="deleteVideo">删除视频动态</button>
                        <button class="deleteImage">删除图片动态</button>
                        <button class="deleteText">删除文字动态</button>
                    </div>
                </div>



                <div class="panel-section repost-actions">
                    <h3>转发相关</h3>
                    <div class="button-group">
                        <button class="onlyDeleteRepost">删除转发动态</button>
                    </div>
                </div>

                <div class="panel-section other-actions">
                    <h3>其他</h3>
                    <div class="button-group">
                        <a href="#" class="tutorial-btn" target="_blank">使用视频教程</a>
                    </div>
                </div>

                <div class="panel-section pin-settings">
                    <h3>置顶动态设置</h3>
                    <div class="setting-group">
                        <label class="switch">
                            <input type="checkbox" id="preservePinned" checked>
                            <span class="slider round"></span>
                            <span class="label">保护置顶动态</span>
                        </label>
                        <div class="pin-content-input">
                            <input type="text" id="pinnedContent" placeholder="输入置顶动态内容关键词">
                            <small class="tip">输入置顶动态中的部分内容,用于识别置顶动态,最好可以复制置顶动态的全部文字内容然后输入,这样更能准确识别</small>
                        </div>
                    </div>
                </div>
            </div>`;
        return node;
    }
 
    function setEventListeners() {
        // 为每个按钮添加确认机制
        document.querySelector(".onlyDeleteRepost").addEventListener("click", () => handleConfirmation("onlyDeleteRepost", () => handleDelete(false)));
        document.querySelector(".deleteVideo").addEventListener("click", () => handleConfirmation("deleteVideo", () => handleDeleteByType(8)));
        document.querySelector(".deleteImage").addEventListener("click", () => handleConfirmation("deleteImage", () => handleDeleteByType(2)));
        document.querySelector(".deleteText").addEventListener("click", () => handleConfirmation("deleteText", () => handleDeleteByType(4)));
        document.querySelector(".deleteCustomType").addEventListener("click", () => {
            const typeValue = parseInt(document.querySelector(".type-input").value);
            if (isValidDynamicType(typeValue)) {
                handleConfirmation("deleteCustomType", () => handleDeleteByType(typeValue));
            } else {
                alert("请输入有效的动态类型值!\n有效值包括:1(转发)、2(图片)、4(文字)、8(视频)、16(小视频)、64(专栏)");
                resetButtonState("deleteCustomType");
                enableAll();
            }
        });
    }
 
    async function handleDelete(deleteLottery) { // 删除参数 unfollow
        disableAll();
        let deleteCount = 0; // 删除计数
        let hasMore = true; // 是否还有更多动态
        let offset = 0; // 动态偏移量
 
        while (hasMore) {
            const { data } = await api.spaceHistory(offset);
            hasMore = data.has_more;
 
            for (const card of data.cards) {
                offset = card.desc.dynamic_id_str;
 
                if (card.desc.orig_dy_id != 0) { // 如果是转发动态
                    try {
                        const content = JSON.parse(card.card);
                        const content2 = JSON.parse(content.origin_extend_json);
 
                        if (!deleteLottery || content2.lott) { // 如果"仅删除抽奖"为假,或判断为抽奖动态
                            const rm = await api.removeDynamic(card.desc.dynamic_id_str);
                            if (rm.code === 0) deleteCount++;
                            else throw new Error("删除出错");
                        }
                        await api.sleep(50);
                        log(`已删除 ${deleteCount} 条动态`);
                    } catch (e) {
                        console.error(e);
                        break;
                    }
                }
            }
        }
        enableAll();
    }
 
    function disableAll() {
        console.log('start');
        buttons.forEach(btn => {
            const button = document.querySelector(btn);
            button.disabled = true;
            resetButtonState(btn.substring(1)); // 移除开头的点号
        });
        confirmStates.deleteStates = {}; // 清除所有确认状态
    }
 
    function enableAll() {
        console.log('done');
        buttons.forEach(btn => {
            const button = document.querySelector(btn);
            if (button) {
                button.disabled = false;
                resetButtonState(btn.substring(1));
            }
        });
        confirmStates.deleteStates = {};
        log('操作已完成!', true);
    }
 
    let currentPopup = null;
    let currentTimer = null;

    function log(message, autoRefresh = false) {
        // 如果存在之前的弹窗和定时器,先清除
        if (currentPopup) {
            currentPopup.remove();
            clearTimeout(currentTimer);
        }

        // 创建新的弹窗
        const popup = document.createElement('div');
        popup.className = 'log-popup';
        popup.textContent = message;
        document.body.appendChild(popup);
        currentPopup = popup;

        if (autoRefresh) {
            let countdown = 3;
            const updateCountdown = () => {
                popup.textContent = `${message} (${countdown}秒后自动刷新)`;
                countdown--;
                if (countdown < 0) {
                    window.location.reload();
                } else {
                    currentTimer = setTimeout(updateCountdown, 1000);
                }
            };
            updateCountdown();
        } else {
            // 3秒后自动隐藏弹窗
            currentTimer = setTimeout(() => {
                popup.classList.add('hide');
                setTimeout(() => popup.remove(), 300);
            }, 3000);
        }
    }
 
    async function handleDeleteByType(targetType) {
        const preservePinned = document.querySelector('#preservePinned').checked;
        const pinnedContent = document.querySelector('#pinnedContent').value.trim();
        
        if (preservePinned && !pinnedContent && await checkPinnedDynamic()) {
            alert('检测到开启保护置顶功能,请输入置顶动态内容关键词以保护置顶动态');
            return;
        }

        try {
            disableAll();
            let deleteCount = 0;
            let hasMore = true;
            let offset = 0;

            while (hasMore) {
                const { data } = await api.spaceHistory(offset);
                hasMore = data.has_more;

                for (const card of data.cards) {
                    offset = card.desc.dynamic_id_str;
                    
                    if (card.desc.type === targetType) {
                        // 检查是否为置顶动态
                        if (preservePinned && pinnedContent) {
                            const cardContent = JSON.parse(card.card);
                            const content = cardContent.item?.content || '';
                            if (content.includes(pinnedContent)) {
                                console.log('跳过置顶动态:', content);
                                continue;
                            }
                        }

                        try {
                            const rm = await api.removeDynamic(card.desc.dynamic_id_str);
                            if (rm.code === 0) deleteCount++;
                            await api.sleep(50);
                            log(`已删除 ${deleteCount} 条类型为 ${targetType} 的动态`);
                        } catch (e) {
                            console.error(e);
                            break;
                        }
                    }
                }
            }
        } catch (error) {
            console.error('删除操作执行出错:', error);
        } finally {
            enableAll();
        }
    }
 
    // 添加确认处理函数
    function handleConfirmation(buttonId, callback) {
        const button = document.querySelector(`.${buttonId}`);
        if (!button) return;
        
        const originalText = button.textContent;
        
        // 如果是首次点击
        if (!confirmStates.deleteStates[buttonId]) {
            // 设置确认状态
            confirmStates.deleteStates[buttonId] = true;
            
            // 修改按钮文字
            button.textContent = "确认删除?";
            button.style.backgroundColor = "#ff6b6b";
            
            // 添加闪烁动画
            button.style.animation = "buttonBlink 1s infinite";
            
            // 5秒后重置状态
            setTimeout(() => {
                resetButtonState(buttonId);
            }, 5000);
            
            // 显示提示
            log("请再次点击确认删除操作");
        } else {
            try {
                // 第二次点击,执行删除
                resetButtonState(buttonId);
                callback();
            } catch (error) {
                console.error('执行删除操作时出错:', error);
                resetButtonState(buttonId);
                enableAll();
                log('操作执行出错,请重试');
            }
        }
    }
 
    // 重置按钮状态
    function resetButtonState(buttonId) {
        const button = document.querySelector(`.${buttonId}`);
        if (!button) return;
        
        // 重置确认状态
        confirmStates.deleteStates[buttonId] = false;
        
        // 重置按钮状态
        button.disabled = false;
        button.textContent = getOriginalButtonText(buttonId);
        button.style.backgroundColor = "";
        button.style.animation = "";
        
        // 清除可能存在的定时器
        if (confirmStates.resetTimer) {
            clearTimeout(confirmStates.resetTimer);
            confirmStates.resetTimer = null;
        }
    }
 
    // 获取按钮原始文字
    function getOriginalButtonText(buttonId) {
        const textMap = {
            'onlyDeleteRepost': '删除转发动态',
            'deleteVideo': '删除视频动态',
            'deleteImage': '删除图片动态',
            'deleteText': '删除文字动态',
            'deleteCustomType': '删除'
        };
        return textMap[buttonId] || '删除';
    }
 
    // 添加闪烁动画样式
    function addConfirmationStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .msc_panel {
                max-width: 100%; /* 修改最大宽度以适应侧边栏 */
                margin: 0 0 20px 0; /* 修改边距以适应新布局 */
                padding: 20px;
                background: #fff;
                border-radius: 8px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            }

            .panel-section {
                margin-bottom: 24px;
                padding-bottom: 20px;
                border-bottom: 1px solid #eee;
            }

            .panel-section:last-child {
                border-bottom: none;
                margin-bottom: 0;
            }

            .panel-section h3 {
                font-size: 16px;
                color: #18191c;
                margin-bottom: 16px;
                font-weight: 500;
            }

            .type-table table {
                width: 100%;
                border-collapse: collapse;
                margin: 10px 0;
                font-size: 14px;
            }

            .type-table th, .type-table td {
                padding: 8px;
                text-align: center;
                border: 1px solid #eee;
            }

            .type-table th {
                background: #f6f7f8;
            }

            .input-group {
                display: flex;
                gap: 10px;
                margin-bottom: 10px;
            }

            .type-input {
                flex: 1;
                padding: 8px 12px;
                border: 1px solid #ddd;
                border-radius: 4px;
                font-size: 14px;
            }

            .button-group {
                display: flex;
                flex-wrap: wrap;
                gap: 10px;
            }

            .msc_panel button {
                padding: 8px 16px;
                border-radius: 4px;
                border: 1px solid #ddd;
                background: #fff;
                cursor: pointer;
                font-size: 14px;
                transition: all 0.2s;
            }

            .msc_panel button:hover {
                background: #f6f7f8;
            }

            .msc_panel button.primary-btn {
                background: #00aeec;
                color: #fff;
                border-color: #00aeec;
            }

            .msc_panel button.primary-btn:hover {
                background: #0096cc;
            }

            .msc_panel button.warning-btn {
                background: #fb7299;
                color: #fff;
                border-color: #fb7299;
            }

            .msc_panel button.warning-btn:hover {
                background: #e45c80;
            }

            .msc_panel button:disabled {
                background: #eee;
                color: #999;
                cursor: not-allowed;
                border-color: #ddd;
            }

            .tutorial-btn {
                display: inline-block;
                padding: 8px 16px;
                background: #6c757d;
                color: #fff;
                text-decoration: none;
                border-radius: 4px;
                transition: all 0.2s;
            }

            .tutorial-btn:hover {
                background: #5a6268;
            }

            .log {
                margin-top: 16px;
                padding: 12px;
                background: #f6f7f8;
                border-radius: 4px;
                font-size: 14px;
                color: #666;
            }

            @keyframes buttonBlink {
                0% { opacity: 1; }
                50% { opacity: 0.7; }
                100% { opacity: 1; }
            }
            
            .msc_panel button.confirming {
                background-color: #ff6b6b !important;
                color: white !important;
            }
            
            .msc_panel button:disabled {
                animation: none !important;
                opacity: 0.5 !important;
            }

            .pin-settings {
                margin: 15px 0;
            }
            .setting-group {
                display: flex;
                flex-direction: column;
                gap: 10px;
            }
            .switch {
                display: flex;
                align-items: center;
                gap: 10px;
            }
            .switch input {
                display: none;
            }
            .slider {
                position: relative;
                width: 40px;
                height: 20px;
                background-color: #ccc;
                border-radius: 20px;
                cursor: pointer;
                transition: .4s;
            }
            .slider:before {
                position: absolute;
                content: "";
                height: 16px;
                width: 16px;
                left: 2px;
                bottom: 2px;
                background-color: white;
                border-radius: 50%;
                transition: .4s;
            }
            input:checked + .slider {
                background-color: #2196F3;
            }
            input:checked + .slider:before {
                transform: translateX(20px);
            }
            .pin-content-input {
                margin-top: 5px;
            }
            .pin-content-input input {
                width: 100%;
                padding: 5px;
                border: 1px solid #ddd;
                border-radius: 4px;
            }
            .tip {
                color: #999;
                font-size: 12px;
                margin-top: 5px;
                display: block;
            }

            /* 弹窗样式 */
            .log-popup {
                position: fixed;
                bottom: 20px;
                right: 20px;
                background: rgba(0, 0, 0, 0.8);
                color: white;
                padding: 12px 20px;
                border-radius: 8px;
                z-index: 999999;
                font-size: 14px;
                max-width: 300px;
                animation: fadeInOut 0.3s ease-in-out;
                box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
            }

            @keyframes fadeInOut {
                0% {
                    opacity: 0;
                    transform: translateY(20px);
                }
                100% {
                    opacity: 1;
                    transform: translateY(0);
                }
            }

            .log-popup.hide {
                animation: fadeOut 0.3s ease-in-out forwards;
            }

            @keyframes fadeOut {
                0% {
                    opacity: 1;
                    transform: translateY(0);
                }
                100% {
                    opacity: 0;
                    transform: translateY(20px);
                }
            }
        `;
        document.head.appendChild(style);
    }
 
    // 添加动态类型验证函数
    function isValidDynamicType(type) {
        const validTypes = [1, 2, 4, 8, 16, 64];
        return validTypes.includes(type);
    }
 
    // 检查是否存在置顶动态
    async function checkPinnedDynamic() {
        try {
            // 检查新版界面
            const newVersionPin = document.evaluate(
                '//*[@id="app"]/main/div[1]/div[2]/div/div/div/div[1]/div[1]/div/div[1]/div/div',
                document,
                null,
                XPathResult.FIRST_ORDERED_NODE_TYPE,
                null
            ).singleNodeValue;

            // 检查旧版界面
            const oldVersionPin = document.evaluate(
                '//*[@id="page-dynamic"]/div[1]/div/div[1]/div/div/div[1]/div/div',
                document,
                null,
                XPathResult.FIRST_ORDERED_NODE_TYPE,
                null
            ).singleNodeValue;

            return !!(newVersionPin || oldVersionPin);
        } catch (error) {
            console.error('检查置顶动态失败:', error);
            return false;
        }
    }
 
    init();
})();