// ==UserScript== // @name 目录内添加条目增强 // @namespace https://bgm.tv/group/topic/409246 // @version 0.2.0 // @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 none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; const createFetch = method => async (url, body) => { const options = method === 'POST' ? { method, body: JSON.stringify(body) } : { method }; try { const response = await fetch(url, options); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } 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 searchAnime = async (keyword, { start = 0 }) => searchSubject(keyword, { type: 2, start }); const getSearchMethod = { 'subject': [searchSubject, 'start'], 'prsn': [searchPrsn, 'offset'], 'crt': [searchCrt, 'offset'], 'ep': [searchAnime, '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 boxes = document.querySelectorAll('.newIndexSection'); boxes.forEach((box) => { const cat = ['subject', 'crt', 'prsn', 'ep'][box.id.at(-1)]; const input = box.querySelector('.inputtext'); input.style.position = 'sticky'; input.style.top = 0; input.zIndex = 2; 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); document.querySelector('#TB_ajaxContent').style.height = '250px'; document.querySelector('#TB_window').style.height = 'unset'; }; box.querySelector('#submitBtnO').append(btn); }); 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 isEp = cat === 'ep'; if (isEp) cat = 'subject'; const loader = (offset) => method(keyword, { [key]: offset }); const clickHandler = e => { e.preventDefault(); if (isEp) { renderEps(e.target, target, append); } else { if (append) { target.value += e.target.href + '\n'; } else { target.value = e.target.href; } } }; renderList(loader, result, cat, a => a.addEventListener('click', clickHandler)); } const listHTML = (list, cat = 'subject') => { return list.reduce((m, { id, type, images, name, name_cn, career, infobox }) => { 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 += `
  • ${ grid ? `` : ''}
    ${ exist(type) }

    ${ name }

    ${ exist(name_cn) }
  • `; return m; }, ''); } const makeMoreBtn = (ul, cat, firstList, loader, applyHandler) => { const searching = makeLoading(); const more = document.createElement('li'); more.classList.add('clearit'); more.textContent = '加载更多'; more.style.cursor = 'pointer'; more.style.textAlign = 'center'; more.style.listStyle = 'none'; more.start = firstList.length + 1; more.onclick = async () => { more.before(searching); const nextList = await loader(more.start); if (!nextList) { searching.remove(); return; } more.start += nextList.length; ul.insertAdjacentHTML('beforeend', listHTML(nextList, cat)); applyHandler(); searching.remove(); } return more; } async function renderList(loader, container, cat, handler = () => {}) { const applyHandler = () => ul.querySelectorAll('a').forEach(handler); const searching = makeLoading(); container.innerHTML = ''; container.append(searching); const firstList = await loader(); if (!firstList) { container.textContent = '搜索失败'; return; } else if (firstList.length === 0) { container.textContent = '未找到相关条目'; return; } const ul = document.createElement('ul'); ul.id = 'subjectList'; ul.classList.add('subjectList', 'ajaxSubjectList'); ul.innerHTML = listHTML(firstList, cat); const more = makeMoreBtn(ul, cat, firstList, loader, applyHandler); container.append(ul, more); applyHandler(); searching.remove(); } const epStyle = document.createElement('style'); epStyle.textContent = ` ul.ajaxSubjectList li ul.prg_list { display: flex; flex-wrap: wrap; li { border-bottom: none; border-top: none; padding: 0; a:hover { color: #333; text-decoration: none; } } } ul.ajaxSubjectList li:hover ul.prg_list li a { color: #06C; } `; document.head.append(epStyle); async function renderEps(title, target, append) { const parent = title.parentNode.parentNode; const fetching = makeLoading('获取中……'); parent.append(fetching); const eps = await getEps(title.href.split('/').pop()); const epsByType = Object.groupBy?.(eps, ({ type }) => ['0', 'SP', 'OP', 'ED'][type]) ?? eps.reduce((acc, ep) => { const type = ['0', 'SP', 'OP', 'ED'][ep.type]; if (!acc[type]) acc[type] = []; acc[type].push(ep); return acc; }, {}); fetching.remove(); if (!eps) { parent.append('获取失败'); return; } const ul = document.createElement('ul'); ul.className = 'prg_list clearit'; Object.entries(epsByType).forEach(([type, eps]) => { if (type !== '0') { const subtitle = document.createElement('li'); subtitle.className = 'subtitle'; const span = document.createElement('span'); span.textContent = type; subtitle.append(span); ul.append(subtitle); } eps.map(({ id, name, sort }) => { const li = document.createElement('li'); const a = document.createElement('a'); a.href = `/ep/${ id }`; a.className = 'load-epinfo epBtnAir'; a.title = name; a.textContent = String(sort).padStart(2, '0'); li.onclick = e => { e.preventDefault(); if (append) { target.value += id + '\n'; } else { target.value = id; } }; li.append(a); ul.append(li); }); }); parent.append(ul); } // 兼容“目录批量添加与编辑”(https://bgm.tv/dev/app/1037) const observer = monitorElement('.bibeBox', bibeBox => { const container = document.createElement('div'); container.style = `display: flex; justify-content: space-evenly; height: 300px; padding: 5px; overflow-y: auto;`; const textarea = bibeBox.querySelector('textarea'); textarea.rows = 8; bibeBox.previousSibling.after(container); bibeBox.parentNode.style.marginTop = '-150px'; const submitWrapper = document.createElement('div'); submitWrapper.style.width = '50%'; submitWrapper.append(bibeBox, document.querySelector('#submit_list')); const searchPanel = document.createElement('div'); searchPanel.style = 'width: 50%' const inputWrapper = document.createElement('div'); inputWrapper.style = `width: fit-content; border-radius: 100px; box-shadow: none; border: 1px solid rgba(200, 200, 200, 0.5); background-color: rgba(255, 255, 255, 0.2);`; const input = document.createElement('input'); input.classList.add('inputtext'); input.type = 'text'; input.addEventListener('keydown', (event) => { if (event.key === 'Enter') newSearchAndRender(); }); input.style = `font-size: 1em; width: 120px; -webkit-appearance: none; -moz-appearance: none; box-shadow: none; background: transparent; line-height: 20px; border: none;`; const result = document.createElement('div'); result.classList.add('subjectListWrapper'); result.style = ` max-height: 250px; overflow-y: scroll; `; const select = document.createElement('select'); select.onchange = newSearchAndRender; select.innerHTML = ` `; select.style = `font-size: 1em; padding: 4px 4px 4px 5px; width: fit-content; border: none; outline: none; box-shadow: none; background-color: transparent; background-image: none; -webkit-appearance: none; -moz-appearance: none; appearance: none; border-radius: 0; border-right: 1px solid rgba(200, 200, 200, 0.5)`; const btn = makeBtn('🔍'); btn.onclick = newSearchAndRender; btn.style = `text-wrap: nowrap; width: 20px; height: 20px; border: none; border-left: 1px solid rgba(200, 200, 200, 0.5); padding: 4px 5px; cursor: pointer;` searchPanel.append(inputWrapper, result); inputWrapper.append(select, input, btn); container.append(submitWrapper, searchPanel); function newSearchAndRender() { const cat = select.value; searchAndRender(cat, input, result, bibeBox.querySelector('textarea'), true); } }); // Microsoft Copilot start function monitorElement(selector, callback) { const targetNode = document.body; // 监视整个文档的变化 const config = { childList: true, subtree: true }; // 配置监视选项 const observer = new MutationObserver((mutationsList, observer) => { for (let mutation of mutationsList) { if (mutation.type === 'childList') { const addedNodes = Array.from(mutation.addedNodes); addedNodes.forEach(node => { if (node.matches?.(selector)) { observer.disconnect(); callback(node); observer.observe(targetNode, config); } else if (node.querySelectorAll) { observer.disconnect(); const matchingElements = node.querySelectorAll(selector); matchingElements.forEach(matchingNode => callback(matchingNode)); observer.observe(targetNode, config); } }); } } }); observer.observe(targetNode, config); return observer; // 返回观察者实例,以便在需要时断开观察 } // end // 添加后跳转 const lastHref = sessionStorage.getItem('incheijs_indexsearch'); if (lastHref) { const addedElem = document.querySelector(`a[href*="${lastHref}"]`); addedElem?.scrollIntoView({ behavior: 'smooth' }); sessionStorage.removeItem('incheijs_indexsearch'); } boxes.forEach(box => { const input = box.querySelector('.inputtext'); const btn = box.querySelector('.inputBtn'); btn.addEventListener('click', () => { const id = input.value.trim().split('/').at(-1); if (!id) return; const type = ['subject', 'character', 'person', 'ep'][document.querySelector('.switchTab.focus').id[4]]; sessionStorage.setItem('incheijs_indexsearch', `${type}/${id}`); }); }); })();