diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 4713dcb..3f616e3 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -3714,6 +3714,10 @@ const NetDiskConfigComponent = ({
const [pan123Enabled, setPan123Enabled] = useState(false);
const [pan123Account, setPan123Account] = useState('');
const [pan123Password, setPan123Password] = useState('');
+ const [ucEnabled, setUcEnabled] = useState(false);
+ const [ucCookie, setUcCookie] = useState('');
+ const [ucToken, setUcToken] = useState('');
+ const [ucSavePath, setUcSavePath] = useState('/');
useEffect(() => {
const quark = config?.NetDiskConfig?.Quark;
@@ -3731,6 +3735,10 @@ const NetDiskConfigComponent = ({
setPan123Enabled(config?.NetDiskConfig?.Pan123?.Enabled || false);
setPan123Account(config?.NetDiskConfig?.Pan123?.Account || '');
setPan123Password(config?.NetDiskConfig?.Pan123?.Password || '');
+ setUcEnabled(config?.NetDiskConfig?.UC?.Enabled || false);
+ setUcCookie(config?.NetDiskConfig?.UC?.Cookie || '');
+ setUcToken(config?.NetDiskConfig?.UC?.Token || '');
+ setUcSavePath(config?.NetDiskConfig?.UC?.SavePath || '/');
}, [config]);
const handleSave = async () => {
@@ -3763,6 +3771,12 @@ const NetDiskConfigComponent = ({
Account: pan123Account,
Password: pan123Password,
},
+ UC: {
+ Enabled: ucEnabled,
+ Cookie: ucCookie,
+ Token: ucToken,
+ SavePath: ucSavePath,
+ },
}),
});
@@ -3918,6 +3932,36 @@ const NetDiskConfigComponent = ({
});
};
+ const handleValidateUC = async () => {
+ await withLoading('validateUCNetDisk', async () => {
+ try {
+ const response = await fetch('/api/admin/netdisk', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ action: 'validate',
+ provider: 'uc',
+ UC: {
+ Cookie: ucCookie,
+ Token: ucToken,
+ SavePath: ucSavePath,
+ },
+ }),
+ });
+
+ const data = await response.json();
+ if (!response.ok) {
+ throw new Error(data.error || '校验失败');
+ }
+
+ showSuccess(data.message || 'UC Cookie 可读', showAlert);
+ } catch (error) {
+ showError(error instanceof Error ? error.message : '校验失败', showAlert);
+ throw error;
+ }
+ });
+ };
+
return (
@@ -4256,6 +4300,92 @@ const NetDiskConfigComponent = ({
+
+
+ UC网盘
+
+
+
+
+
+ 启用UC网盘
+
+
+ 开启后,网盘搜索中的UC网盘资源会显示“立即播放”按钮
+
+
+
+
+
+
+
+
+
+
+
+ setUcToken(e.target.value)}
+ disabled={!ucEnabled}
+ placeholder='可选,填写后优先尝试原画地址'
+ 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-green-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed'
+ />
+
+
+
+
+ setUcSavePath(e.target.value)}
+ disabled={!ucEnabled}
+ placeholder='/影视/UC临时转存'
+ 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-green-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed'
+ />
+
+
+
+
+
+
+
+
+
item.name === quality) || playUrls[0];
+ if (!selected) {
+ return NextResponse.json({ error: '未获取到 UC 播放地址' }, { status: 500 });
+ }
+
+ const range = request.headers.get('range');
+ const abortController = new AbortController();
+ const timeoutId = setTimeout(() => abortController.abort(), 300000);
+
+ try {
+ const upstream = await fetch(selected.url, {
+ headers: {
+ ...(selected.headers || {}),
+ ...(range ? { Range: range } : {}),
+ },
+ cache: 'no-store',
+ signal: abortController.signal,
+ });
+
+ clearTimeout(timeoutId);
+
+ if (!upstream.ok || !upstream.body) {
+ return NextResponse.json(
+ { error: `UC视频代理失败 (${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');
+
+ const { readable, writable } = new TransformStream();
+ const reader = upstream.body.getReader();
+
+ void (async () => {
+ const writer = writable.getWriter();
+ try {
+ let streamDone = false;
+ while (!streamDone) {
+ const { done, value } = await reader.read();
+ if (done) {
+ streamDone = true;
+ } else {
+ await writer.write(value);
+ }
+ }
+ } catch {
+ try {
+ await reader.cancel();
+ } catch {
+ void 0;
+ }
+ } finally {
+ try {
+ reader.releaseLock();
+ } catch {
+ void 0;
+ }
+ try {
+ await writer.close();
+ } catch {
+ void 0;
+ }
+ }
+ })();
+
+ return new Response(readable, {
+ status: range && upstream.headers.get('content-range') ? 206 : upstream.status,
+ headers: responseHeaders,
+ });
+ } catch (error) {
+ clearTimeout(timeoutId);
+ if (error instanceof Error && error.name === 'AbortError') {
+ return NextResponse.json({ error: 'UC网盘代理超时' }, { status: 504 });
+ }
+ throw error;
+ }
+ } catch (error) {
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'UC网盘代理失败' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/api/source-detail/route.ts b/src/app/api/source-detail/route.ts
index 0b271c5..2af55b1 100644
--- a/src/app/api/source-detail/route.ts
+++ b/src/app/api/source-detail/route.ts
@@ -18,12 +18,28 @@ import {
parseMobileNetdiskId,
refreshMobileNetdiskSession,
} from '@/lib/netdisk/mobile-session-cache';
+import {
+ createPan123NetdiskSession,
+ getPan123NetdiskSession,
+ parsePan123NetdiskId,
+ refreshPan123NetdiskSession,
+} from '@/lib/netdisk/pan123-session-cache';
import {
createQuarkNetdiskSession,
getQuarkNetdiskSession,
parseQuarkNetdiskId,
refreshQuarkNetdiskSession,
} from '@/lib/netdisk/quark-session-cache';
+import {
+ LEGACY_QUARK_TEMP_SOURCE,
+ NETDISK_123_SOURCE,
+ NETDISK_BAIDU_SOURCE,
+ NETDISK_MOBILE_SOURCE,
+ NETDISK_QUARK_SOURCE,
+ NETDISK_TIANYI_SOURCE,
+ NETDISK_UC_SOURCE,
+ normalizeNetdiskSource,
+} from '@/lib/netdisk/source';
import {
createTianyiNetdiskSession,
getTianyiNetdiskSession,
@@ -31,12 +47,11 @@ import {
refreshTianyiNetdiskSession,
} from '@/lib/netdisk/tianyi-session-cache';
import {
- createPan123NetdiskSession,
- getPan123NetdiskSession,
- parsePan123NetdiskId,
- refreshPan123NetdiskSession,
-} from '@/lib/netdisk/pan123-session-cache';
-import { LEGACY_QUARK_TEMP_SOURCE, NETDISK_123_SOURCE, NETDISK_BAIDU_SOURCE, NETDISK_MOBILE_SOURCE, NETDISK_QUARK_SOURCE, NETDISK_TIANYI_SOURCE, normalizeNetdiskSource } from '@/lib/netdisk/source';
+ createUCNetdiskSession,
+ getUCNetdiskSession,
+ parseUCNetdiskId,
+ refreshUCNetdiskSession,
+} from '@/lib/netdisk/uc-session-cache';
import {
executeSavedSourceScript,
normalizeScriptDetailResult,
@@ -685,6 +700,76 @@ export async function GET(request: NextRequest) {
}
}
+ if (sourceCode === NETDISK_UC_SOURCE) {
+ try {
+ const config = await getConfig();
+ const ucConfig = config.NetDiskConfig?.UC;
+ if (!ucConfig?.Enabled || !ucConfig.Cookie) {
+ throw new Error('UC网盘未配置或未启用');
+ }
+ const { parseVideoFileName } = await import('@/lib/video-parser');
+
+ let session = refreshUCNetdiskSession(id) || getUCNetdiskSession(id);
+ if (!session) {
+ const payload = parseUCNetdiskId(id);
+ const { listUCShareVideos } = await import('@/lib/netdisk/uc.client');
+ const result = await listUCShareVideos(payload.shareUrl, ucConfig.Cookie, payload.passcode || '');
+ session = createUCNetdiskSession({
+ title: title || result.title,
+ shareUrl: payload.shareUrl,
+ passcode: payload.passcode,
+ shareId: result.shareId,
+ shareToken: result.shareToken,
+ files: result.files,
+ });
+ }
+ if (!session) {
+ throw new Error('UC网盘播放信息恢复失败');
+ }
+
+ const ucSession = session;
+ const episodes = ucSession.files
+ .map((file, index) => {
+ const parsed = parseVideoFileName(file.name);
+ return {
+ originalIndex: index,
+ fileName: file.name,
+ episode: parsed.episode || index + 1,
+ title: formatNetdiskEpisodeTitle(parsed, file.name),
+ isOVA: parsed.isOVA,
+ };
+ })
+ .sort((a, b) => {
+ if (a.isOVA && !b.isOVA) return 1;
+ if (!a.isOVA && b.isOVA) return -1;
+ return a.episode !== b.episode
+ ? a.episode - b.episode
+ : a.fileName.localeCompare(b.fileName);
+ });
+
+ return NextResponse.json({
+ source: NETDISK_UC_SOURCE,
+ source_name: 'UC网盘',
+ id: ucSession.id,
+ title: title || ucSession.title,
+ poster: '',
+ year: '',
+ douban_id: 0,
+ desc: `UC网盘分享:${ucSession.shareUrl}`,
+ episodes: episodes.map((ep) => (
+ `/api/netdisk/uc/play?id=${encodeURIComponent(ucSession.id)}&episodeIndex=${ep.originalIndex}`
+ )),
+ episodes_titles: episodes.map((ep) => ep.title),
+ proxyMode: false,
+ });
+ } catch (error) {
+ return NextResponse.json(
+ { error: (error as Error).message },
+ { status: 500 }
+ );
+ }
+ }
+
// 特殊处理 openlist 源 - 直接调用 /api/detail
if (sourceCode === 'openlist') {
try {
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 1aa6ede..303e758 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -4,7 +4,6 @@
import { BookOpen, Bot, ChevronRight, Link as LinkIcon, ListVideo, Music } from 'lucide-react';
import Link from 'next/link';
-import { useRouter } from 'next/navigation';
import { Suspense, useEffect, useState } from 'react';
import {
@@ -47,8 +46,6 @@ function HomeClient() {
>([]);
const [loading, setLoading] = useState(true);
const { announcement } = useSite();
- const router = useRouter();
-
// 首页模块配置状态
const [homeModules, setHomeModules] = useState([
{ id: 'hotMovies', name: '热门电影', enabled: true, order: 0 },
@@ -75,7 +72,7 @@ function HomeClient() {
const [toast, setToast] = useState(null);
const detectNetdiskLink = (url: string): {
- provider: 'quark' | 'mobile' | 'baidu' | 'tianyi' | '123';
+ provider: 'quark' | 'mobile' | 'baidu' | 'tianyi' | '123' | 'uc';
shareUrl: string;
passcode?: string;
} | null => {
@@ -134,6 +131,17 @@ function HomeClient() {
};
}
+ if (/https:\/\/drive\.uc\.cn\/s\//i.test(trimmed)) {
+ return {
+ provider: 'uc',
+ shareUrl: trimmed,
+ passcode: pickPasscode(
+ trimmed.match(/[?&](?:pwd|passcode)=([^&]+)/i)?.[1],
+ inlinePasscode(trimmed)
+ ),
+ };
+ }
+
if (/https:\/\/(?:yun|caiyun)\.139\.com\//i.test(trimmed)) {
return { provider: 'mobile', shareUrl: trimmed };
}
@@ -157,9 +165,11 @@ function HomeClient() {
netdisk.provider === 'mobile'
? 'netdisk-mobile'
: netdisk.provider === 'baidu'
- ? 'netdisk-baidu'
+ ? 'netdisk-baidu'
: netdisk.provider === 'tianyi'
? 'netdisk-tianyi'
+ : netdisk.provider === 'uc'
+ ? 'netdisk-uc'
: netdisk.provider === '123'
? 'netdisk-123'
: 'netdisk-quark';
@@ -849,6 +859,9 @@ function HomeClient() {
请输入可直接播放的视频链接。
+
+ 支持夸克、UC、百度、天翼、移动、123 网盘在线播放。
+
setDirectPlayUrl(event.target.value)}
diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx
index 2ef1f68..fb5a637 100644
--- a/src/app/play/page.tsx
+++ b/src/app/play/page.tsx
@@ -2506,6 +2506,7 @@ function PlayPageClient() {
newUrl.startsWith('/api/openlist/play') ||
newUrl.startsWith('/api/netdisk/123/play') ||
newUrl.startsWith('/api/netdisk/quark/play') ||
+ newUrl.startsWith('/api/netdisk/uc/play') ||
newUrl.startsWith('/api/netdisk/baidu/play') ||
newUrl.startsWith('/api/source-script/play');
diff --git a/src/components/PansouSearch.tsx b/src/components/PansouSearch.tsx
index ea52caa..e3b2d9b 100644
--- a/src/components/PansouSearch.tsx
+++ b/src/components/PansouSearch.tsx
@@ -170,6 +170,8 @@ export default function PansouSearch({
? '/api/netdisk/baidu/instant-play'
: cloudType === 'tianyi'
? '/api/netdisk/tianyi/instant-play'
+ : cloudType === 'uc'
+ ? '/api/netdisk/uc/instant-play'
: cloudType === '123'
? '/api/netdisk/123/instant-play'
: '/api/netdisk/quark/instant-play';
@@ -181,7 +183,7 @@ export default function PansouSearch({
body: JSON.stringify({
shareUrl: link.url,
passcode: link.password || '',
- title: link.note || keyword,
+ title: keyword,
}),
});
@@ -191,7 +193,7 @@ export default function PansouSearch({
}
router.push(
- `/play?source=${encodeURIComponent(data.source || (cloudType === 'mobile' ? 'netdisk-mobile' : cloudType === 'baidu' ? 'netdisk-baidu' : cloudType === 'tianyi' ? 'netdisk-tianyi' : cloudType === '123' ? 'netdisk-123' : 'netdisk-quark'))}&id=${encodeURIComponent(data.id)}&title=${encodeURIComponent(data.title || keyword)}`
+ `/play?source=${encodeURIComponent(data.source || (cloudType === 'mobile' ? 'netdisk-mobile' : cloudType === 'baidu' ? 'netdisk-baidu' : cloudType === 'tianyi' ? 'netdisk-tianyi' : cloudType === 'uc' ? 'netdisk-uc' : cloudType === '123' ? 'netdisk-123' : 'netdisk-quark'))}&id=${encodeURIComponent(data.id)}&title=${encodeURIComponent(keyword)}`
);
} catch (err: any) {
setToast({
@@ -349,7 +351,7 @@ export default function PansouSearch({
{/* 操作按钮 */}
- {(cloudType === 'quark' || cloudType === 'mobile' || cloudType === 'baidu' || cloudType === 'tianyi' || cloudType === '123') && (
+ {(cloudType === 'quark' || cloudType === 'mobile' || cloudType === 'baidu' || cloudType === 'tianyi' || cloudType === '123' || cloudType === 'uc') && (
<>