Bangumi 终极增强套件 - 集成Wiki按钮、关联按钮、封面上传、批量关联、批量分集编辑等功能
- // ==UserScript==
- // @name Bangumi Ultimate Enhancer
- // @namespace https://tampermonkey.net/
- // @version 2.4
- // @description Bangumi 终极增强套件 - 集成Wiki按钮、关联按钮、封面上传、批量关联、批量分集编辑等功能
- // @author Bios (improved 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/*
- // @connect bgm.tv
- // @grant GM_xmlhttpRequest
- // @license MIT
- // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
- // @run-at document-idle
- // ==/UserScript==
- (function() {
- "use strict";
- // 在页面加载完成后执行
- $(document).ready(function() {
- // 判断当前是否在关联页面
- const isRelatedPage = window.location.href.indexOf('/add_related') !== -1;
- // 判断当前是否在条目/角色/人物主页
- const isMainPage = /\/(subject|character|person)\/\d+$/.test(window.location.pathname);
- // 根据页面类型选择初始化不同功能
- if (isRelatedPage) {
- initBatchRelation();
- } else if (isMainPage) {
- initIDCollector();
- }
- });
- // 样式注入
- function injectStyles() {
- $('head').append(`
- <style>
- /* 通用按钮美化 */
- .btnCustom {
- margin: 5px 0;
- background-color: #1E90FF !important;
- color: white !important;
- border-radius: 5px !important;
- padding: 6px 18px !important;
- border: none !important;
- cursor: pointer !important;
- position: relative;
- top: -4px;
- left: -8px;
- font-size: 14px;
- font-weight: bold;
- text-align: center;
- display: flex;
- justify-content: flex-end;
- align-items: center;
- transition: opacity 0.2s, transform 0.2s;
- }
- .btnCustom:hover {
- opacity: 0.9;
- box-shadow: 3px 6px 12px rgba(0, 0, 0, 0.15);
- }
- /* 输入框优化 */
- .enhancer-textarea {
- width: 100%;
- min-height: 80px;
- max-height: 300px;
- border: 1px solid #ccc;
- border-radius: 12px;
- padding: 10px;
- margin: 10px 0;
- resize: vertical;
- font-size: 14px;
- box-sizing: border-box;
- background: #fdfdfd;
- transition: all 0.05s ease-in-out;
- }
- .enhancer-textarea:focus {
- border-color: #1E90FF;
- box-shadow: 0 0 8px rgba(30, 144, 255, 0.3);
- outline: none;
- }
- /* 卡片式面板美化 */
- .enhancer-panel {
- margin: 10px 0;
- border-radius: 10px;
- padding: 12px;
- background: #ffffff;
- border: 1px solid #e0e0e0;
- box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.05);
- }
- .enhancer-panel h3 {
- margin-top: 0;
- margin-bottom: 10px;
- font-size: 15px;
- color: #333;
- }
- /* 标签和选择框美化 */
- .select-label {
- margin-right: 8px;
- font-weight: bold;
- color: #555;
- }
- .chitanda_item_type {
- display: flex;
- margin-right: auto;
- align-items: center;
- margin-bottom: 8px;
- }
- /* 主要容器美化 */
- .chitanda_wrapper {
- margin: 15px 0;
- background: #fff;
- padding: 15px;
- border-radius: 10px;
- border: 1px solid #eaeaea;
- box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.05);
- }
- /* 进度提示美化 */
- .chitanda_progress {
- margin: 12px 0;
- color: #d346bb;
- font-weight: bold;
- font-size: 18px;
- text-align: center;
- }
- /* 信息提示框优化 */
- .chitanda_item_not_found, .chitanda_item_dupe {
- margin-top: 8px;
- padding: 8px;
- min-height: 15px;
- border-radius: 6px;
- font-size: 14px;
- }
- .chitanda_item_not_found {
- color: #e74c3c;
- background: #ffebeb;
- border: 1px solid #e74c3c;
- }
- .chitanda_item_dupe {
- color: #3498db;
- background: #eaf6ff;
- border: 1px solid #3498db;
- }
- /* 标题优化 */
- .chitanda_header {
- font-size: 16px;
- margin: 12px 0 6px;
- color: #333;
- font-weight: bold;
- }
- /* 按钮区域对齐优化 */
- .chitanda_controls {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- }
- /* 标签导航美化 */
- .tab-nav {
- display: flex;
- border-bottom: 2px solid #ddd;
- margin-bottom: 15px;
- }
- .tab-nav button {
- background: none;
- border: none;
- padding: 10px 20px;
- cursor: pointer;
- font-size: 14px;
- font-weight: bold;
- border-bottom: 3px solid transparent;
- transition: all 0.3s;
- }
- .tab-nav button.active {
- border-bottom: 3px solid #1E90FF;
- color: #1E90FF;
- }
- .tab-nav button:hover {
- color: #1E90FF;
- }
- /* 选项卡面板优化 */
- .tab-panel {
- display: none;
- }
- .tab-panel.active {
- display: block;
- animation: fadeIn 0.3s ease-in-out;
- }
- @keyframes fadeIn {
- from { opacity: 0; transform: translateY(5px); }
- to { opacity: 1; transform: translateY(0); }
- }
- /* 行内表单优化 */
- .flex-row {
- display: flex;
- gap: 12px;
- align-items: center;
- }
- /* 输入框美化 */
- .input-number {
- width: 100px;
- padding: 8px;
- border-radius: 8px;
- border: 1px solid #ddd;
- background: #f9f9f9;
- transition: all 0.2s;
- }
- .input-number:focus {
- border-color: #1E90FF;
- box-shadow: 0 0 5px rgba(30, 144, 255, 0.3);
- outline: none;
- }
- </style>
- `);
- }
- /* Wiki 按钮和关联按钮模块 */
- function initNavButtons() {
- // 排除特定页面
- if (/(edit_detail|edit|add_related|upload_img)/.test(location.pathname)) return;
- // 获取导航栏
- const nav = document.querySelector(".subjectNav .navTabs, .navTabs");
- if (!nav) return;
- // 匹配页面类型和ID
- const pathMatch = location.pathname.match(/\/(subject|person|character)\/(\d+)/);
- if (!pathMatch) return;
- const pageType = pathMatch[1]; // subject, person, or character
- const pageId = pathMatch[2]; // ID number
- // 添加Wiki按钮
- if (!nav.querySelector(".wiki-button")) {
- const wikiUrl = pageType === "subject"
- ? `${location.origin}/${pageType}/${pageId}/edit_detail`
- : `${location.origin}/${pageType}/${pageId}/edit`;
- const wikiLi = document.createElement("li");
- wikiLi.className = "wiki-button";
- wikiLi.innerHTML = `<a href="${wikiUrl}" target="_blank">Wiki</a>`;
- nav.appendChild(wikiLi);
- }
- // 添加关联按钮
- if (!nav.querySelector(".relate-button")) {
- const relateUrl = pageType === "subject"
- ? `${location.origin}/${pageType}/${pageId}/add_related/subject/anime`
- : `${location.origin}/${pageType}/${pageId}/add_related/anime`;
- const relateLi = document.createElement("li");
- relateLi.className = "relate-button";
- relateLi.innerHTML = `<a href="${relateUrl}" target="_blank">关联</a>`;
- nav.appendChild(relateLi);
- }
- }
- // 监听 URL 变化
- function observeURLChanges() {
- let lastURL = location.href;
- new MutationObserver(() => {
- if (location.href !== lastURL) {
- lastURL = location.href;
- initNavButtons();
- }
- }).observe(document, { subtree: true, childList: true });
- }
- /* 封面上传模块 - 仅在页面无封面时显示 */
- async function initCoverUpload() {
- // 检查当前页面类型
- const url = window.location.href;
- const subjectMatch = url.match(/bangumi\.tv\/subject\/(\d+)/);
- const personMatch = url.match(/bangumi\.tv\/person\/(\d+)/);
- const characterMatch = url.match(/bangumi\.tv\/character\/(\d+)/);
- if (!subjectMatch && !personMatch && !characterMatch) return;
- // 检查是否已添加上传模块
- if (document.querySelector("#coverUploadForm")) return;
- // 根据页面类型确定ID和类型
- let id, type;
- if (subjectMatch) {
- id = subjectMatch[1];
- type = "subject";
- } else if (personMatch) {
- id = personMatch[1];
- type = "person";
- } else {
- id = characterMatch[1];
- type = "character";
- }
- // **新逻辑:检查封面是否已存在**
- let coverExists = false;
- if (type === "subject") {
- coverExists = !!document.querySelector("#bangumiInfo img");
- } else {
- coverExists = !!document.querySelector(".infobox_container img") ||
- !!document.querySelector(".subject_sidebar img");
- }
- if (coverExists) {
- console.log("封面已存在,跳过上传模块");
- return;
- }
- // 为不同页面类型找到适当的插入位置
- let targetElement;
- if (type === "subject") {
- targetElement = document.querySelector("#bangumiInfo");
- if (!targetElement) 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);
- targetElement.parentNode.insertBefore(container, targetElement);
- }
- } catch (e) {
- console.error("封面加载失败:", e);
- }
- } else {
- targetElement = document.querySelector(".subject_sidebar") ||
- document.querySelector(".infobox_container") ||
- document.querySelector(".infobox");
- if (!targetElement) return;
- try {
- // 直接获取上传页面
- const uploadUrl = `https://bangumi.tv/${type}/${id}/upload_img`;
- const res = await fetch(uploadUrl);
- const doc = new DOMParser().parseFromString(await res.text(), "text/html");
- const form = doc.querySelector("#columnInSubjectA .text form") ||
- doc.querySelector(".text form") ||
- doc.querySelector("form[enctype='multipart/form-data']");
- 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);
- targetElement.parentNode.insertBefore(container, targetElement);
- }
- } catch (e) {
- console.error("上传模块加载失败:", e);
- }
- }
- }
- // 批量分集编辑器功能模块
- 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)
- );
- }
- };
- // ID收集器 - 用于条目/角色/人物主页
- function initIDCollector() {
- injectStyles();
- // 获取当前页面ID
- const getPageID = () => {
- const match = location.pathname.match(/\/(subject|character|person)\/(\d+)/);
- return match ? parseInt(match[2]) : null;
- };
- const currentID = getPageID();
- // 创建界面容器
- const container = document.createElement("div");
- container.innerHTML = `
- <div class="enhancer-panel">
- <h3>批量关联助手</h3>
- <p>当前位置:ID收集 - 点击下方按钮前往关联页面</p>
- <div style="text-align: center">
- <a href="${window.location.pathname}/add_related/subject" class="btnCustom">
- 前往批量关联页面
- </a>
- </div>
- </div>
- `;
- // 插入到页面
- const targetNode = $("#sbjSearchMod") || $(".main_content");
- if (targetNode.length) targetNode.after(container);
- }
- // 批量关联功能 - 用于关联页面
- function initBatchRelation() {
- injectStyles();
- // 参数配置
- const DELAY_AFTER_CLICK = 250;
- const DELAY_BETWEEN_ITEMS = 500;
- const MAX_RETRY_ATTEMPTS = 10;
- const RETRY_INTERVAL = 100;
- // 全局变量
- let globalItemType = '1';
- let currentProcessingIndex = -1;
- // 根据当前 URL 判断页面类型
- function getCurrentPageType() {
- const url = window.location.href;
- if (url.indexOf('/add_related/character') !== -1) {
- return 'character';
- } else if (url.indexOf('/add_related/subject') !== -1) {
- return 'subject';
- } else {
- return 'person';
- }
- }
- // 生成关联类型选择下拉框
- function generateTypeSelector() {
- const pageType = getCurrentPageType();
- if (pageType === 'character') {
- let options = '';
- const relationTypes = {
- '1': '主角',
- '2': '配角',
- '3': '客串'
- };
- for (let [value, text] of Object.entries(relationTypes)) {
- options += `<option value="${value}">${text}</option>`;
- }
- return `<span class="select-label">类型: </span><select>${options}</select>`;
- } else {
- // 如果有 genPrsnStaffList 函数则调用,否则返回空字符串
- return `<span class="select-label"></span>${(typeof genPrsnStaffList === "function") ? genPrsnStaffList(-1) : ''}`;
- }
- }
- // 针对传入的元素内的下拉框进行设置,并通过递归确保修改成功
- function setRelationTypeWithElement($li, item_type) {
- return new Promise((resolve) => {
- let attempts = 0;
- function trySet() {
- // 确保我们获取的是当前元素内部的select,而不是全局的
- let $select = $li.find('select').first();
- if ($select.length > 0) {
- // 先确保下拉框可交互
- if ($select.prop('disabled')) {
- setTimeout(trySet, RETRY_INTERVAL);
- return;
- }
- $select.val(item_type);
- // 触发 change 事件
- const event = new Event('change', { bubbles: true });
- $select[0].dispatchEvent(event);
- setTimeout(() => {
- if ($select.val() == item_type) {
- resolve(true);
- } else if (attempts < MAX_RETRY_ATTEMPTS) {
- attempts++;
- setTimeout(trySet, RETRY_INTERVAL);
- } else {
- resolve(false);
- }
- }, 200);
- } else if (attempts < MAX_RETRY_ATTEMPTS) {
- attempts++;
- setTimeout(trySet, RETRY_INTERVAL);
- } else {
- resolve(false);
- }
- }
- trySet();
- });
- }
- // 点击项目后利用 MutationObserver 监听新增条目,然后对该条目的下拉框设置类型
- function processItem(element, item_type) {
- return new Promise((resolve) => {
- // 关联列表容器
- const container = document.querySelector('#crtRelat###bjects');
- if (!container) {
- return resolve(false);
- }
- // 保存处理前的条目列表
- const initialItems = Array.from(container.children);
- // 绑定 MutationObserver 监听子节点变化
- const observer = new MutationObserver((mutations) => {
- // 获取当前所有条目
- const currentItems = Array.from(container.children);
- // 找出新增的条目(在当前列表中但不在初始列表中的元素)
- const newItems = currentItems.filter(item => !initialItems.includes(item));
- if (newItems.length > 0) {
- observer.disconnect();
- const newItem = newItems[0]; // 获取第一个新增条目
- // 确保等待DOM完全渲染
- setTimeout(async () => {
- // 使用新的条目元素直接查找其内部的select
- const $select = $(newItem).find('select');
- if ($select.length > 0) {
- const success = await setRelationTypeWithElement($(newItem), item_type);
- resolve(success);
- } else {
- resolve(false);
- }
- }, DELAY_AFTER_CLICK);
- }
- });
- observer.observe(container, { childList: true, subtree: true });
- // 触发点击
- $(element).click();
- // 超时防护
- setTimeout(() => {
- observer.disconnect();
- resolve(false);
- }, MAX_RETRY_ATTEMPTS * RETRY_INTERVAL);
- });
- }
- // 处若搜索结果不唯一且没有完全匹配项则自动选择第一个
- function normalizeText(text) {
- return text.normalize("NFC").replace(/\s+/g, '').replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
- }
- function extractTextFromElement(el) {
- if (!el) return '';
- let text = el.innerText || el.textContent || $(el).text();
- // 尝试从 `iframe` 和 `shadowRoot` 获取文本
- if (!text.trim()) {
- if (el.shadowRoot) {
- text = [...el.shadowRoot.querySelectorAll('*')].map(e => e.textContent).join('');
- }
- let iframe = el.querySelector('iframe');
- if (iframe && iframe.contentDocument) {
- text = iframe.contentDocument.body.textContent;
- }
- }
- return normalizeText(text);
- }
- async function processSingleItem(elements, item_type, search_name) {
- return new Promise(async (resolve) => {
- if (elements.length === 0) {
- $('.chitanda_item_not_found').append(search_name + ' ');
- resolve(false);
- return;
- }
- let elementsArray = elements.toArray();
- let normalizedSearchName = normalizeText(search_name);
- console.log("搜索名(规范化):", normalizedSearchName);
- // 等待元素加载,避免空文本
- await new Promise(res => setTimeout(res, 500));
- let selectedElement = elementsArray.find(el => {
- let normalizedElementText = extractTextFromElement(el);
- console.log("元素文本(规范化):", normalizedElementText); // 调试用
- return normalizedElementText === normalizedSearchName;
- });
- if (!selectedElement) {
- if (elements.length > 1) {
- $('.chitanda_item_dupe').append(`${search_name} `);
- }
- selectedElement = elements[0]; // 没有完全匹配,取第一个
- }
- resolve(await processItem(selectedElement, item_type));
- });
- }
- // 处理下一个项目
- async function proceedToNextItem(idx, item_list, item_type, item_num) {
- if (idx < item_num - 1) {
- setTimeout(async () => {
- await ctd_findItemFunc(item_list, item_type, idx + 1);
- }, DELAY_BETWEEN_ITEMS);
- } else {
- setTimeout(() => {
- $('#subjectList').empty();
- $('#subjectList').show();
- alert('全部添加完成');
- }, DELAY_BETWEEN_ITEMS);
- }
- }
- // 核心查找及处理函数:依次检索每个条目并处理
- var ctd_findItemFunc = async function(item_list, item_type, idx) {
- currentProcessingIndex = idx;
- item_type = globalItemType;
- let search_name = item_list[idx].trim();
- if (!search_name) {
- proceedToNextItem(idx, item_list, item_type, item_list.length);
- return;
- }
- var item_num = item_list.length;
- $('#subjectList').html('<tr><td>正在检索中...</td></tr>');
- var search_mod = $('#sbjSearchMod').attr('value');
- try {
- const response = await new Promise((resolve, reject) => {
- $.ajax({
- type: "GET",
- url: '/json/search-' + search_mod + '/' + encodeURIComponent(search_name),
- dataType: 'json',
- success: resolve,
- error: reject
- });
- });
- var html = '';
- if ($(response).length > 0) {
- subjectList = response;
- for (var i in response) {
- if ($.inArray(search_mod, enableStaffSbjType) != -1) {
- html += genSubjectList(response[i], i, 'submitForm');
- } else {
- html += genSubjectList(response[i], i, 'searchR###lt');
- }
- }
- $('#subjectList').html(html);
- $('.chitanda_current_idx').text(idx + 1);
- $('.chitanda_all_num').text(item_num);
- await new Promise(resolve => setTimeout(resolve, 400)); // 减少等待时间
- var elements = $('#subjectList>li>a.avatar.h');
- if (window.location.pathname.includes('/person/') && window.location.pathname.includes('/add_related/character/anime')) {
- if (elements.length === 0) {
- $('.chitanda_item_not_found').append(search_name + ' ');
- } else {
- $(elements[0]).click();
- if (elements.length > 1) {
- $('.chitanda_item_dupe').append(`${search_name} `);
- }
- }
- $('.chitanda_current_idx').text(idx + 1);
- if (idx < item_num - 1) {
- setTimeout(async () => {
- await ctd_findItemFunc(item_list, item_type, idx + 1);
- }, DELAY_BETWEEN_ITEMS);
- } else {
- setTimeout(() => {
- $('#subjectList').empty();
- $('#subjectList').show();
- alert('全部添加完成');
- }, DELAY_BETWEEN_ITEMS);
- }
- } else {
- await processSingleItem(elements, item_type, search_name, idx, item_list, item_num);
- await proceedToNextItem(idx, item_list, item_type, item_num);
- }
- } else {
- $("#robot").fadeIn(300);
- $("#robot_balloon").html(`没有找到 ${search_name} 的相关结果`);
- $("#robot").animate({ opacity: 1 }, 500).fadeOut(500); // 减少动画时间
- $('.chitanda_item_not_found').append(search_name + ' ');
- $('#subjectList').html(html);
- $('.chitanda_current_idx').text(idx + 1);
- $('.chitanda_all_num').text(item_num);
- await proceedToNextItem(idx, item_list, item_type, item_num);
- }
- } catch (error) {
- console.error('查询出错:', error);
- $("#robot").fadeIn(300);
- $("#robot_balloon").html('通信错误,您是不是重复查询太快了?');
- $("#robot").animate({ opacity: 1 }, 500).fadeOut(1000); // 减少动画时间
- $('#subjectList').html('');
- setTimeout(async () => {
- if (idx < item_list.length - 1) {
- await ctd_findItemFunc(item_list, item_type, idx + 1);
- } else {
- $('#subjectList').empty();
- $('#subjectList').show();
- alert('全部添加完成,但部分查询出错');
- }
- }, 1500); // 减少等待时间
- }
- };
- // 从ID范围中提取ID列表
- function getIDsFromRange(start, end) {
- const startID = parseInt(start, 10);
- const endID = parseInt(end, 10);
- if (isNaN(startID) || isNaN(endID) || startID > endID) {
- alert("ID范围无效");
- return [];
- }
- return Array.from({ length: endID - startID + 1 }, (_, i) => "bgm_id=" + (startID + i));
- }
- // 从自由文本中提取ID列表
- function getIDsFromText(input) {
- if (!input.trim()) {
- alert("请输入ID或内容");
- return [];
- }
- // 先检查是否以 bgm_id= 开头
- if (input.startsWith("bgm_id=")) {
- return input.substring(7)
- .split(/[,\n\r,、\/|;。.()【】<>!?]+| +/)
- .map(id => "bgm_id=" + id.trim())
- .filter(id => id);
- }
- // 识别 URL 形式的 ID
- const urlPattern = /(bgm\.tv|bangumi\.tv|chii\.in)\/(subject|character|person)\/(\d+)/g;
- const urlMatches = [...input.matchAll(urlPattern)].map(m => m[3]);
- if (urlMatches.length > 0) {
- return urlMatches.map(id => "bgm_id=" + id);
- }
- // 识别纯数字 ID
- const numberPattern = /\b\d+\b/g;
- const numberMatches = [...input.matchAll(numberPattern)].map(m => m[0]);
- if (numberMatches.length > 0) {
- return numberMatches.map(id => "bgm_id=" + id);
- }
- // 默认按符号分隔(空格优先级最低)
- return input.split(/[,\n\r,、\/|;。.()【】<>!?]+/) // 先按主要分隔符拆分
- .map(part => part.trim()) // 去除前后空格
- .filter(part => part.length > 0) // 过滤掉空字符串
- .flatMap(part => {
- // 保持以下格式不被拆分:
- // 1. "第3季"、"第2集"
- // 2. "Ⅱ", "Ⅲ", "Ⅳ"(罗马数字)
- // 3. "鬼灭之刃 3" 这种名字+数字
- return /^(第?\d+|[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ]+)$/.test(part) || /\D+\d+$/.test(part) ? [part] : part.split(/ +/);
- });
- }
- // 批量查找入口函数
- var chitanda_MultiFindItemFunc = async function() {
- let item_type = '1';
- let typeSelector = $('.chitanda_item_type select');
- if (typeSelector.length > 0) {
- item_type = typeSelector.val();
- if (item_type == '-999') {
- alert('请先选择关联类型');
- return false;
- }
- globalItemType = item_type;
- }
- let ctd_item_list = [];
- const activeTab = $('.tab-panel.active').attr('id');
- if (activeTab === 'tab-text') {
- // 处理文本输入模式
- const inputVal = $('#custom_ids').val().trim();
- ctd_item_list = getIDsFromText(inputVal);
- } else if (activeTab === 'tab-range') {
- // 处理ID范围模式
- const startID = $('#id_start').val().trim();
- const endID = $('#id_end').val().trim();
- ctd_item_list = getIDsFromRange(startID, endID);
- }
- if (ctd_item_list.length === 0) {
- return false;
- }
- $('#subjectList').hide();
- $('.chitanda_item_not_found').empty();
- $('.chitanda_item_dupe').empty();
- $('.chitanda_current_idx').text('0');
- $('.chitanda_all_num').text(ctd_item_list.length);
- currentProcessingIndex = -1;
- await ctd_findItemFunc(ctd_item_list, item_type, 0);
- };
- // 切换标签页
- function switchTab(tabId) {
- $('.tab-nav button').removeClass('active');
- $(`.tab-nav button[data-tab="${tabId}"]`).addClass('active');
- $('.tab-panel').removeClass('active');
- $(`#${tabId}`).addClass('active');
- }
- // 根据页面类型设定 UI 标题
- let uiTitle = '人物';
- const pageType = getCurrentPageType();
- if (pageType === 'character') {
- uiTitle = '角色';
- } else if (pageType === 'subject') {
- uiTitle = '条目';
- }
- // 创建改进的UI界面
- $('.subjectListWrapper').after(`
- <div class="chitanda_wrapper">
- <h3>批量关联助手</h3>
- <div class="tab-nav">
- <button data-tab="tab-text" class="active">自由文本输入</button>
- <button data-tab="tab-range">ID范围输入</button>
- </div>
- <div id="tab-text" class="tab-panel active">
- <textarea id="custom_ids" class="enhancer-textarea"
- placeholder="输入ID或网址(支持多种格式:纯数字、bgm_id=xx、网址、文本,可用各类符号(空格优先级最低)分隔)"></textarea>
- </div>
- <div id="tab-range" class="tab-panel">
- <div class="flex-row" style="justify-content: center">
- <input id="id_start" type="number" placeholder="起始ID" class="input-number">
- <span style="line-height: 30px">~</span>
- <input id="id_end" type="number" placeholder="结束ID" class="input-number">
- </div>
- </div>
- <div class="chitanda_controls" style="margin-top: 10px">
- <span class="chitanda_item_type"></span>
- <button id="btn_ctd_multi_search" class="btnCustom">批量关联</button>
- </div>
- <div class="chitanda_progress">
- 添加进度:<span class="chitanda_current_idx">0</span>/<span class="chitanda_all_num">0</span>
- </div>
- <div class="chitanda_header">未找到的${uiTitle}:</div>
- <div class="chitanda_item_not_found"></div>
- <div class="chitanda_header">存在多个结果的${uiTitle}(已自动选择第一个):</div>
- <div class="chitanda_item_dupe"></div>
- </div>
- `);
- // 添加关联类型选择器
- $('.chitanda_item_type').append(generateTypeSelector());
- $('.chitanda_item_type select').prepend('<option value="-999">请选择关联类型</option>').val('-999');
- // 绑定事件
- $('#btn_ctd_multi_search').on('click', chitanda_MultiFindItemFunc);
- $('.chitanda_item_type select').on('change', function() {
- globalItemType = $(this).val();
- });
- $('.tab-nav button').on('click', function() {
- switchTab($(this).data('tab'));
- });
- }
- /* 启动所有功能 */
- function startEnhancer() {
- initNavButtons();
- observeURLChanges();
- initCoverUpload();
- BatchEpisodeEditor.init();
- console.log("Bangumi Ultimate Enhancer 已启动");
- }
- // 在DOM加载完成后启动脚本
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', startEnhancer);
- } else {
- startEnhancer();
- }
- })();