// ==UserScript== // @name Bangumi Ultimate Enhancer // @namespace https://tampermonkey.net/ // @version 2.5.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() { // 排除特定编辑页面 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; // 创建上传按钮(保持原有UI设计) const createUploadButton = () => { const uploadLi = document.createElement("li"); uploadLi.id = "coverUploadButton"; uploadLi.className = "upload-button"; uploadLi.style.float = "right"; uploadLi.innerHTML = `上传封面`; return uploadLi; }; // 创建表单容器,保持原有 UI 设计不变 const createFormContainer = () => { const formContainer = document.createElement("div"); formContainer.id = "coverUploadFormContainer"; formContainer.classList.add("cover-upload-modal"); // 保持原设计布局:URL 上传输入框、异步加载的本地上传表单、预览区域均按原有样式呈现 formContainer.innerHTML = `
图片预览
`; // 设置必要的定位和初始隐藏,保持原来的样式控制由外部CSS决定 formContainer.style.position = "absolute"; formContainer.style.zIndex = "9999"; formContainer.style.display = "none"; return formContainer; }; const uploadLi = createUploadButton(); const formContainer = createFormContainer(); nav.appendChild(uploadLi); document.body.appendChild(formContainer); let formLoaded = false; // 标记本地上传表单是否加载完毕 let hideTimeout = null; // 图片下载和转换函数 async function downloadAndConvertImage(imageUrl) { try { // 尝试提取实际的图片 URL let actualImageUrl = imageUrl; // 检查是否是 Google 图片重定向链接 if (imageUrl.includes('google.com/imgres')) { const urlParams = new URL(imageUrl).searchParams; actualImageUrl = urlParams.get('imgurl'); } // 如果没有提取到图片 URL,则使用原始链接 if (!actualImageUrl) { actualImageUrl = imageUrl; } // 发送请求下载图片 const response = await fetch(actualImageUrl); const blob = await response.blob(); // 根据输入图片的类型确定输出格式 const inputType = blob.type; const outputType = inputType.includes('jpeg') || inputType.includes('jpg') ? 'image/jpeg' : inputType.includes('png') ? 'image/png' : 'image/jpeg'; // 如果类型未知,默认转换为 JPEG // 创建画布用于图像转换 const img = await createImageBitmap(blob); const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); // 将图像转换为指定格式的 Blob const convertedBlob = await new Promise(resolve => { canvas.toBlob(resolve, outputType); }); // 根据转换后的格式确定文件扩展名 const fileExtension = outputType === 'image/png' ? 'png' : 'jpg'; const convertedFile = new File([convertedBlob], `cover.${fileExtension}`, { type: outputType }); // 预览转换后的图片 const previewContainer = document.querySelector("#imagePreviewContainer"); const previewImage = document.querySelector("#imagePreview"); previewImage.src = URL.createObjectURL(convertedBlob); previewContainer.style.display = "block"; // 查找文件上传表单 const fileInput = document.querySelector("#coverUploadForm input[type='file']"); if (fileInput) { // 创建 DataTransfer 对象并填充文件 const dataTransfer = new DataTransfer(); dataTransfer.items.add(convertedFile); fileInput.files = dataTransfer.files; // 触发文件上传输入框的 change 事件 const event = new Event('change', { bubbles: true }); fileInput.dispatchEvent(event); // 显示提交按钮 const submitButton = document.querySelector("#coverUploadForm input[type='submit']"); if (submitButton) { submitButton.style.display = 'block'; } } else { alert('未找到文件上传输入框'); } } catch (error) { console.error('下载或转换图片时发生错误:', error); alert('下载图片失败:' + error.message); } } // 新增的全局点击事件,点击表单容器外区域关闭并取消表单容器 function setupGlobalClickHandler(container, trigger) { document.addEventListener('click', function (event) { if (!container.contains(event.target) && !trigger.contains(event.target)) { container.style.display = "none"; } }); } // 预先加载本地上传表单,提升加载速度 async function preloadLocalUpload() { if (formLoaded) return; const uploadFormContainer = formContainer.querySelector("#uploadFormContainer"); uploadFormContainer.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']"); if (form) { form.id = "coverUploadForm"; form.style.margin = "0"; form.style.padding = "0"; uploadFormContainer.innerHTML = form.outerHTML; // 为本地文件上传绑定预览处理事件 const fileInput = document.querySelector("#coverUploadForm input[type='file']"); fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (ev) => { const previewContainer = formContainer.querySelector("#imagePreviewContainer"); const previewImage = formContainer.querySelector("#imagePreview"); previewImage.src = ev.target.result; previewContainer.style.display = "block"; const submitButton = document.querySelector("#coverUploadForm input[type='submit']"); if (submitButton) { submitButton.style.display = 'block'; } }; reader.readAsDataURL(file); } }); formLoaded = true; } else { uploadFormContainer.innerHTML = "无法加载上传表单"; } } catch (e) { uploadFormContainer.innerHTML = "加载失败"; console.error("上传模块加载失败:", e); } } // 事件绑定逻辑,保持原有UI展示行为 const setupEventHandlers = () => { const urlInput = formContainer.querySelector("#imageUrlInput"); const downloadButton = formContainer.querySelector("#downloadUrlButton"); // 显示表单函数 const showForm = () => { clearTimeout(hideTimeout); const buttonRect = uploadLi.getBoundingClientRect(); formContainer.style.top = `${buttonRect.bottom + window.scrollY + 5}px`; formContainer.style.left = `${buttonRect.left + window.scrollX - 180}px`; formContainer.style.display = "block"; }; // 延迟隐藏表单逻辑 const hideForm = () => { const previewContainer = formContainer.querySelector("#imagePreviewContainer"); if (previewContainer.style.display === "block") return; 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); // URL上传部分事件处理 urlInput.addEventListener('focus', () => { urlInput.style.borderColor = '#F4C7CC'; urlInput.style.boxShadow = '0 0 5px rgba(244, 199, 204, 0.5)'; }); urlInput.addEventListener('blur', () => { urlInput.style.borderColor = '#ddd'; urlInput.style.boxShadow = 'none'; }); downloadButton.addEventListener('click', () => { const imageUrl = urlInput.value.trim(); if (imageUrl) { downloadAndConvertImage(imageUrl); } else { alert('请输入图片 URL'); } }); urlInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') downloadButton.click(); }); }; // 注册全局点击事件(点击容器外关闭表单) setupGlobalClickHandler(formContainer, uploadLi); // 预先加载本地上传表单(在页面初始加载时即启动) preloadLocalUpload(); // 事件处理绑定 setupEventHandlers(); } /* MutationObserver 保证上传按钮始终存在 */ const observer = new MutationObserver(() => { if (!document.querySelector("#coverUploadButton")) { initCoverUpload(); } }); observer.observe(document.body, { childList: true, subtree: true }); /* ==================== 批量分集编辑器功能模块 =====================*/ 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(); } })();