BgmSyncF

https://bgm.tv/group/topic/386575

  1. // ==UserScript==
  2. // @name BgmSyncF
  3. // @version 0.4
  4. // @namespace https://jirehlov.com
  5. // @description https://bgm.tv/group/topic/386575
  6. // @include /^https?:\/\/(bgm\.tv|chii\.in|bangumi\.tv)\/user/.+/
  7. // @author Jirehlov
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11. (function () {
  12. 'use strict';
  13. const isUserPage = /^\/user\/[^/]+$/.test(window.location.pathname);
  14. if (!isUserPage) {
  15. return;
  16. }
  17. const limit = 50;
  18. let guess = 1000000;
  19. let totalItems = 0;
  20. let allData = [];
  21. let calculateButton;
  22. let buttonCounter = 0;
  23. let contextMenu = null;
  24. let settingsLink = null;
  25. const [username, page = '', subpage = ''] = (() => {
  26. const {pathname} = window.location;
  27. if (/^\/user/.test(pathname)) {
  28. return pathname.match(/\/user\/(\w+)\/?(\w+)?\/?(\w+)?/).slice(1, 4);
  29. }
  30. return [
  31. '',
  32. '',
  33. ''
  34. ];
  35. })();
  36. if (!username) {
  37. throw new Error('Username is not detected');
  38. }
  39. let likes = 0;
  40. let totalsub = 0;
  41. let subject_type = [
  42. 1,
  43. 2,
  44. 3,
  45. 4,
  46. 6
  47. ];
  48. let collection_type = [
  49. 1,
  50. 2,
  51. 3,
  52. 4,
  53. 5
  54. ];
  55. let collectStatus = {};
  56. let subject_type_index = 0;
  57. let percentageBarDiv = null;
  58. const nameDiv = document.querySelector('.name');
  59. const realname = nameDiv.querySelector('a').textContent;
  60. function setLocalStorageWithExpiration(key, value, expirationTimeInDays) {
  61. const expirationTimestamp = Date.now() + expirationTimeInDays * 24 * 60 * 60 * 1000;
  62. const dataToStore = {
  63. value,
  64. expiration: expirationTimestamp
  65. };
  66. localStorage.setItem(key, JSON.stringify(dataToStore));
  67. }
  68. function getLocalStorage(key) {
  69. const storedData = localStorage.getItem(key);
  70. if (storedData) {
  71. const data = JSON.parse(storedData);
  72. if (data.expiration && data.expiration > Date.now()) {
  73. return data.value;
  74. }
  75. localStorage.removeItem(key);
  76. }
  77. return null;
  78. }
  79. function confirmCacheRefresh() {
  80. const refreshCache = confirm('是否强制刷新缓存\uFF1F非必要请勿频繁刷新\uFF01');
  81. if (refreshCache) {
  82. localStorage.removeItem(`${ username }_totalsub`);
  83. localStorage.removeItem(`${ username }_likes`);
  84. likes = 0;
  85. totalsub = 0;
  86. subject_type_index = 0;
  87. allData = [];
  88. changeButtonText('计算全站同步率');
  89. calculateButton.removeEventListener('dblclick', confirmCacheRefresh);
  90. }
  91. }
  92. async function fetchData(collection_type, offset) {
  93. const url = `https://api.bgm.tv/v0/users/${ username }/collections?subject_type=${ subject_type[subject_type_index] }&type=${ collection_type }&limit=${ limit }&offset=${ offset }`;
  94. const headers = { 'Accept': 'application/json' };
  95. const response = await fetch(url, { headers });
  96. const data = await response.json();
  97. return data;
  98. }
  99. async function main() {
  100. const cachedtotalsub = getLocalStorage(`${ username }_totalsub`);
  101. const cachedlikes = getLocalStorage(`${ username }_likes`);
  102. if (cachedtotalsub !== null && cachedlikes !== null) {
  103. totalsub = cachedtotalsub;
  104. likes = cachedlikes;
  105. changeButtonText('已命中缓存');
  106. let syncRate = 0;
  107. if (totalsub > 0) {
  108. syncRate = likes / totalsub * 100;
  109. }
  110. updateUI();
  111. calculateButton.addEventListener('dblclick', confirmCacheRefresh);
  112. } else {
  113. calculateButton.style.pointerEvents = 'none';
  114. totalsub = 0;
  115. changeButtonText('计算中');
  116. for (let ct = 1; ct < collection_type.length; ct++) {
  117. for (let i = 0; i < subject_type.length; i++) {
  118. subject_type_index = i;
  119. const initialData = await fetchData(ct, guess);
  120. if ('description' in initialData && initialData.description.includes('equal to')) {
  121. totalItems = parseInt(initialData.description.split('equal to ')[1]);
  122. console.log(`Updated totalItems to: ${ totalItems }`);
  123. } else {
  124. totalItems = 0;
  125. }
  126. for (let offset = 0; offset < totalItems; offset += limit) {
  127. const data = await fetchData(ct, offset);
  128. allData.push(...data.data);
  129. console.log(`Fetched ${ offset + 1 }-${ offset + limit } items...`);
  130. updateButtonText();
  131. }
  132. }
  133. }
  134. if (settingsLink !== null) {
  135. for (const item of allData) {
  136. const subjectId = item.subject_id;
  137. collectStatus[subjectId] = 'collect';
  138. }
  139. localStorage.setItem('bangumi_subject_collectStatus', JSON.stringify(collectStatus));
  140. }
  141. for (const item of allData) {
  142. const rate = item.rate === 0 ? 7 : parseFloat(item.rate || 0);
  143. const score = Math.round(parseFloat(item.subject && item.subject.score !== undefined ? item.subject.score : 0));
  144. if (Math.abs(rate - score) === 0) {
  145. likes++;
  146. }
  147. totalsub++;
  148. }
  149. changeButtonText('计算全站同步率');
  150. calculateButton.style.pointerEvents = 'auto';
  151. setLocalStorageWithExpiration(`${ username }_totalsub`, totalsub, 7);
  152. setLocalStorageWithExpiration(`${ username }_likes`, likes, 7);
  153. let syncRate = 0;
  154. if (totalsub > 0) {
  155. syncRate = likes / totalsub * 100;
  156. }
  157. updateUI();
  158. }
  159. }
  160. function updateUI() {
  161. let synchronizeDiv = document.querySelector('.userSynchronize');
  162. if (!synchronizeDiv) {
  163. const userBoxDiv = document.querySelector('.user_box.clearit');
  164. if (userBoxDiv) {
  165. synchronizeDiv = document.createElement('div');
  166. synchronizeDiv.className = 'userSynchronize';
  167. userBoxDiv.appendChild(synchronizeDiv);
  168. }
  169. }
  170. let percentageBarDiv = document.querySelector('.BgmSyncF');
  171. if (!percentageBarDiv) {
  172. const synchronizeDiv = document.querySelector('.userSynchronize');
  173. if (synchronizeDiv) {
  174. percentageBarDiv = document.createElement('div');
  175. percentageBarDiv.className = 'BgmSyncF';
  176. synchronizeDiv.appendChild(percentageBarDiv);
  177. }
  178. }
  179. if (percentageBarDiv) {
  180. let syncRate = 0;
  181. if (totalsub > 0) {
  182. syncRate = likes / totalsub * 100;
  183. }
  184. const percentageBar = `
  185. <h3>${ realname }与全站的同步率</h3>
  186. <small class="hot">/ ${ likes }个同分条目</small>
  187. <p class="bar">
  188. <span class="percent_text rr">${ syncRate.toFixed(2) }%</span>
  189. <span class="percent" style="width:${ syncRate.toFixed(2) }%"></span>
  190. </p>
  191. `;
  192. percentageBarDiv.innerHTML = percentageBar;
  193. }
  194. console.log(`Number of items with same rate and score: ${ likes }`);
  195. console.log(`Number of items in total: ${ totalsub }`);
  196. console.log(`Sync rate: ${ (likes / totalsub).toFixed(2) }`);
  197. }
  198. function changeButtonText(newText) {
  199. const span = document.querySelector('.chiiBtn > span.BgmSyncFButton');
  200. if (span) {
  201. span.textContent = newText;
  202. }
  203. }
  204. function updateButtonText() {
  205. if (buttonCounter < 5) {
  206. changeButtonText('计算中' + '.'.repeat(buttonCounter));
  207. buttonCounter++;
  208. } else {
  209. changeButtonText('计算中');
  210. buttonCounter = 1;
  211. }
  212. }
  213. function addButton() {
  214. const link = document.createElement('a');
  215. const span = document.createElement('span');
  216. span.textContent = '计算全站同步率';
  217. span.className = 'BgmSyncFButton';
  218. link.href = 'javascript:void(0)';
  219. link.className = 'chiiBtn';
  220. link.addEventListener('click', main);
  221. link.appendChild(span);
  222. const actionsDiv = document.querySelector('.nameSingle > .inner > .actions');
  223. settingsLink = actionsDiv.querySelector('a[href="/settings"]');
  224. actionsDiv.appendChild(link);
  225. calculateButton = link;
  226. }
  227. addButton();
  228. async function downloadJSON(data, filename) {
  229. if (data.length === 0) {
  230. alert('没有数据可下载\uFF0C请刷新缓存后重试\u3002');
  231. return;
  232. }
  233. try {
  234. const json = JSON.stringify(data);
  235. const blob = new Blob([json], { type: 'application/json' });
  236. const url = URL.createObjectURL(blob);
  237. const link = document.createElement('a');
  238. link.href = url;
  239. link.download = filename;
  240. link.click();
  241. } catch (error) {
  242. alert('意外错误\uFF01');
  243. console.error(error);
  244. }
  245. }
  246. document.addEventListener('contextmenu', event => {
  247. const target = event.target;
  248. if (target === calculateButton || target.parentElement === calculateButton) {
  249. event.preventDefault();
  250. closeContextMenu();
  251. contextMenu = document.createElement('div');
  252. contextMenu.className = 'context-menu';
  253. contextMenu.style.position = 'absolute';
  254. contextMenu.style.left = event.pageX + 'px';
  255. contextMenu.style.top = event.pageY + 'px';
  256. contextMenu.style.backgroundColor = '#333';
  257. contextMenu.style.border = '0px';
  258. contextMenu.style.padding = '0px';
  259. contextMenu.style.boxShadow = '2px 2px 4px rgba(0, 0, 0, 0.2)';
  260. const jsonOption = document.createElement('div');
  261. jsonOption.textContent = '下载JSON';
  262. jsonOption.style.cursor = 'pointer';
  263. jsonOption.style.padding = '8px 12px';
  264. jsonOption.style.color = 'white';
  265. jsonOption.addEventListener('click', () => {
  266. const timestamp = new Date().toISOString().replace(/:/g, '-');
  267. const filename = `BgmSyncFdata_${ username }_${ timestamp }.json`;
  268. downloadJSON(allData, filename);
  269. closeContextMenu();
  270. });
  271. jsonOption.addEventListener('mouseenter', () => {
  272. jsonOption.style.backgroundColor = '#444';
  273. });
  274. jsonOption.addEventListener('mouseleave', () => {
  275. jsonOption.style.backgroundColor = '#333';
  276. });
  277. contextMenu.appendChild(jsonOption);
  278. document.body.appendChild(contextMenu);
  279. const removeMenu = () => {
  280. closeContextMenu();
  281. document.removeEventListener('click', removeMenu);
  282. };
  283. document.addEventListener('click', removeMenu);
  284. }
  285. });
  286. function closeContextMenu() {
  287. if (contextMenu) {
  288. contextMenu.remove();
  289. contextMenu = null;
  290. }
  291. }
  292. }());

QingJ © 2025

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