Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/entries/offscreen/utils/userInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import { ref, shallowRef, computed } from "vue";
import { useI18n } from "vue-i18n";
import { saveAs } from "file-saver";
import { EResultParseStatus, type IUserInfo, type TSiteID } from "@ptd/site";
Expand All @@ -8,6 +8,7 @@ import type { DataTableHeader } from "vuetify/lib/components/VDataTable/types";
import { sendMessage } from "@/messages.ts";
import { formatNumber, formatSize, formatDate } from "@/options/utils.ts";
import { fixUserInfo, formatRatio } from "./utils.ts";
import { useMetadataStore } from "@/options/stores/metadata.ts";

import SiteName from "@/options/components/SiteName.vue";
import NavButton from "@/options/components/NavButton.vue";
Expand All @@ -17,6 +18,7 @@ const props = defineProps<{
siteId: TSiteID | null;
}>();
const { t } = useI18n();
const metadataStore = useMetadataStore();

const currentDate = formatDate(+new Date(), "yyyy-MM-dd");
const jsonData = ref<any>({});
Expand All @@ -25,6 +27,35 @@ interface IShowUserInfo extends IUserInfo {
date: string;
}

// 获取站点是否离线
const isSiteOffline = computed(() => {
if (!props.siteId) return false;
return metadataStore.sites[props.siteId]?.isOffline ?? false;
});

// 判断数据是否是错误的(站点关闭后通常都是0)
function isDataErroneous(userInfo: IShowUserInfo): boolean {
// 检查关键字段是否都是0或未定义
const uploaded = userInfo.uploaded ?? 0;
const downloaded = userInfo.downloaded ?? 0;
const seeding = userInfo.seeding ?? 0;
const seedingSize = userInfo.seedingSize ?? 0;
const bonus = userInfo.bonus ?? 0;

// 如果所有关键数据都是0,认为是错误数据
return uploaded === 0 && downloaded === 0 && seeding === 0 && seedingSize === 0 && bonus === 0;
}

// 判断是否可以删除当天的数据
function canDeleteTodayData(item: IShowUserInfo): boolean {
// 如果站点已离线且数据是错误的,允许删除
if (isSiteOffline.value && item.date === currentDate && isDataErroneous(item)) {
return true;
}
// 其他情况:不是当天数据,或者不是成功状态,或者不是当天
return item.date !== currentDate || item.status !== EResultParseStatus.success;
}

const siteHistoryData = shallowRef<IShowUserInfo[]>([]);
const tableHeader = [
{ title: t("common.date"), key: "date", align: "center" },
Expand Down Expand Up @@ -53,9 +84,22 @@ function loadSiteHistoryData(siteId: TSiteID) {

function deleteSiteUserInfo(date: string[]) {
if (confirm(t("MyData.HistoryDataView.deleteConfirm"))) {
// 过滤掉当天数据,但如果站点已离线且当天数据是错误的,允许删除
const filteredDates = date.filter((d) => {
if (d === currentDate) {
// 如果是当天数据,检查是否可以删除
const todayItem = siteHistoryData.value.find((item) => item.date === d);
if (todayItem && canDeleteTodayData(todayItem)) {
return true; // 允许删除
}
return false; // 不允许删除
}
return true; // 非当天数据可以删除
});

sendMessage("removeSiteUserInfo", {
siteId: props.siteId!,
date: date.filter((d) => d != currentDate), // 不允许移除当天的数据
date: filteredDates,
}).then(() => {
loadSiteHistoryData(props.siteId!);
});
Expand Down Expand Up @@ -191,7 +235,7 @@ function exportSiteHistoryData() {

<!-- 删除 -->
<v-btn
:disabled="item.status == EResultParseStatus.success && item.date == currentDate"
:disabled="!canDeleteTodayData(item)"
:title="t('common.remove')"
color="error"
icon="mdi-delete"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ onMounted(async () => {
resetTimelineDataWithControl();

isLoading.value = false;
console.debug(fixedLastUserInfo);
});

function exportTimelineImg() {
Expand Down
181 changes: 178 additions & 3 deletions src/packages/site/definitions/haidan.ts
Original file line number Diff line number Diff line change
@@ -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="]'],
Expand Down Expand Up @@ -30,7 +33,7 @@ export const siteMetadata: ISiteMetadata = {
type: "private",
schema: "NexusPHP",

urls: ["uggcf://jjj.unvqna.ivqrb/"],
urls: ["https://www.haidan.video/"],

category: [
{
Expand Down Expand Up @@ -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("<table")) {
return 0;
}
const doc = createDocument(text);
const trAnothers = Sizzle("tr:not(:first-child)", doc);
if (trAnothers.length === 0) {
return 0;
}

// 使用 Set 存储已处理的种子ID,用于去重
const processedTorrentIds = new Set<string>();

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("<table")) {
return 0;
}
const doc = createDocument(text);
const trAnothers = Sizzle("tr:not(:first-child)", doc);
if (trAnothers.length === 0) {
return 0;
}

// 根据自动判断应该用 td:eq(?)
let sizeIndex = 2;
const tdAnothers = Sizzle("> 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<string>();
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: [
Expand Down Expand Up @@ -231,4 +406,4 @@ export const siteMetadata: ISiteMetadata = {
},
// VIP以上等级通过系统智能识别机制自动处理,不在此处配置详细权限信息
],
};
};