Greasy Fork

Greasy Fork is available in English.

Bangumi Ultimate Enhancer

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

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

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