// ==UserScript== // @name 保种统计 // @namespace https://github.com/tanapok/Seeding-Statistics // @version 1.0.4 // @description Try this little tool and figure out the seed data! // @author tanapok // @match https://wintersakura.net/userdetails.php?id=* // @match https://carpt.net/userdetails.php?id=* // @icon https://download.wintersakura.net/uploads/2022/10/23/63551a96a6ddd.png // @grant none // @license GNU GPLv3 // @downloadURL none // ==/UserScript== var siteData = [ { siteName: 'WinterSakura', siteUrl: 'wintersakura.net', spiderModel: 'NexusPHP', // 此参数暂未使用 siteGroups: ['-SakuraWEB', '-SakuraSUB', '-WS', '-WScode', '-Sakura Academic'], seedListSelector: '#ka1', // 做种列表区域选择器 seedItemsSelector: 'table > tbody:first-child > tr:not(:first-child)', // 做种列表条目选择器 seedTitleSelector: 'td:nth-child(2) > a:nth-child(1)', seedSizeSelector: 'td:nth-child(4)', seedersNumberSelector: 'td:nth-child(5)', seedUploadSizeSelector: 'td:nth-child(7)', seedDownloadSizeSelector: 'td:nth-child(8)', seedTimeSelector: 'td:nth-child(10)', hasPagination: true, nextPageButtonSelector: 'p.nexus-pagination:nth-child(3) > a:nth-child(2) > b:nth-child(1)', theLastPageFlagSelector: 'font.gray:nth-child(2) > b:nth-child(1)', // true for the end of pages theLastPageFlagText: '下一页', seedSize: 0, seedItemsNumber: 0, seedersNumber: 0, seedUploadSize: 0, seedDownloadSize: 0, seedTime: 0, }, { siteName: 'CarPT', siteUrl: 'carpt.net', spiderModel: 'NexusPHP', // 此参数暂未使用 siteGroups: ['-CarPT'], seedListSelector: '#ka1', // 做种列表区域选择器 seedItemsSelector: 'table > tbody:first-child > tr:not(:first-child)', // 做种列表条目选择器 seedTitleSelector: 'td:nth-child(2) > a:nth-child(1)', seedSizeSelector: 'td:nth-child(4)', seedersNumberSelector: 'td:nth-child(5)', seedUploadSizeSelector: 'td:nth-child(7)', seedDownloadSizeSelector: 'td:nth-child(8)', seedTimeSelector: 'td:nth-child(10)', hasPagination: true, nextPageButtonSelector: 'p.nexus-pagination:nth-child(3) > a:nth-child(2) > b:nth-child(1)', theLastPageFlagSelector: 'font.gray:nth-child(2) > b:nth-child(1)', // true for the end of pages theLastPageFlagText: '下一页', seedSize: 0, seedItemsNumber: 0, seedersNumber: 0, seedUploadSize: 0, seedDownloadSize: 0, seedTime: 0, }, { siteName: 'Others', // 未找到对应站点时的默认值 siteUrl: '', spiderModel: 'NexusPHP', siteGroups: ['-Others'], seedListSelector: '#ka1', // 做种列表区域选择器 seedItemsSelector: 'table > tbody:first-child > tr:not(:first-child)', // 做种列表条目选择器 seedTitleSelector: 'td:nth-child(2) > a:nth-child(1)', seedSizeSelector: 'td:nth-child(4)', seedersNumberSelector: 'td:nth-child(5)', seedUploadSizeSelector: 'td:nth-child(7)', seedDownloadSizeSelector: 'td:nth-child(8)', seedTimeSelector: 'td:nth-child(10)', hasPagination: false, nextPageButtonSelector: '', theLastPageFlagSelector: '', // true for the end of pages theLastPageFlagText: '下一页', seedSize: 0, seedItemsNumber: 0, seedersNumber: 0, seedUploadSize: 0, seedDownloadSize: 0, seedTime: 0, }, ]; var siteName = ''; var siteIndex = -1; var userID = ''; var seedList = undefined; var seedListHash = []; // 已经爬取过的做种列表的哈希值数组 var notice = undefined; var result = undefined; var hasPagination = false; var nextPageButton = undefined; var theLastPageFlag = undefined; var uiReady = false; var theLastPageFlagText = ''; var ssContainer = undefined; var ssBar = undefined; // 初始化 function init() { let siteUrl = window.location.href; // 获取站点名称及对应的索引 for (let i = 0; i < siteData.length; i++) { if (siteUrl.indexOf(siteData[i].siteUrl) !== -1) { siteName = siteData[i].siteName; siteIndex = i; hasPagination = siteData[siteIndex].hasPagination; theLastPageFlagText = siteData[siteIndex].theLastPageFlagText; break; } } console.log('siteName:', siteName); // 从网址中匹配 userID let reg = /id=(\d+)/; let result = reg.exec(siteUrl); if (result) { userID = result[1]; } console.log('userID:', userID); } /* MD5 哈希函数 */ /* * JavaScript MD5 * https://github.com/blueimp/JavaScript-MD5 * * Copyright 2011, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: * https://opensource.org/licenses/MIT * * Based on * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet * Distributed under the BSD License * See http://pajhome.org.uk/crypt/md5 for more info. */ !function(n){"use strict";function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((u=d(d(t,n),d(e,u)))<>>32-o,r)}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function c(n,t){var r,e,o,u;n[t>>5]|=128<>>9<<4)]=t;for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h>5]>>>e%32&255);return t}function a(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e>5]|=(255&n.charCodeAt(e/8))<>>4&15)+r.charAt(15&t);return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return i(c(a(n=r(n)),8*n.length))}function u(n,t){return function(n,t){var r,e=a(n),o=[],u=[];for(o[15]=u[15]=void 0,16 0) { timeArray.push(timeUnitNumber + timeUnits[i]); seconds -= timeUnitNumber * timeUnitSeconds[i]; } } time = timeArray.join(''); return time; } // 爬取数据 function spider() { // 爬取数据 let items = seedList.querySelectorAll(siteData[siteIndex].seedItemsSelector); items.forEach((item) => { let title = item.querySelector(siteData[siteIndex].seedTitleSelector).title; let size = item.querySelector(siteData[siteIndex].seedSizeSelector).innerText.replace(',', '').split('\n'); let seeders = Number(item.querySelector(siteData[siteIndex].seedersNumberSelector).innerText.replace(',', '')); let uploadSize = item.querySelector(siteData[siteIndex].seedUploadSizeSelector).innerText.replace(',', '').split('\n');; let downloadSize = item.querySelector(siteData[siteIndex].seedDownloadSizeSelector).innerText.replace(',', '').split('\n');; let time = item.querySelector(siteData[siteIndex].seedTimeSelector).innerText.replace('天', ':').replace('小时', ':').replace('分', ':').replace('秒', '').replace(',', ''); // D:HH:MM:SS // 将日志更新到 result 中 appendResult('正在解析:' + title + '
'); // 遍历 siteData 中的所有站点,查找 siteGroup 中的元素包含于 title 中的首个元素 let finded = false; for (let i = 0; i < siteData.length; i++) { for (let j = 0; j < siteData[i].siteGroups.length; j++) { if (title.indexOf(siteData[i].siteGroups[j]) !== -1) { siteData[i].seedSize += sizeToBytes(size); siteData[i].seedersNumber += seeders; siteData[i].seedUploadSize += sizeToBytes(uploadSize); siteData[i].seedDownloadSize += sizeToBytes(downloadSize); siteData[i].seedTime += timeToSeconds(time); siteData[i].seedItemsNumber++; finded = true; break; } } if (finded) { break; } } // 如果没找到,更新 Others 站点的数据 if (!finded) { // 在 siteData 中找到 Others 站点的索引 let othersIndex = siteData.findIndex((site) => { return site.siteName === 'Others'; }); siteData[othersIndex].seedSize += sizeToBytes(size); siteData[othersIndex].seedersNumber += seeders; siteData[othersIndex].seedUploadSize += sizeToBytes(uploadSize); siteData[othersIndex].seedDownloadSize += sizeToBytes(downloadSize); siteData[othersIndex].seedTime += timeToSeconds(time); siteData[othersIndex].seedItemsNumber++; } showResult('解析完成:' + title + '
'); }); } // 输出数据到表格 function outputData() { // 将站点名称和用户的 UID 输出到 result 中 showResult('站点名称:' + siteData[siteIndex].siteName + '
'); appendResult('用户 ID/UID:' + userID + '
'); // 将 siteData 中所有站点的 做种数量 和 做种体积 输出到 result 中 let totalSeedItemsNumber = 0; let totalSeedSize = 0; for (let i = 0; i < siteData.length; i++) { totalSeedItemsNumber += siteData[i].seedItemsNumber; totalSeedSize += siteData[i].seedSize; } appendResult('做种总量:' + totalSeedItemsNumber + '
'); appendResult('做种总大小:' + bytesToSize(totalSeedSize) + '
'); // 输出数据到表格(优先输出当前站点,然后依次输出其他站点) // 在同一行输出站点名称、做种数量、做种体积、平均做种人数、做种上传总量、做种下载总量、平均做种时间 let table = document.createElement('table'); table.innerHTML = '站点名称做种数量做种体积平均做种人数做种上传总量做种下载总量平均做种时间'; // 输出当前站点(仅当 seedItemsNumber 不为 0 时) if (siteData[siteIndex].seedItemsNumber !== 0) { let tr = document.createElement('tr'); tr.innerHTML = '' + siteData[siteIndex].siteName + '' + siteData[siteIndex].seedItemsNumber + '' + bytesToSize(siteData[siteIndex].seedSize) + '' + (siteData[siteIndex].seedersNumber / siteData[siteIndex].seedItemsNumber).toFixed(2) + '' + bytesToSize(siteData[siteIndex].seedUploadSize) + '' + bytesToSize(siteData[siteIndex].seedDownloadSize) + '' + secondsToTime(siteData[siteIndex].seedTime / siteData[siteIndex].seedItemsNumber) + ''; table.appendChild(tr); } // 输出其他站点(仅当 seedItemsNumber 不为 0 时) for (let i = 0; i < siteData.length; i++) { if (i !== siteIndex && siteData[i].seedItemsNumber !== 0) { let tr = document.createElement('tr'); tr.innerHTML = '' + siteData[i].siteName + '' + siteData[i].seedItemsNumber + '' + bytesToSize(siteData[i].seedSize) + '' + (siteData[i].seedersNumber / siteData[i].seedItemsNumber).toFixed(2) + '' + bytesToSize(siteData[i].seedUploadSize) + '' + bytesToSize(siteData[i].seedDownloadSize) + '' + secondsToTime(siteData[i].seedTime / siteData[i].seedItemsNumber) + ''; table.appendChild(tr); } } // 将表格输出到 result 中 appendResult(table.outerHTML); notice.style.backgroundColor = 'rgba(31,177,65,1)'; notice.style.color = 'white'; } /* UI 相关 */ // 向页面注入 UI function injectUI() { let ssHTML = document.createElement('div'); ssHTML.id = 'ss-container'; ssHTML.innerHTML = `
`; let ssCSS = document.createElement('style'); ssCSS.innerHTML = ` #ss-high-friction-bar { display: flex; flex-direction: column; justify-content: start; align-items: center; margin-bottom: 6px; cursor: move; } .ss-graph { height: 1px; width: 60px; background-color: rgba(0, 0, 0, 0.1); margin-bottom: 2px; } #ss-container { font-family: "Helvetica Neue", Helvetica, Arial; font-size: 0.8rem; line-height: 1.45; color: rgba(0, 0, 0, 0.76); box-sizing: border-box; min-width: 20vw; max-width: 60vw; width: auto; height: auto; min-height: 15vh; max-height: 80vh; background-color: rgba(255, 255, 255, 0.96); position: fixed; left: 0; top: 0; border-radius: 6px; margin: 12px; padding: 12px; box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1); transition: box-shadow 0.1s ease-in-out; overflow: auto; z-index: 9999; } #ss-container:hover { box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); background-color: white; } #ss-notice { background-color: rgba(0, 0, 0, 0.05); width: fit-content; padding: 0.2rem; border-radius: 3px; } #ss-result { margin-top: 0.4rem; } #ss-result table { border-collapse: collapse; margin-top: 0.4rem; } #ss-result table, #ss-result th, #ss-result td { border: 0; /* background-color: rgba(4,150,255,1); */ } #ss-result th { background-color: rgba(4, 142, 255, 0.96); color: rgba(255, 255, 255, 0.96); } #ss-result th, #ss-result td { text-align: left; padding: 0.2rem 1rem; } #ss-result tr:nth-child(odd) { background-color: rgba(0, 0, 0, 0.05); } #ss-result tr:nth-child(even) { background-color: rgba(0, 0, 0, 0.1); } #ss-result td { font-size: inherit; } `; document.body.appendChild(ssCSS); document.body.appendChild(ssHTML); notice = document.querySelector('#ss-notice'); result = document.querySelector('#ss-result'); } // 显示提示信息 function showNotice(text) { notice.style.display = 'block'; notice.querySelector('#ss-notice-text').innerText = text; } // 隐藏提示信息 function hideNotice() { notice.style.display = 'none'; } // 显示结果 function showResult(text) { result.style.display = 'block'; result.innerHTML = text; } // 追加结果 function appendResult(text) { result.style.display = 'block'; result.innerHTML += text; } // 隐藏结果 function hideResult() { result.style.display = 'none'; } // UI 事件绑定 function bindUIActions() { ssContainer = document.querySelector("#ss-container"); ssBar = document.querySelector("#ss-high-friction-bar"); // 鼠标在 ssBar 上按下时,使 ssContainer 跟随鼠标移动 ssBar.onmousedown = function (e) { let x = e.clientX - ssContainer.offsetLeft; let y = e.clientY - ssContainer.offsetTop; // 增大 ssContainer 的阴影效果 ssContainer.style.boxShadow = "0 0 10px 5px rgba(0, 0, 0, 0.1)"; document.onmousemove = function (e) { ssContainer.style.left = e.clientX - x - 12 + "px"; ssContainer.style.top = e.clientY - y - 12 + "px"; // 防止鼠标移出浏览器窗口 if (e.clientX - x - 12 < 0) { ssContainer.style.left = 0; } if (e.clientY - y - 12 < 0) { ssContainer.style.top = 0; } if (e.clientX - x - 12 > document.documentElement.clientWidth - ssContainer.offsetWidth) { ssContainer.style.left = document.documentElement.clientWidth - ssContainer.offsetWidth + "px"; } if (e.clientY - y - 12 > document.documentElement.clientHeight - ssContainer.offsetHeight) { ssContainer.style.top = document.documentElement.clientHeight - ssContainer.offsetHeight + "px"; } } } // 鼠标松开时,停止移动 document.onmouseup = function () { // 恢复 ssContainer 的阴影效果 ssContainer.style.boxShadow = "0 0 5px 2px rgba(0, 0, 0, 0.1)"; document.onmousemove = null; } } /* 主函数 */ (function() { 'use strict'; // 初始化 init(); // 持续判断页面是否包含做种列表 let timer = setInterval(() => { console.log('Running...'); if (checkPage()) { if (!uiReady) { injectUI(); bindUIActions(); uiReady = true; } showNotice('正在统计当前页数据,请稍候...'); // 爬取数据 spider(); if(hasNextPage()) { showNotice('当前页面统计完成,稍后自动统计下一页'); } else { showNotice('统计完成,若对结果不满意,可刷新页面后重新尝试'); outputData(); clearInterval(timer); } } else { if (uiReady) { showNotice('当前页面数据已统计过,有可能是没有数据,或需手动切换至下一页'); } } }, 1000); })();