您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
// ==UserScript==
// @name BgmSyncF
// @version 0.4
// @namespace https://jirehlov.com
// @description https://bgm.tv/group/topic/386575
// @include /^https?:\/\/(bgm\.tv|chii\.in|bangumi\.tv)\/user/.+/
// @author Jirehlov
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const isUserPage = /^\/user\/[^/]+$/.test(window.location.pathname);
if (!isUserPage) {
return;
}
const limit = 50;
let guess = 1000000;
let totalItems = 0;
let allData = [];
let calculateButton;
let buttonCounter = 0;
let contextMenu = null;
let settingsLink = null;
const [username, page = '', subpage = ''] = (() => {
const {pathname} = window.location;
if (/^\/user/.test(pathname)) {
return pathname.match(/\/user\/(\w+)\/?(\w+)?\/?(\w+)?/).slice(1, 4);
}
return [
'',
'',
''
];
})();
if (!username) {
throw new Error('Username is not detected');
}
let likes = 0;
let totalsub = 0;
let subject_type = [
1,
2,
3,
4,
6
];
let collection_type = [
1,
2,
3,
4,
5
];
let collectStatus = {};
let subject_type_index = 0;
let percentageBarDiv = null;
const nameDiv = document.querySelector('.name');
const realname = nameDiv.querySelector('a').textContent;
function setLocalStorageWithExpiration(key, value, expirationTimeInDays) {
const expirationTimestamp = Date.now() + expirationTimeInDays * 24 * 60 * 60 * 1000;
const dataToStore = {
value,
expiration: expirationTimestamp
};
localStorage.setItem(key, JSON.stringify(dataToStore));
}
function getLocalStorage(key) {
const storedData = localStorage.getItem(key);
if (storedData) {
const data = JSON.parse(storedData);
if (data.expiration && data.expiration > Date.now()) {
return data.value;
}
localStorage.removeItem(key);
}
return null;
}
function confirmCacheRefresh() {
const refreshCache = confirm('是否强制刷新缓存\uFF1F非必要请勿频繁刷新\uFF01');
if (refreshCache) {
localStorage.removeItem(`${ username }_totalsub`);
localStorage.removeItem(`${ username }_likes`);
likes = 0;
totalsub = 0;
subject_type_index = 0;
allData = [];
changeButtonText('计算全站同步率');
calculateButton.removeEventListener('dblclick', confirmCacheRefresh);
}
}
async function fetchData(collection_type, offset) {
const url = `https://api.bgm.tv/v0/users/${ username }/collections?subject_type=${ subject_type[subject_type_index] }&type=${ collection_type }&limit=${ limit }&offset=${ offset }`;
const headers = { 'Accept': 'application/json' };
const response = await fetch(url, { headers });
const data = await response.json();
return data;
}
async function main() {
const cachedtotalsub = getLocalStorage(`${ username }_totalsub`);
const cachedlikes = getLocalStorage(`${ username }_likes`);
if (cachedtotalsub !== null && cachedlikes !== null) {
totalsub = cachedtotalsub;
likes = cachedlikes;
changeButtonText('已命中缓存');
let syncRate = 0;
if (totalsub > 0) {
syncRate = likes / totalsub * 100;
}
updateUI();
calculateButton.addEventListener('dblclick', confirmCacheRefresh);
} else {
calculateButton.style.pointerEvents = 'none';
totalsub = 0;
changeButtonText('计算中');
for (let ct = 1; ct < collection_type.length; ct++) {
for (let i = 0; i < subject_type.length; i++) {
subject_type_index = i;
const initialData = await fetchData(ct, guess);
if ('description' in initialData && initialData.description.includes('equal to')) {
totalItems = parseInt(initialData.description.split('equal to ')[1]);
console.log(`Updated totalItems to: ${ totalItems }`);
} else {
totalItems = 0;
}
for (let offset = 0; offset < totalItems; offset += limit) {
const data = await fetchData(ct, offset);
allData.push(...data.data);
console.log(`Fetched ${ offset + 1 }-${ offset + limit } items...`);
updateButtonText();
}
}
}
if (settingsLink !== null) {
for (const item of allData) {
const subjectId = item.subject_id;
collectStatus[subjectId] = 'collect';
}
localStorage.setItem('bangumi_subject_collectStatus', JSON.stringify(collectStatus));
}
for (const item of allData) {
const rate = item.rate === 0 ? 7 : parseFloat(item.rate || 0);
const score = Math.round(parseFloat(item.subject && item.subject.score !== undefined ? item.subject.score : 0));
if (Math.abs(rate - score) === 0) {
likes++;
}
totalsub++;
}
changeButtonText('计算全站同步率');
calculateButton.style.pointerEvents = 'auto';
setLocalStorageWithExpiration(`${ username }_totalsub`, totalsub, 7);
setLocalStorageWithExpiration(`${ username }_likes`, likes, 7);
let syncRate = 0;
if (totalsub > 0) {
syncRate = likes / totalsub * 100;
}
updateUI();
}
}
function updateUI() {
let synchronizeDiv = document.querySelector('.userSynchronize');
if (!synchronizeDiv) {
const userBoxDiv = document.querySelector('.user_box.clearit');
if (userBoxDiv) {
synchronizeDiv = document.createElement('div');
synchronizeDiv.className = 'userSynchronize';
userBoxDiv.appendChild(synchronizeDiv);
}
}
let percentageBarDiv = document.querySelector('.BgmSyncF');
if (!percentageBarDiv) {
const synchronizeDiv = document.querySelector('.userSynchronize');
if (synchronizeDiv) {
percentageBarDiv = document.createElement('div');
percentageBarDiv.className = 'BgmSyncF';
synchronizeDiv.appendChild(percentageBarDiv);
}
}
if (percentageBarDiv) {
let syncRate = 0;
if (totalsub > 0) {
syncRate = likes / totalsub * 100;
}
const percentageBar = `
<h3>${ realname }与全站的同步率</h3>
<small class="hot">/ ${ likes }个同分条目</small>
<p class="bar">
<span class="percent_text rr">${ syncRate.toFixed(2) }%</span>
<span class="percent" style="width:${ syncRate.toFixed(2) }%"></span>
</p>
`;
percentageBarDiv.innerHTML = percentageBar;
}
console.log(`Number of items with same rate and score: ${ likes }`);
console.log(`Number of items in total: ${ totalsub }`);
console.log(`Sync rate: ${ (likes / totalsub).toFixed(2) }`);
}
function changeButtonText(newText) {
const span = document.querySelector('.chiiBtn > span.BgmSyncFButton');
if (span) {
span.textContent = newText;
}
}
function updateButtonText() {
if (buttonCounter < 5) {
changeButtonText('计算中' + '.'.repeat(buttonCounter));
buttonCounter++;
} else {
changeButtonText('计算中');
buttonCounter = 1;
}
}
function addButton() {
const link = document.createElement('a');
const span = document.createElement('span');
span.textContent = '计算全站同步率';
span.className = 'BgmSyncFButton';
link.href = 'javascript:void(0)';
link.className = 'chiiBtn';
link.addEventListener('click', main);
link.appendChild(span);
const actionsDiv = document.querySelector('.nameSingle > .inner > .actions');
settingsLink = actionsDiv.querySelector('a[href="/settings"]');
actionsDiv.appendChild(link);
calculateButton = link;
}
addButton();
async function downloadJSON(data, filename) {
if (data.length === 0) {
alert('没有数据可下载\uFF0C请刷新缓存后重试\u3002');
return;
}
try {
const json = JSON.stringify(data);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
} catch (error) {
alert('意外错误\uFF01');
console.error(error);
}
}
document.addEventListener('contextmenu', event => {
const target = event.target;
if (target === calculateButton || target.parentElement === calculateButton) {
event.preventDefault();
closeContextMenu();
contextMenu = document.createElement('div');
contextMenu.className = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.left = event.pageX + 'px';
contextMenu.style.top = event.pageY + 'px';
contextMenu.style.backgroundColor = '#333';
contextMenu.style.border = '0px';
contextMenu.style.padding = '0px';
contextMenu.style.boxShadow = '2px 2px 4px rgba(0, 0, 0, 0.2)';
const jsonOption = document.createElement('div');
jsonOption.textContent = '下载JSON';
jsonOption.style.cursor = 'pointer';
jsonOption.style.padding = '8px 12px';
jsonOption.style.color = 'white';
jsonOption.addEventListener('click', () => {
const timestamp = new Date().toISOString().replace(/:/g, '-');
const filename = `BgmSyncFdata_${ username }_${ timestamp }.json`;
downloadJSON(allData, filename);
closeContextMenu();
});
jsonOption.addEventListener('mouseenter', () => {
jsonOption.style.backgroundColor = '#444';
});
jsonOption.addEventListener('mouseleave', () => {
jsonOption.style.backgroundColor = '#333';
});
contextMenu.appendChild(jsonOption);
document.body.appendChild(contextMenu);
const removeMenu = () => {
closeContextMenu();
document.removeEventListener('click', removeMenu);
};
document.addEventListener('click', removeMenu);
}
});
function closeContextMenu() {
if (contextMenu) {
contextMenu.remove();
contextMenu = null;
}
}
}());