// ==UserScript== // @name 目录内添加条目增强 // @namespace https://bgm.tv/group/topic/409246 // @version 0.3.2 // @description 为 bangumi 增加在目录内搜索条目并添加的功能,添加无需刷新,兼容“目录批量添加与编辑” // @author mmm // @include http*://bgm.tv/index/* // @include http*://chii.in/index/* // @include http*://bangumi.tv/index/* // @icon https://www.google.com/s2/favicons?sz=64&domain=bgm.tv // @grant unsafeWindow // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/516479/%E7%9B%AE%E5%BD%95%E5%86%85%E6%B7%BB%E5%8A%A0%E6%9D%A1%E7%9B%AE%E5%A2%9E%E5%BC%BA.user.js // @updateURL https://update.greasyfork.icu/scripts/516479/%E7%9B%AE%E5%BD%95%E5%86%85%E6%B7%BB%E5%8A%A0%E6%9D%A1%E7%9B%AE%E5%A2%9E%E5%BC%BA.meta.js // ==/UserScript== (function() { 'use strict'; const createFetch = method => async (url, body, serializer = body => JSON.stringify(body)) => { const options = method === 'POST' ? { method, body: serializer(body) } : { method }; try { const response = await fetch(url, options); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const text = await response.text(); try { return JSON.parse(text); } catch { return text; } } catch (e) { console.error(e); return null; } }; const fetchGet = createFetch('GET'); const fetchPost = createFetch('POST'); const postSearch = async (cat, keyword, { filter = {}, offset = 0 }) => { const url = `https://api.bgm.tv/v0/search/${cat}?limit=10&offset=${offset}`; const body = { keyword, filter }; const result = await fetchPost(url, body); return result?.data; }; const searchSubject = async (keyword, { type = '', start = 0 }) => { // 旧API结果为空时发生CORS错误,但新API搜索结果不准确,仍用旧API const url = `https://api.bgm.tv/search/subject/${encodeURIComponent(keyword)}?type=${type}&max_results=10&start=${start}`; const result = await fetchGet(url); return result?.list; }; // const searchSubject = (keyword, type) => postSearch('subjects', keyword, { type: [+type].filter(a => a) }); const searchPrsn = postSearch.bind(null, 'persons'); const searchCrt = postSearch.bind(null, 'characters'); const getSearchMethod = { 'subject': [searchSubject, 'start'], 'person': [searchPrsn, 'offset'], 'character': [searchCrt, 'offset'], 'ep': [searchSubject, 'start'], }; const getEps = async (subject_id) => { const url = `https://api.bgm.tv/v0/episodes?subject_id=${subject_id}`; const result = await fetchGet(url); return result?.data; }; const formhash = document.querySelector('input[name=formhash]').value; const addItem = async (add_related) => { const url = `${ location.pathname }/add_related`; const body = { formhash, add_related, submit: '添加条目关联' }; const result = await fetchPost(url, body, body => new URLSearchParams(body)); return result; }; const modifyItem = async (id, content, order) => { const url = `/index/related/${ id }/modify`; const body = { formhash, content, order, submit: '提交' }; const result = await fetchPost(url, body, body => new URLSearchParams(body)); return result; }; document.querySelector('li.add a').addEventListener('click', () => { document.querySelector('#TB_window').style.height = 'unset'; document.querySelector('#TB_ajaxContent').style.height = '250px'; }); const boxes = document.querySelectorAll('.newIndexSection'); boxes.forEach((box) => { const cat = ['subject', 'character', 'person', 'ep'][box.id.at(-1)]; const input = box.querySelector('.inputtext'); input.style.position = 'sticky'; input.style.top = 0; input.zIndex = 2; // 覆盖ep const result = document.createElement('div'); result.classList.add('subjectListWrapper'); box.firstElementChild.append(result); const btn = makeBtn(); btn.classList.add('chiiBtn'); btn.onclick = async () => { await searchAndRender(cat, input, result); }; box.querySelector('#submitBtnO').append(btn); const makeTip = (text) => { const tip = document.createElement('span'); tip.classList.add('tip'); tip.textContent = text; return tip; }; const contentTextarea = document.createElement('textarea'); contentTextarea.className = 'reply'; const orderInput = document.createElement('input'); orderInput.type = 'text'; orderInput.className = 'inputtext'; input.after(makeTip('评价:'), document.createElement('br'), contentTextarea, document.createElement('br'), makeTip('排序:'), document.createElement('br'), orderInput); const form = box.querySelector('#newIndexRelatedForm'); form.addEventListener('submit', async (e) => { e.preventDefault(); const ukagaka = document.querySelector('#robot'); ukagaka.style.zIndex = '103'; unsafeWindow.chiiLib.ukagaka.presentSpeech('添加中,请稍候...'); const v = input.value.trim(); const add_related = input.value.match(/\d+/) ? `/${cat}/${v}` : v; const id = add_related.split('/').pop(); try { const addedHTML = await addItem(add_related); const content = contentTextarea.value.trim(); const order = parseInt(orderInput.value); const parser = new DOMParser(); const query = `#item_${ cat === 'subject' ? '' : cat }${id}`; const addedDOM = parser.parseFromString(addedHTML, 'text/html'); let added = addedDOM.querySelector(query); if (content || !isNaN(order)) { const rlt = added.querySelector('a.tb_idx_rlt'); const rlt_id = rlt.id.split('_')[1]; const modifiedHTML = await modifyItem(rlt_id, content, order); const modifiedDOM = parser.parseFromString(modifiedHTML, 'text/html'); added = modifiedDOM.querySelector(query); } const previousAnchor = added.previousElementSibling; const nextAnchor = added.nextElementSibling; if (previousAnchor) { document.querySelector(`#${previousAnchor.id}`).after(added); } else if (nextAnchor) { document.querySelector(`#${nextAnchor.id}`).before(added); } else { const parent = added.parentElement; document.querySelector('#columnSubjectBrowserA').append(parent); } added.querySelector('.tools').style.visibility = 'hidden'; // 无法进行同页修改,暂隐藏 const collectBlock = added.querySelector('.collectBlock'); // 只有条目可以收藏 if (collectBlock) collectBlock.style.visibility = 'hidden'; unsafeWindow.chiiLib.ukagaka.presentSpeech('添加成功!', true); } catch (e) { console.error(e); unsafeWindow.chiiLib.ukagaka.presentSpeech('添加失败了T T', true); } finally { setTimeout(() => ukagaka.style.zIndex = '90', 3500); } }); }); function makeBtn(text = '搜索') { const btn = document.createElement('a'); btn.href = 'javascript:;'; btn.innerText = text; return btn; } const makeLoading = (prompt = '搜索中……') => document.createTextNode(prompt); async function searchAndRender(cat, input, result, target=input, append=false) { const [method, key] = getSearchMethod[cat]; const keyword = input.value.trim(); if (keyword === '') return; const loader = (offset) => method(keyword, { [key]: offset }); const clickHandler = e => { e.preventDefault(); if (cat === 'ep') { renderEps(e.currentTarget, target, append); } else { if (append) { target.value += e.currentTarget.href + '\n'; } else { target.value = e.currentTarget.href; } } }; renderList(loader, result, cat, a => a.addEventListener('click', clickHandler)); } const listHTML = (list, cat = 'subject') => { const isEp = cat === 'ep'; if (isEp) cat = 'subject'; return list.reduce((m, { id, type, images, name, name_cn, career, infobox }) => { if (isEp && ![2, 6].includes(type)) return m; // 动画 + 三次元,旧API不支持多重类别筛选 name_cn ??= infobox?.find(({ key }) => key === '简体中文名')?.value; if (cat !== 'subject') cat = career ? 'person' : 'character'; type = cat === 'subject' ? ['书籍', '动画', '音乐', '游戏', '', '三次元'][type - 1] : null; const grid = images?.grid; const exist = v => v ? v : ''; m += `