Greasy Fork

Greasy Fork is available in English.

Bangumi Super Enhancer

Bangumi 增强套件

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

// ==UserScript==
// @name         Bangumi Super Enhancer
// @namespace    https://tampermonkey.net/
// @version      2.2.2
// @description  Bangumi 增强套件
// @author       Bios
// @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;  /* 调整按钮间距 */
        }
    `);

    /* 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 enhanceEpisodes() {
        if (!location.pathname.includes("/ep")) return;
        const formHash = document.querySelector("[name=formhash]")?.value;
        if (!formHash) return;
        document.querySelector("[name=edit_ep_batch]")?.addEventListener("submit", async (e) => {
            e.preventDefault();
            const episodes = [...document.querySelectorAll('[name="ep_mod[]"]:checked')].map(el => el.value);
            let batchSize = 100;
            while (batchSize >= 50 && episodes.length > 0) {
                try {
                    const data = new URLSearchParams();
                    data.append("formhash", formHash);
                    data.append("chkall", "on");
                    data.append("submit", "批量修改");
                    episodes.splice(0, batchSize).forEach(ep => data.append("ep_mod[]", ep));
                    const res = await fetch(location.pathname + "/edit_batch", {
                        method: "POST",
                        headers: { "Content-Type": "application/x-www-form-urlencoded" },
                        body: data
                    });
                    if (!res.ok) throw new Error();
                    alert(`成功提交 ${batchSize} 个章节`);
                    return;
                } catch {
                    batchSize = Math.floor(batchSize / 2);
                }
            }
            alert("提交失败,请减少选中数量!");
        });
    }

    /* 获取当前页面的 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(); /* 获取当前条目 ID */

    /* 批量关联模块 */
    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 = $("#custom_ids").val();
            const ids = extractUniqueIDs(rawInput);
            if (ids.length) processBatch(ids);
        });

        document.getElementById("btn_generate")?.addEventListener("click", () => {
            const start = parseInt($("#id_start").val(), 10);
            const end = parseInt($("#id_end").val(), 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").val(`bgm_id=${batch.join(",")}`);
            $("#findSubject").click();

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

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

    /* 启动功能 */
    function startEnhancer() {
        initWikiButton();
        observeURLChanges();
        initCoverUpload();
        enhanceEpisodes();
        initBatchRelation();
    }
    startEnhancer();
})();