Greasy Fork

Greasy Fork is available in English.

Bangumi Ultimate Enhancer

Bangumi 增强套件 - 集成Wiki按钮、封面上传、批量关联、批量分集编辑等功能

当前为 2025-03-16 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bangumi Ultimate Enhancer
// @namespace    https://tampermonkey.net/
// @version      2.2.3
// @description  Bangumi 增强套件 - 集成Wiki按钮、封面上传、批量关联、批量分集编辑等功能
// @author       Bios & Anonymous (Merged by Claude)
// @match        *://bgm.tv/subject/*
// @match        *://chii.in/subject/*
// @match        *://bangumi.tv/subject/*
// @match        *://bgm.tv/character/*
// @match        *://chii.in/character/*
// @match        *://bangumi.tv/character/*
// @match        *://bgm.tv/person/*
// @match        *://chii.in/person/*
// @match        *://bangumi.tv/person/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license      MIT
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// ==/UserScript==

(function () {
    "use strict";

    // 样式增强
    GM_addStyle(`
        .btnCustom {
            margin: 5px 0;
            background-color: #1E90FF !important;
            color: white !important;
            border-radius: 10px !important;
            padding: 5px 15px !important;
            border: none !important;
            cursor: pointer !important;
            transition: opacity 0.2s;
        }
        .btnCustom:hover {
            opacity: 0.8;
        }
        .enhancer-textarea {
            width: 100%;
            min-height: 60px;
            max-height: 300px;
            border: 1px solid #ddd;
            border-radius: 10px;
            padding: 8px;
            margin: 8px 0;
            resize: vertical;
            font-size: 13px;
            box-sizing: border-box;
        }
        .enhancer-panel {
            margin: 6px 0;  /* 减少外边距 */
            border-radius: 6px;  /* 调小圆角 */
            padding: 5px;  /* 减少内边距 */
            background: #f8f8f8;
            border: 1px solid #e0e0e0;
        }
        #coverUploadForm {
            text-align: center;
            padding: 3px;  /* 减少表单内边距 */
        }
        #coverUploadForm input[type="file"] {
            margin: 3px auto;  /* 缩小文件输入框边距 */
            width: 90%;  /* 调宽输入框减少留白 */
            display: block;
        }
        /* 进一步缩小按钮尺寸 */
        #coverUploadForm input[type="submit"] {
            padding: 4px 8px !important;  /* 更紧凑的按钮尺寸 */
            font-size: 14px !important;  /* 更小的字号 */
            margin: 8px auto 2px !important;  /* 调整按钮间距 */
        }
        .bgm-enhancer-status {
            background: #e6f4ff;
            border-radius: 4px;
            padding: 8px;
            margin: 8px 0;
            border-left: 3px solid #1E90FF;
            font-size: 13px;
            color: #333;
        }
    `);

    // 通用工具函数
    const $ = selector => document.querySelector(selector);
    const $$ = selector => Array.from(document.querySelectorAll(selector));

    // 获取当前页面的 ID(即 URL 里的 subject/xxx、character/xxx、person/xxx)
    function getCurrentPageID() {
        const match = window.location.pathname.match(/\/(subject|character|person)\/(\d+)/);
        return match ? parseInt(match[2], 10) : null;
    }
    const currentPageID = getCurrentPageID();

    /* ------------------------------
      超级增强器功能模块
    ------------------------------ */

    /* Wiki 按钮模块 */
    function initWikiButton() {
        // 排除编辑页面
        if (/(edit_detail|edit)$/.test(location.pathname)) return;
        // 排除 add_related 和 upload_img 页面
        if (/add_related|upload_img/.test(location.pathname)) return;

        const matchSubject = location.pathname.match(/\/subject\/(\d+)/);
        const matchPerson = location.pathname.match(/\/person\/(\d+)/);
        const matchCharacter = location.pathname.match(/\/character\/(\d+)/);

        const nav = document.querySelector(".subjectNav .navTabs, .navTabs");
        if (!nav || nav.querySelector(".wiki-button")) return;

        const li = document.createElement("li");
        li.className = "wiki-button";

        let wikiUrl = "";
        if (matchSubject) {
            wikiUrl = `${location.origin}/subject/${matchSubject[1]}/edit_detail`;
        } else if (matchPerson) {
            wikiUrl = `${location.origin}/person/${matchPerson[1]}/edit`;
        } else if (matchCharacter) {
            wikiUrl = `${location.origin}/character/${matchCharacter[1]}/edit`;
        }
        if (wikiUrl) {
            li.innerHTML = `<a href="${wikiUrl}" target="_blank" onclick="this.closest('li').remove()">Wiki</a>`;
            nav.appendChild(li);
        }
    }

    // 监听 URL 变化
    function observeURLChanges() {
        let lastURL = location.href;
        new MutationObserver(() => {
            if (location.href !== lastURL) {
                lastURL = location.href;
                initWikiButton();
            }
        }).observe(document, { subtree: true, childList: true });
    }

    /* 封面上传模块 */
    async function initCoverUpload() {
        if (document.querySelector("img.cover")) return;
        const infoBox = document.querySelector("#bangumiInfo");
        if (!infoBox) return;
        const links = document.querySelectorAll(".tip_i p a.l");
        if (links.length < 2) return;
        try {
            const res = await fetch(links[1].href);
            const doc = new DOMParser().parseFromString(await res.text(), "text/html");
            const form = doc.querySelector("#columnInSubjectA .text form");
            if (form) {
                const container = document.createElement("div");
                container.className = "enhancer-panel";
                const clone = form.parentElement.cloneNode(true);
                const uploadForm = clone.querySelector("form");
                uploadForm.id = "coverUploadForm";
                const fileInput = uploadForm.querySelector("input[type=file]");
                fileInput.style.width = "100%";
                const submitBtn = uploadForm.querySelector("input[type=submit]");
                submitBtn.className = "btnCustom";
                submitBtn.style.width = "120px";
                submitBtn.style.margin = "10px auto 0";
                container.appendChild(uploadForm);
                infoBox.parentNode.insertBefore(container, infoBox);
            }
        } catch (e) {
            console.error("封面加载失败:", e);
        }
    }

    /* 批量关联模块 */
    function initBatchRelation() {
        if (!document.getElementById("indexCatBox")) return;
        const panelHTML = `
            <div class="enhancer-panel">
                <textarea id="custom_ids" class="enhancer-textarea"
                    placeholder="输入 ID 或网址(可换行,支持各种分隔字符)"></textarea>
                <div style="text-align: center">
                    <button id="btn_execute" class="btnCustom">自动添加关联</button>
                </div>
            </div>
            <div class="enhancer-panel">
                <div style="display: flex; gap: 10px; justify-content: center">
                    <input id="id_start" type="number" placeholder="起始ID"
                        style="width: 100px; padding: 6px; border-radius: 8px; border: 1px solid #ddd;">
                    <span style="line-height: 30px">~</span>
                    <input id="id_end" type="number" placeholder="结束ID"
                        style="width: 100px; padding: 6px; border-radius: 8px; border: 1px solid #ddd;">
                </div>
                <div style="text-align: center; margin-top: 12px">
                    <button id="btn_generate" class="btnCustom">自动添加关联</button>
                </div>
            </div>
        `;
        const searchMod = document.querySelector("#sbjSearchMod");
        if (searchMod) searchMod.insertAdjacentHTML("afterend", panelHTML);

        document.getElementById("btn_execute")?.addEventListener("click", () => {
            const rawInput = document.getElementById("custom_ids").value;
            const ids = extractUniqueIDs(rawInput);
            if (ids.length) processBatch(ids);
        });

        document.getElementById("btn_generate")?.addEventListener("click", () => {
            const start = parseInt(document.getElementById("id_start").value, 10);
            const end = parseInt(document.getElementById("id_end").value, 10);
            if (isNaN(start) || isNaN(end) || start > end) {
                alert("请输入有效的起始和结束 ID!");
                return;
            }
            const ids = Array.from({ length: end - start + 1 }, (_, i) => start + i);
            processBatch([...new Set(ids)]);
        });

        function extractUniqueIDs(input) {
            const allIDs = input.match(/\d+/g) || [];
            let uniqueIDs = [...new Set(allIDs.map(Number))].filter(id => id > 0);
            if (currentPageID) {
                uniqueIDs = uniqueIDs.filter(id => id !== currentPageID);
            }
            return uniqueIDs;
        }

        function processBatch(ids) {
            if (!ids.length) {
                alert("未找到有效的 ID 或所有 ID 都已被排除!");
                return;
            }
            const batch = ids.splice(0, 10);
            $("#subjectName").value = `bgm_id=${batch.join(",")}`;
            $("#findSubject").click();

            const subjectList = document.getElementById("subjectList");
            if (!subjectList) {
                alert("未找到关联列表,请检查页面结构。");
                return;
            }

            const observer = new MutationObserver((mutations, observerInstance) => {
                // 当列表中有 li 元素时认为搜索结果已生成
                if (document.querySelectorAll('#subjectList > li').length > 0) {
                    observerInstance.disconnect();
                    // 延时500ms等待渲染完成
                    setTimeout(function(){
                        var $avatars = document.querySelectorAll('#subjectList>li>a.avatar.h');
                        if ($avatars.length > 1) {
                            $avatars.forEach(function(element, index){
                                setTimeout(function(){
                                    element.click();
                                    console.log("已点击:" + element.textContent);
                                    // 如有需要,可对关联列表中的条目进行样式修改提醒
                                    document.querySelectorAll('#crtRelateSubjects li p.title>a')[0].style.fontWeight = 'bold';
                                }, index * 500);
                            });
                        } else if ($avatars.length === 1) {
                            $avatars[0].click();
                        }
                        // 根据点击数量延时后点击保存按钮
                        setTimeout(function(){
                            $("#saveSubject").click();
                            alert("所有关联项已成功添加!");
                        }, ($avatars.length > 1 ? $avatars.length * 500 + 300 : 800));
                    }, 500);
                }
            });
            observer.observe(subjectList, { childList: true, subtree: true });
        }
    }

    /* ------------------------------
      批量分集编辑器功能模块
    ------------------------------ */

    const BatchEpisodeEditor = {
        CHUNK_SIZE: 20,
        BASE_URL: '',
        CSRF_TOKEN: '',

        // 初始化方法
        init() {
            if (!this.isEpisodePage()) return;

            this.BASE_URL = location.pathname.replace(/\/edit_batch$/, '');
            this.CSRF_TOKEN = $('[name=formhash]')?.value || '';

            if (!this.CSRF_TOKEN) return;

            this.bindHashChange();
            this.upgradeCheckboxes();

            // 添加功能标识
            const header = document.querySelector('h2.subtitle');
            if (header) {
                const notice = document.createElement('div');
                notice.className = 'bgm-enhancer-status';
                notice.textContent = '已启用分批编辑功能,支持超过20集的批量编辑';
                header.parentNode.insertBefore(notice, header.nextSibling);
            }
        },

        // 检查是否为分集页面
        isEpisodePage() {
            return /^\/subject\/\d+\/ep(\/edit_batch)?$/.test(location.pathname);
        },

        // 监听hash变化处理批量编辑
        bindHashChange() {
            const processHash = () => {
                const ids = this.getSelectedIdsFromHash();
                if (ids.length > 0) this.handleBatchEdit(ids);
            };

            window.addEventListener('hashchange', processHash);
            if (location.hash.includes('episodes=')) processHash();
        },

        // 增强复选框功能
        upgradeCheckboxes() {
            // 动态更新表单action
            const updateFormAction = () => {
                const ids = $$('[name="ep_mod[]"]:checked').map(el => el.value);
                $('form[name="edit_ep_batch"]').action =
                    `${this.BASE_URL}/edit_batch#episodes=${ids.join(',')}`;
            };

            $$('[name="ep_mod[]"]').forEach(el =>
                el.addEventListener('change', updateFormAction)
            );

            // 全选功能
            $('[name=chkall]')?.addEventListener('click', () => {
                $$('[name="ep_mod[]"]').forEach(el => el.checked = true);
                updateFormAction();
            });
        },

        // 从hash获取选中ID
        getSelectedIdsFromHash() {
            const match = location.hash.match(/episodes=([\d,]+)/);
            return match ? match[1].split(',').filter(Boolean) : [];
        },

        // 批量编辑主逻辑
        async handleBatchEdit(episodeIds) {
            try {
                // 分块加载数据
                const chunks = this.createChunks(episodeIds, this.CHUNK_SIZE);
                const dataChunks = await this.loadChunkedData(chunks);

                // 填充表单数据
                $('#summary').value = dataChunks.flat().join('\n');
                $('[name=ep_ids]').value = episodeIds.join(',');

                // 增强表单提交
                this.upgradeFormSubmit(chunks, episodeIds);

                window.chiiLib?.ukagaka?.presentSpeech('数据加载完成');
            } catch (err) {
                console.error('批量处理失败:', err);
                alert('数据加载失败,请刷新重试');
            }
        },

        // 分块加载数据
        async loadChunkedData(chunks) {
            window.chiiLib?.ukagaka?.presentSpeech('正在加载分集数据...');
            return Promise.all(chunks.map(chunk =>
                this.fetchChunkData(chunk).then(data => data.split('\n'))
            ));
        },

        // 获取单块数据
        async fetchChunkData(episodeIds) {
            const params = new URLSearchParams();
            params.append('chkall', 'on');
            params.append('submit', '批量修改');
            params.append('formhash', this.CSRF_TOKEN);
            episodeIds.forEach(id => params.append('ep_mod[]', id));

            const res = await fetch(`${this.BASE_URL}/edit_batch`, {
                method: 'POST',
                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                body: params
            });

            const html = await res.text();
            const match = html.match(/<textarea [^>]*name="ep_list"[^>]*>([\s\S]*?)<\/textarea>/i);
            return match?.[1]?.trim() || '';
        },

        // 增强表单提交处理
        upgradeFormSubmit(chunks, originalIds) {
            const form = $('form[name="edit_ep_batch"]');
            if (!form) return;

            form.onsubmit = async (e) => {
                e.preventDefault();

                // 验证数据完整性
                const inputData = $('#summary').value.trim().split('\n');
                if (inputData.length !== originalIds.length) {
                    alert(`数据不匹配 (预期 ${originalIds.length} 行,实际 ${inputData.length} 行)`);
                    return;
                }

                try {
                    window.chiiLib?.ukagaka?.presentSpeech('正在提交数据...');
                    await this.saveChunkedData(chunks, inputData);

                    window.chiiLib?.ukagaka?.presentSpeech('保存成功');
                    location.href = this.BASE_URL;
                } catch (err) {
                    console.error('保存失败:', err);
                    alert('保存过程中发生错误');
                }
            };
        },

        // 分块保存数据
        async saveChunkedData(chunks, fullData) {
            const dataChunks = this.createChunks(fullData, this.CHUNK_SIZE);

            return Promise.all(chunks.map((idChunk, index) =>
                this.saveChunkData(idChunk, dataChunks[index])
            ));
        },

        // 保存单块数据
        async saveChunkData(episodeIds, chunkData) {
            const params = new URLSearchParams();
            params.append('formhash', this.CSRF_TOKEN);
            params.append('rev_version', '0');
            params.append('editSummary', $('#editSummary')?.value || '');
            params.append('ep_ids', episodeIds.join(','));
            params.append('ep_list', chunkData.join('\n'));
            params.append('submit_eps', '改好了');

            await fetch(`${this.BASE_URL}/edit_batch`, {
                method: 'POST',
                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                body: params
            });
        },

        // 通用分块方法
        createChunks(array, size) {
            return Array.from(
                { length: Math.ceil(array.length / size) },
                (_, i) => array.slice(i * size, (i + 1) * size)
            );
        }
    };

    /* 启动所有功能 */
    function startEnhancer() {
        // 启动超级增强器功能
        initWikiButton();
        observeURLChanges();
        initCoverUpload();
        initBatchRelation();

        // 启动批量分集编辑器功能
        BatchEpisodeEditor.init();

        console.log("Bangumi Ultimate Enhancer 已启动");
    }

    // 在DOM加载完成后启动脚本
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startEnhancer);
    } else {
        startEnhancer();
    }
})();