// ==UserScript== // @name Bangumi Ultimate Enhancer // @namespace https://tampermonkey.net/ // @version 2.4.5 // @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/* // @exclude */character/*/add_related/person* // @exclude */person/*/add_related/character* // @connect bgm.tv // @icon https://lain.bgm.tv/pic/icon/l/000/00/01/128.jpg // @grant GM_xmlhttpRequest // @license MIT // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js // @run-at document-idle // @downloadURL none // ==/UserScript== (function() { "use strict"; // 样式注入 function injectStyles() { $('head').append(` `); } /* ==================== Wiki 按钮和关联按钮模块 ======================*/ function initNavButtons() { // 排除特定编辑页面 const EXCLUDE_PATHS = /(edit_detail|edit|add_related|upload_img)/; if (EXCLUDE_PATHS.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, pageId] = pathMatch; const origin = location.origin; // 按钮配置 const buttons = [ { className: "wiki-button", getText: () => "Wiki", getUrl: () => pageType === "subject" ? `${origin}/${pageType}/${pageId}/edit_detail` : `${origin}/${pageType}/${pageId}/edit` }, { className: "relate-button", getText: () => "关联", getUrl: () => pageType === "subject" ? `${origin}/${pageType}/${pageId}/add_related/subject/anime` : `${origin}/${pageType}/${pageId}/add_related/anime` } ]; // 添加按钮 buttons.forEach(button => { if (!nav.querySelector(`.${button.className}`)) { const li = document.createElement("li"); li.className = button.className; li.innerHTML = `${button.getText()}`; nav.appendChild(li); } }); } // 监听 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 SUPPORTED_DOMAINS = ['bangumi\\.tv', 'bgm\\.tv', 'chii\\.in']; const EXCLUDE_PATHS = /\/(edit_detail|edit|add_related|upload_img)$/; // 如果是编辑页面,则不执行 if (EXCLUDE_PATHS.test(location.pathname)) return; // 解析页面类型和 ID const url = window.location.href; const parseId = (path) => { const regex = new RegExp(`(${SUPPORTED_DOMAINS.join('|')})\\/${path}\\/(\\d+)`); const match = url.match(regex); return match ? { id: match[2], type: path } : null; }; const typeMapping = ['subject', 'person', 'character']; const parsedInfo = typeMapping.reduce((result, type) => result || parseId(type), null); if (!parsedInfo) return; // 避免重复添加按钮 if (document.querySelector("#coverUploadButton")) return; // 获取导航栏 const nav = document.querySelector(".subjectNav .navTabs") || document.querySelector(".navTabs"); if (!nav) return; // 创建上传按钮 const createUploadButton = () => { const uploadLi = document.createElement("li"); uploadLi.id = "coverUploadButton"; uploadLi.className = "upload-button"; uploadLi.style.float = "right"; uploadLi.innerHTML = `上传封面`; return uploadLi; }; // 创建表单容器 const createFormContainer = () => { const formContainer = document.createElement("div"); formContainer.id = "coverUploadFormContainer"; Object.assign(formContainer.style, { display: "none", position: "absolute", zIndex: "1000", backgroundColor: "#fff", border: "1px solid #ddd", borderRadius: "4px", padding: "5px", boxShadow: "0 2px 5px rgba(0,0,0,0.2)", width: "240px", height: "100px" }); return formContainer; }; const uploadLi = createUploadButton(); const formContainer = createFormContainer(); nav.appendChild(uploadLi); document.body.appendChild(formContainer); let formLoaded = false; let hideTimeout = null; // 处理鼠标悬停和离开事件 const setupEventHandlers = () => { const showForm = async () => { clearTimeout(hideTimeout); const buttonRect = uploadLi.getBoundingClientRect(); formContainer.style.top = `${buttonRect.bottom + window.scrollY}px`; formContainer.style.left = `${buttonRect.left + window.scrollX - 180}px`; formContainer.style.display = "block"; if (!formLoaded) { formContainer.innerHTML = "加载中..."; try { const uploadUrl = `https://${window.location.host}/${parsedInfo.type}/${parsedInfo.id}/upload_img`; const res = await fetch(uploadUrl); const doc = new DOMParser().parseFromString(await res.text(), "text/html"); const form = doc.querySelector("form[enctype='multipart/form-data']"); formContainer.innerHTML = form ? (form.id = "coverUploadForm", form.outerHTML) : "无法加载上传表单"; formLoaded = !!form; } catch (e) { formContainer.innerHTML = "加载失败"; console.error("上传模块加载失败:", e); } } }; const hideForm = () => { hideTimeout = setTimeout(() => { if (!formContainer.matches(":hover")) { formContainer.style.display = "none"; } }, 200); }; uploadLi.addEventListener("mouseenter", showForm); uploadLi.addEventListener("mouseleave", hideForm); formContainer.addEventListener("mouseenter", () => clearTimeout(hideTimeout)); formContainer.addEventListener("mouseleave", hideForm); }; setupEventHandlers(); } // 使用 MutationObserver 确保按钮持续存在 const observer = new MutationObserver(() => { if (!document.querySelector("#coverUploadButton")) { initCoverUpload(); } }); observer.observe(document.body, { childList: true, subtree: true }); // 初始化 document.addEventListener("DOMContentLoaded", initCoverUpload); setTimeout(initCoverUpload, 1000); /* ==================== 批量分集编辑器功能模块 =====================*/ const BatchEpisodeEditor = { // 常量配置 CHUNK_SIZE: 20, BASE_URL: '', CSRF_TOKEN: '', // 初始化入口 init() { // 非分集编辑页面直接返回 if (!this.isEpisodePage()) return; // 获取基础URL和CSRF令牌 this.BASE_URL = location.pathname.replace(/\/edit_batch$/, ''); this.CSRF_TOKEN = $('[name=formhash]')?.value || ''; if (!this.CSRF_TOKEN) return; // 绑定事件和增强功能 this.bindHashChange(); this.upgradeCheckboxes(); this.addEnhancerNotice(); }, // 添加功能增强提示 addEnhancerNotice() { 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: () => /^\/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() { 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({ chkall: 'on', submit: '批量修改', formhash: this.CSRF_TOKEN, 'ep_mod[]': episodeIds }); 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(/
添加进度:0/0
未找到的${uiTitle}:
存在多结果的${uiTitle}(无最佳匹配结果,将自动选择第一个):
`); // 添加关联类型选择器 $('.Relation_item_type').append(generateTypeSelector()); $('.Relation_item_type select').prepend('').val('-999'); // 绑定事件 $('#btn_ctd_multi_search').on('click', Relation_MultiFindItemFunc); $('.Relation_item_type select').on('change', function() { globalItemType = $(this).val(); }); $('.tab-nav button').on('click', function() { switchTab($(this).data('tab')); }); } // 启动所有功能 function startEnhancer() { initNavButtons(); observeURLChanges(); initCoverUpload(); initBatchRelation() BatchEpisodeEditor.init(); console.log("Bangumi Ultimate Enhancer 已启动"); } // 在DOM加载完成后启动脚本 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startEnhancer); } else { startEnhancer(); } })();