uc和夸克增加自动续期机制
This commit is contained in:
+358
-107
@@ -1,11 +1,152 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
||||||
|
|
||||||
|
import { getConfig, setCachedConfig } from '@/lib/config';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
const QUARK_SHARE_API_BASE = 'https://drive-h.quark.cn/1/clouddrive';
|
const QUARK_SHARE_API_BASE = 'https://drive-h.quark.cn/1/clouddrive';
|
||||||
const QUARK_DRIVE_API_BASE = 'https://drive-pc.quark.cn/1/clouddrive';
|
const QUARK_DRIVE_API_BASE = 'https://drive-pc.quark.cn/1/clouddrive';
|
||||||
const QUARK_QUERY = 'pr=ucpro&fr=pc';
|
const QUARK_QUERY = 'pr=ucpro&fr=pc';
|
||||||
const QUARK_API_USER_AGENT =
|
const QUARK_API_USER_AGENT =
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch';
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch';
|
||||||
|
|
||||||
|
type QuarkRenewableCookieName = '__puus' | '__pus';
|
||||||
|
|
||||||
|
const QUARK_RENEWABLE_COOKIE_NAMES: QuarkRenewableCookieName[] = [
|
||||||
|
'__puus',
|
||||||
|
'__pus',
|
||||||
|
];
|
||||||
|
const runtimeCookieValues: Partial<Record<QuarkRenewableCookieName, string>> =
|
||||||
|
{};
|
||||||
|
let quarkCookiePersistQueue: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
|
function setCookieField(cookie: string, name: string, value: string): string {
|
||||||
|
const normalized = normalizeQuarkCookie(cookie);
|
||||||
|
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const pattern = new RegExp(`(^|;\\s*)${escapedName}=[^;]*`);
|
||||||
|
|
||||||
|
if (pattern.test(normalized)) {
|
||||||
|
return normalized.replace(pattern, `$1${name}=${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${normalized}${
|
||||||
|
normalized && !normalized.endsWith(';') ? '; ' : ''
|
||||||
|
}${name}=${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRuntimeCookieValues(cookie: string): string {
|
||||||
|
let updated = normalizeQuarkCookie(cookie);
|
||||||
|
for (const name of QUARK_RENEWABLE_COOKIE_NAMES) {
|
||||||
|
const value = runtimeCookieValues[name];
|
||||||
|
if (value) {
|
||||||
|
updated = setCookieField(updated, name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResponseSetCookieHeaders(response: Response): string[] {
|
||||||
|
const headers = response.headers as Headers & {
|
||||||
|
getSetCookie?: () => string[];
|
||||||
|
};
|
||||||
|
if (typeof headers.getSetCookie === 'function') {
|
||||||
|
return headers.getSetCookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCookie = response.headers.get('set-cookie');
|
||||||
|
return setCookie ? [setCookie] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRenewableCookieValues(
|
||||||
|
response: Response
|
||||||
|
): Partial<Record<QuarkRenewableCookieName, string>> {
|
||||||
|
const values: Partial<Record<QuarkRenewableCookieName, string>> = {};
|
||||||
|
const setCookieHeaders = getResponseSetCookieHeaders(response);
|
||||||
|
|
||||||
|
for (const header of setCookieHeaders) {
|
||||||
|
for (const name of QUARK_RENEWABLE_COOKIE_NAMES) {
|
||||||
|
const match = header.match(
|
||||||
|
new RegExp(`(?:^|[,;]\\s*)${name}=([^;,\\s]+)`)
|
||||||
|
);
|
||||||
|
if (match?.[1]) {
|
||||||
|
values[name] = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistRenewedQuarkCookie(
|
||||||
|
requestCookie: string,
|
||||||
|
renewedValues: Partial<Record<QuarkRenewableCookieName, string>>
|
||||||
|
): Promise<void> {
|
||||||
|
const names = QUARK_RENEWABLE_COOKIE_NAMES.filter(
|
||||||
|
(name) => renewedValues[name]
|
||||||
|
);
|
||||||
|
if (names.length === 0) return;
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
runtimeCookieValues[name] = renewedValues[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
quarkCookiePersistQueue = quarkCookiePersistQueue
|
||||||
|
.catch(() => undefined)
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
const config = await getConfig();
|
||||||
|
const quarkConfig = config.NetDiskConfig?.Quark;
|
||||||
|
const currentCookie = quarkConfig?.Cookie || requestCookie;
|
||||||
|
let updatedCookie = normalizeQuarkCookie(currentCookie);
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
const value = renewedValues[name];
|
||||||
|
if (value) {
|
||||||
|
updatedCookie = setCookieField(updatedCookie, name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!quarkConfig ||
|
||||||
|
updatedCookie === normalizeQuarkCookie(currentCookie)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.NetDiskConfig = config.NetDiskConfig || {};
|
||||||
|
config.NetDiskConfig.Quark = {
|
||||||
|
...quarkConfig,
|
||||||
|
Cookie: updatedCookie,
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.saveAdminConfig(config);
|
||||||
|
await setCachedConfig(config);
|
||||||
|
console.log(`[quark] renewed cookie fields: ${names.join(', ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[quark] persist renewed cookie failed:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await quarkCookiePersistQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renewQuarkCookieFromResponse(
|
||||||
|
response: Response,
|
||||||
|
requestCookie: string
|
||||||
|
): Promise<void> {
|
||||||
|
const renewedValues = extractRenewableCookieValues(response);
|
||||||
|
await persistRenewedQuarkCookie(requestCookie, renewedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quarkFetch(
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init: RequestInit,
|
||||||
|
requestCookie: string
|
||||||
|
): Promise<Response> {
|
||||||
|
const response = await fetch(input, init);
|
||||||
|
await renewQuarkCookieFromResponse(response, requestCookie);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QuarkShareLinkInfo {
|
export interface QuarkShareLinkInfo {
|
||||||
pwdId: string;
|
pwdId: string;
|
||||||
passcode: string;
|
passcode: string;
|
||||||
@@ -73,7 +214,7 @@ function buildApiUrl(base: string, path: string, query = '') {
|
|||||||
function getHeaders(cookie: string): HeadersInit {
|
function getHeaders(cookie: string): HeadersInit {
|
||||||
return {
|
return {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
cookie,
|
cookie: applyRuntimeCookieValues(cookie),
|
||||||
referer: 'https://pan.quark.cn/',
|
referer: 'https://pan.quark.cn/',
|
||||||
'user-agent': QUARK_API_USER_AGENT,
|
'user-agent': QUARK_API_USER_AGENT,
|
||||||
};
|
};
|
||||||
@@ -81,7 +222,7 @@ function getHeaders(cookie: string): HeadersInit {
|
|||||||
|
|
||||||
export function getQuarkPlayHeaders(cookie: string): Record<string, string> {
|
export function getQuarkPlayHeaders(cookie: string): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
cookie,
|
cookie: applyRuntimeCookieValues(cookie),
|
||||||
referer: 'https://pan.quark.cn/',
|
referer: 'https://pan.quark.cn/',
|
||||||
'user-agent': QUARK_API_USER_AGENT,
|
'user-agent': QUARK_API_USER_AGENT,
|
||||||
};
|
};
|
||||||
@@ -99,7 +240,9 @@ export function assertQuarkCookieHeaderSafe(cookie: string): string {
|
|||||||
const normalized = normalizeQuarkCookie(cookie);
|
const normalized = normalizeQuarkCookie(cookie);
|
||||||
for (let i = 0; i < normalized.length; i += 1) {
|
for (let i = 0; i < normalized.length; i += 1) {
|
||||||
if (normalized.charCodeAt(i) > 255) {
|
if (normalized.charCodeAt(i) > 255) {
|
||||||
throw new Error('夸克 Cookie 含有非法字符,请确认没有中文标点、中文空格或说明文字');
|
throw new Error(
|
||||||
|
'夸克 Cookie 含有非法字符,请确认没有中文标点、中文空格或说明文字'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
@@ -112,10 +255,7 @@ function normalizePath(path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function joinPath(...parts: string[]) {
|
function joinPath(...parts: string[]) {
|
||||||
const joined = parts
|
const joined = parts.filter(Boolean).join('/').replace(/\/+/g, '/');
|
||||||
.filter(Boolean)
|
|
||||||
.join('/')
|
|
||||||
.replace(/\/+/g, '/');
|
|
||||||
return normalizePath(joined);
|
return normalizePath(joined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +284,10 @@ function ensureOk(data: any, fallbackMessage: string) {
|
|||||||
throw new Error(data?.message || data?.msg || fallbackMessage);
|
throw new Error(data?.message || data?.msg || fallbackMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseQuarkShareUrl(url: string, passcode = ''): QuarkShareLinkInfo {
|
export function parseQuarkShareUrl(
|
||||||
|
url: string,
|
||||||
|
passcode = ''
|
||||||
|
): QuarkShareLinkInfo {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
const pwdId =
|
const pwdId =
|
||||||
parsed.pathname.match(/\/s\/([A-Za-z0-9_-]+)/)?.[1] ||
|
parsed.pathname.match(/\/s\/([A-Za-z0-9_-]+)/)?.[1] ||
|
||||||
@@ -166,7 +309,7 @@ export function parseQuarkShareUrl(url: string, passcode = ''): QuarkShareLinkIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchShareToken(cookie: string, share: QuarkShareLinkInfo) {
|
async function fetchShareToken(cookie: string, share: QuarkShareLinkInfo) {
|
||||||
const response = await fetch(
|
const response = await quarkFetch(
|
||||||
buildApiUrl(QUARK_SHARE_API_BASE, '/share/sharepage/token'),
|
buildApiUrl(QUARK_SHARE_API_BASE, '/share/sharepage/token'),
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -175,16 +318,15 @@ async function fetchShareToken(cookie: string, share: QuarkShareLinkInfo) {
|
|||||||
pwd_id: share.pwdId,
|
pwd_id: share.pwdId,
|
||||||
passcode: share.passcode,
|
passcode: share.passcode,
|
||||||
}),
|
}),
|
||||||
}
|
},
|
||||||
|
cookie
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '获取夸克分享 token 失败');
|
ensureOk(data, '获取夸克分享 token 失败');
|
||||||
|
|
||||||
const stoken =
|
const stoken =
|
||||||
data?.data?.stoken ||
|
data?.data?.stoken || data?.data?.share_token || data?.data?.token;
|
||||||
data?.data?.share_token ||
|
|
||||||
data?.data?.token;
|
|
||||||
|
|
||||||
if (!stoken) {
|
if (!stoken) {
|
||||||
throw new Error('夸克分享 token 缺失');
|
throw new Error('夸克分享 token 缺失');
|
||||||
@@ -211,12 +353,17 @@ async function fetchShareFolderItems(
|
|||||||
_fetch_banner: '0',
|
_fetch_banner: '0',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await quarkFetch(
|
||||||
buildApiUrl(QUARK_SHARE_API_BASE, '/share/sharepage/detail', query.toString()),
|
buildApiUrl(
|
||||||
|
QUARK_SHARE_API_BASE,
|
||||||
|
'/share/sharepage/detail',
|
||||||
|
query.toString()
|
||||||
|
),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getHeaders(cookie),
|
headers: getHeaders(cookie),
|
||||||
}
|
},
|
||||||
|
cookie
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
@@ -247,12 +394,13 @@ async function fetchDriveFolderItems(
|
|||||||
_sort: 'file_type:asc,file_name:asc',
|
_sort: 'file_type:asc,file_name:asc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await quarkFetch(
|
||||||
buildApiUrl(QUARK_DRIVE_API_BASE, '/file/sort', query.toString()),
|
buildApiUrl(QUARK_DRIVE_API_BASE, '/file/sort', query.toString()),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getHeaders(cookie),
|
headers: getHeaders(cookie),
|
||||||
}
|
},
|
||||||
|
cookie
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
@@ -294,9 +442,13 @@ async function findDirectoryByName(
|
|||||||
folderName: string
|
folderName: string
|
||||||
): Promise<any | null> {
|
): Promise<any | null> {
|
||||||
const items = await fetchAllDriveFolderItems(cookie, parentFid);
|
const items = await fetchAllDriveFolderItems(cookie, parentFid);
|
||||||
return items.find(
|
return (
|
||||||
(item: any) => Boolean(item.dir || item.is_dir) && getDriveItemName(item) === folderName
|
items.find(
|
||||||
) || null;
|
(item: any) =>
|
||||||
|
Boolean(item.dir || item.is_dir) &&
|
||||||
|
getDriveItemName(item) === folderName
|
||||||
|
) || null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findDriveFileInFolder(
|
async function findDriveFileInFolder(
|
||||||
@@ -308,19 +460,23 @@ async function findDriveFileInFolder(
|
|||||||
if (!fileName) return null;
|
if (!fileName) return null;
|
||||||
|
|
||||||
const items = await fetchAllDriveFolderItems(cookie, parentFid);
|
const items = await fetchAllDriveFolderItems(cookie, parentFid);
|
||||||
return items.find((item: any) => {
|
return (
|
||||||
const isDir = Boolean(item.dir || item.is_dir || item.file_type === 0);
|
items.find((item: any) => {
|
||||||
if (isDir) return false;
|
const isDir = Boolean(item.dir || item.is_dir || item.file_type === 0);
|
||||||
const itemName = String(item.file_name || item.name || '');
|
if (isDir) return false;
|
||||||
if (itemName !== fileName) return false;
|
const itemName = String(item.file_name || item.name || '');
|
||||||
if (input.size && Number(item.size || 0) > 0) {
|
if (itemName !== fileName) return false;
|
||||||
return Number(item.size || 0) === input.size;
|
if (input.size && Number(item.size || 0) > 0) {
|
||||||
}
|
return Number(item.size || 0) === input.size;
|
||||||
return true;
|
}
|
||||||
}) || null;
|
return true;
|
||||||
|
}) || null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateQuarkCookieReadable(cookie: string): Promise<void> {
|
export async function validateQuarkCookieReadable(
|
||||||
|
cookie: string
|
||||||
|
): Promise<void> {
|
||||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||||
await fetchDriveFolderItems(safeCookie, '0');
|
await fetchDriveFolderItems(safeCookie, '0');
|
||||||
}
|
}
|
||||||
@@ -333,7 +489,12 @@ export async function listQuarkShareVideos(
|
|||||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||||
const share = parseQuarkShareUrl(shareUrl, passcode);
|
const share = parseQuarkShareUrl(shareUrl, passcode);
|
||||||
const { stoken, shareTitle } = await fetchShareToken(safeCookie, share);
|
const { stoken, shareTitle } = await fetchShareToken(safeCookie, share);
|
||||||
const allItems = await collectShareItemsRecursive(safeCookie, share.pwdId, stoken, '0');
|
const allItems = await collectShareItemsRecursive(
|
||||||
|
safeCookie,
|
||||||
|
share.pwdId,
|
||||||
|
stoken,
|
||||||
|
'0'
|
||||||
|
);
|
||||||
const files = allItems
|
const files = allItems
|
||||||
.filter((item) => !item.dir && isVideoFile(item.fileName))
|
.filter((item) => !item.dir && isVideoFile(item.fileName))
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
@@ -361,24 +522,25 @@ async function createDriveFolder(
|
|||||||
parentFid: string,
|
parentFid: string,
|
||||||
folderName: string
|
folderName: string
|
||||||
) {
|
) {
|
||||||
const response = await fetch(buildApiUrl(QUARK_DRIVE_API_BASE, '/file'), {
|
const response = await quarkFetch(
|
||||||
method: 'POST',
|
buildApiUrl(QUARK_DRIVE_API_BASE, '/file'),
|
||||||
headers: getHeaders(cookie),
|
{
|
||||||
body: JSON.stringify({
|
method: 'POST',
|
||||||
pdir_fid: parentFid,
|
headers: getHeaders(cookie),
|
||||||
file_name: folderName,
|
body: JSON.stringify({
|
||||||
dir_path: '',
|
pdir_fid: parentFid,
|
||||||
dir_init_lock: false,
|
file_name: folderName,
|
||||||
}),
|
dir_path: '',
|
||||||
});
|
dir_init_lock: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
cookie
|
||||||
|
);
|
||||||
|
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, `创建夸克目录失败:${folderName}`);
|
ensureOk(data, `创建夸克目录失败:${folderName}`);
|
||||||
|
|
||||||
const fid =
|
const fid = data?.data?.fid || data?.data?.file_id || data?.metadata?.fid;
|
||||||
data?.data?.fid ||
|
|
||||||
data?.data?.file_id ||
|
|
||||||
data?.metadata?.fid;
|
|
||||||
|
|
||||||
if (!fid) {
|
if (!fid) {
|
||||||
throw new Error(`夸克目录创建成功但未返回 fid:${folderName}`);
|
throw new Error(`夸克目录创建成功但未返回 fid:${folderName}`);
|
||||||
@@ -466,7 +628,7 @@ async function submitSaveTask(
|
|||||||
throw new Error('没有可保存的文件');
|
throw new Error('没有可保存的文件');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await quarkFetch(
|
||||||
buildApiUrl(QUARK_SHARE_API_BASE, '/share/sharepage/save'),
|
buildApiUrl(QUARK_SHARE_API_BASE, '/share/sharepage/save'),
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -482,7 +644,8 @@ async function submitSaveTask(
|
|||||||
fid_token_list: items.map((item) => item.shareFidToken || ''),
|
fid_token_list: items.map((item) => item.shareFidToken || ''),
|
||||||
share_fid_token_list: items.map((item) => item.shareFidToken || ''),
|
share_fid_token_list: items.map((item) => item.shareFidToken || ''),
|
||||||
}),
|
}),
|
||||||
}
|
},
|
||||||
|
cookie
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
@@ -497,10 +660,14 @@ async function pollTask(cookie: string, taskId: string) {
|
|||||||
retry_index: String(i),
|
retry_index: String(i),
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()), {
|
const response = await quarkFetch(
|
||||||
method: 'GET',
|
buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()),
|
||||||
headers: getHeaders(cookie),
|
{
|
||||||
});
|
method: 'GET',
|
||||||
|
headers: getHeaders(cookie),
|
||||||
|
},
|
||||||
|
cookie
|
||||||
|
);
|
||||||
|
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '查询夸克任务状态失败');
|
ensureOk(data, '查询夸克任务状态失败');
|
||||||
@@ -515,11 +682,7 @@ async function pollTask(cookie: string, taskId: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (task?.status === -1 || task?.status === 'failed' || task?.err_code) {
|
||||||
task?.status === -1 ||
|
|
||||||
task?.status === 'failed' ||
|
|
||||||
task?.err_code
|
|
||||||
) {
|
|
||||||
throw new Error(task?.message || task?.err_msg || '夸克任务执行失败');
|
throw new Error(task?.message || task?.err_msg || '夸克任务执行失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,11 +703,20 @@ export async function transferQuarkShare(
|
|||||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||||
const share = parseQuarkShareUrl(input.shareUrl, input.passcode);
|
const share = parseQuarkShareUrl(input.shareUrl, input.passcode);
|
||||||
const { stoken } = await fetchShareToken(safeCookie, share);
|
const { stoken } = await fetchShareToken(safeCookie, share);
|
||||||
const topLevelItems = await fetchShareFolderItems(safeCookie, share.pwdId, stoken, '0');
|
const topLevelItems = await fetchShareFolderItems(
|
||||||
|
safeCookie,
|
||||||
|
share.pwdId,
|
||||||
|
stoken,
|
||||||
|
'0'
|
||||||
|
);
|
||||||
const target = await ensureQuarkDrivePath(safeCookie, input.savePath);
|
const target = await ensureQuarkDrivePath(safeCookie, input.savePath);
|
||||||
const existedItems = await fetchAllDriveFolderItems(safeCookie, target.fid);
|
const existedItems = await fetchAllDriveFolderItems(safeCookie, target.fid);
|
||||||
const existedNames = new Set(existedItems.map((item: any) => getDriveItemName(item)));
|
const existedNames = new Set(
|
||||||
const pendingItems = topLevelItems.filter((item) => !existedNames.has(item.fileName));
|
existedItems.map((item: any) => getDriveItemName(item))
|
||||||
|
);
|
||||||
|
const pendingItems = topLevelItems.filter(
|
||||||
|
(item) => !existedNames.has(item.fileName)
|
||||||
|
);
|
||||||
|
|
||||||
if (pendingItems.length === 0) {
|
if (pendingItems.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -554,7 +726,13 @@ export async function transferQuarkShare(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskId = await submitSaveTask(safeCookie, share, stoken, target.fid, pendingItems);
|
const taskId = await submitSaveTask(
|
||||||
|
safeCookie,
|
||||||
|
share,
|
||||||
|
stoken,
|
||||||
|
target.fid,
|
||||||
|
pendingItems
|
||||||
|
);
|
||||||
|
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
await pollTask(safeCookie, taskId);
|
await pollTask(safeCookie, taskId);
|
||||||
@@ -579,16 +757,33 @@ export async function createQuarkInstantPlayFolder(
|
|||||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||||
const share = parseQuarkShareUrl(input.shareUrl, input.passcode);
|
const share = parseQuarkShareUrl(input.shareUrl, input.passcode);
|
||||||
const { stoken, shareTitle } = await fetchShareToken(safeCookie, share);
|
const { stoken, shareTitle } = await fetchShareToken(safeCookie, share);
|
||||||
const allItems = await collectShareItemsRecursive(safeCookie, share.pwdId, stoken, '0');
|
const allItems = await collectShareItemsRecursive(
|
||||||
const videoItems = allItems.filter((item) => !item.dir && isVideoFile(item.fileName));
|
safeCookie,
|
||||||
|
share.pwdId,
|
||||||
|
stoken,
|
||||||
|
'0'
|
||||||
|
);
|
||||||
|
const videoItems = allItems.filter(
|
||||||
|
(item) => !item.dir && isVideoFile(item.fileName)
|
||||||
|
);
|
||||||
|
|
||||||
if (videoItems.length === 0) {
|
if (videoItems.length === 0) {
|
||||||
throw new Error('分享中没有可播放的视频文件');
|
throw new Error('分享中没有可播放的视频文件');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempRoot = await ensureQuarkDrivePath(safeCookie, input.playTempSavePath);
|
const tempRoot = await ensureQuarkDrivePath(
|
||||||
const folderName = buildInstantPlayFolderName(share.pwdId, input.title || shareTitle);
|
safeCookie,
|
||||||
const existedFolder = await findDirectoryByName(safeCookie, tempRoot.fid, folderName);
|
input.playTempSavePath
|
||||||
|
);
|
||||||
|
const folderName = buildInstantPlayFolderName(
|
||||||
|
share.pwdId,
|
||||||
|
input.title || shareTitle
|
||||||
|
);
|
||||||
|
const existedFolder = await findDirectoryByName(
|
||||||
|
safeCookie,
|
||||||
|
tempRoot.fid,
|
||||||
|
folderName
|
||||||
|
);
|
||||||
|
|
||||||
if (existedFolder) {
|
if (existedFolder) {
|
||||||
return {
|
return {
|
||||||
@@ -599,8 +794,18 @@ export async function createQuarkInstantPlayFolder(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderFid = await createDriveFolder(safeCookie, tempRoot.fid, folderName);
|
const folderFid = await createDriveFolder(
|
||||||
const taskId = await submitSaveTask(safeCookie, share, stoken, folderFid, videoItems);
|
safeCookie,
|
||||||
|
tempRoot.fid,
|
||||||
|
folderName
|
||||||
|
);
|
||||||
|
const taskId = await submitSaveTask(
|
||||||
|
safeCookie,
|
||||||
|
share,
|
||||||
|
stoken,
|
||||||
|
folderFid,
|
||||||
|
videoItems
|
||||||
|
);
|
||||||
|
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
await pollTask(safeCookie, taskId);
|
await pollTask(safeCookie, taskId);
|
||||||
@@ -625,7 +830,11 @@ export async function ensureQuarkPlayFolder(
|
|||||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||||
const tempRoot = await ensureQuarkDrivePath(safeCookie, playTempSavePath);
|
const tempRoot = await ensureQuarkDrivePath(safeCookie, playTempSavePath);
|
||||||
const folderName = buildInstantPlayFolderName(shareId, title);
|
const folderName = buildInstantPlayFolderName(shareId, title);
|
||||||
const existedFolder = await findDirectoryByName(safeCookie, tempRoot.fid, folderName);
|
const existedFolder = await findDirectoryByName(
|
||||||
|
safeCookie,
|
||||||
|
tempRoot.fid,
|
||||||
|
folderName
|
||||||
|
);
|
||||||
if (existedFolder) {
|
if (existedFolder) {
|
||||||
return {
|
return {
|
||||||
folderFid: String(existedFolder.fid || existedFolder.file_id),
|
folderFid: String(existedFolder.fid || existedFolder.file_id),
|
||||||
@@ -634,7 +843,11 @@ export async function ensureQuarkPlayFolder(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderFid = await createDriveFolder(safeCookie, tempRoot.fid, folderName);
|
const folderFid = await createDriveFolder(
|
||||||
|
safeCookie,
|
||||||
|
tempRoot.fid,
|
||||||
|
folderName
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
folderFid,
|
folderFid,
|
||||||
folderPath: joinPath(tempRoot.path, folderName),
|
folderPath: joinPath(tempRoot.path, folderName),
|
||||||
@@ -656,10 +869,14 @@ export async function saveQuarkShareFile(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||||
|
|
||||||
const existedFile = await findDriveFileInFolder(safeCookie, input.playFolderFid, {
|
const existedFile = await findDriveFileInFolder(
|
||||||
fileName: input.fileName,
|
safeCookie,
|
||||||
size: input.size,
|
input.playFolderFid,
|
||||||
});
|
{
|
||||||
|
fileName: input.fileName,
|
||||||
|
size: input.size,
|
||||||
|
}
|
||||||
|
);
|
||||||
if (existedFile) {
|
if (existedFile) {
|
||||||
return String(existedFile.fid || existedFile.file_id);
|
return String(existedFile.fid || existedFile.file_id);
|
||||||
}
|
}
|
||||||
@@ -690,10 +907,14 @@ export async function saveQuarkShareFile(
|
|||||||
retry_index: String(i),
|
retry_index: String(i),
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()), {
|
const response = await quarkFetch(
|
||||||
method: 'GET',
|
buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()),
|
||||||
headers: getHeaders(safeCookie),
|
{
|
||||||
});
|
method: 'GET',
|
||||||
|
headers: getHeaders(safeCookie),
|
||||||
|
},
|
||||||
|
safeCookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '查询夸克任务状态失败');
|
ensureOk(data, '查询夸克任务状态失败');
|
||||||
|
|
||||||
@@ -702,20 +923,31 @@ export async function saveQuarkShareFile(
|
|||||||
return String(saveAsTopFids[0]);
|
return String(saveAsTopFids[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedFile = await findDriveFileInFolder(safeCookie, input.playFolderFid, {
|
const savedFile = await findDriveFileInFolder(
|
||||||
fileName: input.fileName,
|
safeCookie,
|
||||||
size: input.size,
|
input.playFolderFid,
|
||||||
});
|
{
|
||||||
|
fileName: input.fileName,
|
||||||
|
size: input.size,
|
||||||
|
}
|
||||||
|
);
|
||||||
if (savedFile) {
|
if (savedFile) {
|
||||||
return String(savedFile.fid || savedFile.file_id);
|
return String(savedFile.fid || savedFile.file_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = data?.data?.status;
|
const status = data?.data?.status;
|
||||||
if (status === -1 || status === 'failed' || data?.data?.err_code) {
|
if (status === -1 || status === 'failed' || data?.data?.err_code) {
|
||||||
throw new Error(data?.data?.message || data?.data?.err_msg || '夸克任务执行失败');
|
throw new Error(
|
||||||
|
data?.data?.message || data?.data?.err_msg || '夸克任务执行失败'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 2 || status === 'finished' || status === 'success' || data?.data?.finished_at) {
|
if (
|
||||||
|
status === 2 ||
|
||||||
|
status === 'finished' ||
|
||||||
|
status === 'success' ||
|
||||||
|
data?.data?.finished_at
|
||||||
|
) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,14 +967,18 @@ export async function getQuarkPlayUrls(
|
|||||||
const urls: Array<{ name: string; url: string; priority: number }> = [];
|
const urls: Array<{ name: string; url: string; priority: number }> = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(buildApiUrl(QUARK_DRIVE_API_BASE, '/file/download'), {
|
const response = await quarkFetch(
|
||||||
method: 'POST',
|
buildApiUrl(QUARK_DRIVE_API_BASE, '/file/download'),
|
||||||
headers,
|
{
|
||||||
body: JSON.stringify({
|
method: 'POST',
|
||||||
fids: [savedFileId],
|
headers,
|
||||||
}),
|
body: JSON.stringify({
|
||||||
cache: 'no-store',
|
fids: [savedFileId],
|
||||||
});
|
}),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
safeCookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '获取夸克下载地址失败');
|
ensureOk(data, '获取夸克下载地址失败');
|
||||||
const downloadUrl = data?.data?.[0]?.download_url;
|
const downloadUrl = data?.data?.[0]?.download_url;
|
||||||
@@ -758,16 +994,20 @@ export async function getQuarkPlayUrls(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(buildApiUrl(QUARK_DRIVE_API_BASE, '/file/v2/play'), {
|
const response = await quarkFetch(
|
||||||
method: 'POST',
|
buildApiUrl(QUARK_DRIVE_API_BASE, '/file/v2/play'),
|
||||||
headers,
|
{
|
||||||
body: JSON.stringify({
|
method: 'POST',
|
||||||
fid: savedFileId,
|
headers,
|
||||||
resolutions: 'normal,low,high,super,2k,4k',
|
body: JSON.stringify({
|
||||||
supports: 'fmp4',
|
fid: savedFileId,
|
||||||
}),
|
resolutions: 'normal,low,high,super,2k,4k',
|
||||||
cache: 'no-store',
|
supports: 'fmp4',
|
||||||
});
|
}),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
safeCookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '获取夸克转码地址失败');
|
ensureOk(data, '获取夸克转码地址失败');
|
||||||
const nameMap: Record<string, string> = {
|
const nameMap: Record<string, string> = {
|
||||||
@@ -795,7 +1035,9 @@ export async function getQuarkPlayUrls(
|
|||||||
console.warn('[quark] get transcoding play url failed:', error);
|
console.warn('[quark] get transcoding play url failed:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deduped = urls.filter((item, index, array) => array.findIndex((v) => v.url === item.url) === index);
|
const deduped = urls.filter(
|
||||||
|
(item, index, array) => array.findIndex((v) => v.url === item.url) === index
|
||||||
|
);
|
||||||
deduped.sort((a, b) => {
|
deduped.sort((a, b) => {
|
||||||
if (playMode === 'transcode_first') {
|
if (playMode === 'transcode_first') {
|
||||||
if (a.name === '原画' && b.name !== '原画') return 1;
|
if (a.name === '原画' && b.name !== '原画') return 1;
|
||||||
@@ -811,14 +1053,21 @@ export async function getQuarkPlayUrls(
|
|||||||
return deduped;
|
return deduped;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseContentRangeHeader(contentRange: string | null): QuarkRangeWindow | null {
|
function parseContentRangeHeader(
|
||||||
|
contentRange: string | null
|
||||||
|
): QuarkRangeWindow | null {
|
||||||
if (!contentRange) return null;
|
if (!contentRange) return null;
|
||||||
const match = contentRange.match(/^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/i);
|
const match = contentRange.match(/^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/i);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
const start = Number(match[1]);
|
const start = Number(match[1]);
|
||||||
const end = Number(match[2]);
|
const end = Number(match[2]);
|
||||||
const total = Number(match[3]);
|
const total = Number(match[3]);
|
||||||
if (!Number.isFinite(start) || !Number.isFinite(end) || !Number.isFinite(total)) return null;
|
if (
|
||||||
|
!Number.isFinite(start) ||
|
||||||
|
!Number.isFinite(end) ||
|
||||||
|
!Number.isFinite(total)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
return { start, end, total };
|
return { start, end, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,7 +1092,9 @@ export async function probeQuarkPlayRange(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const window = parseContentRangeHeader(response.headers.get('content-range'));
|
const window = parseContentRangeHeader(
|
||||||
|
response.headers.get('content-range')
|
||||||
|
);
|
||||||
return { response, window };
|
return { response, window };
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
+557
-132
@@ -1,7 +1,10 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
||||||
|
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
|
import { getConfig, setCachedConfig } from '@/lib/config';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
const UC_SHARE_API_BASE = 'https://pc-api.uc.cn/1/clouddrive';
|
const UC_SHARE_API_BASE = 'https://pc-api.uc.cn/1/clouddrive';
|
||||||
const UC_DRIVE_API_BASE = 'https://pc-api.uc.cn/1/clouddrive';
|
const UC_DRIVE_API_BASE = 'https://pc-api.uc.cn/1/clouddrive';
|
||||||
const UC_OPEN_API_BASE = 'https://open-api-drive.uc.cn';
|
const UC_OPEN_API_BASE = 'https://open-api-drive.uc.cn';
|
||||||
@@ -11,6 +14,135 @@ const UC_OPEN_API_SIGN_KEY = 'l3srvtd7p42l0d0x1u8d7yc8ye9kki4d';
|
|||||||
const UC_OPEN_API_APP_VER = '1.6.8';
|
const UC_OPEN_API_APP_VER = '1.6.8';
|
||||||
const UC_OPEN_API_CHANNEL = 'UCTVOFFICIALWEB';
|
const UC_OPEN_API_CHANNEL = 'UCTVOFFICIALWEB';
|
||||||
|
|
||||||
|
type UCRenewableCookieName = '__puus' | '__pus';
|
||||||
|
|
||||||
|
const UC_RENEWABLE_COOKIE_NAMES: UCRenewableCookieName[] = ['__puus', '__pus'];
|
||||||
|
const runtimeCookieValues: Partial<Record<UCRenewableCookieName, string>> = {};
|
||||||
|
let ucCookiePersistQueue: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
|
function setCookieField(cookie: string, name: string, value: string): string {
|
||||||
|
const normalized = normalizeUCCookie(cookie);
|
||||||
|
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const pattern = new RegExp(`(^|;\\s*)${escapedName}=[^;]*`);
|
||||||
|
|
||||||
|
if (pattern.test(normalized)) {
|
||||||
|
return normalized.replace(pattern, `$1${name}=${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${normalized}${
|
||||||
|
normalized && !normalized.endsWith(';') ? '; ' : ''
|
||||||
|
}${name}=${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRuntimeCookieValues(cookie: string): string {
|
||||||
|
let updated = normalizeUCCookie(cookie);
|
||||||
|
for (const name of UC_RENEWABLE_COOKIE_NAMES) {
|
||||||
|
const value = runtimeCookieValues[name];
|
||||||
|
if (value) {
|
||||||
|
updated = setCookieField(updated, name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResponseSetCookieHeaders(response: Response): string[] {
|
||||||
|
const headers = response.headers as Headers & {
|
||||||
|
getSetCookie?: () => string[];
|
||||||
|
};
|
||||||
|
if (typeof headers.getSetCookie === 'function') {
|
||||||
|
return headers.getSetCookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCookie = response.headers.get('set-cookie');
|
||||||
|
return setCookie ? [setCookie] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRenewableCookieValues(
|
||||||
|
response: Response
|
||||||
|
): Partial<Record<UCRenewableCookieName, string>> {
|
||||||
|
const values: Partial<Record<UCRenewableCookieName, string>> = {};
|
||||||
|
const setCookieHeaders = getResponseSetCookieHeaders(response);
|
||||||
|
|
||||||
|
for (const header of setCookieHeaders) {
|
||||||
|
for (const name of UC_RENEWABLE_COOKIE_NAMES) {
|
||||||
|
const match = header.match(
|
||||||
|
new RegExp(`(?:^|[,;]\\s*)${name}=([^;,\\s]+)`)
|
||||||
|
);
|
||||||
|
if (match?.[1]) {
|
||||||
|
values[name] = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistRenewedUCCookie(
|
||||||
|
requestCookie: string,
|
||||||
|
renewedValues: Partial<Record<UCRenewableCookieName, string>>
|
||||||
|
): Promise<void> {
|
||||||
|
const names = UC_RENEWABLE_COOKIE_NAMES.filter((name) => renewedValues[name]);
|
||||||
|
if (names.length === 0) return;
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
runtimeCookieValues[name] = renewedValues[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
ucCookiePersistQueue = ucCookiePersistQueue
|
||||||
|
.catch(() => undefined)
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
const config = await getConfig();
|
||||||
|
const ucConfig = config.NetDiskConfig?.UC;
|
||||||
|
const currentCookie = ucConfig?.Cookie || requestCookie;
|
||||||
|
let updatedCookie = normalizeUCCookie(currentCookie);
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
const value = renewedValues[name];
|
||||||
|
if (value) {
|
||||||
|
updatedCookie = setCookieField(updatedCookie, name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ucConfig || updatedCookie === normalizeUCCookie(currentCookie)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.NetDiskConfig = config.NetDiskConfig || {};
|
||||||
|
config.NetDiskConfig.UC = {
|
||||||
|
...ucConfig,
|
||||||
|
Cookie: updatedCookie,
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.saveAdminConfig(config);
|
||||||
|
await setCachedConfig(config);
|
||||||
|
console.log(`[uc] renewed cookie fields: ${names.join(', ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[uc] persist renewed cookie failed:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await ucCookiePersistQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renewUCCookieFromResponse(
|
||||||
|
response: Response,
|
||||||
|
requestCookie: string
|
||||||
|
): Promise<void> {
|
||||||
|
const renewedValues = extractRenewableCookieValues(response);
|
||||||
|
await persistRenewedUCCookie(requestCookie, renewedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ucFetch(
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init: RequestInit,
|
||||||
|
requestCookie: string
|
||||||
|
): Promise<Response> {
|
||||||
|
const response = await fetch(input, init);
|
||||||
|
await renewUCCookieFromResponse(response, requestCookie);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UCShareLinkInfo {
|
export interface UCShareLinkInfo {
|
||||||
pwdId: string;
|
pwdId: string;
|
||||||
passcode: string;
|
passcode: string;
|
||||||
@@ -39,7 +171,23 @@ export interface UCShareVideoListResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_EXTENSIONS = [
|
const VIDEO_EXTENSIONS = [
|
||||||
'.mp4', '.mkv', '.avi', '.m3u8', '.flv', '.ts', '.mov', '.wmv', '.webm', '.rmvb', '.rm', '.mpg', '.mpeg', '.3gp', '.f4v', '.m4v', '.vob',
|
'.mp4',
|
||||||
|
'.mkv',
|
||||||
|
'.avi',
|
||||||
|
'.m3u8',
|
||||||
|
'.flv',
|
||||||
|
'.ts',
|
||||||
|
'.mov',
|
||||||
|
'.wmv',
|
||||||
|
'.webm',
|
||||||
|
'.rmvb',
|
||||||
|
'.rm',
|
||||||
|
'.mpg',
|
||||||
|
'.mpeg',
|
||||||
|
'.3gp',
|
||||||
|
'.f4v',
|
||||||
|
'.m4v',
|
||||||
|
'.vob',
|
||||||
];
|
];
|
||||||
|
|
||||||
const utCache = new Map<string, string>();
|
const utCache = new Map<string, string>();
|
||||||
@@ -55,7 +203,7 @@ function buildApiUrl(base: string, path: string, ut: string, query = '') {
|
|||||||
function getHeaders(cookie: string): HeadersInit {
|
function getHeaders(cookie: string): HeadersInit {
|
||||||
return {
|
return {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
cookie,
|
cookie: applyRuntimeCookieValues(cookie),
|
||||||
referer: 'https://drive.uc.cn',
|
referer: 'https://drive.uc.cn',
|
||||||
'user-agent':
|
'user-agent':
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) uc-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) uc-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch',
|
||||||
@@ -63,8 +211,9 @@ function getHeaders(cookie: string): HeadersInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getUCPlayHeaders(cookie: string): Record<string, string> {
|
export function getUCPlayHeaders(cookie: string): Record<string, string> {
|
||||||
|
const renewedCookie = applyRuntimeCookieValues(cookie);
|
||||||
const keepKeys = ['_UP_A4A_11_', 'tfstk', '__uid', '__pus', '__kp', '__puus'];
|
const keepKeys = ['_UP_A4A_11_', 'tfstk', '__uid', '__pus', '__kp', '__puus'];
|
||||||
const filteredCookie = cookie
|
const filteredCookie = renewedCookie
|
||||||
.split(';')
|
.split(';')
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter((item) => keepKeys.some((key) => item.includes(key)))
|
.filter((item) => keepKeys.some((key) => item.includes(key)))
|
||||||
@@ -90,7 +239,9 @@ export function assertUCCookieHeaderSafe(cookie: string): string {
|
|||||||
const normalized = normalizeUCCookie(cookie);
|
const normalized = normalizeUCCookie(cookie);
|
||||||
for (let i = 0; i < normalized.length; i += 1) {
|
for (let i = 0; i < normalized.length; i += 1) {
|
||||||
if (normalized.charCodeAt(i) > 255) {
|
if (normalized.charCodeAt(i) > 255) {
|
||||||
throw new Error('UC Cookie 含有非法字符,请确认没有中文标点、中文空格或说明文字');
|
throw new Error(
|
||||||
|
'UC Cookie 含有非法字符,请确认没有中文标点、中文空格或说明文字'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
@@ -136,11 +287,21 @@ async function resolveUCUt(cookie: string) {
|
|||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const tryFetchUt = async (headers?: HeadersInit) => {
|
const tryFetchUt = async (headers?: HeadersInit) => {
|
||||||
const response = await fetch(`${UC_DRIVE_API_BASE}/file`, {
|
const response = headers
|
||||||
method: 'GET',
|
? await ucFetch(
|
||||||
headers,
|
`${UC_DRIVE_API_BASE}/file`,
|
||||||
cache: 'no-store',
|
{
|
||||||
});
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
safeCookie
|
||||||
|
)
|
||||||
|
: await fetch(`${UC_DRIVE_API_BASE}/file`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
const text = (await response.text()).trim();
|
const text = (await response.text()).trim();
|
||||||
if (!response.ok || !text) return '';
|
if (!response.ok || !text) return '';
|
||||||
return text;
|
return text;
|
||||||
@@ -177,24 +338,37 @@ export function parseUCShareUrl(url: string, passcode = ''): UCShareLinkInfo {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pwdId,
|
pwdId,
|
||||||
passcode: passcode || parsed.searchParams.get('pwd') || parsed.searchParams.get('passcode') || '',
|
passcode:
|
||||||
|
passcode ||
|
||||||
|
parsed.searchParams.get('pwd') ||
|
||||||
|
parsed.searchParams.get('passcode') ||
|
||||||
|
'',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchShareToken(cookie: string, share: UCShareLinkInfo, ut: string) {
|
async function fetchShareToken(
|
||||||
const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/token', ut), {
|
cookie: string,
|
||||||
method: 'POST',
|
share: UCShareLinkInfo,
|
||||||
headers: getHeaders(cookie),
|
ut: string
|
||||||
body: JSON.stringify({
|
) {
|
||||||
pwd_id: share.pwdId,
|
const response = await ucFetch(
|
||||||
passcode: share.passcode,
|
buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/token', ut),
|
||||||
}),
|
{
|
||||||
cache: 'no-store',
|
method: 'POST',
|
||||||
});
|
headers: getHeaders(cookie),
|
||||||
|
body: JSON.stringify({
|
||||||
|
pwd_id: share.pwdId,
|
||||||
|
passcode: share.passcode,
|
||||||
|
}),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
cookie
|
||||||
|
);
|
||||||
|
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '获取 UC 分享 token 失败');
|
ensureOk(data, '获取 UC 分享 token 失败');
|
||||||
const stoken = data?.data?.stoken || data?.data?.share_token || data?.data?.token;
|
const stoken =
|
||||||
|
data?.data?.stoken || data?.data?.share_token || data?.data?.token;
|
||||||
if (!stoken) {
|
if (!stoken) {
|
||||||
throw new Error('UC 分享 token 缺失');
|
throw new Error('UC 分享 token 缺失');
|
||||||
}
|
}
|
||||||
@@ -204,7 +378,14 @@ async function fetchShareToken(cookie: string, share: UCShareLinkInfo, ut: strin
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchShareFolderItems(cookie: string, pwdId: string, stoken: string, ut: string, pdirFid = '0', page = 1): Promise<{ items: UCShareItem[]; total?: number }> {
|
async function fetchShareFolderItems(
|
||||||
|
cookie: string,
|
||||||
|
pwdId: string,
|
||||||
|
stoken: string,
|
||||||
|
ut: string,
|
||||||
|
pdirFid = '0',
|
||||||
|
page = 1
|
||||||
|
): Promise<{ items: UCShareItem[]; total?: number }> {
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
pwd_id: pwdId,
|
pwd_id: pwdId,
|
||||||
stoken,
|
stoken,
|
||||||
@@ -214,11 +395,20 @@ async function fetchShareFolderItems(cookie: string, pwdId: string, stoken: stri
|
|||||||
_size: '100',
|
_size: '100',
|
||||||
_sort: 'file_type:asc,file_name:asc',
|
_sort: 'file_type:asc,file_name:asc',
|
||||||
});
|
});
|
||||||
const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/detail', ut, query.toString()), {
|
const response = await ucFetch(
|
||||||
method: 'GET',
|
buildApiUrl(
|
||||||
headers: getHeaders(cookie),
|
UC_SHARE_API_BASE,
|
||||||
cache: 'no-store',
|
'/share/sharepage/detail',
|
||||||
});
|
ut,
|
||||||
|
query.toString()
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: getHeaders(cookie),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
cookie
|
||||||
|
);
|
||||||
|
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '获取 UC 分享详情失败');
|
ensureOk(data, '获取 UC 分享详情失败');
|
||||||
@@ -228,7 +418,8 @@ async function fetchShareFolderItems(cookie: string, pwdId: string, stoken: stri
|
|||||||
fid: String(item.fid || item.file_id || ''),
|
fid: String(item.fid || item.file_id || ''),
|
||||||
fileName: String(item.file_name || item.name || ''),
|
fileName: String(item.file_name || item.name || ''),
|
||||||
dir: Boolean(item.dir || item.is_dir || item.file_type === 0),
|
dir: Boolean(item.dir || item.is_dir || item.file_type === 0),
|
||||||
shareFidToken: item.share_fid_token || item.fid_token || item.share_token || undefined,
|
shareFidToken:
|
||||||
|
item.share_fid_token || item.fid_token || item.share_token || undefined,
|
||||||
pdirFid: String(item.pdir_fid || pdirFid || '0'),
|
pdirFid: String(item.pdir_fid || pdirFid || '0'),
|
||||||
size: Number(item.size || 0),
|
size: Number(item.size || 0),
|
||||||
})),
|
})),
|
||||||
@@ -236,20 +427,40 @@ async function fetchShareFolderItems(cookie: string, pwdId: string, stoken: stri
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectShareItemsRecursive(cookie: string, pwdId: string, stoken: string, ut: string, pdirFid = '0'): Promise<UCShareItem[]> {
|
async function collectShareItemsRecursive(
|
||||||
|
cookie: string,
|
||||||
|
pwdId: string,
|
||||||
|
stoken: string,
|
||||||
|
ut: string,
|
||||||
|
pdirFid = '0'
|
||||||
|
): Promise<UCShareItem[]> {
|
||||||
const result: UCShareItem[] = [];
|
const result: UCShareItem[] = [];
|
||||||
const pageSize = 100;
|
const pageSize = 100;
|
||||||
for (let page = 1; page < 100; page += 1) {
|
for (let page = 1; page < 100; page += 1) {
|
||||||
const { items, total } = await fetchShareFolderItems(cookie, pwdId, stoken, ut, pdirFid, page);
|
const { items, total } = await fetchShareFolderItems(
|
||||||
|
cookie,
|
||||||
|
pwdId,
|
||||||
|
stoken,
|
||||||
|
ut,
|
||||||
|
pdirFid,
|
||||||
|
page
|
||||||
|
);
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.dir) {
|
if (item.dir) {
|
||||||
const children = await collectShareItemsRecursive(cookie, pwdId, stoken, ut, item.fid);
|
const children = await collectShareItemsRecursive(
|
||||||
|
cookie,
|
||||||
|
pwdId,
|
||||||
|
stoken,
|
||||||
|
ut,
|
||||||
|
item.fid
|
||||||
|
);
|
||||||
result.push(...children);
|
result.push(...children);
|
||||||
} else {
|
} else {
|
||||||
result.push(item);
|
result.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (items.length < pageSize || page >= Math.ceil((total || 0) / pageSize)) break;
|
if (items.length < pageSize || page >= Math.ceil((total || 0) / pageSize))
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -259,46 +470,75 @@ function isVideoFile(fileName: string) {
|
|||||||
return VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
return VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDriveFolderItems(cookie: string, ut: string, pdirFid = '0', page = 1, size = 200): Promise<any[]> {
|
async function fetchDriveFolderItems(
|
||||||
|
cookie: string,
|
||||||
|
ut: string,
|
||||||
|
pdirFid = '0',
|
||||||
|
page = 1,
|
||||||
|
size = 200
|
||||||
|
): Promise<any[]> {
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
pdir_fid: pdirFid,
|
pdir_fid: pdirFid,
|
||||||
_page: String(page),
|
_page: String(page),
|
||||||
_size: String(size),
|
_size: String(size),
|
||||||
_sort: 'file_type:asc,file_name:asc',
|
_sort: 'file_type:asc,file_name:asc',
|
||||||
});
|
});
|
||||||
const response = await fetch(buildApiUrl(UC_DRIVE_API_BASE, '/file/sort', ut, query.toString()), {
|
const response = await ucFetch(
|
||||||
method: 'GET',
|
buildApiUrl(UC_DRIVE_API_BASE, '/file/sort', ut, query.toString()),
|
||||||
headers: getHeaders(cookie),
|
{
|
||||||
cache: 'no-store',
|
method: 'GET',
|
||||||
});
|
headers: getHeaders(cookie),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
cookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '获取 UC 目录列表失败');
|
ensureOk(data, '获取 UC 目录列表失败');
|
||||||
return data?.data?.list || [];
|
return data?.data?.list || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAllDriveFolderItems(cookie: string, ut: string, pdirFid = '0'): Promise<any[]> {
|
async function fetchAllDriveFolderItems(
|
||||||
|
cookie: string,
|
||||||
|
ut: string,
|
||||||
|
pdirFid = '0'
|
||||||
|
): Promise<any[]> {
|
||||||
const allItems: any[] = [];
|
const allItems: any[] = [];
|
||||||
const pageSize = 200;
|
const pageSize = 200;
|
||||||
for (let page = 1; page < 100; page += 1) {
|
for (let page = 1; page < 100; page += 1) {
|
||||||
const items = await fetchDriveFolderItems(cookie, ut, pdirFid, page, pageSize);
|
const items = await fetchDriveFolderItems(
|
||||||
|
cookie,
|
||||||
|
ut,
|
||||||
|
pdirFid,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
allItems.push(...items);
|
allItems.push(...items);
|
||||||
if (items.length < pageSize) break;
|
if (items.length < pageSize) break;
|
||||||
}
|
}
|
||||||
return allItems;
|
return allItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createDriveFolder(cookie: string, ut: string, parentFid: string, folderName: string) {
|
async function createDriveFolder(
|
||||||
const response = await fetch(buildApiUrl(UC_DRIVE_API_BASE, '/file', ut), {
|
cookie: string,
|
||||||
method: 'POST',
|
ut: string,
|
||||||
headers: getHeaders(cookie),
|
parentFid: string,
|
||||||
body: JSON.stringify({
|
folderName: string
|
||||||
pdir_fid: parentFid,
|
) {
|
||||||
file_name: folderName,
|
const response = await ucFetch(
|
||||||
dir_path: '',
|
buildApiUrl(UC_DRIVE_API_BASE, '/file', ut),
|
||||||
dir_init_lock: false,
|
{
|
||||||
}),
|
method: 'POST',
|
||||||
cache: 'no-store',
|
headers: getHeaders(cookie),
|
||||||
});
|
body: JSON.stringify({
|
||||||
|
pdir_fid: parentFid,
|
||||||
|
file_name: folderName,
|
||||||
|
dir_path: '',
|
||||||
|
dir_init_lock: false,
|
||||||
|
}),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
cookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, `创建 UC 目录失败:${folderName}`);
|
ensureOk(data, `创建 UC 目录失败:${folderName}`);
|
||||||
const fid = data?.data?.fid || data?.data?.file_id || data?.metadata?.fid;
|
const fid = data?.data?.fid || data?.data?.file_id || data?.metadata?.fid;
|
||||||
@@ -306,9 +546,20 @@ async function createDriveFolder(cookie: string, ut: string, parentFid: string,
|
|||||||
return String(fid);
|
return String(fid);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findDirectoryByName(cookie: string, ut: string, parentFid: string, folderName: string): Promise<any | null> {
|
async function findDirectoryByName(
|
||||||
|
cookie: string,
|
||||||
|
ut: string,
|
||||||
|
parentFid: string,
|
||||||
|
folderName: string
|
||||||
|
): Promise<any | null> {
|
||||||
const items = await fetchAllDriveFolderItems(cookie, ut, parentFid);
|
const items = await fetchAllDriveFolderItems(cookie, ut, parentFid);
|
||||||
return items.find((item: any) => Boolean(item.dir || item.is_dir) && getDriveItemName(item) === folderName) || null;
|
return (
|
||||||
|
items.find(
|
||||||
|
(item: any) =>
|
||||||
|
Boolean(item.dir || item.is_dir) &&
|
||||||
|
getDriveItemName(item) === folderName
|
||||||
|
) || null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateUCCookieReadable(cookie: string): Promise<void> {
|
export async function validateUCCookieReadable(cookie: string): Promise<void> {
|
||||||
@@ -317,14 +568,29 @@ export async function validateUCCookieReadable(cookie: string): Promise<void> {
|
|||||||
await fetchDriveFolderItems(safeCookie, ut, '0');
|
await fetchDriveFolderItems(safeCookie, ut, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listUCShareVideos(shareUrl: string, cookie: string, passcode = ''): Promise<UCShareVideoListResult> {
|
export async function listUCShareVideos(
|
||||||
|
shareUrl: string,
|
||||||
|
cookie: string,
|
||||||
|
passcode = ''
|
||||||
|
): Promise<UCShareVideoListResult> {
|
||||||
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
||||||
const ut = await resolveUCUt(safeCookie);
|
const ut = await resolveUCUt(safeCookie);
|
||||||
const share = parseUCShareUrl(shareUrl, passcode);
|
const share = parseUCShareUrl(shareUrl, passcode);
|
||||||
const { stoken, shareTitle } = await fetchShareToken(safeCookie, share, ut);
|
const { stoken, shareTitle } = await fetchShareToken(safeCookie, share, ut);
|
||||||
const allItems = await collectShareItemsRecursive(safeCookie, share.pwdId, stoken, ut, '0');
|
const allItems = await collectShareItemsRecursive(
|
||||||
|
safeCookie,
|
||||||
|
share.pwdId,
|
||||||
|
stoken,
|
||||||
|
ut,
|
||||||
|
'0'
|
||||||
|
);
|
||||||
const files = allItems
|
const files = allItems
|
||||||
.filter((item) => !item.dir && isVideoFile(item.fileName) && Number(item.size || 0) >= 5 * 1024 * 1024)
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
!item.dir &&
|
||||||
|
isVideoFile(item.fileName) &&
|
||||||
|
Number(item.size || 0) >= 5 * 1024 * 1024
|
||||||
|
)
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
fid: item.fid,
|
fid: item.fid,
|
||||||
name: item.fileName,
|
name: item.fileName,
|
||||||
@@ -341,7 +607,10 @@ export async function listUCShareVideos(shareUrl: string, cookie: string, passco
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureUCDrivePath(cookie: string, inputPath: string): Promise<{ fid: string; path: string }> {
|
export async function ensureUCDrivePath(
|
||||||
|
cookie: string,
|
||||||
|
inputPath: string
|
||||||
|
): Promise<{ fid: string; path: string }> {
|
||||||
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
||||||
const ut = await resolveUCUt(safeCookie);
|
const ut = await resolveUCUt(safeCookie);
|
||||||
const normalized = normalizePath(inputPath);
|
const normalized = normalizePath(inputPath);
|
||||||
@@ -352,7 +621,10 @@ export async function ensureUCDrivePath(cookie: string, inputPath: string): Prom
|
|||||||
let currentPath = '';
|
let currentPath = '';
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
const items = await fetchDriveFolderItems(safeCookie, ut, currentFid);
|
const items = await fetchDriveFolderItems(safeCookie, ut, currentFid);
|
||||||
const existed = items.find((item: any) => Boolean(item.dir || item.is_dir) && getDriveItemName(item) === segment);
|
const existed = items.find(
|
||||||
|
(item: any) =>
|
||||||
|
Boolean(item.dir || item.is_dir) && getDriveItemName(item) === segment
|
||||||
|
);
|
||||||
currentPath = joinPath(currentPath, segment);
|
currentPath = joinPath(currentPath, segment);
|
||||||
if (existed) {
|
if (existed) {
|
||||||
currentFid = String(existed.fid || existed.file_id);
|
currentFid = String(existed.fid || existed.file_id);
|
||||||
@@ -363,24 +635,35 @@ export async function ensureUCDrivePath(cookie: string, inputPath: string): Prom
|
|||||||
return { fid: currentFid, path: currentPath || '/' };
|
return { fid: currentFid, path: currentPath || '/' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitSaveTask(cookie: string, ut: string, share: UCShareLinkInfo, stoken: string, toPdirFid: string, items: UCShareItem[]) {
|
async function submitSaveTask(
|
||||||
|
cookie: string,
|
||||||
|
ut: string,
|
||||||
|
share: UCShareLinkInfo,
|
||||||
|
stoken: string,
|
||||||
|
toPdirFid: string,
|
||||||
|
items: UCShareItem[]
|
||||||
|
) {
|
||||||
if (items.length === 0) throw new Error('没有可保存的文件');
|
if (items.length === 0) throw new Error('没有可保存的文件');
|
||||||
const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/save', ut), {
|
const response = await ucFetch(
|
||||||
method: 'POST',
|
buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/save', ut),
|
||||||
headers: getHeaders(cookie),
|
{
|
||||||
body: JSON.stringify({
|
method: 'POST',
|
||||||
pwd_id: share.pwdId,
|
headers: getHeaders(cookie),
|
||||||
stoken,
|
body: JSON.stringify({
|
||||||
pdir_fid: '0',
|
pwd_id: share.pwdId,
|
||||||
to_pdir_fid: toPdirFid,
|
stoken,
|
||||||
scene: 'link',
|
pdir_fid: '0',
|
||||||
filelist: items.map((item) => item.fid),
|
to_pdir_fid: toPdirFid,
|
||||||
fid_list: items.map((item) => item.fid),
|
scene: 'link',
|
||||||
fid_token_list: items.map((item) => item.shareFidToken || ''),
|
filelist: items.map((item) => item.fid),
|
||||||
share_fid_token_list: items.map((item) => item.shareFidToken || ''),
|
fid_list: items.map((item) => item.fid),
|
||||||
}),
|
fid_token_list: items.map((item) => item.shareFidToken || ''),
|
||||||
cache: 'no-store',
|
share_fid_token_list: items.map((item) => item.shareFidToken || ''),
|
||||||
});
|
}),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
cookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '提交 UC 转存任务失败');
|
ensureOk(data, '提交 UC 转存任务失败');
|
||||||
return data?.data?.task_id ? String(data.data.task_id) : undefined;
|
return data?.data?.task_id ? String(data.data.task_id) : undefined;
|
||||||
@@ -388,14 +671,29 @@ async function submitSaveTask(cookie: string, ut: string, share: UCShareLinkInfo
|
|||||||
|
|
||||||
async function _pollTask(cookie: string, ut: string, taskId: string) {
|
async function _pollTask(cookie: string, ut: string, taskId: string) {
|
||||||
for (let i = 0; i < 25; i += 1) {
|
for (let i = 0; i < 25; i += 1) {
|
||||||
const query = new URLSearchParams({ task_id: taskId, retry_index: String(i) });
|
const query = new URLSearchParams({
|
||||||
const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/task', ut, query.toString()), {
|
task_id: taskId,
|
||||||
method: 'GET', headers: getHeaders(cookie), cache: 'no-store',
|
retry_index: String(i),
|
||||||
});
|
});
|
||||||
|
const response = await ucFetch(
|
||||||
|
buildApiUrl(UC_SHARE_API_BASE, '/task', ut, query.toString()),
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: getHeaders(cookie),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
cookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '查询 UC 任务状态失败');
|
ensureOk(data, '查询 UC 任务状态失败');
|
||||||
const task = data?.data || {};
|
const task = data?.data || {};
|
||||||
if (task?.status === 2 || task?.status === 'finished' || task?.status === 'success' || task?.finished_at) return;
|
if (
|
||||||
|
task?.status === 2 ||
|
||||||
|
task?.status === 'finished' ||
|
||||||
|
task?.status === 'success' ||
|
||||||
|
task?.finished_at
|
||||||
|
)
|
||||||
|
return;
|
||||||
if (task?.status === -1 || task?.status === 'failed' || task?.err_code) {
|
if (task?.status === -1 || task?.status === 'failed' || task?.err_code) {
|
||||||
throw new Error(task?.message || task?.err_msg || 'UC 任务执行失败');
|
throw new Error(task?.message || task?.err_msg || 'UC 任务执行失败');
|
||||||
}
|
}
|
||||||
@@ -404,12 +702,22 @@ async function _pollTask(cookie: string, ut: string, taskId: string) {
|
|||||||
throw new Error('UC 任务处理超时');
|
throw new Error('UC 任务处理超时');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureUCPlayFolder(cookie: string, playTempSavePath: string, shareId: string, title?: string): Promise<{ folderFid: string; folderPath: string; folderName: string }> {
|
export async function ensureUCPlayFolder(
|
||||||
|
cookie: string,
|
||||||
|
playTempSavePath: string,
|
||||||
|
shareId: string,
|
||||||
|
title?: string
|
||||||
|
): Promise<{ folderFid: string; folderPath: string; folderName: string }> {
|
||||||
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
||||||
const ut = await resolveUCUt(safeCookie);
|
const ut = await resolveUCUt(safeCookie);
|
||||||
const tempRoot = await ensureUCDrivePath(safeCookie, playTempSavePath);
|
const tempRoot = await ensureUCDrivePath(safeCookie, playTempSavePath);
|
||||||
const folderName = buildInstantPlayFolderName(shareId, title);
|
const folderName = buildInstantPlayFolderName(shareId, title);
|
||||||
const existedFolder = await findDirectoryByName(safeCookie, ut, tempRoot.fid, folderName);
|
const existedFolder = await findDirectoryByName(
|
||||||
|
safeCookie,
|
||||||
|
ut,
|
||||||
|
tempRoot.fid,
|
||||||
|
folderName
|
||||||
|
);
|
||||||
if (existedFolder) {
|
if (existedFolder) {
|
||||||
return {
|
return {
|
||||||
folderFid: String(existedFolder.fid || existedFolder.file_id),
|
folderFid: String(existedFolder.fid || existedFolder.file_id),
|
||||||
@@ -417,19 +725,58 @@ export async function ensureUCPlayFolder(cookie: string, playTempSavePath: strin
|
|||||||
folderName,
|
folderName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const folderFid = await createDriveFolder(safeCookie, ut, tempRoot.fid, folderName);
|
const folderFid = await createDriveFolder(
|
||||||
return { folderFid, folderPath: joinPath(tempRoot.path, folderName), folderName };
|
safeCookie,
|
||||||
|
ut,
|
||||||
|
tempRoot.fid,
|
||||||
|
folderName
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
folderFid,
|
||||||
|
folderPath: joinPath(tempRoot.path, folderName),
|
||||||
|
folderName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveUCShareFile(cookie: string, input: { shareId: string; shareToken: string; fileId: string; shareFileToken?: string; playFolderFid: string; }): Promise<string> {
|
export async function saveUCShareFile(
|
||||||
|
cookie: string,
|
||||||
|
input: {
|
||||||
|
shareId: string;
|
||||||
|
shareToken: string;
|
||||||
|
fileId: string;
|
||||||
|
shareFileToken?: string;
|
||||||
|
playFolderFid: string;
|
||||||
|
}
|
||||||
|
): Promise<string> {
|
||||||
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
||||||
const ut = await resolveUCUt(safeCookie);
|
const ut = await resolveUCUt(safeCookie);
|
||||||
const taskId = await submitSaveTask(safeCookie, ut, { pwdId: input.shareId, passcode: '' }, input.shareToken, input.playFolderFid, [{ fid: input.fileId, fileName: '', dir: false, shareFidToken: input.shareFileToken }]);
|
const taskId = await submitSaveTask(
|
||||||
|
safeCookie,
|
||||||
|
ut,
|
||||||
|
{ pwdId: input.shareId, passcode: '' },
|
||||||
|
input.shareToken,
|
||||||
|
input.playFolderFid,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
fid: input.fileId,
|
||||||
|
fileName: '',
|
||||||
|
dir: false,
|
||||||
|
shareFidToken: input.shareFileToken,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
if (!taskId) throw new Error('UC 转存任务创建失败');
|
if (!taskId) throw new Error('UC 转存任务创建失败');
|
||||||
|
|
||||||
for (let i = 0; i < 25; i += 1) {
|
for (let i = 0; i < 25; i += 1) {
|
||||||
const query = new URLSearchParams({ task_id: taskId, retry_index: String(i) });
|
const query = new URLSearchParams({
|
||||||
const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/task', ut, query.toString()), { method: 'GET', headers: getHeaders(safeCookie), cache: 'no-store' });
|
task_id: taskId,
|
||||||
|
retry_index: String(i),
|
||||||
|
});
|
||||||
|
const response = await ucFetch(
|
||||||
|
buildApiUrl(UC_SHARE_API_BASE, '/task', ut, query.toString()),
|
||||||
|
{ method: 'GET', headers: getHeaders(safeCookie), cache: 'no-store' },
|
||||||
|
safeCookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '查询 UC 任务状态失败');
|
ensureOk(data, '查询 UC 任务状态失败');
|
||||||
const saveAsTopFids = data?.data?.save_as?.save_as_top_fids;
|
const saveAsTopFids = data?.data?.save_as?.save_as_top_fids;
|
||||||
@@ -438,9 +785,17 @@ export async function saveUCShareFile(cookie: string, input: { shareId: string;
|
|||||||
}
|
}
|
||||||
const status = data?.data?.status;
|
const status = data?.data?.status;
|
||||||
if (status === -1 || status === 'failed' || data?.data?.err_code) {
|
if (status === -1 || status === 'failed' || data?.data?.err_code) {
|
||||||
throw new Error(data?.data?.message || data?.data?.err_msg || 'UC 任务执行失败');
|
throw new Error(
|
||||||
|
data?.data?.message || data?.data?.err_msg || 'UC 任务执行失败'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (status === 2 || status === 'finished' || status === 'success' || data?.data?.finished_at) break;
|
if (
|
||||||
|
status === 2 ||
|
||||||
|
status === 'finished' ||
|
||||||
|
status === 'success' ||
|
||||||
|
data?.data?.finished_at
|
||||||
|
)
|
||||||
|
break;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||||
}
|
}
|
||||||
throw new Error('UC 转存结果获取失败');
|
throw new Error('UC 转存结果获取失败');
|
||||||
@@ -451,17 +806,45 @@ function generateDeviceID(timestamp: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateReqId(deviceID: string, timestamp: string) {
|
function generateReqId(deviceID: string, timestamp: string) {
|
||||||
return crypto.createHash('md5').update(deviceID + timestamp).digest('hex').slice(0, 16);
|
return crypto
|
||||||
|
.createHash('md5')
|
||||||
|
.update(deviceID + timestamp)
|
||||||
|
.digest('hex')
|
||||||
|
.slice(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateXPanToken(method: string, pathname: string, timestamp: string, key: string) {
|
function generateXPanToken(
|
||||||
return crypto.createHash('sha256').update(`${method}&${pathname}&${timestamp}&${key}`).digest('hex');
|
method: string,
|
||||||
|
pathname: string,
|
||||||
|
timestamp: string,
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
|
return crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(`${method}&${pathname}&${timestamp}&${key}`)
|
||||||
|
.digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUCPlayUrls(cookie: string, savedFileId: string, token = ''): Promise<Array<{ name: string; url: string; priority: number; headers?: Record<string, string> }>> {
|
export async function getUCPlayUrls(
|
||||||
|
cookie: string,
|
||||||
|
savedFileId: string,
|
||||||
|
token = ''
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
priority: number;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
const safeCookie = assertUCCookieHeaderSafe(cookie);
|
||||||
const ut = await resolveUCUt(safeCookie);
|
const ut = await resolveUCUt(safeCookie);
|
||||||
const urls: Array<{ name: string; url: string; priority: number; headers?: Record<string, string> }> = [];
|
const urls: Array<{
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
priority: number;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
@@ -469,7 +852,12 @@ export async function getUCPlayUrls(cookie: string, savedFileId: string, token =
|
|||||||
const timestamp = `${Math.floor(Date.now() / 1000)}000`;
|
const timestamp = `${Math.floor(Date.now() / 1000)}000`;
|
||||||
const deviceId = generateDeviceID(timestamp);
|
const deviceId = generateDeviceID(timestamp);
|
||||||
const reqId = generateReqId(deviceId, timestamp);
|
const reqId = generateReqId(deviceId, timestamp);
|
||||||
const xPanToken = generateXPanToken('GET', pathname, timestamp, UC_OPEN_API_SIGN_KEY);
|
const xPanToken = generateXPanToken(
|
||||||
|
'GET',
|
||||||
|
pathname,
|
||||||
|
timestamp,
|
||||||
|
UC_OPEN_API_SIGN_KEY
|
||||||
|
);
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
req_id: reqId,
|
req_id: reqId,
|
||||||
access_token: token,
|
access_token: token,
|
||||||
@@ -490,25 +878,36 @@ export async function getUCPlayUrls(cookie: string, savedFileId: string, token =
|
|||||||
resolution: 'low,normal,high,super,2k,4k',
|
resolution: 'low,normal,high,super,2k,4k',
|
||||||
support: 'dolby_vision',
|
support: 'dolby_vision',
|
||||||
});
|
});
|
||||||
const response = await fetch(`${UC_OPEN_API_BASE}${pathname}?${query.toString()}`, {
|
const response = await fetch(
|
||||||
method: 'GET',
|
`${UC_OPEN_API_BASE}${pathname}?${query.toString()}`,
|
||||||
headers: {
|
{
|
||||||
'user-agent': 'Mozilla/5.0 (Linux; Android 9) AppleWebKit/533.1 (KHTML, like Gecko) Mobile Safari/533.1',
|
method: 'GET',
|
||||||
connection: 'Keep-Alive',
|
headers: {
|
||||||
'accept-encoding': 'gzip',
|
'user-agent':
|
||||||
'x-pan-tm': timestamp,
|
'Mozilla/5.0 (Linux; Android 9) AppleWebKit/533.1 (KHTML, like Gecko) Mobile Safari/533.1',
|
||||||
'x-pan-token': xPanToken,
|
connection: 'Keep-Alive',
|
||||||
'content-type': 'text/plain;charset=UTF-8',
|
'accept-encoding': 'gzip',
|
||||||
'x-pan-client-id': UC_OPEN_API_CLIENT_ID,
|
'x-pan-tm': timestamp,
|
||||||
},
|
'x-pan-token': xPanToken,
|
||||||
cache: 'no-store',
|
'content-type': 'text/plain;charset=UTF-8',
|
||||||
});
|
'x-pan-client-id': UC_OPEN_API_CLIENT_ID,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
}
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
const openVideoInfo = Array.isArray(data?.data?.video_info)
|
const openVideoInfo = Array.isArray(data?.data?.video_info)
|
||||||
? data.data.video_info.find((item: any) => item?.accessable && item?.url)
|
? data.data.video_info.find(
|
||||||
|
(item: any) => item?.accessable && item?.url
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
if (openVideoInfo?.url) {
|
if (openVideoInfo?.url) {
|
||||||
urls.push({ name: '原画', url: String(openVideoInfo.url), priority: 9999, headers: {} });
|
urls.push({
|
||||||
|
name: '原画',
|
||||||
|
url: String(openVideoInfo.url),
|
||||||
|
priority: 9999,
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore token failure, fallback to cookie mode
|
// ignore token failure, fallback to cookie mode
|
||||||
@@ -516,43 +915,67 @@ export async function getUCPlayUrls(cookie: string, savedFileId: string, token =
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(buildApiUrl(UC_DRIVE_API_BASE, '/file/download', ut), {
|
const response = await ucFetch(
|
||||||
method: 'POST',
|
buildApiUrl(UC_DRIVE_API_BASE, '/file/download', ut),
|
||||||
headers: getHeaders(safeCookie),
|
{
|
||||||
body: JSON.stringify({ fids: [savedFileId] }),
|
method: 'POST',
|
||||||
cache: 'no-store',
|
headers: getHeaders(safeCookie),
|
||||||
});
|
body: JSON.stringify({ fids: [savedFileId] }),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
safeCookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '获取 UC 下载地址失败');
|
ensureOk(data, '获取 UC 下载地址失败');
|
||||||
const downloadUrl = data?.data?.[0]?.download_url;
|
const downloadUrl = data?.data?.[0]?.download_url;
|
||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
urls.push({ name: '原画', url: String(downloadUrl), priority: 9999, headers: getUCPlayHeaders(safeCookie) });
|
urls.push({
|
||||||
|
name: '原画',
|
||||||
|
url: String(downloadUrl),
|
||||||
|
priority: 9999,
|
||||||
|
headers: getUCPlayHeaders(safeCookie),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(buildApiUrl(UC_DRIVE_API_BASE, '/file/v2/play', ut), {
|
const response = await ucFetch(
|
||||||
method: 'POST',
|
buildApiUrl(UC_DRIVE_API_BASE, '/file/v2/play', ut),
|
||||||
headers: getHeaders(safeCookie),
|
{
|
||||||
body: JSON.stringify({
|
method: 'POST',
|
||||||
fid: savedFileId,
|
headers: getHeaders(safeCookie),
|
||||||
resolutions: 'normal,low,high,super,2k,4k',
|
body: JSON.stringify({
|
||||||
supports: 'fmp4',
|
fid: savedFileId,
|
||||||
}),
|
resolutions: 'normal,low,high,super,2k,4k',
|
||||||
cache: 'no-store',
|
supports: 'fmp4',
|
||||||
});
|
}),
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
safeCookie
|
||||||
|
);
|
||||||
const data = await parseJson(response);
|
const data = await parseJson(response);
|
||||||
ensureOk(data, '获取 UC 转码地址失败');
|
ensureOk(data, '获取 UC 转码地址失败');
|
||||||
const nameMap: Record<string, string> = { FOUR_K: '4K', SUPER: '超清', HIGH: '高清', NORMAL: '流畅', LOW: '低清' };
|
const nameMap: Record<string, string> = {
|
||||||
|
FOUR_K: '4K',
|
||||||
|
SUPER: '超清',
|
||||||
|
HIGH: '高清',
|
||||||
|
NORMAL: '流畅',
|
||||||
|
LOW: '低清',
|
||||||
|
};
|
||||||
if (Array.isArray(data?.data?.video_list)) {
|
if (Array.isArray(data?.data?.video_list)) {
|
||||||
for (const video of data.data.video_list) {
|
for (const video of data.data.video_list) {
|
||||||
const resolution = video?.video_info?.resoultion;
|
const resolution = video?.video_info?.resoultion;
|
||||||
const playUrl = video?.video_info?.url;
|
const playUrl = video?.video_info?.url;
|
||||||
const priority = Number(video?.video_info?.width || 0);
|
const priority = Number(video?.video_info?.width || 0);
|
||||||
if (resolution && playUrl) {
|
if (resolution && playUrl) {
|
||||||
urls.push({ name: nameMap[String(resolution)] || String(resolution), url: String(playUrl), priority, headers: getUCPlayHeaders(safeCookie) });
|
urls.push({
|
||||||
|
name: nameMap[String(resolution)] || String(resolution),
|
||||||
|
url: String(playUrl),
|
||||||
|
priority,
|
||||||
|
headers: getUCPlayHeaders(safeCookie),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,7 +983,9 @@ export async function getUCPlayUrls(cookie: string, savedFileId: string, token =
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
const deduped = urls.filter((item, index, array) => array.findIndex((v) => v.url === item.url) === index);
|
const deduped = urls.filter(
|
||||||
|
(item, index, array) => array.findIndex((v) => v.url === item.url) === index
|
||||||
|
);
|
||||||
deduped.sort((a, b) => b.priority - a.priority);
|
deduped.sort((a, b) => b.priority - a.priority);
|
||||||
if (deduped.length === 0) throw new Error('未获取到 UC 播放地址');
|
if (deduped.length === 0) throw new Error('未获取到 UC 播放地址');
|
||||||
return deduped;
|
return deduped;
|
||||||
|
|||||||
Reference in New Issue
Block a user