From 9a15584c6ab418ccb0c9f88cb9a4259ea22cfb44 Mon Sep 17 00:00:00 2001
From: madrays <87717138@qq.com>
Date: Sat, 1 Nov 2025 22:28:04 +0800
Subject: [PATCH 1/2] fix(haidan): Fix seeding quantity and size extraction
with deduplication
- Fix seeding quantity: Count deduplicated table rows instead of reading incorrect 588
- Fix seeding size: Add deduplication when accumulating torrent sizes
- Use torrent ID (details.php?id=XXX) as unique identifier for deduplication
- Resolve issue where haidan site returns 100% duplicated data in HTML fragment
---
src/packages/site/definitions/haidan.ts | 181 +++++++++++++++++++++++-
1 file changed, 178 insertions(+), 3 deletions(-)
diff --git a/src/packages/site/definitions/haidan.ts b/src/packages/site/definitions/haidan.ts
index 2298b56ea..5354c448d 100644
--- a/src/packages/site/definitions/haidan.ts
+++ b/src/packages/site/definitions/haidan.ts
@@ -1,6 +1,9 @@
import { type ISiteMetadata } from "../types";
import { CategoryInclbookmarked, CategoryIncldead, CategorySpstate, SchemaMetadata } from "../schemas/NexusPHP.ts";
import { userInfoWithInvitesInUserDetailsPage } from "./kunlun.ts";
+import { parseSizeString, sizePattern } from "../utils/filesize";
+import Sizzle from "sizzle";
+import { createDocument } from "../utils/html";
const linkQuery = {
selector: ['a[href*="download.php?id="]'],
@@ -30,7 +33,7 @@ export const siteMetadata: ISiteMetadata = {
type: "private",
schema: "NexusPHP",
- urls: ["uggcf://jjj.unvqna.ivqrb/"],
+ urls: ["https://www.haidan.video/"],
category: [
{
@@ -143,12 +146,184 @@ export const siteMetadata: ISiteMetadata = {
selector: ["td.rowhead:contains('等级积分') + td"],
filters: [
(query: string) => {
- query = query.replace(/[,\s]/g, "");
+ query = query.replace(/[\s,]/g, "");
return parseFloat(query.split("[")[0]);
},
],
},
+ // 从 ajax 页面获取做种信息(这些选择器仅用于 userdetails.php 页面,实际上会在 process 步骤中从 getusertorrentlistajax.php 获取)
+ seeding: {
+ selector: [":self"],
+ filters: [
+ (query: any) => {
+ // 这个选择器实际上不会被用到,因为会在 process 步骤中从 ajax 页面获取
+ return 0;
+ },
+ ],
+ },
+ seedingSize: {
+ selector: [":self"],
+ elementProcess: (element: HTMLElement) => {
+ // 这个选择器实际上不会被用到,因为会在 process 步骤中从 ajax 页面获取
+ return 0;
+ },
+ },
},
+ process: [
+ // 第一步:获取用户ID
+ {
+ requestConfig: { url: "/index.php", responseType: "document" },
+ fields: ["id"],
+ },
+ // 第二步:获取用户详细信息(但不包含seeding和seedingSize)
+ {
+ requestConfig: { url: "/userdetails.php", responseType: "document" },
+ assertion: { id: "params.id" },
+ fields: [
+ "name",
+ "messageCount",
+ "uploaded",
+ "trueUploaded",
+ "downloaded",
+ "trueDownloaded",
+ "levelName",
+ "bonus",
+ "seedingBonus",
+ "joinTime",
+ "hnrUnsatisfied",
+ "hnrPreWarning",
+ // 注意:这里不包含 seeding 和 seedingSize
+ ],
+ },
+ // 第三步:获取做种信息(使用文本响应类型,因为返回的是 HTML 片段)
+ {
+ requestConfig: {
+ url: "/getusertorrentlistajax.php",
+ params: { type: "seeding" },
+ responseType: "text", // 使用文本响应,因为返回的是 HTML 片段
+ },
+ assertion: { id: "params.userid" },
+ fields: ["seeding", "seedingSize"],
+ // 使用自定义选择器,直接从文本响应中解析
+ selectors: {
+ seeding: {
+ selector: [":self"], // :self 会返回整个响应对象(字符串)
+ filters: [
+ (query: any) => {
+ // 当 responseType 为 "text" 时,query 就是响应字符串
+ const text = typeof query === "string" ? query : String(query || "");
+ // 从文本中创建 Document,然后解析表格并去重统计行数
+ if (!text || !text.includes("
();
+
+ trAnothers.forEach((trAnother) => {
+ // 尝试从行中提取种子ID(从 details.php?id=XXX 链接中)
+ const linkElement = Sizzle("a[href*='details.php?id=']", trAnother)[0] as HTMLAnchorElement;
+ let torrentId: string | null = null;
+ if (linkElement && linkElement.href) {
+ const idMatch = linkElement.href.match(/details\.php\?id=(\d+)/);
+ if (idMatch && idMatch[1]) {
+ torrentId = idMatch[1];
+ }
+ }
+
+ // 如果没有找到ID,使用行的innerHTML作为唯一标识(备用方案)
+ if (!torrentId) {
+ torrentId = (trAnother as HTMLElement).innerHTML.trim();
+ }
+
+ // 标记为已处理(Set会自动去重)
+ processedTorrentIds.add(torrentId);
+ });
+
+ // 返回去重后的数量
+ return processedTorrentIds.size;
+ },
+ ],
+ },
+ seedingSize: {
+ selector: [":self"],
+ filters: [
+ (query: any) => {
+ // 当 responseType 为 "text" 时,query 就是响应字符串
+ const text = typeof query === "string" ? query : String(query || "");
+ // 从文本中创建 Document,然后解析表格
+ if (!text || !text.includes(" td", trAnothers[0]);
+ for (let i = 0; i < tdAnothers.length; i++) {
+ const tdText = (tdAnothers[i] as HTMLElement).innerText.trim();
+ if (sizePattern.test(tdText)) {
+ sizeIndex = i;
+ break;
+ }
+ }
+
+ // 使用 Set 存储已处理的种子ID,用于去重
+ const processedTorrentIds = new Set();
+ let totalSize = 0;
+
+ trAnothers.forEach((trAnother) => {
+ // 尝试从行中提取种子ID(从 details.php?id=XXX 链接中)
+ const linkElement = Sizzle("a[href*='details.php?id=']", trAnother)[0] as HTMLAnchorElement;
+ let torrentId: string | null = null;
+ if (linkElement && linkElement.href) {
+ const idMatch = linkElement.href.match(/details\.php\?id=(\d+)/);
+ if (idMatch && idMatch[1]) {
+ torrentId = idMatch[1];
+ }
+ }
+
+ // 如果没有找到ID,使用行的innerHTML作为唯一标识(备用方案)
+ if (!torrentId) {
+ torrentId = (trAnother as HTMLElement).innerHTML.trim();
+ }
+
+ // 如果这个种子ID已经处理过,跳过(去重)
+ if (processedTorrentIds.has(torrentId)) {
+ return;
+ }
+
+ // 标记为已处理
+ processedTorrentIds.add(torrentId);
+
+ // 累加大小
+ const sizeSelector = Sizzle(`td:eq(${sizeIndex})`, trAnother)[0] as HTMLElement;
+ if (sizeSelector) {
+ totalSize += parseSizeString(sizeSelector.innerText.trim());
+ }
+ });
+
+ return totalSize;
+ },
+ ],
+ },
+ },
+ },
+ // 第四步:获取魔力值相关信息
+ {
+ requestConfig: { url: "/mybonus.php", responseType: "document" },
+ fields: ["bonusPerHour", "seedingBonusPerHour"],
+ },
+ ],
},
levelRequirements: [
@@ -231,4 +406,4 @@ export const siteMetadata: ISiteMetadata = {
},
// VIP以上等级通过系统智能识别机制自动处理,不在此处配置详细权限信息
],
-};
+};
\ No newline at end of file
From 9215315f62ee90418e8a9aa6b2213714895ce47e Mon Sep 17 00:00:00 2001
From: madrays <87717138@qq.com>
Date: Sat, 1 Nov 2025 22:56:19 +0800
Subject: [PATCH 2/2] fix: Allow deleting today erroneous data when site is
offline
- When site is marked as offline, allow deleting today data with all zeros
- Fix metadata.lastUserInfo cache not updating after deletion
- Remove debug console.log from UserDataTimeline
---
src/entries/offscreen/utils/userInfo.ts | 34 +++++++++++++
.../Overview/MyData/HistoryDataViewDialog.vue | 50 +++++++++++++++++--
.../MyData/UserDataTimeline/Index.vue | 1 -
3 files changed, 81 insertions(+), 4 deletions(-)
diff --git a/src/entries/offscreen/utils/userInfo.ts b/src/entries/offscreen/utils/userInfo.ts
index 84b1fec96..2e2b44942 100644
--- a/src/entries/offscreen/utils/userInfo.ts
+++ b/src/entries/offscreen/utils/userInfo.ts
@@ -121,4 +121,38 @@ onMessage("removeSiteUserInfo", async ({ data: { siteId, date } }) => {
unset(userInfoStore, `${siteId}.${day}`);
}
await sendMessage("setExtStorage", { key: "userInfo", value: userInfoStore! });
+
+ // 检查删除的数据是否是 metadata.lastUserInfo 中对应的日期
+ // 如果是,需要清除或更新 metadata.lastUserInfo,避免显示已删除的数据
+ const metadataStore = ((await sendMessage("getExtStorage", "metadata")) ?? {}) as IMetadataPiniaStorageSchema;
+ const lastUserInfo = metadataStore.lastUserInfo?.[siteId];
+ if (lastUserInfo) {
+ const lastUserInfoDate = format(lastUserInfo.updateAt, "yyyy-MM-dd");
+ // 如果删除的日期包含了最后一次成功数据的日期,需要清除 metadata 中的缓存
+ if (date.includes(lastUserInfoDate)) {
+ // 尝试从剩余的历史记录中找到最新的成功数据
+ const siteUserInfoHistory = userInfoStore[siteId] ?? {};
+ let maxDate = null;
+ let maxDateUserInfo = null;
+ for (const historyDate in siteUserInfoHistory) {
+ const historyUserInfo = siteUserInfoHistory[historyDate];
+ if (
+ historyUserInfo.status === EResultParseStatus.success &&
+ (!maxDate || new Date(historyDate) > new Date(maxDate))
+ ) {
+ maxDate = historyDate;
+ maxDateUserInfo = historyUserInfo;
+ }
+ }
+
+ // 如果有新的最新数据,更新 metadata;否则清除
+ metadataStore.lastUserInfo ??= {};
+ if (maxDateUserInfo) {
+ metadataStore.lastUserInfo[siteId] = maxDateUserInfo;
+ } else {
+ delete metadataStore.lastUserInfo[siteId];
+ }
+ await sendMessage("setExtStorage", { key: "metadata", value: metadataStore });
+ }
+ }
});
diff --git a/src/entries/options/views/Overview/MyData/HistoryDataViewDialog.vue b/src/entries/options/views/Overview/MyData/HistoryDataViewDialog.vue
index cc0ea3159..cefe7c22d 100644
--- a/src/entries/options/views/Overview/MyData/HistoryDataViewDialog.vue
+++ b/src/entries/options/views/Overview/MyData/HistoryDataViewDialog.vue
@@ -1,5 +1,5 @@