// ==UserScript==
// @name bangumi 自动生成编辑摘要
// @namespace https://bgm.tv/group/topic/433505
// @version 0.4
// @description 自动生成Bangumi编辑摘要
// @author You
// @match https://bgm.tv/subject/*/edit_detail
// @match https://bgm.tv/character/*/edit
// @match https://bgm.tv/person/*/edit
// @match https://bangumi.tv/subject/*/edit_detail
// @match https://bangumi.tv/character/*/edit
// @match https://bangumi.tv/person/*/edit
// @match https://chii.in/subject/*/edit_detail
// @match https://chii.in/character/*/edit
// @match https://chii.in/person/*/edit
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 存储初始值
const SPLIT_RULE = /[()\[\]{}()<>《》「」『』【】+×·→//、,,;;::&&\\等]/;
let initialTitle, initialPlatform, initialPros, initialSeries, initialWcode, initialSummary, initialNsfw, initialTags;
const editSummaryInput = document.querySelector('#editSummary');
const titleInput = document.querySelector('[name="subject_title"], [name="crt_name"]');
const getPlatform = () => document.querySelector(`[for="${[...document.querySelectorAll('input[name=platform]')].find(i => i.checked)?.id}"]`)?.textContent;
const getPros = () => [...document.querySelectorAll('[name^=prsn_pro]')].filter(c => c.checked).map(c => document.querySelector(`[for=${c.id}]`).textContent);
const seriesCheckbox = document.querySelector('#subjectSeries');
const summaryTextarea = document.querySelector('#subject_summary, #crt_summary');
const nsfwCheckbox = document.querySelector('input[name="subject_nsfw"]');
const tagsInput = document.querySelector('input#tags');
const lockedSummary = localStorage.getItem('lockedEditSummary');
if (lockedSummary) editSummaryInput.value = lockedSummary;
// 初始化函数
function init() {
// 获取初始值
initialTitle = titleInput.value;
initialPlatform = getPlatform();
initialPros = getPros();
initialSeries = seriesCheckbox?.checked;
initialWcode = getWcode();
initialSummary = summaryTextarea.value;
initialNsfw = nsfwCheckbox?.checked;
initialTags = tagsInput?.value;
editSummaryInput.style.width = '60%';
editSummaryInput.after(newGenBtn(), newLockButton(), document.createElement('br'), newAutoGenBtn());
}
// 添加自动生成按钮
function newAutoGenBtn() {
const autoGenBtn = document.createElement('input');
autoGenBtn.type = 'checkbox';
autoGenBtn.id = 'autoGenEditSummary';
autoGenBtn.style.marginRight = '5px';
const autoGenLabel = document.createElement('label');
autoGenLabel.htmlFor = 'autoGenEditSummary';
autoGenLabel.textContent = '提交时自动生成';
autoGenBtn.onchange = function () {
localStorage.setItem('autoGenEditSummary', this.checked);
};
autoGenBtn.checked = localStorage.getItem('autoGenEditSummary') === 'true';
autoGenLabel.prepend(autoGenBtn);
return autoGenLabel;
}
// 添加生成按钮
function newGenBtn() {
const genBtn = document.createElement('button');
genBtn.type = 'button';
genBtn.textContent = '生成摘要';
genBtn.onclick = generateEditSummary;
return genBtn;
}
// 添加锁定按钮
function newLockButton() {
let isLocked = lockedSummary;
const lockBtn = document.createElement('button');
lockBtn.type = 'button';
lockBtn.textContent = isLocked ? '🔒' : '🔓';
lockBtn.title = isLocked ? '编辑摘要已锁定' : '编辑摘要未锁定';
lockBtn.style.marginLeft = '5px';
lockBtn.style.cursor = 'pointer';
lockBtn.style.background = 'none';
lockBtn.style.border = 'none';
lockBtn.style.fontSize = '16px';
lockBtn.onclick = function () {
isLocked = !isLocked;
this.textContent = isLocked ? '🔒' : '🔓';
this.title = isLocked ? '编辑摘要已锁定' : '编辑摘要未锁定';
localStorage.setItem('lockedEditSummary', isLocked ? editSummaryInput.value : '');
};
return lockBtn;
}
// 劫持提交按钮
const submitBtn = document.querySelector('input.inputBtn[name="submit"]');
submitBtn.addEventListener('click', () => {
localStorage.getItem('autoGenEditSummary') === 'true' && generateEditSummary();
}, { capture: false });
// 生成编辑摘要
function generateEditSummary() {
const newWcode = getWcode();
const newSummary = summaryTextarea.value;
const newTags = tagsInput?.value;
const changes = [];
// 分析标题变化
if (initialTitle !== titleInput.value) {
changes.push(`修改标题(${initialTitle} → ${titleInput.value})`);
}
// 分析类型变化
const newPlatform = getPlatform();
if (initialPlatform !== newPlatform) {
changes.push(`修改类型(${initialPlatform} → ${newPlatform})`);
}
// 分析人物职业变化
const newPros = getPros();
const proChanges = analyzeArrChanges(initialPros, newPros, '职业');
if (proChanges.length > 0) changes.push(...proChanges);
// 分析系列变化
if (initialSeries !== seriesCheckbox?.checked) {
changes.push(seriesCheckbox?.checked ? '标记为系列' : '取消系列标记');
}
// 分析wcode变化
const wcodeChanges = analyzeWcodeChanges(initialWcode, newWcode);
if (wcodeChanges.length > 0) {
changes.push(...wcodeChanges);
}
// 分析summary变化
if (initialSummary !== newSummary) {
changes.push(`${initialSummary ? '修改' : '添加'}简介`);
}
// 分析nsfw变化
if (initialNsfw !== nsfwCheckbox?.checked) {
changes.push(nsfwCheckbox?.checked ? '标记为受限内容' : '取消受限内容标记');
}
// 分析tags变化
const tagsToArr = tags => tags ? tags.split(/\s+/).filter(t => t) : [];
const tagChanges = analyzeArrChanges(tagsToArr(initialTags), tagsToArr(newTags), '标签');
if (tagChanges.length > 0) changes.push(...tagChanges);
// 肖像变化
if (document.querySelector('input[type=file]')?.value) {
changes.push('新肖像');
}
// 更新编辑摘要输入框
const editSummaryInput = document.querySelector('input#editSummary');
if (editSummaryInput && changes.length > 0) {
if (!editSummaryInput.dataset.userModified || editSummaryInput.value === '') {
editSummaryInput.value = changes.join(';');
}
} else if (editSummaryInput) {
editSummaryInput.value = '空编辑';
}
}
// 分析wcode变化
function analyzeWcodeChanges(oldWcode, newWcode) {
// 解析wcode为对象
const oldData = parseWcode(oldWcode);
const newData = parseWcode(newWcode);
const getMultiData = data => Object.fromEntries(Object.entries(data).filter(([, v]) => typeof v === 'object'));
const oldMultiData = getMultiData(oldData);
const newMultiData = getMultiData(newData);
const multiKeyChanges = [];
for (const key in oldMultiData) {
if (key in newMultiData) {
const subChanges = genFieldChanges(oldMultiData[key], newMultiData[key]);
multiKeyChanges.push(...subChanges.map(change => `${key}${change}`));
delete oldData[key];
delete newData[key];
}
}
return genFieldChanges(oldData, newData).concat(multiKeyChanges);
}
function genFieldChanges(oldData, newData) {
const changes = [];
for (const key in oldData) {
const movedTo = getMovedTo(oldData, newData, key);
if (movedTo) {
changes.push(`${key} → ${movedTo}`);
continue;
}
if (!(key in newData)) {
changes.push(`删除${key}${ oldData[key] ? `(${oldData[key]})`: ''}`);
}
}
for (const key in newData) {
if (!(key in oldData)) {
changes.push(`添加${key}`);
}
}
for (const key in oldData) {
if (key in newData) {
const oldValue = oldData[key];
const newValue = newData[key];
if (oldValue !== newValue) {
if (oldValue === null) {
changes.push(`添加${key}`); // 纯值字段
} else {
const oldSubValues = oldValue.split(SPLIT_RULE).map(v => v.trim()).filter(v => v);
const newSubValues = newValue.split(SPLIT_RULE).map(v => v.trim()).filter(v => v);
if (oldSubValues.length > 1 || newSubValues.length > 1) {
const subChanges = analyzeArrChanges(oldSubValues, newSubValues, key);
if (subChanges.length) {
changes.push(...subChanges);
continue;
}
}
changes.push(`修改${key}(${oldValue} → ${newValue})`);
}
}
}
}
// 若是移动的字段,不再重复记录删除和添加
const moves = changes.filter(c => c.includes('→')).map(c => c.split('→').map(s => s.trim()));
return changes.filter(c =>
c.includes('→') && !c.includes('修改') || !moves.some(([from, to]) =>
c.startsWith(`删除${from}`) ||
c === `添加${to}` ||
c.startsWith(`修改${from}`) ||
c.startsWith(`修改${to}`)
)
);
}
function getMovedTo(oldData, newData, key) {
const oldValue = oldData[key];
if (!oldValue) return null;
for (const newKey in newData) {
if (newKey !== key && newData[newKey] === oldValue && oldValue !== oldData[newKey]) {
return newKey;
}
}
return null;
}
// 解析wcode为对象
function parseWcode(wcode) {
const result = {};
const lines = wcode.split('\n').map(l => l.trim().replace(/^[|\[]/, '').replace(/]$/, ''))
.filter(l => !['', '{{', '}}'].includes(l));
let currentKey = null;
let inMultiValue = false;
for (const line of lines) {
// 处理多值字段开始
if (line.endsWith('={')) {
currentKey = line.replace('={', '').trim();
inMultiValue = true;
continue;
}
// 处理多值字段结束
if (inMultiValue && line === '}') {
if (currentKey) {
currentKey = null;
inMultiValue = false;
}
continue;
}
// 处理多值字段内容
if (inMultiValue) {
const [subKey, ...subValueParts] = line.split('|');
const subValue = subValueParts.join('|').trim();
if (subKey.trim()) {
if (!result[currentKey]) result[currentKey] = {};
if (subValue) {
result[currentKey][subKey.trim()] = subValue;
} else { // 纯值子段
result[currentKey][subKey.trim()] = null;
}
continue;
}
}
// 处理普通字段
if (line.includes('=')) {
const [key, ...valueParts] = line.split('=');
const value = valueParts.join('=').trim();
if (key.trim() && value) result[key.trim()] = value;
}
}
return result;
}
// 分析数组变化
function analyzeArrChanges(oldArr, newArr, label) {
const changes = [];
const addedItems = [...newArr].filter(item => !oldArr.includes(item));
const removedItems = [...oldArr].filter(item => !newArr.includes(item));
if (addedItems.length) {
changes.push(`添加${label}${addedItems.join('、')}`);
}
if (removedItems.length) {
changes.push(`删除${label}${removedItems.join('、')}`);
}
if (!(addedItems.length + removedItems.length) &&
oldArr.join('') !== newArr.join('')) {
changes.push(`调整${label}顺序`)
}
return changes;
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
function getWcode() {
if (nowmode === 'wcode') {
return document.getElementById('subject_infobox').value;
} else if (nowmode === 'normal') {
info = new Array();
ids = new Object();
props = new Object();
input_num = $("#infobox_normal input.id").length;
ids = $("#infobox_normal input.id");
props = $("#infobox_normal input.prop");
for (i = 0; i < input_num; i++) {
id = $(ids).get(i);
prop = $(props).get(i);
if ($(id).hasClass('multiKey')) {
multiKey = $(id).val();
info[multiKey] = new Object();
var subKey = 0;
i++;
id = $(ids).get(i);
prop = $(props).get(i);
while (($(id).hasClass('multiSubKey') || $(prop).hasClass('multiSubVal')) && i < input_num) {
if (isNaN($(id).val())) {
info[multiKey][subKey] = {
key: $(id).val(),
value: $(prop).val()
};
} else {
info[multiKey][subKey] = $(prop).val();
}
subKey++;
i++;
id = $(ids).get(i);
prop = $(props).get(i);
}
i--;
} else if ($.trim($(id).val()) != "") {
info[$(id).val()] = $(prop).val();
}
}
return WCODEDump(info);
}
}