Files
MoonTVPlus/src/lib/netdisk-check-task.ts
T
2026-04-27 21:25:31 +08:00

240 lines
6.8 KiB
TypeScript

import { nanoid } from 'nanoid';
import { checkNetdiskLink } from '@/lib/pancheck';
import type {
NetdiskCheckPlatform,
NetdiskCheckResult,
NetdiskCheckStartInput,
NetdiskCheckTask,
NetdiskCheckTaskResultItem,
} from '@/lib/pancheck/types';
const NETDISK_CHECK_RULE = {
batchSize: 3,
batchIntervalMs: 2000,
requestTimeoutMs: 12000,
maxTaskLinks: 60,
cooldownMs: 60000,
taskRetentionMs: 60 * 60 * 1000,
maxActiveTasks: 5,
} as const;
const tasks = new Map<string, NetdiskCheckTask>();
const activeTaskIds = new Set<string>();
let cooldownUntil = 0;
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function withTimeout<T>(promise: Promise<T>, timeoutMs: number) {
return Promise.race([
promise,
new Promise<T>((_, reject) => {
setTimeout(() => reject(new Error(`执行超时(${timeoutMs}ms)`)), timeoutMs);
}),
]);
}
function cleanupOldTasks() {
const now = Date.now();
for (const [id, task] of Array.from(tasks.entries())) {
if (now - task.updatedAt > NETDISK_CHECK_RULE.taskRetentionMs) {
tasks.delete(id);
}
}
}
function cloneTask(task: NetdiskCheckTask): NetdiskCheckTask {
return JSON.parse(JSON.stringify(task)) as NetdiskCheckTask;
}
function createTaskResultItem(status: NetdiskCheckTaskResultItem['status']): NetdiskCheckTaskResultItem {
return { status };
}
function countBatchSize(total: number) {
return Math.max(1, Math.ceil(total / NETDISK_CHECK_RULE.batchSize));
}
function updateTaskAfterResult(task: NetdiskCheckTask, url: string, result: NetdiskCheckResult) {
task.results[url] = {
status: result.status,
reason: result.reason,
fromCache: result.fromCache,
checkedAt: result.checkedAt,
durationMs: result.durationMs,
};
task.progress.done += 1;
if (result.status === 'valid') task.progress.valid += 1;
else if (result.status === 'invalid') task.progress.invalid += 1;
else if (result.status === 'rate_limited') task.progress.rateLimited += 1;
else task.progress.unknown += 1;
task.updatedAt = Date.now();
}
async function runTask(taskId: string, links: string[]) {
const task = tasks.get(taskId);
if (!task) return;
try {
const pendingLinks = links.slice(0, NETDISK_CHECK_RULE.maxTaskLinks);
let batchIndex = 0;
while (pendingLinks.length > 0) {
if (task.shouldStop) {
task.status = 'cancelled';
task.updatedAt = Date.now();
return;
}
if (Date.now() < cooldownUntil) {
task.status = 'failed';
task.error = '检测功能冷却中,请稍后再试';
task.updatedAt = Date.now();
return;
}
batchIndex += 1;
task.progress.currentBatch = batchIndex;
task.updatedAt = Date.now();
const batch = pendingLinks.splice(0, NETDISK_CHECK_RULE.batchSize);
batch.forEach((url) => {
task.results[url] = createTaskResultItem('checking');
});
const batchResults = await Promise.all(
batch.map(async (url) => {
try {
return await withTimeout(checkNetdiskLink(task.platform, url), NETDISK_CHECK_RULE.requestTimeoutMs);
} catch (error) {
const reason = error instanceof Error ? error.message : '检测失败';
return {
platform: task.platform,
url,
normalizedUrl: url,
status: 'unknown',
valid: null,
reason,
checkedAt: Date.now(),
durationMs: NETDISK_CHECK_RULE.requestTimeoutMs,
} satisfies NetdiskCheckResult;
}
})
);
let hasRateLimited = false;
batchResults.forEach((result) => {
updateTaskAfterResult(task, result.url, result);
if (result.status === 'rate_limited') {
hasRateLimited = true;
}
});
if (hasRateLimited) {
cooldownUntil = Date.now() + NETDISK_CHECK_RULE.cooldownMs;
task.status = 'failed';
task.error = '触发网盘平台限流,请稍后重试';
task.updatedAt = Date.now();
return;
}
if (pendingLinks.length > 0) {
await sleep(NETDISK_CHECK_RULE.batchIntervalMs);
}
}
task.status = 'completed';
task.updatedAt = Date.now();
} catch (error) {
task.status = 'failed';
task.error = error instanceof Error ? error.message : '检测任务失败';
task.updatedAt = Date.now();
} finally {
activeTaskIds.delete(taskId);
}
}
export function startNetdiskCheckTask(input: NetdiskCheckStartInput) {
cleanupOldTasks();
if (Date.now() < cooldownUntil) {
throw new Error('检测功能冷却中,请稍后再试');
}
for (const taskId of Array.from(activeTaskIds)) {
const activeTask = tasks.get(taskId);
if (!activeTask || activeTask.status !== 'running') {
activeTaskIds.delete(taskId);
}
}
if (activeTaskIds.size >= NETDISK_CHECK_RULE.maxActiveTasks) {
throw new Error(`当前最多只允许 ${NETDISK_CHECK_RULE.maxActiveTasks} 个检测任务同时执行,请稍后再试`);
}
const uniqueLinks = Array.from(
new Set(
input.links
.map((item) => item.trim())
.filter(Boolean)
)
).slice(0, NETDISK_CHECK_RULE.maxTaskLinks);
if (uniqueLinks.length === 0) {
throw new Error('没有可检测的链接');
}
const taskId = `netdisk_check_${nanoid(10)}`;
const task: NetdiskCheckTask = {
id: taskId,
platform: input.platform,
status: 'running',
progress: {
total: uniqueLinks.length,
done: 0,
valid: 0,
invalid: 0,
unknown: 0,
rateLimited: 0,
currentBatch: 0,
totalBatches: countBatchSize(uniqueLinks.length),
},
results: Object.fromEntries(uniqueLinks.map((url) => [url, createTaskResultItem('pending')])),
createdAt: Date.now(),
updatedAt: Date.now(),
};
tasks.set(taskId, task);
activeTaskIds.add(taskId);
void runTask(taskId, uniqueLinks);
return cloneTask(task);
}
export function getNetdiskCheckTask(taskId: string) {
cleanupOldTasks();
const task = tasks.get(taskId);
return task ? cloneTask(task) : null;
}
export function cancelNetdiskCheckTask(taskId: string) {
const task = tasks.get(taskId);
if (!task) return null;
task.shouldStop = true;
task.updatedAt = Date.now();
if (task.status !== 'running') {
return cloneTask(task);
}
return cloneTask(task);
}
export function getNetdiskCheckCooldownRemainingMs() {
return Math.max(0, cooldownUntil - Date.now());
}
export function assertNetdiskCheckPlatform(value: string): NetdiskCheckPlatform {
const platform = value as NetdiskCheckPlatform;
const allowed: NetdiskCheckPlatform[] = ['115', 'aliyun', 'baidu', 'cmcc', 'pan123', 'quark', 'tianyi', 'uc', 'xunlei'];
if (!allowed.includes(platform)) {
throw new Error('不支持的网盘平台');
}
return platform;
}