时光机查询特定条目评价

经济的同步率查询

  1. // ==UserScript==
  2. // @name 时光机查询特定条目评价
  3. // @namespace https://bgm.tv/group/topic/411925
  4. // @version 0.2.1
  5. // @description 经济的同步率查询
  6. // @author mmv
  7. // @include /^https?:\/\/(((fast\.)?bgm\.tv)|chii\.in|bangumi\.tv)\/user\/[^/]+$/
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=bgm.tv
  9. // @license MIT
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const style = document.createElement('style');
  17. style.innerHTML = `
  18. div.userSynchronize.userSynchronizeSpecial {
  19. #subjectList {
  20. .tip {
  21. color: #666;
  22. }
  23. li:hover small {
  24. color: #EEE;
  25. }
  26. img.avatar {
  27. border-radius: 5px;
  28. }
  29. }
  30. input[type=search]:focus {
  31. &:focus {
  32. outline: none;
  33. }
  34. }
  35. select {
  36. color: #222;
  37. }
  38. }
  39. html[data-theme="dark"] div.userSynchronize.userSynchronizeSpecial {
  40. #subjectList {
  41. .tip {
  42. color: #d8d8d8;
  43. }
  44. small {
  45. color: #999;
  46. }
  47. li:hover small {
  48. color: #EEE;
  49. }
  50. }
  51. select {
  52. color: #e0e0e1;
  53. }
  54. }
  55. `;
  56. document.body.append(style);
  57.  
  58. const username = location.pathname.split('/').pop();
  59. const synchronize = document.querySelector('.userSynchronize');
  60. if (!synchronize) return;
  61. const frag = document.createDocumentFragment();
  62.  
  63. const box = document.createElement('div');
  64. box.classList.add('userSynchronize', 'userSynchronizeSpecial');
  65.  
  66. const inner = document.createElement('div');
  67.  
  68. const title = document.createElement('h3');
  69. title.textContent = '特定同步率';
  70.  
  71. const searchPanel = document.createElement('div');
  72. const dataPanel = document.createElement('div');
  73. const searchInputs = document.createElement('div');
  74.  
  75. const input = document.createElement('input');
  76. input.classList.add('inputtext');
  77. input.enterkeyhint = 'search';
  78. input.type = 'search';
  79. input.autocomplete = 'false';
  80. input.addEventListener('keydown', (event) => {
  81. if (event.key === 'Enter') searchAndRender();
  82. });
  83.  
  84. const searchResult = document.createElement('div');
  85. searchResult.classList.add('subjectListWrapper');
  86. searchResult.style = `
  87. max-height: 200px;
  88. overflow-y: scroll;
  89. `;
  90. const dataResult = document.createElement('div');
  91.  
  92. const searchSelect = document.createElement('select');
  93. searchSelect.onchange = searchAndRender;
  94.  
  95. const searchBtn = makeBtn('🔍');
  96. const makeSearching = () => document.createTextNode('搜索中……');
  97. searchBtn.onclick = searchAndRender;
  98.  
  99. const dataBtn = makeBtn('🆔');
  100. dataBtn.onclick = async () => {
  101. const subject_id = input.value;
  102. if (!/\d+/.test(subject_id)) return;
  103. dataResult.innerHTML = '查询中……';
  104. const collection = await getUserCollection(subject_id);
  105. const name = collection.subject?.name;
  106. renderCollection(collection, dataResult, `/subject/${ subject_id }`, name);
  107. }
  108.  
  109. frag.append(box);
  110. box.append(title, inner);
  111. inner.append(searchPanel, dataPanel);
  112. searchPanel.append(searchInputs, searchResult);
  113. searchInputs.append(searchSelect, input, searchBtn, dataBtn);
  114. dataPanel.append(dataResult);
  115. searchSelect.innerHTML = `<option value="all">全部</option>
  116. <option value="2">动画</option>
  117. <option value="1">书籍</option>
  118. <option value="4">游戏</option>
  119. <option value="3">音乐</option>
  120. <option value="6">三次元</option>`;
  121. inner.style = `display: flex;
  122. flex-wrap: wrap;`;
  123. searchPanel.style.flex = '0 1 300px';
  124. dataPanel.style.flex = '1 1 200px';
  125. searchInputs.style = `width: fit-content;
  126. border-radius: 100px;
  127. box-shadow: none;
  128. border: 1px solid rgba(200, 200, 200, 0.5);
  129. background-color: rgba(255, 255, 255, 0.2);`;
  130. searchSelect.style = `font-size: 1em;
  131. padding: 4px 0 4px 5px;
  132. width: fit-content;
  133. border: none;
  134. outline: none;
  135. box-shadow: none;
  136. background-color: transparent;
  137. background-image: none;
  138. -webkit-appearance: none;
  139. -moz-appearance: none;
  140. appearance: none;
  141. border-radius: 0;
  142. border-right: 1px solid rgba(200, 200, 200, 0.5)`;
  143. input.style = `font-size: 1em;
  144. width: 120px;
  145. -webkit-appearance: none;
  146. -moz-appearance: none;
  147. box-shadow: none;
  148. background: transparent;
  149. line-height: 20px;
  150. border: none;`;
  151.  
  152. synchronize.after(frag);
  153.  
  154. async function searchAndRender() {
  155. const keyword = input.value;
  156. if (keyword === '') return;
  157. searchResult.innerHTML = '';
  158. const searching = makeSearching();
  159. searchResult.append(searching);
  160. const type = searchSelect.value;
  161. const data = await search(keyword, type);
  162. const list = data?.list;
  163. if (!list) {
  164. searchResult.innerText = '搜索失败';
  165. return;
  166. }
  167. if (list.length === 0) {
  168. searchResult.innerText = '未找到相关条目';
  169. return;
  170. }
  171. renderList(list, keyword, type, searchResult, async ({ href, textContent }) => {
  172. const subject_id = href.split('/').pop();
  173. dataResult.innerHTML = '查询中……';
  174. renderCollection((await getUserCollection(subject_id)), dataResult, href, textContent);
  175. });
  176. searching.remove();
  177. }
  178.  
  179. function makeBtn(text) {
  180. const btn = document.createElement('a');
  181. btn.href = 'javascript:;';
  182. btn.innerText = text;
  183. btn.style = `text-wrap: nowrap;
  184. border: none;
  185. border-left: 1px solid rgba(200, 200, 200, 0.5);
  186. padding: 4px 5px;
  187. cursor: pointer;`
  188. return btn;
  189. }
  190.  
  191. async function search(keyword, type, start=0) {
  192. try {
  193. const response = await fetch(`https://api.bgm.tv/search/subject/${encodeURI(keyword)}?type=${type}&max_results=10&start=${start}`);
  194. if (!response.ok) throw new Error(`API request ${ response.status } ${ response.statusText }`);
  195. return await response.json();
  196. } catch (error) {
  197. console.error(error);
  198. return null;
  199. }
  200. }
  201.  
  202. function listHTML(list) {
  203. return list.reduce((m, { id, type, images, name, name_cn }) => {
  204. type = ['书籍', '动画', '音乐', '游戏', '', '三次元'][type - 1];
  205. const grid = images?.grid;
  206. m += `<li class="clearit">
  207. <a href="/subject/${id}" class="avatar h">
  208. ${grid ? `<img src="${grid}" class="avatar ll">` : ''}
  209. </a>
  210. <div class="inner">
  211. <small class="grey rr">${type}</small>
  212. <p><a href="/subject/${id}" class="avatar h">${name}</a></p>
  213. <small class="tip">${name_cn}</small>
  214. </div>
  215. </li>`;
  216. return m;
  217. }, '');
  218. }
  219.  
  220. function renderList(list, keyword, type, container, clickHandler) {
  221. const ul = document.createElement('ul');
  222. ul.id = 'subjectList';
  223. ul.classList.add('subjectList', 'ajaxSubjectList');
  224. ul.innerHTML = listHTML(list);
  225.  
  226. const more = document.createElement('li');
  227. more.classList.add('clearit');
  228. more.textContent = '加载更多';
  229. more.style.cursor = 'pointer';
  230. more.style.textAlign = 'center';
  231. more.style.listStyle = 'none';
  232. more.start = list.length + 1;
  233. more.onclick = async () => {
  234. const searching = makeSearching();
  235. more.before(searching);
  236. const moreData = await search(keyword, type, more.start);
  237. const newlist = moreData.list;
  238. if (!newlist) {
  239. searching.remove();
  240. return;
  241. }
  242. more.start += newlist.length;
  243. ul.insertAdjacentHTML('beforeend', listHTML(newlist));
  244. applyHandler();
  245. searching.remove();
  246. }
  247.  
  248. container.append(ul, more);
  249. applyHandler();
  250.  
  251. function applyHandler() {
  252. ul.querySelectorAll('a').forEach(a => {
  253. a.addEventListener('click', e => {
  254. e.preventDefault();
  255. clickHandler(a);
  256. });
  257. });
  258. }
  259. }
  260.  
  261. async function getUserCollection(subject_id) {
  262. try {
  263. const headers = {};
  264. if (accessToken) headers.Authorization = `Bearer ${ accessToken }`;
  265. const response = await fetch(`https://api.bgm.tv/v0/users/${username}/collections/${subject_id}`, { headers });
  266. if (response.ok) {
  267. return await response.json();
  268. } else if (response.status === 404) {
  269. return { not_found: true };
  270. } else if (response.status === 401) {
  271. return { auth_failed: true };
  272. } else {
  273. throw new Error(`API request ${ response.status } ${ response.statusText }`);
  274. }
  275. } catch (error) {
  276. console.error(error);
  277. return null;
  278. }
  279. }
  280.  
  281. function renderCollection(data, container, fallbackLink='', fallbackName='本作') {
  282. if (!data) {
  283. container.innerHTML = '查询失败';
  284. return;
  285. }
  286. if (data.not_found || data.auth_failed) {
  287. let message = '';
  288. if (data.not_found) {
  289. message = `未找到${ fallbackLink ? `<a class="l" href="${fallbackLink}" target="_blank">${fallbackName}</a>` : fallbackName }的收藏记录`;
  290. } else if (data.auth_failed) {
  291. message = '个人令牌认证失败';
  292. }
  293. if (!accessToken || data.auth_failed) {
  294. 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>创建个人令牌';
  295. }
  296. container.innerHTML = message;
  297. container.querySelector('#incheiat')?.addEventListener('click', () => {
  298. accessToken = prompt('请填写个人令牌 token');
  299. if (!accessToken) return;
  300. if (!accessToken.match(/^[a-zA-Z0-9]+$/)) {
  301. accessToken = null;
  302. alert('格式错误,请重新填写');
  303. return;
  304. }
  305. });
  306. return;
  307. }
  308.  
  309. const { rate, subject_type, type, comment, updated_at, ep_status, vol_status, subject } = data;
  310. const { id, name, name_cn, volumes, eps } = subject;
  311. const verb = ['读', '看', '听', '玩', '', '看'][subject_type - 1];
  312. const html = `<li id="item_${id}" class="item even clearit" style="list-style: none;">
  313. <div class="inner" style="margin-left: 10px">
  314. <h3>
  315. ${ name_cn ? `<a href="/subject/${id}" class="l">${name_cn}</a> <small class="grey">${name}</small>`
  316. : `<a href="/subject/${id}" class="l">${name}</a>`
  317. }
  318. </h3>
  319. <p class="collectInfo">
  320. ${ rate ? `<span class="starstop-s"><span class="starlight stars${rate}"></span></span>`
  321. : ''
  322. }
  323. <span class="tip_j">${updated_at.slice(0, 10)}</span>
  324. <span class="tip_i">/</span>
  325. <span class="tip"> ${[`想${verb}`, `${verb}过`, `在${verb}`, '搁置', '抛弃'][type - 1]}</span>
  326. ${ ep_status ? `
  327. <span class="tip_i">/</span>
  328. <span class="tip">
  329. ${ ep_status }${ eps ? ` / ${eps}` : ''}话
  330. </span>
  331. ` : ''}
  332. ${ vol_status ? `
  333. <span class="tip_i">/</span>
  334. <span class="tip">
  335. ${ vol_status }${ eps ? ` / ${volumes}` : ''}卷
  336. </span>
  337. ` : ''}
  338. </p>
  339. ${ comment ? `
  340. <div id="comment_box"><div class="item"><div class="text_main_even" style="float:none;width:unset">
  341. <div class="text"> ${comment}</div>
  342. <div class="text_bottom"></div>
  343. </div></div></div></div>
  344. ` : '' }
  345. </li>`;
  346. container.innerHTML = html;
  347. }
  348. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址