bangumi 自动生成编辑摘要

自动生成Bangumi编辑摘要

// ==UserScript==
// @name         bangumi 自动生成编辑摘要
// @namespace    https://bgm.tv/group/topic/433505
// @version      0.3
// @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';

    // 存储初始值
    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);

        // 更新编辑摘要输入框
        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) {
                    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 oldSet = new Set(oldArr);
        const newSet = new Set(newArr);

        // 检查新增的标签
        const addedItems = [];
        for (const item of newArr) {
            if (!oldSet.has(item)) {
                addedItems.push(item);
            }
        }
        if (addedItems.length) {
            changes.push(`添加${label}${addedItems.join('、')}`);
        }

        // 检查删除的标签
        const removedItems = [];
        for (const item of oldArr) {
            if (!newSet.has(item)) {
                removedItems.push(item);
            }
        }
        if (removedItems.length) {
            changes.push(`删除${label}${removedItems.join('、')}`);
        }

        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);
    }
}