您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动生成Bangumi编辑摘要
// ==UserScript== // @name bangumi 自动生成编辑摘要 // @namespace https://bgm.tv/group/topic/433505 // @version 0.5.1 // @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://bgm.tv/*/add_related/* // @match https://bangumi.tv/subject/*/edit_detail // @match https://bangumi.tv/character/*/edit // @match https://bangumi.tv/person/*/edit // @match https://bangumi.tv/*/add_related/* // @match https://chii.in/subject/*/edit_detail // @match https://chii.in/character/*/edit // @match https://chii.in/person/*/edit // @match https://chii.in/*/add_related/* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; const editSummaryInput = document.querySelector('#editSummary'); if (!editSummaryInput) return; const SPLIT_RULE = /[()[\]{}()<>《》「」『』【】+×·→//、,,;;:&&\\等]/; const isObject = v => v !== null && typeof v === 'object'; const short = s => s.length > 13 ? `${s.slice(0, 6)}...${s.slice(-4)}` : s; // #region 获取页面类型和初始值 let initialTitle, initialPlatform, initialPros, initialSeries, initialWcode, initialSummary, initialNsfw, initialTags; let titleInput, getPlatform, getPros, seriesCheckbox, summaryTextarea, nsfwCheckbox, tagsInput; let initialRelatedItems = []; let pageFeatures = { isRelated: false, hasWcode: false, relatedContextType: null }; let autoStorageKey, lockedStorageKey; const path = window.location.pathname; pageFeatures.isRelated = path.includes('/add_related/'); pageFeatures.hasWcode = path.match(/\/edit(_detail)?$/) if (pageFeatures.isRelated) { if (path.match(/add_related\/subject\//)) { pageFeatures.relatedContextType = 1; } else if (path.match(/(character|person)\/\d+\/add_related\/anime/) || path.match(/subject\/\d+\/add_related\/(person|character)/)) { pageFeatures.relatedContextType = 2; } initialRelatedItems = Array.from(document.querySelectorAll('#crtRelateSubjects li.old')).map(li => { return getRelItemData(li); }); } else if (pageFeatures.hasWcode) { titleInput = document.querySelector('[name="subject_title"], [name="crt_name"]'); getPlatform = () => document.querySelector(`[for="${[...document.querySelectorAll('input[name=platform]')].find(i => i.checked)?.id}"]`)?.textContent; getPros = () => [...document.querySelectorAll('[name^=prsn_pro]')].filter(c => c.checked).map(c => document.querySelector(`[for=${c.id}]`).textContent); seriesCheckbox = document.querySelector('#subjectSeries'); summaryTextarea = document.querySelector('#subject_summary, #crt_summary'); nsfwCheckbox = document.querySelector('input[name="subject_nsfw"]'); tagsInput = document.querySelector('input#tags'); initialTitle = titleInput?.value; initialPlatform = getPlatform?.(); initialPros = getPros?.(); initialSeries = seriesCheckbox?.checked; initialWcode = getWcode(); initialSummary = summaryTextarea?.value; initialNsfw = nsfwCheckbox?.checked; initialTags = tagsInput?.value; } autoStorageKey = pageFeatures.hasWcode ? 'autoGenEditSummary' : 'autoGenRelEditSummary'; lockedStorageKey = pageFeatures.hasWcode ? 'lockedEditSummary' : 'lockedERelditSummary'; const lockedSummary = localStorage.getItem(lockedStorageKey); if (lockedSummary) editSummaryInput.value = lockedSummary; // #endregion if (pageFeatures.hasWcode) { const wrapper = document.createElement('div'); wrapper.style.width = '100%'; wrapper.style.display = 'flex'; wrapper.style.gap = '5px'; wrapper.style.marginBlock = '5px'; editSummaryInput.style.width = '100%'; editSummaryInput.after(wrapper); wrapper.append(editSummaryInput.previousElementSibling, editSummaryInput, newGenBtn(), newLockBtn()); wrapper.after(newAutoGenBtn()); } else if (pageFeatures.isRelated) { // 避免批量修改关联关系造成崩坏 editSummaryInput.after(newGenBtn(), newLockBtn(), document.createElement('br'), newAutoGenBtn()); } const submitBtn = document.querySelector('input.inputBtn[name="submit"]'); submitBtn.addEventListener('click', () => { localStorage.getItem(autoStorageKey) === 'true' && genEditSummary(); }, { capture: false }); // #region UI function newAutoGenBtn() { const autoGenBtn = document.createElement('input'); autoGenBtn.type = 'checkbox'; autoGenBtn.id = autoStorageKey; autoGenBtn.style.marginRight = '5px'; const autoGenLabel = document.createElement('label'); autoGenLabel.htmlFor = autoStorageKey; autoGenLabel.textContent = '提交时自动生成'; autoGenBtn.onchange = function () { localStorage.setItem(autoStorageKey, this.checked); }; autoGenBtn.checked = localStorage.getItem(autoStorageKey) === 'true'; autoGenLabel.prepend(autoGenBtn); return autoGenLabel; } function newGenBtn() { const genBtn = document.createElement('button'); genBtn.type = 'button'; genBtn.style.wordBreak = 'keep-all'; genBtn.textContent = '生成摘要'; genBtn.onclick = genEditSummary; return genBtn; } function newLockBtn() { let isLocked = lockedSummary; const lockBtn = document.createElement('button'); lockBtn.type = 'button'; lockBtn.textContent = isLocked ? '🔒' : '🔓'; lockBtn.title = isLocked ? '编辑摘要已锁定' : '编辑摘要未锁定'; lockBtn.style.cursor = 'pointer'; lockBtn.style.background = 'none'; lockBtn.style.border = 'none'; lockBtn.style.fontSize = '16px'; lockBtn.style.padding = '0'; lockBtn.onclick = function () { isLocked = !isLocked; this.textContent = isLocked ? '🔒' : '🔓'; this.title = isLocked ? '编辑摘要已锁定' : '编辑摘要未锁定'; localStorage.setItem(lockedStorageKey, isLocked ? editSummaryInput.value : ''); }; return lockBtn; } // #endregion // #region 生成摘要 function genEditSummary() { const changes = []; if (pageFeatures.hasWcode) { const newWcode = getWcode(); const newSummary = summaryTextarea?.value; const newTags = tagsInput?.value; if (initialTitle !== titleInput?.value) { changes.push(`修改标题(${initialTitle} → ${titleInput.value})`); } const newPlatform = getPlatform?.(); if (initialPlatform !== newPlatform) { changes.push(`修改类型(${initialPlatform} → ${newPlatform})`); } const newPros = getPros?.(); const proChanges = genArrChanges(initialPros, newPros, '职业'); if (proChanges.length > 0) changes.push(...proChanges); if (initialSeries !== seriesCheckbox?.checked) { changes.push(seriesCheckbox?.checked ? '标记为系列' : '取消系列标记'); } const wcodeChanges = genWcodeChanges(initialWcode, newWcode); if (wcodeChanges.length > 0) { changes.push(...wcodeChanges); } if (initialSummary !== newSummary) { changes.push(`${initialSummary ? '修改' : '添加'}简介`); } if (initialNsfw !== nsfwCheckbox?.checked) { changes.push(nsfwCheckbox?.checked ? '标记为受限内容' : '取消受限内容标记'); } const tagsToArr = tags => tags ? tags.split(/\s+/).filter(t => t) : []; const tagChanges = genArrChanges(tagsToArr(initialTags), tagsToArr(newTags), '标签'); if (tagChanges.length > 0) changes.push(...tagChanges); if (document.querySelector('input[type=file]')?.value) { changes.push('新肖像'); } } else if (pageFeatures.isRelated) { const currentLis = Array.from(document.querySelectorAll('#crtRelateSubjects li:has(.title)')); const currentOldLis = currentLis.filter(li => li.classList.contains('old')); const currentNewLis = currentLis.filter(li => !li.classList.contains('old')); currentOldLis.forEach(li => { const initialItem = initialRelatedItems.find(item => item.element === li); if (initialItem) { const currentItem = getRelItemData(li); const modifyChanges = genRelModifyChanges(initialItem, currentItem); if (modifyChanges.length > 0) { changes.push(...modifyChanges); } } }); currentNewLis.forEach(li => { const item = getRelItemData(li); changes.push(genRelExsistChanges(item, '添加')); }); initialRelatedItems.forEach(initialItem => { const stillExists = currentOldLis.some(li => li === initialItem.element); if (!stillExists) { changes.push(genRelExsistChanges(initialItem, '删除')); } }); } if (editSummaryInput && changes.length > 0) { if (!editSummaryInput.dataset.userModified || editSummaryInput.value === '') { editSummaryInput.value = [...new Set(changes)].join(';'); } } else if (editSummaryInput) { editSummaryInput.value = '空编辑'; } } // #region 相关条目变化 function genRelExsistChanges(item, verb) { if (item.infoName) { return `${verb}《${item.name}》${item.type}${item.infoName}`; } return `${verb}${item.type}${item.name}`; } function genRelModifyChanges(initial, current) { const changes = []; if (initial.type !== current.type) { const name = current.infoName ? `《${current.name}》${current.infoName}` : current.name; changes.push(`${name} ${initial.type}→${current.type}`); } if (initial.remark !== current.remark) { if (pageFeatures.relatedContextType === 1) { changes.push('排序'); } else { const remarkName = pageFeatures.relatedContextType === 2 ? '参与' : '备注' if (current.remark && !initial.remark) { changes.push(`添加${current.type}${current.name}${remarkName}`); } else if (!current.remark && initial.remark) { changes.push(`删除${current.type}${current.name}${remarkName}`); } else { changes.push(`${current.type}${current.name}${remarkName} ${initial.remark} → ${current.remark}`); } } } if (initial.checkboxes.length && current.checkboxes.length) { current.checkboxes.forEach((currentCheckbox, index) => { const initialCheckbox = initial.checkboxes[index]; if (initialCheckbox && initialCheckbox.checked !== currentCheckbox.checked) { changes.push(`${currentCheckbox.checked ? '标记' : '取消标记'}${current.type}${current.name}${currentCheckbox.title}`); } }); } return changes; } // #endregion // #region wcode变化 function genWcodeChanges(oldWcode, newWcode) { const oldData = parseWcode(oldWcode); const newData = parseWcode(newWcode); const getMultiData = data => Object.fromEntries(Object.entries(data).filter(([, v]) => isObject(v))); 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 = []; const moves = { from: new Set(), to: new Set() }; const movedKeys = new Set(); const oldValueDel = (k, v) => v ? (`删除${k}${isObject(v) ? '' : `(${short(v)})`}`) : ''; for (const key in oldData) { const oldValue = oldData[key]; const newKey = getNewKey(oldData, newData, key); if (newKey) { if (oldData[newKey]) changes.push(oldValueDel(newKey, oldValue)); changes.push(`${key} → ${newKey}`); if (newData[key]) changes.push(`添加${key}`); moves.from.add(key); moves.to.add(newKey); movedKeys.add(key).add(newKey); continue; } } for (const key in oldData) { if (key in newData || moves.from.has(key)) continue; changes.push(oldValueDel(key, oldData[key])); } for (const key in newData) { if (key in oldData || moves.to.has(key)) continue; changes.push(`添加${key}`); } for (const key in oldData) { if (movedKeys.has(key)) continue; if (key in newData) { const oldValue = oldData[key]; const newValue = newData[key]; if (oldValue === newValue) continue; if (oldValue === null) { changes.push(`添加${key}`); // 纯值字段 } else { const splitValue = value => value.split(SPLIT_RULE).flatMap(v => { v = v.trim(); return /^\d+(:\d{2})*$/.test(v) ? v : v.split(':').map(u => u.trim()); // 时间 }).filter(v => v); const oldSubValues = splitValue(oldValue); const newSubValues = splitValue(newValue); if (oldSubValues.length > 1 || newSubValues.length > 1) { const subChanges = genArrChanges(oldSubValues, newSubValues, key); if (subChanges.length) { changes.push(...subChanges); continue; } } changes.push(`修改${key}(${oldValue} → ${newValue})`); } } } return changes; } function getNewKey(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; } 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; } // #endregion function genArrChanges(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; } // #endregion function getRelItemData(li) { return { name: short(li.querySelector('.title a').textContent) || '', infoName: li.querySelector('.info a')?.textContent || '', type: li.querySelectorAll('option')[li.querySelector('select').selectedIndex].textContent.split(' / ')[0], remark: li.querySelector('input[type=text]')?.value.trim() || '', checkboxes: [...li.querySelectorAll('input[type=checkbox]')].map(checkbox => ({ checked: checkbox.checked, title: checkbox.previousElementSibling.textContent.slice(0, -1).trim() || '' })), element: li }; } })(); 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); } }