diff --git a/src/lib/netdisk/quark.client.ts b/src/lib/netdisk/quark.client.ts index 6e156f0..a46aba8 100644 --- a/src/lib/netdisk/quark.client.ts +++ b/src/lib/netdisk/quark.client.ts @@ -1,11 +1,152 @@ /* 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_DRIVE_API_BASE = 'https://drive-pc.quark.cn/1/clouddrive'; const QUARK_QUERY = 'pr=ucpro&fr=pc'; 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'; +type QuarkRenewableCookieName = '__puus' | '__pus'; + +const QUARK_RENEWABLE_COOKIE_NAMES: QuarkRenewableCookieName[] = [ + '__puus', + '__pus', +]; +const runtimeCookieValues: Partial> = + {}; +let quarkCookiePersistQueue: Promise = 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> { + const values: Partial> = {}; + 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> +): Promise { + 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 { + const renewedValues = extractRenewableCookieValues(response); + await persistRenewedQuarkCookie(requestCookie, renewedValues); +} + +async function quarkFetch( + input: RequestInfo | URL, + init: RequestInit, + requestCookie: string +): Promise { + const response = await fetch(input, init); + await renewQuarkCookieFromResponse(response, requestCookie); + return response; +} + export interface QuarkShareLinkInfo { pwdId: string; passcode: string; @@ -73,7 +214,7 @@ function buildApiUrl(base: string, path: string, query = '') { function getHeaders(cookie: string): HeadersInit { return { 'content-type': 'application/json', - cookie, + cookie: applyRuntimeCookieValues(cookie), referer: 'https://pan.quark.cn/', 'user-agent': QUARK_API_USER_AGENT, }; @@ -81,7 +222,7 @@ function getHeaders(cookie: string): HeadersInit { export function getQuarkPlayHeaders(cookie: string): Record { return { - cookie, + cookie: applyRuntimeCookieValues(cookie), referer: 'https://pan.quark.cn/', 'user-agent': QUARK_API_USER_AGENT, }; @@ -99,7 +240,9 @@ export function assertQuarkCookieHeaderSafe(cookie: string): string { const normalized = normalizeQuarkCookie(cookie); for (let i = 0; i < normalized.length; i += 1) { if (normalized.charCodeAt(i) > 255) { - throw new Error('夸克 Cookie 含有非法字符,请确认没有中文标点、中文空格或说明文字'); + throw new Error( + '夸克 Cookie 含有非法字符,请确认没有中文标点、中文空格或说明文字' + ); } } return normalized; @@ -112,10 +255,7 @@ function normalizePath(path: string): string { } function joinPath(...parts: string[]) { - const joined = parts - .filter(Boolean) - .join('/') - .replace(/\/+/g, '/'); + const joined = parts.filter(Boolean).join('/').replace(/\/+/g, '/'); return normalizePath(joined); } @@ -144,7 +284,10 @@ function ensureOk(data: any, fallbackMessage: string) { 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 pwdId = 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) { - const response = await fetch( + const response = await quarkFetch( buildApiUrl(QUARK_SHARE_API_BASE, '/share/sharepage/token'), { method: 'POST', @@ -175,16 +318,15 @@ async function fetchShareToken(cookie: string, share: QuarkShareLinkInfo) { pwd_id: share.pwdId, passcode: share.passcode, }), - } + }, + cookie ); const data = await parseJson(response); ensureOk(data, '获取夸克分享 token 失败'); const stoken = - data?.data?.stoken || - data?.data?.share_token || - data?.data?.token; + data?.data?.stoken || data?.data?.share_token || data?.data?.token; if (!stoken) { throw new Error('夸克分享 token 缺失'); @@ -211,12 +353,17 @@ async function fetchShareFolderItems( _fetch_banner: '0', }); - const response = await fetch( - buildApiUrl(QUARK_SHARE_API_BASE, '/share/sharepage/detail', query.toString()), + const response = await quarkFetch( + buildApiUrl( + QUARK_SHARE_API_BASE, + '/share/sharepage/detail', + query.toString() + ), { method: 'GET', headers: getHeaders(cookie), - } + }, + cookie ); const data = await parseJson(response); @@ -247,12 +394,13 @@ async function fetchDriveFolderItems( _sort: 'file_type:asc,file_name:asc', }); - const response = await fetch( + const response = await quarkFetch( buildApiUrl(QUARK_DRIVE_API_BASE, '/file/sort', query.toString()), { method: 'GET', headers: getHeaders(cookie), - } + }, + cookie ); const data = await parseJson(response); @@ -294,9 +442,13 @@ async function findDirectoryByName( folderName: string ): Promise { const items = await fetchAllDriveFolderItems(cookie, 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 + ); } async function findDriveFileInFolder( @@ -308,19 +460,23 @@ async function findDriveFileInFolder( if (!fileName) return null; const items = await fetchAllDriveFolderItems(cookie, parentFid); - return items.find((item: any) => { - const isDir = Boolean(item.dir || item.is_dir || item.file_type === 0); - if (isDir) return false; - const itemName = String(item.file_name || item.name || ''); - if (itemName !== fileName) return false; - if (input.size && Number(item.size || 0) > 0) { - return Number(item.size || 0) === input.size; - } - return true; - }) || null; + return ( + items.find((item: any) => { + const isDir = Boolean(item.dir || item.is_dir || item.file_type === 0); + if (isDir) return false; + const itemName = String(item.file_name || item.name || ''); + if (itemName !== fileName) return false; + if (input.size && Number(item.size || 0) > 0) { + return Number(item.size || 0) === input.size; + } + return true; + }) || null + ); } -export async function validateQuarkCookieReadable(cookie: string): Promise { +export async function validateQuarkCookieReadable( + cookie: string +): Promise { const safeCookie = assertQuarkCookieHeaderSafe(cookie); await fetchDriveFolderItems(safeCookie, '0'); } @@ -333,7 +489,12 @@ export async function listQuarkShareVideos( 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 allItems = await collectShareItemsRecursive( + safeCookie, + share.pwdId, + stoken, + '0' + ); const files = allItems .filter((item) => !item.dir && isVideoFile(item.fileName)) .map((item) => ({ @@ -361,24 +522,25 @@ async function createDriveFolder( parentFid: string, folderName: string ) { - const response = await fetch(buildApiUrl(QUARK_DRIVE_API_BASE, '/file'), { - method: 'POST', - headers: getHeaders(cookie), - body: JSON.stringify({ - pdir_fid: parentFid, - file_name: folderName, - dir_path: '', - dir_init_lock: false, - }), - }); + const response = await quarkFetch( + buildApiUrl(QUARK_DRIVE_API_BASE, '/file'), + { + method: 'POST', + headers: getHeaders(cookie), + body: JSON.stringify({ + pdir_fid: parentFid, + file_name: folderName, + dir_path: '', + dir_init_lock: false, + }), + }, + cookie + ); const data = await parseJson(response); ensureOk(data, `创建夸克目录失败:${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; if (!fid) { throw new Error(`夸克目录创建成功但未返回 fid:${folderName}`); @@ -466,7 +628,7 @@ async function submitSaveTask( throw new Error('没有可保存的文件'); } - const response = await fetch( + const response = await quarkFetch( buildApiUrl(QUARK_SHARE_API_BASE, '/share/sharepage/save'), { method: 'POST', @@ -482,7 +644,8 @@ async function submitSaveTask( fid_token_list: items.map((item) => item.shareFidToken || ''), share_fid_token_list: items.map((item) => item.shareFidToken || ''), }), - } + }, + cookie ); const data = await parseJson(response); @@ -497,10 +660,14 @@ async function pollTask(cookie: string, taskId: string) { retry_index: String(i), }); - const response = await fetch(buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()), { - method: 'GET', - headers: getHeaders(cookie), - }); + const response = await quarkFetch( + buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()), + { + method: 'GET', + headers: getHeaders(cookie), + }, + cookie + ); const data = await parseJson(response); ensureOk(data, '查询夸克任务状态失败'); @@ -515,11 +682,7 @@ async function pollTask(cookie: string, taskId: string) { 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 || '夸克任务执行失败'); } @@ -540,11 +703,20 @@ export async function transferQuarkShare( const safeCookie = assertQuarkCookieHeaderSafe(cookie); const share = parseQuarkShareUrl(input.shareUrl, input.passcode); 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 existedItems = await fetchAllDriveFolderItems(safeCookie, target.fid); - const existedNames = new Set(existedItems.map((item: any) => getDriveItemName(item))); - const pendingItems = topLevelItems.filter((item) => !existedNames.has(item.fileName)); + const existedNames = new Set( + existedItems.map((item: any) => getDriveItemName(item)) + ); + const pendingItems = topLevelItems.filter( + (item) => !existedNames.has(item.fileName) + ); if (pendingItems.length === 0) { 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) { await pollTask(safeCookie, taskId); @@ -579,16 +757,33 @@ export async function createQuarkInstantPlayFolder( const safeCookie = assertQuarkCookieHeaderSafe(cookie); const share = parseQuarkShareUrl(input.shareUrl, input.passcode); const { stoken, shareTitle } = await fetchShareToken(safeCookie, share); - const allItems = await collectShareItemsRecursive(safeCookie, share.pwdId, stoken, '0'); - const videoItems = allItems.filter((item) => !item.dir && isVideoFile(item.fileName)); + const allItems = await collectShareItemsRecursive( + safeCookie, + share.pwdId, + stoken, + '0' + ); + const videoItems = allItems.filter( + (item) => !item.dir && isVideoFile(item.fileName) + ); if (videoItems.length === 0) { throw new Error('分享中没有可播放的视频文件'); } - const tempRoot = await ensureQuarkDrivePath(safeCookie, input.playTempSavePath); - const folderName = buildInstantPlayFolderName(share.pwdId, input.title || shareTitle); - const existedFolder = await findDirectoryByName(safeCookie, tempRoot.fid, folderName); + const tempRoot = await ensureQuarkDrivePath( + safeCookie, + input.playTempSavePath + ); + const folderName = buildInstantPlayFolderName( + share.pwdId, + input.title || shareTitle + ); + const existedFolder = await findDirectoryByName( + safeCookie, + tempRoot.fid, + folderName + ); if (existedFolder) { return { @@ -599,8 +794,18 @@ export async function createQuarkInstantPlayFolder( }; } - const folderFid = await createDriveFolder(safeCookie, tempRoot.fid, folderName); - const taskId = await submitSaveTask(safeCookie, share, stoken, folderFid, videoItems); + const folderFid = await createDriveFolder( + safeCookie, + tempRoot.fid, + folderName + ); + const taskId = await submitSaveTask( + safeCookie, + share, + stoken, + folderFid, + videoItems + ); if (taskId) { await pollTask(safeCookie, taskId); @@ -625,7 +830,11 @@ export async function ensureQuarkPlayFolder( const safeCookie = assertQuarkCookieHeaderSafe(cookie); const tempRoot = await ensureQuarkDrivePath(safeCookie, playTempSavePath); const folderName = buildInstantPlayFolderName(shareId, title); - const existedFolder = await findDirectoryByName(safeCookie, tempRoot.fid, folderName); + const existedFolder = await findDirectoryByName( + safeCookie, + tempRoot.fid, + folderName + ); if (existedFolder) { return { 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 { folderFid, folderPath: joinPath(tempRoot.path, folderName), @@ -656,10 +869,14 @@ export async function saveQuarkShareFile( ): Promise { const safeCookie = assertQuarkCookieHeaderSafe(cookie); - const existedFile = await findDriveFileInFolder(safeCookie, input.playFolderFid, { - fileName: input.fileName, - size: input.size, - }); + const existedFile = await findDriveFileInFolder( + safeCookie, + input.playFolderFid, + { + fileName: input.fileName, + size: input.size, + } + ); if (existedFile) { return String(existedFile.fid || existedFile.file_id); } @@ -690,10 +907,14 @@ export async function saveQuarkShareFile( retry_index: String(i), }); - const response = await fetch(buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()), { - method: 'GET', - headers: getHeaders(safeCookie), - }); + const response = await quarkFetch( + buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()), + { + method: 'GET', + headers: getHeaders(safeCookie), + }, + safeCookie + ); const data = await parseJson(response); ensureOk(data, '查询夸克任务状态失败'); @@ -702,20 +923,31 @@ export async function saveQuarkShareFile( return String(saveAsTopFids[0]); } - const savedFile = await findDriveFileInFolder(safeCookie, input.playFolderFid, { - fileName: input.fileName, - size: input.size, - }); + const savedFile = await findDriveFileInFolder( + safeCookie, + input.playFolderFid, + { + fileName: input.fileName, + size: input.size, + } + ); if (savedFile) { return String(savedFile.fid || savedFile.file_id); } const status = data?.data?.status; 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; } @@ -735,14 +967,18 @@ export async function getQuarkPlayUrls( 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 response = await quarkFetch( + buildApiUrl(QUARK_DRIVE_API_BASE, '/file/download'), + { + method: 'POST', + headers, + body: JSON.stringify({ + fids: [savedFileId], + }), + cache: 'no-store', + }, + safeCookie + ); const data = await parseJson(response); ensureOk(data, '获取夸克下载地址失败'); const downloadUrl = data?.data?.[0]?.download_url; @@ -758,16 +994,20 @@ export async function getQuarkPlayUrls( } 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 response = await quarkFetch( + 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', + }, + safeCookie + ); const data = await parseJson(response); ensureOk(data, '获取夸克转码地址失败'); const nameMap: Record = { @@ -795,7 +1035,9 @@ export async function getQuarkPlayUrls( 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) => { if (playMode === 'transcode_first') { if (a.name === '原画' && b.name !== '原画') return 1; @@ -811,14 +1053,21 @@ export async function getQuarkPlayUrls( return deduped; } -function parseContentRangeHeader(contentRange: string | null): QuarkRangeWindow | null { +function parseContentRangeHeader( + contentRange: string | null +): QuarkRangeWindow | null { if (!contentRange) return null; const match = contentRange.match(/^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/i); if (!match) return null; const start = Number(match[1]); const end = Number(match[2]); 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 }; } @@ -843,7 +1092,9 @@ export async function probeQuarkPlayRange( return null; } - const window = parseContentRangeHeader(response.headers.get('content-range')); + const window = parseContentRangeHeader( + response.headers.get('content-range') + ); return { response, window }; } finally { clearTimeout(timeoutId); diff --git a/src/lib/netdisk/uc.client.ts b/src/lib/netdisk/uc.client.ts index 3ecc09e..91e288c 100644 --- a/src/lib/netdisk/uc.client.ts +++ b/src/lib/netdisk/uc.client.ts @@ -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 { getConfig, setCachedConfig } from '@/lib/config'; +import { db } from '@/lib/db'; + 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_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_CHANNEL = 'UCTVOFFICIALWEB'; +type UCRenewableCookieName = '__puus' | '__pus'; + +const UC_RENEWABLE_COOKIE_NAMES: UCRenewableCookieName[] = ['__puus', '__pus']; +const runtimeCookieValues: Partial> = {}; +let ucCookiePersistQueue: Promise = 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> { + const values: Partial> = {}; + 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> +): Promise { + 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 { + const renewedValues = extractRenewableCookieValues(response); + await persistRenewedUCCookie(requestCookie, renewedValues); +} + +async function ucFetch( + input: RequestInfo | URL, + init: RequestInit, + requestCookie: string +): Promise { + const response = await fetch(input, init); + await renewUCCookieFromResponse(response, requestCookie); + return response; +} + export interface UCShareLinkInfo { pwdId: string; passcode: string; @@ -39,7 +171,23 @@ export interface UCShareVideoListResult { } 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(); @@ -55,7 +203,7 @@ function buildApiUrl(base: string, path: string, ut: string, query = '') { function getHeaders(cookie: string): HeadersInit { return { 'content-type': 'application/json', - cookie, + cookie: applyRuntimeCookieValues(cookie), referer: 'https://drive.uc.cn', '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', @@ -63,8 +211,9 @@ function getHeaders(cookie: string): HeadersInit { } export function getUCPlayHeaders(cookie: string): Record { + const renewedCookie = applyRuntimeCookieValues(cookie); const keepKeys = ['_UP_A4A_11_', 'tfstk', '__uid', '__pus', '__kp', '__puus']; - const filteredCookie = cookie + const filteredCookie = renewedCookie .split(';') .map((item) => item.trim()) .filter((item) => keepKeys.some((key) => item.includes(key))) @@ -90,7 +239,9 @@ export function assertUCCookieHeaderSafe(cookie: string): string { const normalized = normalizeUCCookie(cookie); for (let i = 0; i < normalized.length; i += 1) { if (normalized.charCodeAt(i) > 255) { - throw new Error('UC Cookie 含有非法字符,请确认没有中文标点、中文空格或说明文字'); + throw new Error( + 'UC Cookie 含有非法字符,请确认没有中文标点、中文空格或说明文字' + ); } } return normalized; @@ -136,11 +287,21 @@ async function resolveUCUt(cookie: string) { if (cached) return cached; const tryFetchUt = async (headers?: HeadersInit) => { - const response = await fetch(`${UC_DRIVE_API_BASE}/file`, { - method: 'GET', - headers, - cache: 'no-store', - }); + const response = headers + ? await ucFetch( + `${UC_DRIVE_API_BASE}/file`, + { + 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(); if (!response.ok || !text) return ''; return text; @@ -177,24 +338,37 @@ export function parseUCShareUrl(url: string, passcode = ''): UCShareLinkInfo { } return { 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) { - const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/token', ut), { - method: 'POST', - headers: getHeaders(cookie), - body: JSON.stringify({ - pwd_id: share.pwdId, - passcode: share.passcode, - }), - cache: 'no-store', - }); +async function fetchShareToken( + cookie: string, + share: UCShareLinkInfo, + ut: string +) { + const response = await ucFetch( + buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/token', ut), + { + method: 'POST', + headers: getHeaders(cookie), + body: JSON.stringify({ + pwd_id: share.pwdId, + passcode: share.passcode, + }), + cache: 'no-store', + }, + cookie + ); const data = await parseJson(response); 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) { 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({ pwd_id: pwdId, stoken, @@ -214,11 +395,20 @@ async function fetchShareFolderItems(cookie: string, pwdId: string, stoken: stri _size: '100', _sort: 'file_type:asc,file_name:asc', }); - const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/detail', ut, query.toString()), { - method: 'GET', - headers: getHeaders(cookie), - cache: 'no-store', - }); + const response = await ucFetch( + buildApiUrl( + UC_SHARE_API_BASE, + '/share/sharepage/detail', + ut, + query.toString() + ), + { + method: 'GET', + headers: getHeaders(cookie), + cache: 'no-store', + }, + cookie + ); const data = await parseJson(response); ensureOk(data, '获取 UC 分享详情失败'); @@ -228,7 +418,8 @@ async function fetchShareFolderItems(cookie: string, pwdId: string, stoken: stri fid: String(item.fid || item.file_id || ''), fileName: String(item.file_name || item.name || ''), 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'), 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 { +async function collectShareItemsRecursive( + cookie: string, + pwdId: string, + stoken: string, + ut: string, + pdirFid = '0' +): Promise { const result: UCShareItem[] = []; const pageSize = 100; 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) { 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); } else { 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; } @@ -259,46 +470,75 @@ function isVideoFile(fileName: string) { return VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext)); } -async function fetchDriveFolderItems(cookie: string, ut: string, pdirFid = '0', page = 1, size = 200): Promise { +async function fetchDriveFolderItems( + cookie: string, + ut: string, + pdirFid = '0', + page = 1, + size = 200 +): Promise { const query = new URLSearchParams({ pdir_fid: pdirFid, _page: String(page), _size: String(size), _sort: 'file_type:asc,file_name:asc', }); - const response = await fetch(buildApiUrl(UC_DRIVE_API_BASE, '/file/sort', ut, query.toString()), { - method: 'GET', - headers: getHeaders(cookie), - cache: 'no-store', - }); + const response = await ucFetch( + buildApiUrl(UC_DRIVE_API_BASE, '/file/sort', ut, query.toString()), + { + method: 'GET', + headers: getHeaders(cookie), + cache: 'no-store', + }, + cookie + ); const data = await parseJson(response); ensureOk(data, '获取 UC 目录列表失败'); return data?.data?.list || []; } -async function fetchAllDriveFolderItems(cookie: string, ut: string, pdirFid = '0'): Promise { +async function fetchAllDriveFolderItems( + cookie: string, + ut: string, + pdirFid = '0' +): Promise { const allItems: any[] = []; const pageSize = 200; 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); if (items.length < pageSize) break; } return allItems; } -async function createDriveFolder(cookie: string, ut: string, parentFid: string, folderName: string) { - const response = await fetch(buildApiUrl(UC_DRIVE_API_BASE, '/file', ut), { - method: 'POST', - headers: getHeaders(cookie), - body: JSON.stringify({ - pdir_fid: parentFid, - file_name: folderName, - dir_path: '', - dir_init_lock: false, - }), - cache: 'no-store', - }); +async function createDriveFolder( + cookie: string, + ut: string, + parentFid: string, + folderName: string +) { + const response = await ucFetch( + buildApiUrl(UC_DRIVE_API_BASE, '/file', ut), + { + method: 'POST', + 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); ensureOk(data, `创建 UC 目录失败:${folderName}`); 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); } -async function findDirectoryByName(cookie: string, ut: string, parentFid: string, folderName: string): Promise { +async function findDirectoryByName( + cookie: string, + ut: string, + parentFid: string, + folderName: string +): Promise { 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 { @@ -317,14 +568,29 @@ export async function validateUCCookieReadable(cookie: string): Promise { await fetchDriveFolderItems(safeCookie, ut, '0'); } -export async function listUCShareVideos(shareUrl: string, cookie: string, passcode = ''): Promise { +export async function listUCShareVideos( + shareUrl: string, + cookie: string, + passcode = '' +): Promise { const safeCookie = assertUCCookieHeaderSafe(cookie); const ut = await resolveUCUt(safeCookie); const share = parseUCShareUrl(shareUrl, passcode); 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 - .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) => ({ fid: item.fid, 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 ut = await resolveUCUt(safeCookie); const normalized = normalizePath(inputPath); @@ -352,7 +621,10 @@ export async function ensureUCDrivePath(cookie: string, inputPath: string): Prom let currentPath = ''; for (const segment of segments) { 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); if (existed) { 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 || '/' }; } -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('没有可保存的文件'); - const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/save', ut), { - method: 'POST', - headers: getHeaders(cookie), - body: JSON.stringify({ - pwd_id: share.pwdId, - stoken, - pdir_fid: '0', - to_pdir_fid: toPdirFid, - scene: 'link', - filelist: items.map((item) => item.fid), - fid_list: items.map((item) => item.fid), - fid_token_list: items.map((item) => item.shareFidToken || ''), - share_fid_token_list: items.map((item) => item.shareFidToken || ''), - }), - cache: 'no-store', - }); + const response = await ucFetch( + buildApiUrl(UC_SHARE_API_BASE, '/share/sharepage/save', ut), + { + method: 'POST', + headers: getHeaders(cookie), + body: JSON.stringify({ + pwd_id: share.pwdId, + stoken, + pdir_fid: '0', + to_pdir_fid: toPdirFid, + scene: 'link', + filelist: items.map((item) => item.fid), + fid_list: items.map((item) => item.fid), + fid_token_list: items.map((item) => item.shareFidToken || ''), + share_fid_token_list: items.map((item) => item.shareFidToken || ''), + }), + cache: 'no-store', + }, + cookie + ); const data = await parseJson(response); ensureOk(data, '提交 UC 转存任务失败'); 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) { for (let i = 0; i < 25; i += 1) { - const query = new URLSearchParams({ task_id: taskId, retry_index: String(i) }); - const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/task', ut, query.toString()), { - method: 'GET', headers: getHeaders(cookie), cache: 'no-store', + const query = new URLSearchParams({ + task_id: taskId, + 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); ensureOk(data, '查询 UC 任务状态失败'); 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) { 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 任务处理超时'); } -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 ut = await resolveUCUt(safeCookie); const tempRoot = await ensureUCDrivePath(safeCookie, playTempSavePath); 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) { return { folderFid: String(existedFolder.fid || existedFolder.file_id), @@ -417,19 +725,58 @@ export async function ensureUCPlayFolder(cookie: string, playTempSavePath: strin folderName, }; } - const folderFid = await createDriveFolder(safeCookie, ut, tempRoot.fid, folderName); - return { folderFid, folderPath: joinPath(tempRoot.path, folderName), folderName }; + const folderFid = await createDriveFolder( + 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 { +export async function saveUCShareFile( + cookie: string, + input: { + shareId: string; + shareToken: string; + fileId: string; + shareFileToken?: string; + playFolderFid: string; + } +): Promise { const safeCookie = assertUCCookieHeaderSafe(cookie); 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 转存任务创建失败'); for (let i = 0; i < 25; i += 1) { - const query = new URLSearchParams({ task_id: taskId, retry_index: String(i) }); - const response = await fetch(buildApiUrl(UC_SHARE_API_BASE, '/task', ut, query.toString()), { method: 'GET', headers: getHeaders(safeCookie), cache: 'no-store' }); + const query = new URLSearchParams({ + 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); ensureOk(data, '查询 UC 任务状态失败'); 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; 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)); } throw new Error('UC 转存结果获取失败'); @@ -451,17 +806,45 @@ function generateDeviceID(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) { - return crypto.createHash('sha256').update(`${method}&${pathname}&${timestamp}&${key}`).digest('hex'); +function generateXPanToken( + 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 }>> { +export async function getUCPlayUrls( + cookie: string, + savedFileId: string, + token = '' +): Promise< + Array<{ + name: string; + url: string; + priority: number; + headers?: Record; + }> +> { const safeCookie = assertUCCookieHeaderSafe(cookie); const ut = await resolveUCUt(safeCookie); - const urls: Array<{ name: string; url: string; priority: number; headers?: Record }> = []; + const urls: Array<{ + name: string; + url: string; + priority: number; + headers?: Record; + }> = []; if (token) { try { @@ -469,7 +852,12 @@ export async function getUCPlayUrls(cookie: string, savedFileId: string, token = const timestamp = `${Math.floor(Date.now() / 1000)}000`; const deviceId = generateDeviceID(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({ req_id: reqId, access_token: token, @@ -490,25 +878,36 @@ export async function getUCPlayUrls(cookie: string, savedFileId: string, token = resolution: 'low,normal,high,super,2k,4k', support: 'dolby_vision', }); - const response = await fetch(`${UC_OPEN_API_BASE}${pathname}?${query.toString()}`, { - method: 'GET', - headers: { - 'user-agent': 'Mozilla/5.0 (Linux; Android 9) AppleWebKit/533.1 (KHTML, like Gecko) Mobile Safari/533.1', - connection: 'Keep-Alive', - 'accept-encoding': 'gzip', - 'x-pan-tm': timestamp, - 'x-pan-token': xPanToken, - 'content-type': 'text/plain;charset=UTF-8', - 'x-pan-client-id': UC_OPEN_API_CLIENT_ID, - }, - cache: 'no-store', - }); + const response = await fetch( + `${UC_OPEN_API_BASE}${pathname}?${query.toString()}`, + { + method: 'GET', + headers: { + 'user-agent': + 'Mozilla/5.0 (Linux; Android 9) AppleWebKit/533.1 (KHTML, like Gecko) Mobile Safari/533.1', + connection: 'Keep-Alive', + 'accept-encoding': 'gzip', + 'x-pan-tm': timestamp, + 'x-pan-token': xPanToken, + '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 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; if (openVideoInfo?.url) { - urls.push({ name: '原画', url: String(openVideoInfo.url), priority: 9999, headers: {} }); + urls.push({ + name: '原画', + url: String(openVideoInfo.url), + priority: 9999, + headers: {}, + }); } } catch { // ignore token failure, fallback to cookie mode @@ -516,43 +915,67 @@ export async function getUCPlayUrls(cookie: string, savedFileId: string, token = } try { - const response = await fetch(buildApiUrl(UC_DRIVE_API_BASE, '/file/download', ut), { - method: 'POST', - headers: getHeaders(safeCookie), - body: JSON.stringify({ fids: [savedFileId] }), - cache: 'no-store', - }); + const response = await ucFetch( + buildApiUrl(UC_DRIVE_API_BASE, '/file/download', ut), + { + method: 'POST', + headers: getHeaders(safeCookie), + body: JSON.stringify({ fids: [savedFileId] }), + cache: 'no-store', + }, + safeCookie + ); const data = await parseJson(response); ensureOk(data, '获取 UC 下载地址失败'); const downloadUrl = data?.data?.[0]?.download_url; 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 { // ignore } try { - const response = await fetch(buildApiUrl(UC_DRIVE_API_BASE, '/file/v2/play', ut), { - method: 'POST', - headers: getHeaders(safeCookie), - body: JSON.stringify({ - fid: savedFileId, - resolutions: 'normal,low,high,super,2k,4k', - supports: 'fmp4', - }), - cache: 'no-store', - }); + const response = await ucFetch( + buildApiUrl(UC_DRIVE_API_BASE, '/file/v2/play', ut), + { + method: 'POST', + headers: getHeaders(safeCookie), + body: JSON.stringify({ + fid: savedFileId, + resolutions: 'normal,low,high,super,2k,4k', + supports: 'fmp4', + }), + cache: 'no-store', + }, + safeCookie + ); const data = await parseJson(response); ensureOk(data, '获取 UC 转码地址失败'); - const nameMap: Record = { FOUR_K: '4K', SUPER: '超清', HIGH: '高清', NORMAL: '流畅', LOW: '低清' }; + const nameMap: Record = { + 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, 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 } - 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); if (deduped.length === 0) throw new Error('未获取到 UC 播放地址'); return deduped;