夸克网盘在线播放移除openlist依赖
This commit is contained in:
@@ -3704,8 +3704,6 @@ const NetDiskConfigComponent = ({
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [cookie, setCookie] = useState('');
|
||||
const [savePath, setSavePath] = useState('/');
|
||||
const [playTempSavePath, setPlayTempSavePath] = useState('/');
|
||||
const [openListTempPath, setOpenListTempPath] = useState('/');
|
||||
const [mobileEnabled, setMobileEnabled] = useState(false);
|
||||
const [mobileAuthorization, setMobileAuthorization] = useState('');
|
||||
const [baiduEnabled, setBaiduEnabled] = useState(false);
|
||||
@@ -3717,8 +3715,6 @@ const NetDiskConfigComponent = ({
|
||||
setEnabled(quark?.Enabled || false);
|
||||
setCookie(quark?.Cookie || '');
|
||||
setSavePath(quark?.SavePath || '/');
|
||||
setPlayTempSavePath(quark?.PlayTempSavePath || '/');
|
||||
setOpenListTempPath(quark?.OpenListTempPath || '/');
|
||||
setMobileEnabled(mobile?.Enabled || false);
|
||||
setMobileAuthorization(mobile?.Authorization || '');
|
||||
setBaiduEnabled(config?.NetDiskConfig?.Baidu?.Enabled || false);
|
||||
@@ -3736,8 +3732,6 @@ const NetDiskConfigComponent = ({
|
||||
Enabled: enabled,
|
||||
Cookie: cookie,
|
||||
SavePath: savePath,
|
||||
PlayTempSavePath: playTempSavePath,
|
||||
OpenListTempPath: openListTempPath,
|
||||
},
|
||||
Mobile: {
|
||||
Enabled: mobileEnabled,
|
||||
@@ -3771,7 +3765,6 @@ const NetDiskConfigComponent = ({
|
||||
Quark: {
|
||||
Cookie: cookie,
|
||||
SavePath: savePath,
|
||||
PlayTempSavePath: playTempSavePath,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -3852,20 +3845,6 @@ const NetDiskConfigComponent = ({
|
||||
夸克网盘
|
||||
</summary>
|
||||
<div className='mt-4 space-y-4'>
|
||||
<div className='bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4'>
|
||||
<div className='flex items-center gap-2 mb-2'>
|
||||
<Cloud className='w-5 h-5 text-blue-600 dark:text-blue-400' />
|
||||
<span className='text-sm font-medium text-blue-800 dark:text-blue-300'>
|
||||
夸克网盘说明
|
||||
</span>
|
||||
</div>
|
||||
<div className='text-sm text-blue-700 dark:text-blue-400 space-y-1'>
|
||||
<p>• 转存:把整个分享保存到夸克正式目录。</p>
|
||||
<p>• 立即播放:将分享内所有视频文件转存到临时播放目录,再通过 OpenList 临时目录直接播放。</p>
|
||||
<p>• OpenList 临时目录必须映射到夸克临时播放目录,否则立即播放无法找到文件。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'>
|
||||
<div>
|
||||
<h3 className='text-sm font-medium text-gray-900 dark:text-gray-100'>
|
||||
@@ -3914,37 +3893,6 @@ const NetDiskConfigComponent = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
播放临时转存位置
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
value={playTempSavePath}
|
||||
onChange={(e) => setPlayTempSavePath(e.target.value)}
|
||||
disabled={!enabled}
|
||||
placeholder='/影视/.play-temp'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
OpenList 临时目录
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
value={openListTempPath}
|
||||
onChange={(e) => setOpenListTempPath(e.target.value)}
|
||||
disabled={!enabled}
|
||||
placeholder='/Quark/影视/.play-temp'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
OpenList 中能访问到临时播放目录的路径。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3'>
|
||||
<button
|
||||
onClick={handleValidate}
|
||||
|
||||
@@ -60,8 +60,6 @@ export async function POST(request: NextRequest) {
|
||||
Enabled: Boolean(Quark?.Enabled),
|
||||
Cookie: normalizedCookie,
|
||||
SavePath: Quark?.SavePath || '/',
|
||||
PlayTempSavePath: Quark?.PlayTempSavePath || '/',
|
||||
OpenListTempPath: Quark?.OpenListTempPath || '/',
|
||||
};
|
||||
adminConfig.NetDiskConfig.Mobile = {
|
||||
Enabled: Boolean(Mobile?.Enabled),
|
||||
|
||||
@@ -2,21 +2,13 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { createQuarkInstantPlayFolder } from '@/lib/netdisk/quark.client';
|
||||
import { listQuarkShareVideos } from '@/lib/netdisk/quark.client';
|
||||
import { createQuarkNetdiskSession } from '@/lib/netdisk/quark-session-cache';
|
||||
import { NETDISK_QUARK_SOURCE } from '@/lib/netdisk/source';
|
||||
import { hasFeaturePermission } from '@/lib/permissions';
|
||||
import { base58Encode } from '@/lib/utils';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
function joinPath(...parts: string[]) {
|
||||
const joined = parts
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
.replace(/\/+/g, '/');
|
||||
return joined.startsWith('/') ? joined : `/${joined}`;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
@@ -34,54 +26,26 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const config = await getConfig();
|
||||
const quarkConfig = config.NetDiskConfig?.Quark;
|
||||
|
||||
if (!quarkConfig?.Enabled || !quarkConfig.Cookie) {
|
||||
return NextResponse.json({ error: '夸克网盘未配置或未启用' }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await createQuarkInstantPlayFolder(quarkConfig.Cookie, {
|
||||
const result = await listQuarkShareVideos(shareUrl, quarkConfig.Cookie, passcode || '');
|
||||
const session = createQuarkNetdiskSession({
|
||||
title: title || result.title,
|
||||
shareUrl,
|
||||
passcode,
|
||||
playTempSavePath: quarkConfig.PlayTempSavePath,
|
||||
title,
|
||||
shareId: result.shareId,
|
||||
shareToken: result.shareToken,
|
||||
files: result.files,
|
||||
});
|
||||
|
||||
if (!result.folderName) {
|
||||
throw new Error('未生成临时播放目录');
|
||||
}
|
||||
|
||||
const openlistFolderPath = joinPath(
|
||||
quarkConfig.OpenListTempPath,
|
||||
result.folderName
|
||||
);
|
||||
|
||||
if (
|
||||
config.OpenListConfig?.Enabled &&
|
||||
config.OpenListConfig.URL &&
|
||||
config.OpenListConfig.Username &&
|
||||
config.OpenListConfig.Password
|
||||
) {
|
||||
try {
|
||||
const { OpenListClient } = await import('@/lib/openlist.client');
|
||||
const openListClient = new OpenListClient(
|
||||
config.OpenListConfig.URL,
|
||||
config.OpenListConfig.Username,
|
||||
config.OpenListConfig.Password
|
||||
);
|
||||
await openListClient.refreshDirectory(quarkConfig.OpenListTempPath || '/');
|
||||
await openListClient.refreshDirectory(openlistFolderPath);
|
||||
} catch (refreshError) {
|
||||
console.warn('[quark instant-play] 刷新 OpenList 临时目录失败:', refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
source: NETDISK_QUARK_SOURCE,
|
||||
id: base58Encode(openlistFolderPath),
|
||||
title: title || result.folderName,
|
||||
openlistFolderPath,
|
||||
...result,
|
||||
id: session.id,
|
||||
title: title || result.title,
|
||||
fileCount: result.files.length,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getQuarkNetdiskSession, refreshQuarkNetdiskSession } from '@/lib/netdisk/quark-session-cache';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo?.username) {
|
||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
const episodeIndexRaw = searchParams.get('episodeIndex');
|
||||
const format = searchParams.get('format');
|
||||
if (!id || episodeIndexRaw == null) {
|
||||
return NextResponse.json({ error: '缺少参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
const episodeIndex = Number.parseInt(episodeIndexRaw, 10);
|
||||
if (!Number.isInteger(episodeIndex) || episodeIndex < 0) {
|
||||
return NextResponse.json({ error: '无效的 episodeIndex' }, { status: 400 });
|
||||
}
|
||||
|
||||
const proxyUrl = `/api/netdisk/quark/proxy?id=${encodeURIComponent(id)}&episodeIndex=${episodeIndex}`;
|
||||
refreshQuarkNetdiskSession(id) || getQuarkNetdiskSession(id);
|
||||
|
||||
if (format === 'json') {
|
||||
return NextResponse.json({ url: proxyUrl, headers: {} });
|
||||
}
|
||||
|
||||
return NextResponse.redirect(new URL(proxyUrl, request.url));
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : '获取播放地址失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { ensureQuarkPlayFolder, getQuarkPlayHeaders, getQuarkPlayUrls, saveQuarkShareFile } from '@/lib/netdisk/quark.client';
|
||||
import { refreshQuarkNetdiskSession } from '@/lib/netdisk/quark-session-cache';
|
||||
import { resolveQuarkSession } from '@/lib/netdisk/quark-session-resolver';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo?.username) {
|
||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
const episodeIndexRaw = searchParams.get('episodeIndex');
|
||||
const quality = searchParams.get('quality') || '';
|
||||
if (!id || episodeIndexRaw == null) {
|
||||
return NextResponse.json({ error: '缺少参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
const episodeIndex = Number.parseInt(episodeIndexRaw, 10);
|
||||
if (!Number.isInteger(episodeIndex) || episodeIndex < 0) {
|
||||
return NextResponse.json({ error: '无效的 episodeIndex' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { session, cookie, savePath } = await resolveQuarkSession(id);
|
||||
const file = session.files[episodeIndex];
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: '播放文件不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!session.playFolderFid || !session.playFolderPath) {
|
||||
const folder = await ensureQuarkPlayFolder(cookie, savePath, session.shareId, session.title);
|
||||
session.playFolderFid = folder.folderFid;
|
||||
session.playFolderPath = folder.folderPath;
|
||||
}
|
||||
|
||||
let savedFileId = session.savedFileIds[file.fid];
|
||||
if (!savedFileId) {
|
||||
savedFileId = await saveQuarkShareFile(cookie, {
|
||||
shareId: session.shareId,
|
||||
shareToken: session.shareToken,
|
||||
fileId: file.fid,
|
||||
shareFileToken: file.shareFidToken,
|
||||
playFolderFid: session.playFolderFid,
|
||||
});
|
||||
session.savedFileIds[file.fid] = savedFileId;
|
||||
}
|
||||
refreshQuarkNetdiskSession(id);
|
||||
|
||||
const playUrls = await getQuarkPlayUrls(cookie, savedFileId);
|
||||
const selected = playUrls.find((item) => item.name === quality) || playUrls[0];
|
||||
if (!selected) {
|
||||
return NextResponse.json({ error: '未获取到夸克播放地址' }, { status: 500 });
|
||||
}
|
||||
|
||||
const range = request.headers.get('range');
|
||||
const upstream = await fetch(selected.url, {
|
||||
headers: {
|
||||
...getQuarkPlayHeaders(cookie),
|
||||
...(range ? { Range: range } : {}),
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!upstream.ok || !upstream.body) {
|
||||
return NextResponse.json(
|
||||
{ error: `夸克视频代理失败 (${upstream.status})` },
|
||||
{ status: upstream.status || 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const responseHeaders = new Headers();
|
||||
const copyHeaders = ['content-type', 'content-length', 'content-range', 'accept-ranges', 'etag', 'last-modified'];
|
||||
copyHeaders.forEach((name) => {
|
||||
const value = upstream.headers.get(name);
|
||||
if (value) responseHeaders.set(name, value);
|
||||
});
|
||||
responseHeaders.set('Cache-Control', 'private, no-store');
|
||||
|
||||
return new Response(upstream.body, {
|
||||
status: range && upstream.headers.get('content-range') ? 206 : 200,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : '夸克网盘代理失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@ import {
|
||||
parseMobileNetdiskId,
|
||||
refreshMobileNetdiskSession,
|
||||
} from '@/lib/netdisk/mobile-session-cache';
|
||||
import {
|
||||
createQuarkNetdiskSession,
|
||||
getQuarkNetdiskSession,
|
||||
parseQuarkNetdiskId,
|
||||
refreshQuarkNetdiskSession,
|
||||
} from '@/lib/netdisk/quark-session-cache';
|
||||
import { LEGACY_QUARK_TEMP_SOURCE, NETDISK_BAIDU_SOURCE, NETDISK_MOBILE_SOURCE, NETDISK_QUARK_SOURCE, normalizeNetdiskSource } from '@/lib/netdisk/source';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
@@ -433,106 +439,39 @@ export async function GET(request: NextRequest) {
|
||||
if (sourceCode === NETDISK_QUARK_SOURCE || sourceCode === LEGACY_QUARK_TEMP_SOURCE) {
|
||||
try {
|
||||
const config = await getConfig();
|
||||
const openListConfig = config.OpenListConfig;
|
||||
|
||||
if (
|
||||
!openListConfig ||
|
||||
!openListConfig.Enabled ||
|
||||
!openListConfig.URL ||
|
||||
!openListConfig.Username ||
|
||||
!openListConfig.Password
|
||||
) {
|
||||
throw new Error('OpenList 未配置或未启用');
|
||||
const quarkConfig = config.NetDiskConfig?.Quark;
|
||||
if (!quarkConfig?.Enabled || !quarkConfig.Cookie) {
|
||||
throw new Error('夸克网盘未配置或未启用');
|
||||
}
|
||||
|
||||
const { base58Decode } = await import('@/lib/utils');
|
||||
const { OpenListClient } = await import('@/lib/openlist.client');
|
||||
const { parseVideoFileName } = await import('@/lib/video-parser');
|
||||
|
||||
const folderPath = base58Decode(id);
|
||||
if (!folderPath) {
|
||||
throw new Error('无效的临时播放目录');
|
||||
let session = refreshQuarkNetdiskSession(id) || getQuarkNetdiskSession(id);
|
||||
if (!session) {
|
||||
const payload = parseQuarkNetdiskId(id);
|
||||
const { listQuarkShareVideos } = await import('@/lib/netdisk/quark.client');
|
||||
const result = await listQuarkShareVideos(payload.shareUrl, quarkConfig.Cookie, payload.passcode || '');
|
||||
session = createQuarkNetdiskSession({
|
||||
title: title || result.title,
|
||||
shareUrl: payload.shareUrl,
|
||||
passcode: payload.passcode,
|
||||
shareId: result.shareId,
|
||||
shareToken: result.shareToken,
|
||||
files: result.files,
|
||||
});
|
||||
}
|
||||
if (!session) {
|
||||
throw new Error('夸克网盘播放信息恢复失败');
|
||||
}
|
||||
|
||||
const client = new OpenListClient(
|
||||
openListConfig.URL,
|
||||
openListConfig.Username,
|
||||
openListConfig.Password
|
||||
);
|
||||
|
||||
const videoExtensions = ['.mp4', '.mkv', '.avi', '.m3u8', '.flv', '.ts', '.mov', '.wmv', '.webm', '.rmvb', '.rm', '.mpg', '.mpeg', '.3gp', '.f4v', '.m4v', '.vob'];
|
||||
|
||||
const listTempDirectory = async (currentPath: string, page: number, pageSize: number) => {
|
||||
const load = async (refresh = false) => client.listDirectory(currentPath, page, pageSize, refresh);
|
||||
|
||||
let response = await load(page === 1);
|
||||
if (response.code === 200) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const parentPath = currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
|
||||
await client.refreshDirectory(parentPath);
|
||||
response = await load(true);
|
||||
|
||||
if (response.code !== 200) {
|
||||
const message = response.message || '目录不存在或 OpenList 路径未映射';
|
||||
throw new Error(`读取临时目录失败: ${message}(路径: ${currentPath})`);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const collectFiles = async (currentPath: string): Promise<Array<{ path: string; name: string }>> => {
|
||||
const allFiles: Array<{ path: string; name: string }> = [];
|
||||
let currentPage = 1;
|
||||
const pageSize = 100;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await listTempDirectory(currentPath, currentPage, pageSize);
|
||||
|
||||
for (const item of response.data.content) {
|
||||
const itemPath = `${currentPath}${currentPath.endsWith('/') ? '' : '/'}${item.name}`;
|
||||
if (item.is_dir) {
|
||||
const nested = await collectFiles(itemPath);
|
||||
allFiles.push(...nested);
|
||||
} else if (
|
||||
!item.name.startsWith('.') &&
|
||||
videoExtensions.some((ext) => item.name.toLowerCase().endsWith(ext))
|
||||
) {
|
||||
allFiles.push({
|
||||
path: itemPath,
|
||||
name: item.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hasMore = !(
|
||||
response.data.content.length < pageSize ||
|
||||
currentPage * pageSize >= response.data.total
|
||||
);
|
||||
currentPage += 1;
|
||||
}
|
||||
|
||||
return allFiles;
|
||||
};
|
||||
|
||||
const files = await collectFiles(folderPath);
|
||||
if (files.length === 0) {
|
||||
throw new Error('临时播放目录中没有视频文件');
|
||||
}
|
||||
|
||||
const episodes = files
|
||||
const quarkSession = session;
|
||||
const episodes = quarkSession.files
|
||||
.map((file, index) => {
|
||||
const parsed = parseVideoFileName(file.name);
|
||||
const fileDir = file.path.substring(0, file.path.lastIndexOf('/')) || '/';
|
||||
return {
|
||||
originalIndex: index,
|
||||
fileName: file.name,
|
||||
fileDir,
|
||||
episode: parsed.episode || index + 1,
|
||||
title:
|
||||
parsed.title ||
|
||||
(parsed.episode ? `第${parsed.episode}集` : file.name),
|
||||
title: parsed.title || (parsed.episode ? `第${parsed.episode}集` : file.name),
|
||||
isOVA: parsed.isOVA,
|
||||
};
|
||||
})
|
||||
@@ -546,14 +485,16 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
source: NETDISK_QUARK_SOURCE,
|
||||
source_name: '夸克临时播放',
|
||||
id,
|
||||
title: title || folderPath.split('/').filter(Boolean).pop() || '夸克临时播放',
|
||||
source_name: '夸克网盘',
|
||||
id: quarkSession.id,
|
||||
title: title || quarkSession.title,
|
||||
poster: '',
|
||||
year: '',
|
||||
douban_id: 0,
|
||||
desc: `临时播放目录:${folderPath}`,
|
||||
episodes: episodes.map((ep) => `/api/openlist/play?folder=${encodeURIComponent(ep.fileDir)}&fileName=${encodeURIComponent(ep.fileName)}`),
|
||||
desc: `夸克网盘分享:${quarkSession.shareUrl}`,
|
||||
episodes: episodes.map((ep) => (
|
||||
`/api/netdisk/quark/play?id=${encodeURIComponent(quarkSession.id)}&episodeIndex=${ep.originalIndex}`
|
||||
)),
|
||||
episodes_titles: episodes.map((ep) => ep.title),
|
||||
proxyMode: false,
|
||||
});
|
||||
|
||||
@@ -2504,6 +2504,7 @@ function PlayPageClient() {
|
||||
const isSpecialLazyPlayUrl =
|
||||
isXiaoyaLazyPlayUrl ||
|
||||
newUrl.startsWith('/api/openlist/play') ||
|
||||
newUrl.startsWith('/api/netdisk/quark/play') ||
|
||||
newUrl.startsWith('/api/netdisk/baidu/play') ||
|
||||
newUrl.startsWith('/api/source-script/play');
|
||||
|
||||
|
||||
@@ -151,8 +151,6 @@ export interface AdminConfig {
|
||||
Enabled: boolean;
|
||||
Cookie: string;
|
||||
SavePath: string;
|
||||
PlayTempSavePath: string;
|
||||
OpenListTempPath: string;
|
||||
};
|
||||
Mobile?: {
|
||||
Enabled: boolean;
|
||||
|
||||
@@ -669,8 +669,6 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
Enabled: false,
|
||||
Cookie: '',
|
||||
SavePath: '/',
|
||||
PlayTempSavePath: '/',
|
||||
OpenListTempPath: '/',
|
||||
},
|
||||
Mobile: {
|
||||
Enabled: false,
|
||||
@@ -688,8 +686,6 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
Enabled: false,
|
||||
Cookie: '',
|
||||
SavePath: '/',
|
||||
PlayTempSavePath: '/',
|
||||
OpenListTempPath: '/',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { base58Decode, base58Encode } from '@/lib/utils';
|
||||
|
||||
export interface QuarkNetdiskSessionFile {
|
||||
fid: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
shareFidToken?: string;
|
||||
pdirFid?: string;
|
||||
}
|
||||
|
||||
export interface QuarkNetdiskSession {
|
||||
id: string;
|
||||
provider: 'quark';
|
||||
title: string;
|
||||
shareUrl: string;
|
||||
passcode?: string;
|
||||
shareId: string;
|
||||
shareToken: string;
|
||||
files: QuarkNetdiskSessionFile[];
|
||||
savedFileIds: Record<string, string>;
|
||||
playFolderFid?: string;
|
||||
playFolderPath?: string;
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
const TTL_MS = 30 * 60 * 1000;
|
||||
const sessionStore = new Map<string, QuarkNetdiskSession>();
|
||||
|
||||
export function buildQuarkNetdiskId(input: { shareUrl: string; passcode?: string }): string {
|
||||
return base58Encode(
|
||||
JSON.stringify({
|
||||
shareUrl: input.shareUrl,
|
||||
passcode: input.passcode || '',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function parseQuarkNetdiskId(id: string): { shareUrl: string; passcode?: string } {
|
||||
try {
|
||||
const decoded = base58Decode(id);
|
||||
const parsed = JSON.parse(decoded);
|
||||
if (!parsed?.shareUrl || typeof parsed.shareUrl !== 'string') {
|
||||
throw new Error('invalid quark netdisk id');
|
||||
}
|
||||
return {
|
||||
shareUrl: parsed.shareUrl,
|
||||
passcode: typeof parsed.passcode === 'string' ? parsed.passcode : '',
|
||||
};
|
||||
} catch {
|
||||
throw new Error('无效的夸克网盘播放 ID');
|
||||
}
|
||||
}
|
||||
|
||||
function pruneExpiredSessions() {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of Array.from(sessionStore.entries())) {
|
||||
if (value.expiresAt <= now) {
|
||||
sessionStore.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createQuarkNetdiskSession(input: {
|
||||
title: string;
|
||||
shareUrl: string;
|
||||
passcode?: string;
|
||||
shareId: string;
|
||||
shareToken: string;
|
||||
files: QuarkNetdiskSessionFile[];
|
||||
}): QuarkNetdiskSession {
|
||||
pruneExpiredSessions();
|
||||
const now = Date.now();
|
||||
const id = buildQuarkNetdiskId({ shareUrl: input.shareUrl, passcode: input.passcode });
|
||||
const session: QuarkNetdiskSession = {
|
||||
id,
|
||||
provider: 'quark',
|
||||
title: input.title,
|
||||
shareUrl: input.shareUrl,
|
||||
passcode: input.passcode,
|
||||
shareId: input.shareId,
|
||||
shareToken: input.shareToken,
|
||||
files: input.files,
|
||||
savedFileIds: {},
|
||||
createdAt: now,
|
||||
expiresAt: now + TTL_MS,
|
||||
};
|
||||
sessionStore.set(id, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
export function getQuarkNetdiskSession(id: string): QuarkNetdiskSession | null {
|
||||
pruneExpiredSessions();
|
||||
const session = sessionStore.get(id);
|
||||
if (!session) return null;
|
||||
if (session.expiresAt <= Date.now()) {
|
||||
sessionStore.delete(id);
|
||||
return null;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
export function refreshQuarkNetdiskSession(id: string): QuarkNetdiskSession | null {
|
||||
const session = getQuarkNetdiskSession(id);
|
||||
if (!session) return null;
|
||||
session.expiresAt = Date.now() + TTL_MS;
|
||||
sessionStore.set(id, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { getConfig } from '@/lib/config';
|
||||
|
||||
import { listQuarkShareVideos } from './quark.client';
|
||||
import {
|
||||
createQuarkNetdiskSession,
|
||||
getQuarkNetdiskSession,
|
||||
parseQuarkNetdiskId,
|
||||
refreshQuarkNetdiskSession,
|
||||
} from './quark-session-cache';
|
||||
|
||||
export async function resolveQuarkSession(id: string) {
|
||||
const config = await getConfig();
|
||||
const quarkConfig = config.NetDiskConfig?.Quark;
|
||||
if (!quarkConfig?.Enabled || !quarkConfig.Cookie) {
|
||||
throw new Error('夸克网盘未配置或未启用');
|
||||
}
|
||||
|
||||
let session = refreshQuarkNetdiskSession(id) || getQuarkNetdiskSession(id);
|
||||
if (!session) {
|
||||
const payload = parseQuarkNetdiskId(id);
|
||||
const result = await listQuarkShareVideos(payload.shareUrl, quarkConfig.Cookie, payload.passcode || '');
|
||||
session = createQuarkNetdiskSession({
|
||||
title: result.title,
|
||||
shareUrl: payload.shareUrl,
|
||||
passcode: payload.passcode,
|
||||
shareId: result.shareId,
|
||||
shareToken: result.shareToken,
|
||||
files: result.files,
|
||||
});
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
throw new Error('夸克网盘播放信息恢复失败');
|
||||
}
|
||||
|
||||
return { session, cookie: quarkConfig.Cookie, savePath: quarkConfig.SavePath || '/' };
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export interface QuarkShareItem {
|
||||
dir: boolean;
|
||||
shareFidToken?: string;
|
||||
pdirFid?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface QuarkTransferTaskResult {
|
||||
@@ -26,6 +27,19 @@ export interface QuarkTransferTaskResult {
|
||||
reused?: boolean;
|
||||
}
|
||||
|
||||
export interface QuarkShareVideoListResult {
|
||||
title: string;
|
||||
shareId: string;
|
||||
shareToken: string;
|
||||
files: Array<{
|
||||
fid: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
shareFidToken?: string;
|
||||
pdirFid?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const VIDEO_EXTENSIONS = [
|
||||
'.mp4',
|
||||
'.mkv',
|
||||
@@ -62,6 +76,16 @@ function getHeaders(cookie: string): HeadersInit {
|
||||
};
|
||||
}
|
||||
|
||||
export function getQuarkPlayHeaders(cookie: string): Record<string, string> {
|
||||
return {
|
||||
cookie,
|
||||
origin: 'https://pan.quark.cn',
|
||||
referer: 'https://pan.quark.cn/',
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeQuarkCookie(cookie: string): string {
|
||||
return cookie
|
||||
.replace(/;/g, ';')
|
||||
@@ -205,6 +229,7 @@ async function fetchShareFolderItems(
|
||||
shareFidToken:
|
||||
item.share_fid_token || item.fid_token || item.share_token || undefined,
|
||||
pdirFid: String(item.pdir_fid || pdirFid || '0'),
|
||||
size: Number(item.size || 0),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -278,6 +303,37 @@ export async function validateQuarkCookieReadable(cookie: string): Promise<void>
|
||||
await fetchDriveFolderItems(safeCookie, '0');
|
||||
}
|
||||
|
||||
export async function listQuarkShareVideos(
|
||||
shareUrl: string,
|
||||
cookie: string,
|
||||
passcode = ''
|
||||
): Promise<QuarkShareVideoListResult> {
|
||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||
const share = parseQuarkShareUrl(shareUrl, passcode);
|
||||
const { stoken, shareTitle } = await fetchShareToken(safeCookie, share);
|
||||
const allItems = await collectShareItemsRecursive(safeCookie, share.pwdId, stoken, '0');
|
||||
const files = allItems
|
||||
.filter((item) => !item.dir && isVideoFile(item.fileName))
|
||||
.map((item) => ({
|
||||
fid: item.fid,
|
||||
name: item.fileName,
|
||||
size: item.size,
|
||||
shareFidToken: item.shareFidToken,
|
||||
pdirFid: item.pdirFid,
|
||||
}));
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error('分享中没有可播放的视频文件');
|
||||
}
|
||||
|
||||
return {
|
||||
title: shareTitle || '夸克网盘立即播放',
|
||||
shareId: share.pwdId,
|
||||
shareToken: stoken,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
async function createDriveFolder(
|
||||
cookie: string,
|
||||
parentFid: string,
|
||||
@@ -537,3 +593,171 @@ export async function createQuarkInstantPlayFolder(
|
||||
folderName,
|
||||
};
|
||||
}
|
||||
|
||||
export async function ensureQuarkPlayFolder(
|
||||
cookie: string,
|
||||
playTempSavePath: string,
|
||||
shareId: string,
|
||||
title?: string
|
||||
): Promise<{ folderFid: string; folderPath: string; folderName: string }> {
|
||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||
const tempRoot = await ensureQuarkDrivePath(safeCookie, playTempSavePath);
|
||||
const folderName = buildInstantPlayFolderName(shareId, title);
|
||||
const existedFolder = await findDirectoryByName(safeCookie, tempRoot.fid, folderName);
|
||||
if (existedFolder) {
|
||||
return {
|
||||
folderFid: String(existedFolder.fid || existedFolder.file_id),
|
||||
folderPath: joinPath(tempRoot.path, folderName),
|
||||
folderName,
|
||||
};
|
||||
}
|
||||
|
||||
const folderFid = await createDriveFolder(safeCookie, tempRoot.fid, folderName);
|
||||
return {
|
||||
folderFid,
|
||||
folderPath: joinPath(tempRoot.path, folderName),
|
||||
folderName,
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveQuarkShareFile(
|
||||
cookie: string,
|
||||
input: {
|
||||
shareId: string;
|
||||
shareToken: string;
|
||||
fileId: string;
|
||||
shareFileToken?: string;
|
||||
playFolderFid: string;
|
||||
}
|
||||
): Promise<string> {
|
||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||
const taskId = await submitSaveTask(
|
||||
safeCookie,
|
||||
{ pwdId: input.shareId, passcode: '' },
|
||||
input.shareToken,
|
||||
input.playFolderFid,
|
||||
[
|
||||
{
|
||||
fid: input.fileId,
|
||||
fileName: '',
|
||||
dir: false,
|
||||
shareFidToken: input.shareFileToken,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('夸克转存任务创建失败');
|
||||
}
|
||||
|
||||
for (let i = 0; i < 25; i += 1) {
|
||||
const query = new URLSearchParams({
|
||||
task_id: taskId,
|
||||
retry_index: String(i),
|
||||
});
|
||||
|
||||
const response = await fetch(buildApiUrl(QUARK_SHARE_API_BASE, '/task', query.toString()), {
|
||||
method: 'GET',
|
||||
headers: getHeaders(safeCookie),
|
||||
});
|
||||
const data = await parseJson(response);
|
||||
ensureOk(data, '查询夸克任务状态失败');
|
||||
|
||||
const saveAsTopFids = data?.data?.save_as?.save_as_top_fids;
|
||||
if (Array.isArray(saveAsTopFids) && saveAsTopFids.length > 0) {
|
||||
return String(saveAsTopFids[0]);
|
||||
}
|
||||
|
||||
const status = data?.data?.status;
|
||||
if (status === -1 || status === 'failed' || data?.data?.err_code) {
|
||||
throw new Error(data?.data?.message || data?.data?.err_msg || '夸克任务执行失败');
|
||||
}
|
||||
|
||||
if (status === 2 || status === 'finished' || status === 'success' || data?.data?.finished_at) {
|
||||
break;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
}
|
||||
|
||||
throw new Error('夸克转存结果获取失败');
|
||||
}
|
||||
|
||||
export async function getQuarkPlayUrls(
|
||||
cookie: string,
|
||||
savedFileId: string
|
||||
): Promise<Array<{ name: string; url: string; priority: number }>> {
|
||||
const safeCookie = assertQuarkCookieHeaderSafe(cookie);
|
||||
const headers = getHeaders(safeCookie);
|
||||
const urls: Array<{ name: string; url: string; priority: number }> = [];
|
||||
|
||||
try {
|
||||
const response = await fetch(buildApiUrl(QUARK_DRIVE_API_BASE, '/file/download'), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
fids: [savedFileId],
|
||||
}),
|
||||
cache: 'no-store',
|
||||
});
|
||||
const data = await parseJson(response);
|
||||
ensureOk(data, '获取夸克下载地址失败');
|
||||
const downloadUrl = data?.data?.[0]?.download_url;
|
||||
if (downloadUrl) {
|
||||
urls.push({
|
||||
name: '原画',
|
||||
url: String(downloadUrl),
|
||||
priority: 9999,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore download failure, continue transcoding fallback
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(buildApiUrl(QUARK_DRIVE_API_BASE, '/file/v2/play'), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
fid: savedFileId,
|
||||
resolutions: 'normal,low,high,super,2k,4k',
|
||||
supports: 'fmp4',
|
||||
}),
|
||||
cache: 'no-store',
|
||||
});
|
||||
const data = await parseJson(response);
|
||||
ensureOk(data, '获取夸克转码地址失败');
|
||||
const nameMap: Record<string, string> = {
|
||||
FOUR_K: '4K',
|
||||
SUPER: '超清',
|
||||
HIGH: '高清',
|
||||
NORMAL: '流畅',
|
||||
LOW: '低清',
|
||||
};
|
||||
if (Array.isArray(data?.data?.video_list)) {
|
||||
for (const video of data.data.video_list) {
|
||||
const resolution = video?.video_info?.resoultion;
|
||||
const playUrl = video?.video_info?.url;
|
||||
const priority = Number(video?.video_info?.width || 0);
|
||||
if (resolution && playUrl) {
|
||||
urls.push({
|
||||
name: nameMap[String(resolution)] || String(resolution),
|
||||
url: String(playUrl),
|
||||
priority,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore transcoding failure
|
||||
}
|
||||
|
||||
const deduped = urls.filter((item, index, array) => array.findIndex((v) => v.url === item.url) === index);
|
||||
deduped.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
if (deduped.length === 0) {
|
||||
throw new Error('未获取到夸克播放地址');
|
||||
}
|
||||
|
||||
return deduped;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user