在 qBittorrent WebUI 中添加按钮,用于标记tracker状态出错的种子
// ==UserScript==
// @name qB-WebUI 标记tracker异常
// @name:en qB-WebUI tag trackerERR
// @namespace localhost
// @version 0.2.2
// @author ColderCoder, avatasia, Schalkiii, flashlab
// @description 在 qBittorrent WebUI 中添加按钮,用于标记tracker状态出错的种子
// @description:en add a button in qBittorrent WebUI to tag torrents with tracker error
// @license MIT
// @run-at document-end
// @match http://192.168.10.72:9091
// @match http://127.0.0.1:9091
// ==/UserScript==
/* globals torrentsTable */
//require qB API v2.3.0 +
const extraStatus = [1]; // 需要额外标记的种子状态,包括:未联系(1)/更新中(3)/未工作(4)
const keywords = ['registered', 'deleted', 'exist', 'banned', 'err', '删']; // 关键词用于匹配未注册种子的服务器消息
const tagNames = ['trackerErr', 'notContact', 'unregister', 'updating', 'notWork']; //待写入的标签名
const host = window.location.href;
const baseURL = host + 'api/v2/torrents/';
function getList(scope) {
if (scope == "all") {
return getFetch('info')
} else if (scope == "list") {
return torrentsTable.getFilteredAndSortedRows().map(item => item.full_data);
} else {
return null
}
}
async function getFetch(route) {
try {
const response = await fetch(baseURL + route);
if (!response.ok) {
throw new Error('Error fetching info!');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error: ', route, error);
return null;
}
}
async function processTorrents(scope = 'all') {
const tagList = new Map();
let count = 0;
let torrentCounts = 0;
let ignoreDiscontact = false; //累计超过10次网络请求失败是否终止操作
const torrentList = await getList(scope);
if (!torrentList || torrentList.length == 0) return;
for (const torrent of torrentList) {
const trackers = await getFetch(`trackers?hash=${torrent.hash}`);
if (!trackers) {
count++
if (!ignoreDiscontact && count > 10 && window.confirm("网络请求失败过多,是否终止?")) {
break;
} else {
ignoreDiscontact = true;
continue
}
}
let newtag = null;
let allTrackersNotWorking = true;
for (const tracker of trackers) {
if (tracker.status === 4) { //tracker is not working
for (const msg of keywords) {
if (tracker.msg.includes(msg)) {
console.log(`Unregistered: ${torrent.name}: ${tracker.msg}`);
newtag = tagNames[2];
}
}
} else if (tracker.status !== 4){
// 如果有任何一个 tracker 不是状态 4,则该种子不符合所有 tracker 都不工作的条件
allTrackersNotWorking = false;
} else if (extraStatus.includes(tracker.status)) {
newtag = tagNames[tracker.status]
}
}
// 如果所有 tracker 都不工作并且没有其他标记,则打上 tagName[0]
if (allTrackersNotWorking && !newtag) newtag = tagNames[0];
if (newtag && !torrent.tags.includes(newtag)) {
tagList.set(newtag, tagList.get(newtag) ? `${tagList.get(newtag)}|${torrent.hash}` : torrent.hash)
torrentCounts++
}
}
if (!window.confirm(`共获取 ${torrentList.length} 项(失败 ${count}项),共 ${torrentCounts} 项需要标记,是否写入标签?`)) return;
if (torrentCounts == 0) return;
const url = `${baseURL}addTags`;
for (const [tags, hashes] of tagList) {
let data = new URLSearchParams();
data.append('hashes', hashes);
data.append('tags', tags);
try {
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data
})
.then(response => {
console.log(response);
})
.catch(error => {
console.error('Error:', tags, hashes, error);
});
} catch (error) {
console.error('Error:', error.message);
}
}
alert("完成!")
}
const newBtn = document.createElement("li");
newBtn.innerHTML = "<a class='js-modal'><b> 标记tracker异常 </b></a>";
document.querySelector("#desktopNavbar > ul").append(newBtn);
newBtn.addEventListener("click", async function() {
const scope = window.prompt("!!!请先手动删除[tagNames]中包含的标签!!!\n检查全部种子请输入 [all]\n仅检查当前表格中显示的种子请输入 [list]", "all")
if (!scope) return;
await processTorrents(scope);
});