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 @@