// ==UserScript== // @name Bangumi Ultimate Enhancer // @namespace https://tampermonkey.net/ // @version 2.6 // @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; }; // 创建表单容器,移除格式选择部分 const createFormContainer = () => { const formContainer = document.createElement("div"); formContainer.id = "coverUploadFormContainer"; formContainer.classList.add("cover-upload-modal"); // 移除格式选择下拉框 formContainer.innerHTML = `
图片预览
`; // 设置必要的定位和初始隐藏 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; let isFormPinned = false; // 新增:标记表单是否被固定(点击按钮后) // 显示状态消息 function showStatus(message, isError = false) { const statusDiv = document.getElementById('statusMessage'); statusDiv.textContent = message; statusDiv.style.display = 'block'; statusDiv.style.backgroundColor = isError ? '#ffeeee' : '#eeffee'; statusDiv.style.color = isError ? '#cc0000' : '#007700'; statusDiv.style.border = `1px solid ${isError ? '#cc0000' : '#007700'}`; console.log(`[状态] ${message}`); } // 创建一个隐藏的iframe用于POST请求 function createHiddenIframe() { const existingIframe = document.getElementById('hiddenUploadFrame'); if (existingIframe) { return existingIframe; } const iframe = document.createElement('iframe'); iframe.id = 'hiddenUploadFrame'; iframe.name = 'hiddenUploadFrame'; iframe.style.display = 'none'; document.body.appendChild(iframe); return iframe; } // 从上传结果页面中提取投票链接并自动投票 function processUploadResult(iframe) { try { // 等待iframe加载完成 return new Promise((resolve, reject) => { iframe.onload = function() { try { // 获取iframe中的文档内容 const iframeDocument = iframe.contentDocument || iframe.contentWindow.document; console.log("Iframe加载完成,检查投票链接..."); // 获取所有投票链接 const allVoteLinks = iframeDocument.querySelectorAll('a[href*="/vote/cover/"]'); console.log(`找到 ${allVoteLinks.length} 个投票链接`); // 选择最后一个投票链接 const voteLink = allVoteLinks.length > 0 ? allVoteLinks[allVoteLinks.length - 1] : null; if (voteLink) { const href = voteLink.getAttribute('href'); console.log("选择最后一个投票链接:", href); // 构建完整的投票URL const host = window.location.host; const voteUrl = href.startsWith('http') ? href : `https://${host}${href.startsWith('/') ? '' : '/'}${href}`; console.log("完整投票URL:", voteUrl); // 使用fetch API进行投票 showStatus('封面上传成功,正在投票...'); fetch(voteUrl, { method: 'GET', credentials: 'include', // 重要:包含cookies headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml', 'Referer': window.location.href } }) .then(response => { if (response.ok) { console.log("投票成功,状态:", response.status); showStatus('投票成功!页面将在3秒后刷新...'); setTimeout(() => window.location.reload(), 3000); resolve(true); } else { console.error("投票失败,状态:", response.status); showStatus('封面上传成功,但投票失败。3秒后跳转到手动投票页面...', true); setTimeout(() => { window.location.href = `${window.location.href.split('?')[0]}/upload_img`; }, 3000); reject(new Error(`投票请求失败,状态 ${response.status}`)); } }) .catch(error => { console.error("投票请求错误:", error); showStatus('封面上传成功,但投票失败。3秒后跳转到手动投票页面...', true); setTimeout(() => { window.location.href = `${window.location.href.split('?')[0]}/upload_img`; }, 3000); reject(error); }); } else { // 检查是否有错误信息 console.log("未找到投票链接,检查是否有错误消息"); const errorMsg = iframeDocument.querySelector('.error, .errorMessage, [class*="error"]'); if (errorMsg) { console.error("上传错误:", errorMsg.textContent); showStatus(`上传失败: ${errorMsg.textContent},3秒后跳转到手动上传页面...`, true); setTimeout(() => { window.location.href = `${window.location.href.split('?')[0]}/upload_img`; }, 3000); reject(new Error(errorMsg.textContent)); } else { console.log("未找到投票链接或错误消息"); showStatus('封面似乎已上传成功,但未找到投票链接。3秒后跳转到手动处理页面...', true); setTimeout(() => { window.location.href = `${window.location.href.split('?')[0]}/upload_img`; }, 3000); reject(new Error('未找到投票链接')); } } } catch (error) { console.error('处理上传结果时出错:', error); showStatus('处理上传结果时出错,3秒后跳转到手动上传页面...', true); setTimeout(() => { window.location.href = `${window.location.href.split('?')[0]}/upload_img`; }, 3000); reject(error); } }; // 处理iframe加载错误 iframe.onerror = function(error) { console.error("Iframe加载错误:", error); showStatus('上传请求失败,3秒后跳转到手动上传页面...', true); setTimeout(() => { window.location.href = `${window.location.href.split('?')[0]}/upload_img`; }, 3000); reject(new Error('上传请求失败')); }; }); } catch (error) { console.error('处理上传结果时出错:', error); showStatus(`处理出错: ${error.message},3秒后跳转到手动上传页面...`, true); setTimeout(() => { window.location.href = `${window.location.href.split('?')[0]}/upload_img`; }, 3000); return Promise.reject(error); } } // 修改表单提交处理函数,使用iframe提交 function setupFormForIframeSubmission(form) { // 创建隐藏的iframe const iframe = createHiddenIframe(); // 设置表单的target为iframe form.target = 'hiddenUploadFrame'; // 监听表单提交事件 form.addEventListener('submit', function(e) { // 不阻止默认提交行为,而是让它提交到iframe showStatus('正在上传封面...'); // 处理iframe的响应 processUploadResult(iframe).catch(error => { console.error('处理上传结果失败:', error); }); }); } // 优化的图片转换函数 - 自动选择最佳格式 async function convertImageFormat(file) { return new Promise((resolve, reject) => { try { // 读取文件 const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { // 检查图片类型并自动决定最佳格式 const fileType = file.type.toLowerCase(); let hasTransparency = false; let finalFormat; // 创建画布检测透明度 const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); // 检查图片是否包含透明像素 try { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const pixels = imageData.data; // 检查透明度通道 for (let i = 3; i < pixels.length; i += 4) { if (pixels[i] < 255) { hasTransparency = true; break; } } } catch (e) { console.warn("无法检查透明度,默认使用原格式", e); } // 决定最终格式 if (hasTransparency) { // 有透明度,使用PNG finalFormat = 'image/png'; console.log("检测到透明像素,使用PNG格式"); } else if (fileType.includes('png') && !hasTransparency) { // 无透明度的PNG图片转为JPEG以减小体积 finalFormat = 'image/jpeg'; console.log("PNG图片无透明像素,转换为JPEG格式以优化体积"); } else { // 保持原格式 finalFormat = fileType.includes('jpeg') || fileType.includes('jpg') ? 'image/jpeg' : fileType.includes('png') ? 'image/png' : 'image/jpeg'; // 默认JPEG console.log(`保持原始格式: ${finalFormat}`); } // 重新创建画布以适应格式 canvas.width = img.width; canvas.height = img.height; const newCtx = canvas.getContext('2d'); // 如果是JPEG,先填充白色背景 if (finalFormat === 'image/jpeg') { newCtx.fillStyle = '#FFFFFF'; newCtx.fillRect(0, 0, canvas.width, canvas.height); } // 绘制图片 newCtx.drawImage(img, 0, 0); // 转换为指定格式 const quality = finalFormat === 'image/jpeg' ? 0.92 : undefined; // JPEG质量设置 canvas.toBlob((blob) => { if (!blob) { reject(new Error('转换图片失败')); return; } // 确定文件扩展名 const ext = finalFormat === 'image/png' ? 'png' : 'jpg'; const newFileName = file.name.split('.')[0] + '.' + ext; // 创建新文件对象 const convertedFile = new File([blob], newFileName, { type: finalFormat }); console.log(`图片已转换为 ${finalFormat}`); resolve({ file: convertedFile, dataURL: canvas.toDataURL(finalFormat, quality), format: finalFormat.split('/')[1] }); }, finalFormat, quality); }; img.onerror = () => { reject(new Error('加载图片失败')); }; img.src = e.target.result; }; reader.onerror = () => { reject(new Error('读取文件失败')); }; reader.readAsDataURL(file); } catch (error) { reject(error); } }); } // 图片下载和转换函数 - 更新为自动转换格式 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; } // 显示正在下载的状态 showStatus('正在下载图片...'); // 发送请求下载图片 const response = await fetch(actualImageUrl); const blob = await response.blob(); // 创建临时文件对象 const tempFileName = actualImageUrl.split('/').pop() || 'image'; const tempFile = new File([blob], tempFileName, { type: blob.type }); // 使用自动转换函数 showStatus('正在优化图片格式...'); const convertedData = await convertImageFormat(tempFile); // 预览转换后的图片 const previewContainer = document.querySelector("#imagePreviewContainer"); const previewImage = document.querySelector("#imagePreview"); previewImage.src = convertedData.dataURL; previewContainer.style.display = "block"; // 查找文件上传表单 const fileInput = document.querySelector("#coverUploadForm input[type='file']"); if (fileInput) { // 创建 DataTransfer 对象并填充文件 const dataTransfer = new DataTransfer(); dataTransfer.items.add(convertedData.file); 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'; } showStatus(`图片已优化为 ${convertedData.format.toUpperCase()} 格式,点击提交按钮上传`); } else { showStatus('未找到文件上传输入框', true); } } catch (error) { console.error('下载或转换图片时发生错误:', error); showStatus(`下载图片失败:${error.message}`, true); } } // 全局点击事件,点击表单容器外区域关闭表单 function setupGlobalClickHandler(container, trigger) { document.addEventListener('click', function (event) { if (!container.contains(event.target) && !trigger.contains(event.target)) { container.style.display = "none"; isFormPinned = false; // 复位固定状态 } }); } // 预先加载本地上传表单,提升加载速度 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"; // 配置表单使用iframe提交 uploadFormContainer.innerHTML = form.outerHTML; // 获取插入到DOM中的表单元素并设置 const insertedForm = document.getElementById("coverUploadForm"); setupFormForIframeSubmission(insertedForm); // 为本地文件上传绑定预览处理事件 const fileInput = document.querySelector("#coverUploadForm input[type='file']"); fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (file) { try { // 显示正在处理的状态 showStatus('正在处理图片...'); // 使用自动转换函数处理图片 const convertedData = await convertImageFormat(file); // 更新文件输入框的文件 const dataTransfer = new DataTransfer(); dataTransfer.items.add(convertedData.file); fileInput.files = dataTransfer.files; // 更新预览 const previewContainer = formContainer.querySelector("#imagePreviewContainer"); const previewImage = formContainer.querySelector("#imagePreview"); previewImage.src = convertedData.dataURL; previewContainer.style.display = "block"; // 显示提交按钮 const submitButton = document.querySelector("#coverUploadForm input[type='submit']"); if (submitButton) { submitButton.style.display = 'block'; } showStatus(`图片已优化为 ${convertedData.format.toUpperCase()} 格式,点击提交按钮上传`); } catch (error) { console.error('处理本地图片失败:', error); showStatus(`处理图片失败: ${error.message}`, true); // 如果转换失败,回退到原始文件的简单预览 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"; showStatus('使用原始格式,点击提交按钮上传'); }; reader.readAsDataURL(file); } } }); formLoaded = true; } else { uploadFormContainer.innerHTML = "无法加载上传表单"; showStatus("无法加载上传表单", true); } } catch (e) { uploadFormContainer.innerHTML = "加载失败"; showStatus("加载上传表单失败", true); 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 = () => { // 如果表单已被固定,不自动隐藏 if (isFormPinned) return; const previewContainer = formContainer.querySelector("#imagePreviewContainer"); const statusMessage = formContainer.querySelector("#statusMessage"); if (previewContainer.style.display === "block" || statusMessage.style.display === "block") return; hideTimeout = setTimeout(() => { if (!formContainer.matches(":hover") && !isFormPinned) { formContainer.style.display = "none"; } }, 200); }; // 固定表单(点击按钮时) uploadLi.addEventListener("click", () => { showForm(); isFormPinned = true; // 固定表单 console.log("表单已固定,鼠标移出不会自动关闭"); }); // 鼠标悬停显示表单(暂时保留,提供更好的用户体验) uploadLi.addEventListener("mouseenter", showForm); // 鼠标移出事件(仅当表单未固定时才尝试隐藏) uploadLi.addEventListener("mouseleave", () => { if (!isFormPinned) { hideForm(); } }); formContainer.addEventListener("mouseenter", () => clearTimeout(hideTimeout)); // 表单鼠标移出事件(仅当表单未固定时才尝试隐藏) formContainer.addEventListener("mouseleave", () => { if (!isFormPinned) { 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 { showStatus('请输入图片 URL', true); } }); 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 BatchEpisodeCreator = { // 初始化入口 init() { // 检查是否为添加章节页面 if (!this.isEpisodeCreatePage()) { return; } // 设置元素监听 this.setupElementObserver(); // 初始检查 this.checkTargetElement(); }, // 检查是否为添加章节页面 isEpisodeCreatePage() { const pattern = /\/subject\/\d+\/ep\/create/; const isMatch = pattern.test(window.location.href); return isMatch; }, // 设置元素监听器 setupElementObserver() { // 使用MutationObserver监听DOM变化 this.observer = new MutationObserver((mutations) => { this.checkTargetElement(); }); // 监听整个文档的变化 this.observer.observe(document.body, { childList: true, subtree: true }); }, // 检查目标元素 checkTargetElement() { // 寻找 div id="batch" style="display: block;" const targetElement = document.querySelector('div#batch[style*="display: block"]'); if (targetElement) { this.addBatchCreationInterface(); } else { // 移除已有界面 const existingInterface = document.querySelector('.batch-creator-area'); if (existingInterface) { existingInterface.remove(); } } }, // 添加批量创建界面 addBatchCreationInterface() { // 避免重复添加 if (document.querySelector('.batch-creator-area')) { return; } // 使用目标元素作为插入点 const targetElement = document.querySelector('div#batch[style*="display: block"]'); if (!targetElement) { return; } // 创建批量添加区域 const batchArea = document.createElement('div'); batchArea.className = 'batch-creator-area Relation_wrapper'; batchArea.innerHTML = `

批量添加章节

范围: -
`; // 容器样式调整 batchArea.style.cssText = ` width: 200px; margin-bottom: var(--margin-medium); padding: 18px; background: var(--background-light); `; // 在创建batchArea之后插入到指定位置 const subjectInnerInfo = document.getElementById('subject_inner_info'); if (subjectInnerInfo) { // 使用 insertAdjacentElement 插入到目标元素下方 subjectInnerInfo.insertAdjacentElement('afterend', batchArea); // 增加间距样式(使用CSS变量) batchArea.style.marginTop = 'var(--margin-medium)'; batchArea.style.marginBottom = 'var(--margin-medium)'; } else { console.warn('未找到 subject_inner_info 元素,已附加到body末尾'); document.body.appendChild(batchArea); } // 绑定事件 this.bindBatchEvents(); }, // 绑定批量事件 bindBatchEvents() { const generateBtn = document.getElementById('generate-episodes'); const applyBtn = document.getElementById('apply-episodes'); if (generateBtn) { generateBtn.addEventListener('click', () => this.generateEpisodes()); } else { console.error('BatchEpisodeCreator: 生成按钮未找到'); } if (applyBtn) { applyBtn.addEventListener('click', () => this.applyToForm()); } else { console.error('BatchEpisodeCreator: 应用按钮未找到'); } }, // 生成章节模板 generateEpisodes() { const start = parseInt(document.getElementById('batch-start').value) || 1; const end = parseInt(document.getElementById('batch-end').value) || 20; const isEmpty = document.getElementById('empty-content').checked; if (start > end) { alert('起始数字不能大于结束数字'); return; } if (end - start >= 100) { if (!confirm(`您将生成${end - start + 1}个章节,确定继续吗?`)) { return; } } let result = ''; for (let i = start; i <= end; i++) { if (isEmpty) { // 空内容模式: 1| | | | result += `${i}| | | |\n`; } else { // 完整模式: 1| | | m| 0000-00-00 result += `${i}| | | m| 0000-00-00\n`; } } const resultArea = document.getElementById('batch-result'); if (resultArea) { resultArea.value = result.trim(); } else { console.error('BatchEpisodeCreator: 结果区域未找到'); } }, // 应用到表单 applyToForm() { const episodeText = document.getElementById('batch-result')?.value; // 修改选择器以匹配 name="eplist" 或 id="eplist" const epTextarea = document.querySelector('textarea[name="eplist"]') || document.querySelector('textarea#eplist') || document.querySelector('form textarea'); if (epTextarea && episodeText) { // 如果表单已有内容,追加到末尾 if (epTextarea.value.trim()) { epTextarea.value += '\n' + episodeText; } else { epTextarea.value = episodeText; } // 提示成功 alert('章节模板已应用到表单'); } else { console.error('BatchEpisodeCreator: 未找到章节输入区域'); alert('未找到章节输入区域,请手动复制生成的内容'); } } }; // 为防止DOM加载顺序问题,增加一个延迟初始化 setTimeout(function() { BatchEpisodeCreator.init(); }, 1000); /* ============= 批量关联增强版 ===============*/ function initBatchRelation() { injectStyles(); // 参数配置 const DELAY_AFTER_CLICK = 150; const DELAY_BETWEEN_ITEMS = 300; const MAX_RETRY_ATTEMPTS = 10; const RETRY_INTERVAL = 100; // 全局变量 let globalItemType = '1'; let currentProcessingIndex = -1; // 添加全局设置变量 let enableExistingRelationCheck = true; // 根据当前 URL 判断页面类型(支持排除特定路径) function getCurrentPageType() { const path = window.location.pathname; // 调整正则表达式优先级,先检查更具体的路径 if (/^\/(?:subject\/\d+\/add_related\/character|character\/\d+\/add_related\/)/.test(path)) { return 'character'; } else if (/^\/subject\/\d+\/add_related\//.test(path)) { return 'subject'; } else { return 'person'; } } // 增强版下拉框生成 function generateTypeSelector() { const pageType = getCurrentPageType(); // 公共选项生成逻辑 const generateOptions = (types) => { return Object.entries(types) .map(([value, text]) => ``) .join(''); }; switch(pageType) { case 'character': return `类型: `; default: return `${ 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(); }); } // 修改 checkAndHandleExistingRelation 函数 function checkAndHandleExistingRelation(search_name, item_id, item_type) { return new Promise(async (resolve) => { // 如果开关关闭,直接返回未关联状态 if (!enableExistingRelationCheck) { resolve({ exists: false }); return; } // 获取所有已关联条目的容器 const relatedContainer = document.querySelector('#crtRelateSubjects'); if (!relatedContainer) { resolve({ exists: false }); return; } // 原有的检查逻辑保持不变... const relatedItems = relatedContainer.querySelectorAll('li'); for (const item of relatedItems) { // 检查条目ID是否匹配 - 从URL中提取ID const itemLink = item.querySelector('a[href*="/subject/"], a[href*="/character/"], a[href*="/person/"]'); if (!itemLink) continue; const urlMatch = itemLink.href.match(/\/(subject|character|person)\/(\d+)/); if (!urlMatch || urlMatch[2] !== item_id.toString()) continue; // 找到匹配的已关联条目,检查并更新类型 const $select = $(item).find('select').first(); if ($select.length > 0) { const currentType = $select.val(); if (currentType !== item_type) { // 类型不同,需要更新 const success = await setRelationTypeWithElement($(item), item_type); if (success) { $('.Relation_item_type_changed').append(`${search_name} `); resolve({ exists: true, typeChanged: true }); return; } } else { // 类型相同,无需更新 $('.Relation_item_unchanged').append(`${search_name} `); resolve({ exists: true, typeChanged: false }); return; } } } // 未找到匹配的已关联条目 resolve({ exists: false }); }); } // 点击项目后利用 MutationObserver 监听新增条目,然后对该条目的下拉框设置类型 function processItem(element, item_type, item_id, search_name) { return new Promise(async (resolve) => { // 先检查条目是否已关联 const existingCheck = await checkAndHandleExistingRelation(search_name, item_id, item_type); if (existingCheck.exists) { return resolve(true); // 已处理完毕,无需继续 } // 条目未关联,进行新增操作 // 关联列表容器 const container = document.querySelector('#crtRelateSubjects'); 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) { $('.Relation_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) { $('.Relation_item_dupe').append(`${search_name} `); } selectedElement = elements[0]; // 没有完全匹配,取第一个 } // 提取条目ID let item_id = null; const itemHref = $(selectedElement).attr('href'); const idMatch = itemHref && itemHref.match(/\/(subject|character|person)\/(\d+)/); if (idMatch) { item_id = idMatch[2]; } resolve(await processItem(selectedElement, item_type, item_id, search_name)); }); } // 处理下一个项目 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('正在检索中...'); 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, 'searchResult'); } } $('#subjectList').html(html); $('.Relation_current_idx').text(idx + 1); $('.Relation_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) { $('.Relation_item_not_found').append(search_name + ' '); } else { // 提取条目ID let item_id = null; const itemHref = $(elements[0]).attr('href'); const idMatch = itemHref && itemHref.match(/\/(subject|character|person)\/(\d+)/); if (idMatch) { item_id = idMatch[2]; } // 检查是否已关联 const existingCheck = await checkAndHandleExistingRelation(search_name, item_id, item_type); if (!existingCheck.exists) { $(elements[0]).click(); } if (elements.length > 1) { $('.Relation_item_dupe').append(`${search_name} `); } } $('.Relation_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); // 减少动画时间 $('.Relation_item_not_found').append(search_name + ' '); $('#subjectList').html(html); $('.Relation_current_idx').text(idx + 1); $('.Relation_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分隔和准确搜索 function parsePersonInput(input) { input = input.trim(); // 支持URL格式 const urlMatch = input.match(/(?:bgm\.tv|bangumi\.tv|chii\.in)\/(?:person|character|subject)\/(\d+)/i); if (urlMatch) return urlMatch[1]; // 提取纯数字ID - 每次只返回一个ID const numberMatch = input.match(/^\d+$/); if (numberMatch) return numberMatch[0]; // 支持姓名直接搜索 if (/^[\u4e00-\u9fa5a-zA-Z0-9\s]+$/.test(input)) { return encodeURIComponent(input); } return input; // 如果无法识别,返回原始输入 } // 从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)); } const numberMap = { '0': '零', '1': '一', '2': '二', '3': '三', '4': '四', '5': '五', '6': '六', '7': '七', '8': '八', '9': '九', '10': '十', 'Ⅰ': '一', 'Ⅱ': '二', 'Ⅲ': '三', 'Ⅳ': '四', 'Ⅴ': '五', 'Ⅵ': '六', 'Ⅶ': '七', 'Ⅷ': '八', 'Ⅸ': '九', 'Ⅹ': '十' }; // 修改后的 normalizeSeasonOrEpisode 函数 function normalizeSeasonOrEpisode(text) { text = text.replace(/\s+/g, ''); // 如果完全由数字组成,则直接返回原文本 if (/^\d+$/.test(text)) return text; // 处理带数字的情况(包括直接的数字转换) const numberMatch = text.match(/(\d+)季$/); if (numberMatch) { const number = numberMatch[1]; const chineseNumber = numberMap[number] || number; return text.replace(/\d+季$/, `${chineseNumber}季`); } // 处理原有的罗马数字模式 const romanMatch = text.match(/[^\d]([ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ])$/); if (romanMatch) { const romanNumber = romanMatch[1]; const chineseNumber = numberMap[romanNumber]; return text.replace(/[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ]$/, `${chineseNumber}季`); } // 新增:处理"标题 数字"格式 const simpleTitleNumberMatch = text.match(/(.+?)(\d+)$/); if (simpleTitleNumberMatch) { const title = simpleTitleNumberMatch[1]; const number = simpleTitleNumberMatch[2]; const chineseNumber = numberMap[number] || number; return `${title}第${chineseNumber}季`; } return text; } // 修改后的 getIDsFromText 函数 function getIDsFromText(input) { input = input.trim(); if (!input) { alert("请输入ID或内容"); return []; } // 先识别 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); } // 如果以 "bgm_id=" 开头,则去掉前缀后进行分割,使用 /[^0-9]+/ 作为分隔符 if (input.startsWith("bgm_id=")) { return input.substring(7) .split(/[^0-9]+/) .filter(token => token) .map(token => "bgm_id=" + token); } // 否则先按标点和各种分隔符拆分,再进行标准化和数字提取 return input.split(/[,\n\r,、\/|;。.()【】<>!?]+/) .map(part => part.trim()) .filter(part => part.length > 0) .map(normalizeSeasonOrEpisode) .map(part => { // 处理纯数字ID(此时 normalizeSeasonOrEpisode 不会修改纯数字) const numberMatch = part.match(/\b\d+\b/); if (numberMatch) { return "bgm_id=" + numberMatch[0]; } return part; }) .filter(part => part); } // 批量查找入口函数 var Relation_MultiFindItemFunc = async function() { let item_type = '1'; let typeSelector = $('.Relation_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(); $('.Relation_item_not_found').empty(); $('.Relation_item_dupe').empty(); $('.Relation_item_type_changed').empty(); $('.Relation_item_unchanged').empty(); $('.Relation_current_idx').text('0'); $('.Relation_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'); } let uiTitle = '条目'; const pathname = window.location.pathname; if (pathname.includes('/subject/') && pathname.includes('/add_related/person')) { uiTitle = '人物'; } else if (pathname.includes('/subject/') && pathname.includes('/add_related/character')) { uiTitle = '角色'; } // 创建改进的UI界面 $('.subjectListWrapper').after(`

批量关联助手

(取消勾选将不检查已关联项)
添加进度:0/0
未找到的${uiTitle}:
存在多结果的${uiTitle}(无最佳匹配结果,将自动选择第一个):
已修改类型的${uiTitle}:
无需修改类型的${uiTitle}:
`); // 绑定开关事件 $('#toggle_existing_check').on('change', function() { enableExistingRelationCheck = $(this).prop('checked'); console.log("已关联条目检查功能:", enableExistingRelationCheck ? "已启用" : "已禁用"); }); // 添加关联类型选择器 $('.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(); } })();