夸克网盘在线播放移除openlist依赖

This commit is contained in:
mtvpls
2026-04-26 21:11:00 +08:00
parent 59706b17c1
commit 1ccfb9cdce
12 changed files with 556 additions and 202 deletions
-2
View File
@@ -151,8 +151,6 @@ export interface AdminConfig {
Enabled: boolean;
Cookie: string;
SavePath: string;
PlayTempSavePath: string;
OpenListTempPath: string;
};
Mobile?: {
Enabled: boolean;
-4
View File
@@ -669,8 +669,6 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
Enabled: false,
Cookie: '',
SavePath: '/',
PlayTempSavePath: '/',
OpenListTempPath: '/',
},
Mobile: {
Enabled: false,
@@ -688,8 +686,6 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
Enabled: false,
Cookie: '',
SavePath: '/',
PlayTempSavePath: '/',
OpenListTempPath: '/',
};
}
+110
View File
@@ -0,0 +1,110 @@
import { base58Decode, base58Encode } from '@/lib/utils';
export interface QuarkNetdiskSessionFile {
fid: string;
name: string;
size?: number;
shareFidToken?: string;
pdirFid?: string;
}
export interface QuarkNetdiskSession {
id: string;
provider: 'quark';
title: string;
shareUrl: string;
passcode?: string;
shareId: string;
shareToken: string;
files: QuarkNetdiskSessionFile[];
savedFileIds: Record<string, string>;
playFolderFid?: string;
playFolderPath?: string;
createdAt: number;
expiresAt: number;
}
const TTL_MS = 30 * 60 * 1000;
const sessionStore = new Map<string, QuarkNetdiskSession>();
export function buildQuarkNetdiskId(input: { shareUrl: string; passcode?: string }): string {
return base58Encode(
JSON.stringify({
shareUrl: input.shareUrl,
passcode: input.passcode || '',
})
);
}
export function parseQuarkNetdiskId(id: string): { shareUrl: string; passcode?: string } {
try {
const decoded = base58Decode(id);
const parsed = JSON.parse(decoded);
if (!parsed?.shareUrl || typeof parsed.shareUrl !== 'string') {
throw new Error('invalid quark netdisk id');
}
return {
shareUrl: parsed.shareUrl,
passcode: typeof parsed.passcode === 'string' ? parsed.passcode : '',
};
} catch {
throw new Error('无效的夸克网盘播放 ID');
}
}
function pruneExpiredSessions() {
const now = Date.now();
for (const [key, value] of Array.from(sessionStore.entries())) {
if (value.expiresAt <= now) {
sessionStore.delete(key);
}
}
}
export function createQuarkNetdiskSession(input: {
title: string;
shareUrl: string;
passcode?: string;
shareId: string;
shareToken: string;
files: QuarkNetdiskSessionFile[];
}): QuarkNetdiskSession {
pruneExpiredSessions();
const now = Date.now();
const id = buildQuarkNetdiskId({ shareUrl: input.shareUrl, passcode: input.passcode });
const session: QuarkNetdiskSession = {
id,
provider: 'quark',
title: input.title,
shareUrl: input.shareUrl,
passcode: input.passcode,
shareId: input.shareId,
shareToken: input.shareToken,
files: input.files,
savedFileIds: {},
createdAt: now,
expiresAt: now + TTL_MS,
};
sessionStore.set(id, session);
return session;
}
export function getQuarkNetdiskSession(id: string): QuarkNetdiskSession | null {
pruneExpiredSessions();
const session = sessionStore.get(id);
if (!session) return null;
if (session.expiresAt <= Date.now()) {
sessionStore.delete(id);
return null;
}
return session;
}
export function refreshQuarkNetdiskSession(id: string): QuarkNetdiskSession | null {
const session = getQuarkNetdiskSession(id);
if (!session) return null;
session.expiresAt = Date.now() + TTL_MS;
sessionStore.set(id, session);
return session;
}
+37
View File
@@ -0,0 +1,37 @@
import { getConfig } from '@/lib/config';
import { listQuarkShareVideos } from './quark.client';
import {
createQuarkNetdiskSession,
getQuarkNetdiskSession,
parseQuarkNetdiskId,
refreshQuarkNetdiskSession,
} from './quark-session-cache';
export async function resolveQuarkSession(id: string) {
const config = await getConfig();
const quarkConfig = config.NetDiskConfig?.Quark;
if (!quarkConfig?.Enabled || !quarkConfig.Cookie) {
throw new Error('夸克网盘未配置或未启用');
}
let session = refreshQuarkNetdiskSession(id) || getQuarkNetdiskSession(id);
if (!session) {
const payload = parseQuarkNetdiskId(id);
const result = await listQuarkShareVideos(payload.shareUrl, quarkConfig.Cookie, payload.passcode || '');
session = createQuarkNetdiskSession({
title: result.title,
shareUrl: payload.shareUrl,
passcode: payload.passcode,
shareId: result.shareId,
shareToken: result.shareToken,
files: result.files,
});
}
if (!session) {
throw new Error('夸克网盘播放信息恢复失败');
}
return { session, cookie: quarkConfig.Cookie, savePath: quarkConfig.SavePath || '/' };
}
+224
View File
@@ -15,6 +15,7 @@ export interface QuarkShareItem {
dir: boolean;
shareFidToken?: string;
pdirFid?: string;
size?: number;
}
export interface QuarkTransferTaskResult {
@@ -26,6 +27,19 @@ export interface QuarkTransferTaskResult {
reused?: boolean;
}
export interface QuarkShareVideoListResult {
title: string;
shareId: string;
shareToken: string;
files: Array<{
fid: string;
name: string;
size?: number;
shareFidToken?: string;
pdirFid?: string;
}>;
}
const VIDEO_EXTENSIONS = [
'.mp4',
'.mkv',
@@ -62,6 +76,16 @@ function getHeaders(cookie: string): HeadersInit {
};
}
export function getQuarkPlayHeaders(cookie: string): Record<string, string> {
return {
cookie,
origin: 'https://pan.quark.cn',
referer: 'https://pan.quark.cn/',
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
};
}
export function normalizeQuarkCookie(cookie: string): string {
return cookie
.replace(//g, ';')
@@ -205,6 +229,7 @@ async function fetchShareFolderItems(
shareFidToken:
item.share_fid_token || item.fid_token || item.share_token || undefined,
pdirFid: String(item.pdir_fid || pdirFid || '0'),
size: Number(item.size || 0),
}));
}
@@ -278,6 +303,37 @@ export async function validateQuarkCookieReadable(cookie: string): Promise<void>
await fetchDriveFolderItems(safeCookie, '0');
}
export async function listQuarkShareVideos(
shareUrl: string,
cookie: string,
passcode = ''
): Promise<QuarkShareVideoListResult> {
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
const share = parseQuarkShareUrl(shareUrl, passcode);
const { stoken, shareTitle } = await fetchShareToken(safeCookie, share);
const allItems = await collectShareItemsRecursive(safeCookie, share.pwdId, stoken, '0');
const files = allItems
.filter((item) => !item.dir && isVideoFile(item.fileName))
.map((item) => ({
fid: item.fid,
name: item.fileName,
size: item.size,
shareFidToken: item.shareFidToken,
pdirFid: item.pdirFid,
}));
if (files.length === 0) {
throw new Error('分享中没有可播放的视频文件');
}
return {
title: shareTitle || '夸克网盘立即播放',
shareId: share.pwdId,
shareToken: stoken,
files,
};
}
async function createDriveFolder(
cookie: string,
parentFid: string,
@@ -537,3 +593,171 @@ export async function createQuarkInstantPlayFolder(
folderName,
};
}
export async function ensureQuarkPlayFolder(
cookie: string,
playTempSavePath: string,
shareId: string,
title?: string
): Promise<{ folderFid: string; folderPath: string; folderName: string }> {
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
const tempRoot = await ensureQuarkDrivePath(safeCookie, playTempSavePath);
const folderName = buildInstantPlayFolderName(shareId, title);
const existedFolder = await findDirectoryByName(safeCookie, tempRoot.fid, folderName);
if (existedFolder) {
return {
folderFid: String(existedFolder.fid || existedFolder.file_id),
folderPath: joinPath(tempRoot.path, folderName),
folderName,
};
}
const folderFid = await createDriveFolder(safeCookie, tempRoot.fid, folderName);
return {
folderFid,
folderPath: joinPath(tempRoot.path, folderName),
folderName,
};
}
export async function saveQuarkShareFile(
cookie: string,
input: {
shareId: string;
shareToken: string;
fileId: string;
shareFileToken?: string;
playFolderFid: string;
}
): Promise<string> {
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
const taskId = await submitSaveTask(
safeCookie,
{ pwdId: input.shareId, passcode: '' },
input.shareToken,
input.playFolderFid,
[
{
fid: input.fileId,
fileName: '',
dir: false,
shareFidToken: input.shareFileToken,
},
]
);
if (!taskId) {
throw new Error('夸克转存任务创建失败');
}
for (let i = 0; i < 25; i += 1) {
const query = new URLSearchParams({
task_id: taskId,
retry_index: String(i),
});
const response = await fetch(buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()), {
method: 'GET',
headers: getHeaders(safeCookie),
});
const data = await parseJson(response);
ensureOk(data, '查询夸克任务状态失败');
const saveAsTopFids = data?.data?.save_as?.save_as_top_fids;
if (Array.isArray(saveAsTopFids) && saveAsTopFids.length > 0) {
return String(saveAsTopFids[0]);
}
const status = data?.data?.status;
if (status === -1 || status === 'failed' || data?.data?.err_code) {
throw new Error(data?.data?.message || data?.data?.err_msg || '夸克任务执行失败');
}
if (status === 2 || status === 'finished' || status === 'success' || data?.data?.finished_at) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 1200));
}
throw new Error('夸克转存结果获取失败');
}
export async function getQuarkPlayUrls(
cookie: string,
savedFileId: string
): Promise<Array<{ name: string; url: string; priority: number }>> {
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
const headers = getHeaders(safeCookie);
const urls: Array<{ name: string; url: string; priority: number }> = [];
try {
const response = await fetch(buildApiUrl(QUARK_DRIVE_API_BASE, '/file/download'), {
method: 'POST',
headers,
body: JSON.stringify({
fids: [savedFileId],
}),
cache: 'no-store',
});
const data = await parseJson(response);
ensureOk(data, '获取夸克下载地址失败');
const downloadUrl = data?.data?.[0]?.download_url;
if (downloadUrl) {
urls.push({
name: '原画',
url: String(downloadUrl),
priority: 9999,
});
}
} catch {
// ignore download failure, continue transcoding fallback
}
try {
const response = await fetch(buildApiUrl(QUARK_DRIVE_API_BASE, '/file/v2/play'), {
method: 'POST',
headers,
body: JSON.stringify({
fid: savedFileId,
resolutions: 'normal,low,high,super,2k,4k',
supports: 'fmp4',
}),
cache: 'no-store',
});
const data = await parseJson(response);
ensureOk(data, '获取夸克转码地址失败');
const nameMap: Record<string, string> = {
FOUR_K: '4K',
SUPER: '超清',
HIGH: '高清',
NORMAL: '流畅',
LOW: '低清',
};
if (Array.isArray(data?.data?.video_list)) {
for (const video of data.data.video_list) {
const resolution = video?.video_info?.resoultion;
const playUrl = video?.video_info?.url;
const priority = Number(video?.video_info?.width || 0);
if (resolution && playUrl) {
urls.push({
name: nameMap[String(resolution)] || String(resolution),
url: String(playUrl),
priority,
});
}
}
}
} catch {
// ignore transcoding failure
}
const deduped = urls.filter((item, index, array) => array.findIndex((v) => v.url === item.url) === index);
deduped.sort((a, b) => b.priority - a.priority);
if (deduped.length === 0) {
throw new Error('未获取到夸克播放地址');
}
return deduped;
}