- // ==UserScript==
- // @name 时光机查询特定条目评价
- // @namespace https://bgm.tv/group/topic/411925
- // @version 0.2.1
- // @description 经济的同步率查询
- // @author mmv
- // @include /^https?:\/\/(((fast\.)?bgm\.tv)|chii\.in|bangumi\.tv)\/user\/[^/]+$/
- // @icon https://www.google.com/s2/favicons?sz=64&domain=bgm.tv
- // @license MIT
- // @grant none
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- const style = document.createElement('style');
- style.innerHTML = `
- div.userSynchronize.userSynchronizeSpecial {
- #subjectList {
- .tip {
- color: #666;
- }
- li:hover small {
- color: #EEE;
- }
- img.avatar {
- border-radius: 5px;
- }
- }
- input[type=search]:focus {
- &:focus {
- outline: none;
- }
- }
- select {
- color: #222;
- }
- }
- html[data-theme="dark"] div.userSynchronize.userSynchronizeSpecial {
- #subjectList {
- .tip {
- color: #d8d8d8;
- }
- small {
- color: #999;
- }
- li:hover small {
- color: #EEE;
- }
- }
- select {
- color: #e0e0e1;
- }
- }
- `;
- document.body.append(style);
-
- const username = location.pathname.split('/').pop();
- const synchronize = document.querySelector('.userSynchronize');
- if (!synchronize) return;
- const frag = document.createDocumentFragment();
-
- const box = document.createElement('div');
- box.classList.add('userSynchronize', 'userSynchronizeSpecial');
-
- const inner = document.createElement('div');
-
- const title = document.createElement('h3');
- title.textContent = '特定同步率';
-
- const searchPanel = document.createElement('div');
- const dataPanel = document.createElement('div');
- const searchInputs = document.createElement('div');
-
- const input = document.createElement('input');
- input.classList.add('inputtext');
- input.enterkeyhint = 'search';
- input.type = 'search';
- input.autocomplete = 'false';
- input.addEventListener('keydown', (event) => {
- if (event.key === 'Enter') searchAndRender();
- });
-
- const searchResult = document.createElement('div');
- searchResult.classList.add('subjectListWrapper');
- searchResult.style = `
- max-height: 200px;
- overflow-y: scroll;
- `;
- const dataResult = document.createElement('div');
-
- const searchSelect = document.createElement('select');
- searchSelect.onchange = searchAndRender;
-
- const searchBtn = makeBtn('🔍');
- const makeSearching = () => document.createTextNode('搜索中……');
- searchBtn.onclick = searchAndRender;
-
- const dataBtn = makeBtn('🆔');
- dataBtn.onclick = async () => {
- const subject_id = input.value;
- if (!/\d+/.test(subject_id)) return;
- dataResult.innerHTML = '查询中……';
- const collection = await getUserCollection(subject_id);
- const name = collection.subject?.name;
- renderCollection(collection, dataResult, `/subject/${ subject_id }`, name);
- }
-
- frag.append(box);
- box.append(title, inner);
- inner.append(searchPanel, dataPanel);
- searchPanel.append(searchInputs, searchResult);
- searchInputs.append(searchSelect, input, searchBtn, dataBtn);
- dataPanel.append(dataResult);
- searchSelect.innerHTML = `<option value="all">全部</option>
- <option value="2">动画</option>
- <option value="1">书籍</option>
- <option value="4">游戏</option>
- <option value="3">音乐</option>
- <option value="6">三次元</option>`;
- inner.style = `display: flex;
- flex-wrap: wrap;`;
- searchPanel.style.flex = '0 1 300px';
- dataPanel.style.flex = '1 1 200px';
- searchInputs.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);`;
- searchSelect.style = `font-size: 1em;
- padding: 4px 0 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)`;
- input.style = `font-size: 1em;
- width: 120px;
- -webkit-appearance: none;
- -moz-appearance: none;
- box-shadow: none;
- background: transparent;
- line-height: 20px;
- border: none;`;
-
- synchronize.after(frag);
-
- async function searchAndRender() {
- const keyword = input.value;
- if (keyword === '') return;
- searchResult.innerHTML = '';
- const searching = makeSearching();
- searchResult.append(searching);
- const type = searchSelect.value;
- const data = await search(keyword, type);
- const list = data?.list;
- if (!list) {
- searchResult.innerText = '搜索失败';
- return;
- }
- if (list.length === 0) {
- searchResult.innerText = '未找到相关条目';
- return;
- }
- renderList(list, keyword, type, searchResult, async ({ href, textContent }) => {
- const subject_id = href.split('/').pop();
- dataResult.innerHTML = '查询中……';
- renderCollection((await getUserCollection(subject_id)), dataResult, href, textContent);
- });
- searching.remove();
- }
-
- function makeBtn(text) {
- const btn = document.createElement('a');
- btn.href = 'javascript:;';
- btn.innerText = text;
- btn.style = `text-wrap: nowrap;
- border: none;
- border-left: 1px solid rgba(200, 200, 200, 0.5);
- padding: 4px 5px;
- cursor: pointer;`
- return btn;
- }
-
- async function search(keyword, type, start=0) {
- try {
- const response = await fetch(`https://api.bgm.tv/search/subject/${encodeURI(keyword)}?type=${type}&max_results=10&start=${start}`);
- if (!response.ok) throw new Error(`API request ${ response.status } ${ response.statusText }`);
- return await response.json();
- } catch (error) {
- console.error(error);
- return null;
- }
- }
-
- function listHTML(list) {
- return list.reduce((m, { id, type, images, name, name_cn }) => {
- type = ['书籍', '动画', '音乐', '游戏', '', '三次元'][type - 1];
- const grid = images?.grid;
- m += `<li class="clearit">
- <a href="/subject/${id}" class="avatar h">
- ${grid ? `<img src="${grid}" class="avatar ll">` : ''}
- </a>
- <div class="inner">
- <small class="grey rr">${type}</small>
- <p><a href="/subject/${id}" class="avatar h">${name}</a></p>
- <small class="tip">${name_cn}</small>
- </div>
- </li>`;
- return m;
- }, '');
- }
-
- function renderList(list, keyword, type, container, clickHandler) {
- const ul = document.createElement('ul');
- ul.id = 'subjectList';
- ul.classList.add('subjectList', 'ajaxSubjectList');
- ul.innerHTML = listHTML(list);
-
- 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 = list.length + 1;
- more.onclick = async () => {
- const searching = makeSearching();
- more.before(searching);
- const moreData = await search(keyword, type, more.start);
- const newlist = moreData.list;
- if (!newlist) {
- searching.remove();
- return;
- }
- more.start += newlist.length;
- ul.insertAdjacentHTML('beforeend', listHTML(newlist));
- applyHandler();
- searching.remove();
- }
-
- container.append(ul, more);
- applyHandler();
-
- function applyHandler() {
- ul.querySelectorAll('a').forEach(a => {
- a.addEventListener('click', e => {
- e.preventDefault();
- clickHandler(a);
- });
- });
- }
- }
-
- async function getUserCollection(subject_id) {
- try {
- const headers = {};
- if (accessToken) headers.Authorization = `Bearer ${ accessToken }`;
- const response = await fetch(`https://api.bgm.tv/v0/users/${username}/collections/${subject_id}`, { headers });
- if (response.ok) {
- return await response.json();
- } else if (response.status === 404) {
- return { not_found: true };
- } else if (response.status === 401) {
- return { auth_failed: true };
- } else {
- throw new Error(`API request ${ response.status } ${ response.statusText }`);
- }
- } catch (error) {
- console.error(error);
- return null;
- }
- }
-
- function renderCollection(data, container, fallbackLink='', fallbackName='本作') {
- if (!data) {
- container.innerHTML = '查询失败';
- return;
- }
- if (data.not_found || data.auth_failed) {
- let message = '';
- if (data.not_found) {
- message = `未找到${ fallbackLink ? `<a class="l" href="${fallbackLink}" target="_blank">${fallbackName}</a>` : fallbackName }的收藏记录`;
- } else if (data.auth_failed) {
- message = '个人令牌认证失败';
- }
- if (!accessToken || data.auth_failed) {
- message += '<br>试试<a class="l" href="javascript:" id="incheiat">填写</a>个人令牌后再试一遍?你可以在<a class="l" href="https://next.bgm.tv/demo/access-token/create" target="_blank">这里</a>创建个人令牌';
- }
- container.innerHTML = message;
- container.querySelector('#incheiat')?.addEventListener('click', () => {
- accessToken = prompt('请填写个人令牌 token');
- if (!accessToken) return;
- if (!accessToken.match(/^[a-zA-Z0-9]+$/)) {
- accessToken = null;
- alert('格式错误,请重新填写');
- return;
- }
- });
- return;
- }
-
- const { rate, subject_type, type, comment, updated_at, ep_status, vol_status, subject } = data;
- const { id, name, name_cn, volumes, eps } = subject;
- const verb = ['读', '看', '听', '玩', '', '看'][subject_type - 1];
- const html = `<li id="item_${id}" class="item even clearit" style="list-style: none;">
- <div class="inner" style="margin-left: 10px">
- <h3>
- ${ name_cn ? `<a href="/subject/${id}" class="l">${name_cn}</a> <small class="grey">${name}</small>`
- : `<a href="/subject/${id}" class="l">${name}</a>`
- }
- </h3>
- <p class="collectInfo">
- ${ rate ? `<span class="starstop-s"><span class="starlight stars${rate}"></span></span>`
- : ''
- }
- <span class="tip_j">${updated_at.slice(0, 10)}</span>
- <span class="tip_i">/</span>
- <span class="tip"> ${[`想${verb}`, `${verb}过`, `在${verb}`, '搁置', '抛弃'][type - 1]}</span>
- ${ ep_status ? `
- <span class="tip_i">/</span>
- <span class="tip">
- ${ ep_status }${ eps ? ` / ${eps}` : ''}话
- </span>
- ` : ''}
- ${ vol_status ? `
- <span class="tip_i">/</span>
- <span class="tip">
- ${ vol_status }${ eps ? ` / ${volumes}` : ''}卷
- </span>
- ` : ''}
- </p>
- ${ comment ? `
- <div id="comment_box"><div class="item"><div class="text_main_even" style="float:none;width:unset">
- <div class="text"> ${comment}</div>
- <div class="text_bottom"></div>
- </div></div></div></div>
- ` : '' }
- </li>`;
- container.innerHTML = html;
- }
- })();