// ==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 // @downloadURL https://update.greasyfork.icu/scripts/508516/qB-WebUI%20%E6%A0%87%E8%AE%B0tracker%E5%BC%82%E5%B8%B8.user.js // @updateURL https://update.greasyfork.icu/scripts/508516/qB-WebUI%20%E6%A0%87%E8%AE%B0tracker%E5%BC%82%E5%B8%B8.meta.js // ==/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 = " 标记tracker异常 "; 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); });