// ==UserScript==
// @name Bangumi Ultimate Enhancer
// @namespace https://tampermonkey.net/
// @version 2.2.3
// @description Bangumi 增强套件 - 集成Wiki按钮、封面上传、批量关联、批量分集编辑等功能
// @author Bios & Anonymous (Merged 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/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @license MIT
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// ==/UserScript==
(function () {
"use strict";
// 样式增强
GM_addStyle(`
.btnCustom {
margin: 5px 0;
background-color: #1E90FF !important;
color: white !important;
border-radius: 10px !important;
padding: 5px 15px !important;
border: none !important;
cursor: pointer !important;
transition: opacity 0.2s;
}
.btnCustom:hover {
opacity: 0.8;
}
.enhancer-textarea {
width: 100%;
min-height: 60px;
max-height: 300px;
border: 1px solid #ddd;
border-radius: 10px;
padding: 8px;
margin: 8px 0;
resize: vertical;
font-size: 13px;
box-sizing: border-box;
}
.enhancer-panel {
margin: 6px 0; /* 减少外边距 */
border-radius: 6px; /* 调小圆角 */
padding: 5px; /* 减少内边距 */
background: #f8f8f8;
border: 1px solid #e0e0e0;
}
#coverUploadForm {
text-align: center;
padding: 3px; /* 减少表单内边距 */
}
#coverUploadForm input[type="file"] {
margin: 3px auto; /* 缩小文件输入框边距 */
width: 90%; /* 调宽输入框减少留白 */
display: block;
}
/* 进一步缩小按钮尺寸 */
#coverUploadForm input[type="submit"] {
padding: 4px 8px !important; /* 更紧凑的按钮尺寸 */
font-size: 14px !important; /* 更小的字号 */
margin: 8px auto 2px !important; /* 调整按钮间距 */
}
.bgm-enhancer-status {
background: #e6f4ff;
border-radius: 4px;
padding: 8px;
margin: 8px 0;
border-left: 3px solid #1E90FF;
font-size: 13px;
color: #333;
}
`);
// 通用工具函数
const $ = selector => document.querySelector(selector);
const $$ = selector => Array.from(document.querySelectorAll(selector));
// 获取当前页面的 ID(即 URL 里的 subject/xxx、character/xxx、person/xxx)
function getCurrentPageID() {
const match = window.location.pathname.match(/\/(subject|character|person)\/(\d+)/);
return match ? parseInt(match[2], 10) : null;
}
const currentPageID = getCurrentPageID();
/* ------------------------------
超级增强器功能模块
------------------------------ */
/* Wiki 按钮模块 */
function initWikiButton() {
// 排除编辑页面
if (/(edit_detail|edit)$/.test(location.pathname)) return;
// 排除 add_related 和 upload_img 页面
if (/add_related|upload_img/.test(location.pathname)) return;
const matchSubject = location.pathname.match(/\/subject\/(\d+)/);
const matchPerson = location.pathname.match(/\/person\/(\d+)/);
const matchCharacter = location.pathname.match(/\/character\/(\d+)/);
const nav = document.querySelector(".subjectNav .navTabs, .navTabs");
if (!nav || nav.querySelector(".wiki-button")) return;
const li = document.createElement("li");
li.className = "wiki-button";
let wikiUrl = "";
if (matchSubject) {
wikiUrl = `${location.origin}/subject/${matchSubject[1]}/edit_detail`;
} else if (matchPerson) {
wikiUrl = `${location.origin}/person/${matchPerson[1]}/edit`;
} else if (matchCharacter) {
wikiUrl = `${location.origin}/character/${matchCharacter[1]}/edit`;
}
if (wikiUrl) {
li.innerHTML = `<a href="${wikiUrl}" target="_blank" onclick="this.closest('li').remove()">Wiki</a>`;
nav.appendChild(li);
}
}
// 监听 URL 变化
function observeURLChanges() {
let lastURL = location.href;
new MutationObserver(() => {
if (location.href !== lastURL) {
lastURL = location.href;
initWikiButton();
}
}).observe(document, { subtree: true, childList: true });
}
/* 封面上传模块 */
async function initCoverUpload() {
if (document.querySelector("img.cover")) return;
const infoBox = document.querySelector("#bangumiInfo");
if (!infoBox) 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);
infoBox.parentNode.insertBefore(container, infoBox);
}
} catch (e) {
console.error("封面加载失败:", e);
}
}
/* 批量关联模块 */
function initBatchRelation() {
if (!document.getElementById("indexCatBox")) return;
const panelHTML = `
<div class="enhancer-panel">
<textarea id="custom_ids" class="enhancer-textarea"
placeholder="输入 ID 或网址(可换行,支持各种分隔字符)"></textarea>
<div style="text-align: center">
<button id="btn_execute" class="btnCustom">自动添加关联</button>
</div>
</div>
<div class="enhancer-panel">
<div style="display: flex; gap: 10px; justify-content: center">
<input id="id_start" type="number" placeholder="起始ID"
style="width: 100px; padding: 6px; border-radius: 8px; border: 1px solid #ddd;">
<span style="line-height: 30px">~</span>
<input id="id_end" type="number" placeholder="结束ID"
style="width: 100px; padding: 6px; border-radius: 8px; border: 1px solid #ddd;">
</div>
<div style="text-align: center; margin-top: 12px">
<button id="btn_generate" class="btnCustom">自动添加关联</button>
</div>
</div>
`;
const searchMod = document.querySelector("#sbjSearchMod");
if (searchMod) searchMod.insertAdjacentHTML("afterend", panelHTML);
document.getElementById("btn_execute")?.addEventListener("click", () => {
const rawInput = document.getElementById("custom_ids").value;
const ids = extractUniqueIDs(rawInput);
if (ids.length) processBatch(ids);
});
document.getElementById("btn_generate")?.addEventListener("click", () => {
const start = parseInt(document.getElementById("id_start").value, 10);
const end = parseInt(document.getElementById("id_end").value, 10);
if (isNaN(start) || isNaN(end) || start > end) {
alert("请输入有效的起始和结束 ID!");
return;
}
const ids = Array.from({ length: end - start + 1 }, (_, i) => start + i);
processBatch([...new Set(ids)]);
});
function extractUniqueIDs(input) {
const allIDs = input.match(/\d+/g) || [];
let uniqueIDs = [...new Set(allIDs.map(Number))].filter(id => id > 0);
if (currentPageID) {
uniqueIDs = uniqueIDs.filter(id => id !== currentPageID);
}
return uniqueIDs;
}
function processBatch(ids) {
if (!ids.length) {
alert("未找到有效的 ID 或所有 ID 都已被排除!");
return;
}
const batch = ids.splice(0, 10);
$("#subjectName").value = `bgm_id=${batch.join(",")}`;
$("#findSubject").click();
const subjectList = document.getElementById("subjectList");
if (!subjectList) {
alert("未找到关联列表,请检查页面结构。");
return;
}
const observer = new MutationObserver((mutations, observerInstance) => {
// 当列表中有 li 元素时认为搜索结果已生成
if (document.querySelectorAll('#subjectList > li').length > 0) {
observerInstance.disconnect();
// 延时500ms等待渲染完成
setTimeout(function(){
var $avatars = document.querySelectorAll('#subjectList>li>a.avatar.h');
if ($avatars.length > 1) {
$avatars.forEach(function(element, index){
setTimeout(function(){
element.click();
console.log("已点击:" + element.textContent);
// 如有需要,可对关联列表中的条目进行样式修改提醒
document.querySelectorAll('#crtRelateSubjects li p.title>a')[0].style.fontWeight = 'bold';
}, index * 500);
});
} else if ($avatars.length === 1) {
$avatars[0].click();
}
// 根据点击数量延时后点击保存按钮
setTimeout(function(){
$("#saveSubject").click();
alert("所有关联项已成功添加!");
}, ($avatars.length > 1 ? $avatars.length * 500 + 300 : 800));
}, 500);
}
});
observer.observe(subjectList, { childList: true, subtree: true });
}
}
/* ------------------------------
批量分集编辑器功能模块
------------------------------ */
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)
);
}
};
/* 启动所有功能 */
function startEnhancer() {
// 启动超级增强器功能
initWikiButton();
observeURLChanges();
initCoverUpload();
initBatchRelation();
// 启动批量分集编辑器功能
BatchEpisodeEditor.init();
console.log("Bangumi Ultimate Enhancer 已启动");
}
// 在DOM加载完成后启动脚本
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startEnhancer);
} else {
startEnhancer();
}
})();