// ==UserScript== // @name Bangumi Ultimate Enhancer // @namespace https://tampermonkey.net/ // @version 2.4.2 // @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() { // 排除特定页面 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 = `Wiki`; 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 = `关联`; 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() { if (/\/(edit_detail|edit|add_related|upload_img)/.test(location.pathname)) return; const url = window.location.href; let id, type; if (/bangumi\.tv\/subject\/(\d+)/.test(url)) { id = url.match(/bangumi\.tv\/subject\/(\d+)/)[1]; type = "subject"; } else if (/bangumi\.tv\/person\/(\d+)/.test(url)) { id = url.match(/bangumi\.tv\/person\/(\d+)/)[1]; type = "person"; } else if (/bangumi\.tv\/character\/(\d+)/.test(url)) { id = url.match(/bangumi\.tv\/character\/(\d+)/)[1]; type = "character"; } else { return; } // 检查按钮是否已存在,避免重复插入 if (document.querySelector("#coverUploadButton")) return; // 获取导航栏 const nav = document.querySelector(".subjectNav .navTabs") || document.querySelector(".navTabs"); if (!nav) return; // 创建上传按钮 const uploadLi = document.createElement("li"); uploadLi.id = "coverUploadButton"; uploadLi.className = "upload-button"; uploadLi.style.float = "right"; uploadLi.innerHTML = `上传封面`; nav.appendChild(uploadLi); // 创建上传表单容器(初始隐藏) const formContainer = document.createElement("div"); formContainer.id = "coverUploadFormContainer"; formContainer.style.display = "none"; formContainer.style.position = "absolute"; formContainer.style.zIndex = "1000"; formContainer.style.backgroundColor = "#fff"; formContainer.style.border = "1px solid #ddd"; formContainer.style.borderRadius = "4px"; formContainer.style.padding = "10px"; formContainer.style.boxShadow = "0 2px 5px rgba(0,0,0,0.2)"; formContainer.style.width = "240px"; document.body.appendChild(formContainer); let formLoaded = false; let hideTimeout = null; uploadLi.addEventListener("mouseenter", async function () { 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://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("form[enctype='multipart/form-data']"); if (form) { formContainer.innerHTML = ""; const clone = form.cloneNode(true); clone.id = "coverUploadForm"; formContainer.appendChild(clone); formLoaded = true; } else { formContainer.innerHTML = "无法加载上传表单"; } } catch (e) { formContainer.innerHTML = "加载失败"; console.error("上传模块加载失败:", e); } } }); uploadLi.addEventListener("mouseleave", function () { hideTimeout = setTimeout(() => { if (!formContainer.matches(":hover")) { formContainer.style.display = "none"; } }, 200); }); formContainer.addEventListener("mouseenter", function () { clearTimeout(hideTimeout); }); formContainer.addEventListener("mouseleave", function () { hideTimeout = setTimeout(() => { formContainer.style.display = "none"; }, 200); }); } // 监听整个页面的变化,确保按钮不会消失 const observer = new MutationObserver(() => { if (!document.querySelector("#coverUploadButton")) { initCoverUpload(); } }); // 监听整个 body,防止 Bangumi 的 SPA 机制导致按钮被删除 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; 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(/
添加进度: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(); } })();