diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index ac407c3..9424738 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -3706,14 +3706,19 @@ const NetDiskConfigComponent = ({
const [savePath, setSavePath] = useState('/');
const [playTempSavePath, setPlayTempSavePath] = useState('/');
const [openListTempPath, setOpenListTempPath] = useState('/');
+ const [mobileEnabled, setMobileEnabled] = useState(false);
+ const [mobileAuthorization, setMobileAuthorization] = useState('');
useEffect(() => {
const quark = config?.NetDiskConfig?.Quark;
+ const mobile = config?.NetDiskConfig?.Mobile;
setEnabled(quark?.Enabled || false);
setCookie(quark?.Cookie || '');
setSavePath(quark?.SavePath || '/');
setPlayTempSavePath(quark?.PlayTempSavePath || '/');
setOpenListTempPath(quark?.OpenListTempPath || '/');
+ setMobileEnabled(mobile?.Enabled || false);
+ setMobileAuthorization(mobile?.Authorization || '');
}, [config]);
const handleSave = async () => {
@@ -3730,6 +3735,10 @@ const NetDiskConfigComponent = ({
PlayTempSavePath: playTempSavePath,
OpenListTempPath: openListTempPath,
},
+ Mobile: {
+ Enabled: mobileEnabled,
+ Authorization: mobileAuthorization,
+ },
}),
});
@@ -3772,6 +3781,34 @@ const NetDiskConfigComponent = ({
});
};
+ const handleValidateMobile = async () => {
+ await withLoading('validateMobileNetDisk', async () => {
+ try {
+ const response = await fetch('/api/admin/netdisk', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ action: 'validate',
+ provider: 'mobile',
+ Mobile: {
+ Authorization: mobileAuthorization,
+ },
+ }),
+ });
+
+ const data = await response.json();
+ if (!response.ok) {
+ throw new Error(data.error || '校验失败');
+ }
+
+ showSuccess(data.message || '移动云盘验证头格式正常', showAlert);
+ } catch (error) {
+ showError(error instanceof Error ? error.message : '校验失败', showAlert);
+ throw error;
+ }
+ });
+ };
+
return (
@@ -3891,6 +3928,64 @@ const NetDiskConfigComponent = ({
+
+
+ 移动云盘
+
+
+
+
+
+ 启用移动云盘
+
+
+ 开启后,网盘搜索中的移动云盘资源会显示“立即播放”按钮
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ const parsed = parseVideoFileName(file.name);
+ return {
+ ...file,
+ originalIndex: index,
+ sortEpisode: parsed.episode || index + 1,
+ isOVA: parsed.isOVA,
+ displayTitle:
+ parsed.title ||
+ (parsed.episode ? `第${parsed.episode}集` : file.name),
+ };
+ }).sort((a, b) => {
+ if (a.isOVA && !b.isOVA) return 1;
+ if (!a.isOVA && b.isOVA) return -1;
+ return a.sortEpisode !== b.sortEpisode
+ ? a.sortEpisode - b.sortEpisode
+ : a.name.localeCompare(b.name, 'zh-Hans-CN', {
+ numeric: true,
+ sensitivity: 'base',
+ });
+ });
+
+ const episodes = parsedFiles.map((file) => (
+ `/api/netdisk/mobile/play?id=${encodeURIComponent(mobileSession.id)}&episodeIndex=${file.originalIndex}`
+ ));
+
+ return NextResponse.json({
+ source: NETDISK_MOBILE_SOURCE,
+ source_name: '移动云盘',
+ id: mobileSession.id,
+ title: title || mobileSession.title,
+ poster: '',
+ year: '',
+ douban_id: 0,
+ desc: `移动云盘分享:${mobileSession.shareUrl}`,
+ episodes,
+ episodes_titles: parsedFiles.map((file) => file.displayTitle),
+ proxyMode: false,
+ });
+ } catch (error) {
+ return NextResponse.json(
+ { error: (error as Error).message },
+ { status: 500 }
+ );
+ }
+ }
+
+ if (sourceCode === NETDISK_QUARK_SOURCE || sourceCode === LEGACY_QUARK_TEMP_SOURCE) {
try {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
@@ -390,7 +469,7 @@ export async function GET(request: NextRequest) {
});
return NextResponse.json({
- source: 'quark-temp',
+ source: NETDISK_QUARK_SOURCE,
source_name: '夸克临时播放',
id,
title: title || folderPath.split('/').filter(Boolean).pop() || '夸克临时播放',
diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx
index a6c26be..f691ee4 100644
--- a/src/app/play/page.tsx
+++ b/src/app/play/page.tsx
@@ -8,8 +8,8 @@ import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
import {
- convertDanmakuFormat,
clearDanmakuCacheByTitle,
+ convertDanmakuFormat,
getDanmakuById,
getDanmakuFromCache,
getEpisodes,
@@ -54,12 +54,13 @@ import {
pruneLocalEpisodeProgressStorage,
saveLocalEpisodeProgress,
} from '@/lib/episode-progress';
-import { getTMDBImageUrl } from '@/lib/tmdb.search';
+import { isNetdiskSource, normalizeNetdiskSource } from '@/lib/netdisk/source';
import {
getRecommendationCache,
recommendationCacheKeys,
setRecommendationCache,
} from '@/lib/recommendations/cache';
+import { getTMDBImageUrl } from '@/lib/tmdb.search';
import { DanmakuFilterConfig, EpisodeFilterConfig, SearchResult } from '@/lib/types';
import { base58Decode, getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
import { useEnableAIComments } from '@/hooks/useEnableAIComments';
@@ -605,27 +606,28 @@ function PlayPageClient() {
// 纠错后的描述信息(用于显示,不触发 detail 更新)
const [correctedDesc, setCorrectedDesc] = useState('');
- const [quarkTempTMDBMeta, setQuarkTempTMDBMeta] = useState<{
+ const [netdiskTMDBMeta, setNetdiskTMDBMeta] = useState<{
desc?: string;
poster?: string;
year?: string;
tmdbId?: number;
} | null>(null);
- const [pendingQuarkTempTMDBData, setPendingQuarkTempTMDBData] = useState(null);
+ const [pendingNetdiskTMDBData, setPendingNetdiskTMDBData] = useState(null);
// 当前源和ID - source 直接存储完整格式(如 'emby_wumei' 或 'emby')
- const [currentSource, setCurrentSource] = useState(searchParams.get('source') || '');
+ const [currentSource, setCurrentSource] = useState(normalizeNetdiskSource(searchParams.get('source')) || '');
const [currentId, setCurrentId] = useState(searchParams.get('id') || '');
const [fileName] = useState(searchParams.get('fileName') || ''); // 小雅源:用户点击的文件名
const isDirectPlay = currentSource === 'directplay';
useEffect(() => {
- setQuarkTempTMDBMeta(null);
- setPendingQuarkTempTMDBData(null);
+ setNetdiskTMDBMeta(null);
+ setPendingNetdiskTMDBData(null);
}, [currentSource, currentId]);
// 解析 source 参数以获取 embyKey(仅用于 API 调用)
const parseSourceForApi = (source: string): { source: string; embyKey?: string } => {
+ source = normalizeNetdiskSource(source);
if (source.startsWith('emby_')) {
const key = source.substring(5);
return { source: 'emby', embyKey: key };
@@ -1300,19 +1302,22 @@ function PlayPageClient() {
const populatePlayMetadataFromTMDB = (tmdbData: any) => {
const currentDetail = detailRef.current;
- if (!currentDetail || currentDetail.source !== 'quark-temp') {
- setPendingQuarkTempTMDBData(tmdbData);
+ if (!currentDetail || !isNetdiskSource(currentDetail.source)) {
+ setPendingNetdiskTMDBData(tmdbData);
return;
}
const tmdbYear = tmdbData.releaseDate?.split('-')[0] || '';
- const shouldReplaceDesc = !currentDetail.desc || currentDetail.desc.startsWith('临时播放目录:');
+ const shouldReplaceDesc =
+ !currentDetail.desc ||
+ currentDetail.desc.startsWith('临时播放目录:') ||
+ currentDetail.desc.startsWith('移动云盘分享:');
const resolvedTmdbId = typeof tmdbData.tmdbId === 'string'
? Number(String(tmdbData.tmdbId).split(':')[1] || 0)
: tmdbData.tmdbId;
- setQuarkTempTMDBMeta({
+ setNetdiskTMDBMeta({
desc: shouldReplaceDesc ? (tmdbData.overview || currentDetail.desc) : currentDetail.desc,
poster: currentDetail.poster || tmdbData.poster || '',
year: currentDetail.year || tmdbYear,
@@ -1320,7 +1325,7 @@ function PlayPageClient() {
});
setDetail((prev) => {
- if (!prev || prev.source !== 'quark-temp') {
+ if (!prev || !isNetdiskSource(prev.source)) {
return prev;
}
@@ -1381,25 +1386,32 @@ function PlayPageClient() {
useEffect(() => {
if (
- pendingQuarkTempTMDBData &&
- detail?.source === 'quark-temp'
+ pendingNetdiskTMDBData &&
+ isNetdiskSource(detail?.source)
) {
- const pending = pendingQuarkTempTMDBData;
- setPendingQuarkTempTMDBData(null);
+ const currentDetail = detail;
+ if (!currentDetail) {
+ return;
+ }
+ const pending = pendingNetdiskTMDBData;
+ setPendingNetdiskTMDBData(null);
const tmdbYear = pending.releaseDate?.split('-')[0] || '';
- const shouldReplaceDesc = !detail.desc || detail.desc.startsWith('临时播放目录:');
+ const shouldReplaceDesc =
+ !currentDetail.desc ||
+ currentDetail.desc.startsWith('临时播放目录:') ||
+ currentDetail.desc.startsWith('移动云盘分享:');
const resolvedTmdbId = typeof pending.tmdbId === 'string'
? Number(String(pending.tmdbId).split(':')[1] || 0)
: pending.tmdbId;
- setQuarkTempTMDBMeta({
- desc: shouldReplaceDesc ? (pending.overview || detail.desc) : detail.desc,
- poster: detail.poster || pending.poster || '',
- year: detail.year || tmdbYear,
- tmdbId: detail.tmdb_id || resolvedTmdbId,
+ setNetdiskTMDBMeta({
+ desc: shouldReplaceDesc ? (pending.overview || currentDetail.desc) : currentDetail.desc,
+ poster: currentDetail.poster || pending.poster || '',
+ year: currentDetail.year || tmdbYear,
+ tmdbId: currentDetail.tmdb_id || resolvedTmdbId,
});
- setDetail((prev) => prev && prev.source === 'quark-temp' ? {
+ setDetail((prev) => prev && isNetdiskSource(prev.source) ? {
...prev,
poster: prev.poster || pending.poster || '',
year: prev.year || tmdbYear,
@@ -1407,17 +1419,17 @@ function PlayPageClient() {
tmdb_id: prev.tmdb_id || resolvedTmdbId,
} : prev);
- if (pending.poster && !detail.poster) {
+ if (pending.poster && !currentDetail.poster) {
setVideoCover(processImageUrl(pending.poster));
}
- if (tmdbYear && !detail.year) {
+ if (tmdbYear && !currentDetail.year) {
setVideoYear(tmdbYear);
}
if (pending.overview) {
setCorrectedDesc(pending.overview);
}
}
- }, [pendingQuarkTempTMDBData, detail]);
+ }, [pendingNetdiskTMDBData, detail]);
// 视频播放地址
const [videoUrl, setVideoUrl] = useState('');
@@ -1535,7 +1547,7 @@ function PlayPageClient() {
!isM3u8LikeUrl(videoUrl) &&
(
detail.source === 'openlist' ||
- detail.source === 'quark-temp' ||
+ isNetdiskSource(detail.source) ||
detail.source === 'xiaoya' ||
detail.source.startsWith('emby')
)
@@ -4165,7 +4177,7 @@ function PlayPageClient() {
// 监听 URL 参数变化,处理换源和换视频(用于房员跟随房主操作)
useEffect(() => {
- const urlSource = searchParams.get('source');
+ const urlSource = normalizeNetdiskSource(searchParams.get('source'));
const urlId = searchParams.get('id');
// 只在URL参数存在且与当前状态不同时才处理
@@ -9639,12 +9651,12 @@ function PlayPageClient() {
)}
{/* 优先使用 doubanYear,如果没有则使用 detail.year 或 videoYear */}
- {(doubanYear || quarkTempTMDBMeta?.year || detail?.year || videoYear) && (
- {doubanYear || quarkTempTMDBMeta?.year || detail?.year || videoYear}
+ {(doubanYear || netdiskTMDBMeta?.year || detail?.year || videoYear) && (
+ {doubanYear || netdiskTMDBMeta?.year || detail?.year || videoYear}
)}
{detail?.source_name && (
@@ -9664,7 +9676,7 @@ function PlayPageClient() {
{detail?.type_name && {detail.type_name}}
{/* 剧情简介 */}
- {(doubanCardSubtitle || quarkTempTMDBMeta?.desc || correctedDesc || detail?.desc) && (
+ {(doubanCardSubtitle || netdiskTMDBMeta?.desc || correctedDesc || detail?.desc) && (
)}
- {quarkTempTMDBMeta?.desc || correctedDesc || detail?.desc}
+ {netdiskTMDBMeta?.desc || correctedDesc || detail?.desc}
)}
@@ -9720,9 +9732,15 @@ function PlayPageClient() {
)}
>
) : (
-
- 封面图片
-
+ isNetdiskSource(detail?.source) ? (
+
+
+
+ ) : (
+
+ 封面图片
+
+ )
)}
@@ -9929,7 +9947,7 @@ function PlayPageClient() {
// 特殊源使用 tmdb,其他使用 cms(通过 doubanId)
// 如果有豆瓣ID且不为0,传入doubanId
detail.source === 'openlist' ||
- detail.source === 'quark-temp' ||
+ isNetdiskSource(detail.source) ||
detail.source?.startsWith('emby') ||
detail.source === 'xiaoya'
? undefined
@@ -9940,7 +9958,7 @@ function PlayPageClient() {
tmdbId={
// 特殊源使用 tmdb
detail.source === 'openlist' ||
- detail.source === 'quark-temp' ||
+ isNetdiskSource(detail.source) ||
detail.source?.startsWith('emby') ||
detail.source === 'xiaoya'
? detail.tmdb_id
@@ -9952,7 +9970,7 @@ function PlayPageClient() {
// 非特殊源使用 cms 数据
// 但如果有豆瓣ID且不为0,则不传入cmsData,优先使用豆瓣数据
detail.source !== 'openlist' &&
- detail.source !== 'quark-temp' &&
+ !isNetdiskSource(detail.source) &&
!detail.source?.startsWith('emby') &&
detail.source !== 'xiaoya' &&
!(detail.douban_id && detail.douban_id !== 0)
diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx
index 661ba6c..82f24ba 100644
--- a/src/app/search/page.tsx
+++ b/src/app/search/page.tsx
@@ -962,40 +962,6 @@ function SearchPageClient() {
}, [activeTab]);
useEffect(() => {
- // 从 URL 读取搜索类型参数
- const typeParam = searchParams.get('type');
- const query = searchParams.get('q');
-
- if (
- (typeParam === 'pansou' && netdiskSearchEnabled) ||
- (typeParam === 'acg' && magnetSearchEnabled)
- ) {
- setActiveTab(typeParam);
-
- // 如果有搜索关键词且显示结果,触发对应的搜索
- if (query && query.trim()) {
- setSearchQuery(query);
- setShowResults(true);
-
- // 延迟触发搜索,确保组件已经切换到正确的标签页
- setTimeout(() => {
- if (typeParam === 'pansou') {
- setTriggerPansouSearch((prev) => !prev);
- } else if (typeParam === 'acg') {
- setTriggerAcgSearch((prev) => !prev);
- }
- }, 100);
- }
- } else if (typeParam === 'video') {
- setActiveTab('video');
- } else if (!typeParam && query) {
- // 如果没有 type 参数但有查询,默认为 video
- setActiveTab('video');
- }
-
- // 无搜索参数时聚焦搜索框
- !searchParams.get('q') && document.getElementById('searchInput')?.focus();
-
// 获取用户权限
const authInfo = getAuthInfoFromBrowserCookie();
setUserRole(authInfo?.role || null);
@@ -1095,6 +1061,31 @@ function SearchPageClient() {
};
}, []);
+ useEffect(() => {
+ const typeParam = searchParams.get('type');
+ const query = searchParams.get('q');
+
+ if (typeParam === 'pansou') {
+ if (netdiskSearchEnabled) {
+ setActiveTab('pansou');
+ } else {
+ setActiveTab('video');
+ }
+ } else if (typeParam === 'acg') {
+ if (magnetSearchEnabled) {
+ setActiveTab('acg');
+ } else {
+ setActiveTab('video');
+ }
+ } else {
+ setActiveTab('video');
+ }
+
+ if (!query) {
+ document.getElementById('searchInput')?.focus();
+ }
+ }, [searchParams, netdiskSearchEnabled, magnetSearchEnabled]);
+
useEffect(() => {
// 等待转换器初始化完成
if (!converterReady) {
@@ -1348,7 +1339,27 @@ function SearchPageClient() {
setShowResults(false);
setShowSuggestions(false);
}
- }, [searchParams, forceRefresh, converterReady, netdiskSearchEnabled, magnetSearchEnabled]);
+ }, [searchParams, forceRefresh, converterReady]);
+
+ useEffect(() => {
+ const typeParam = searchParams.get('type');
+ const query = searchParams.get('q');
+ if (!query || !query.trim()) return;
+
+ if (typeParam === 'pansou' && netdiskSearchEnabled) {
+ setSearchQuery(query);
+ setShowResults(true);
+ setTimeout(() => {
+ setTriggerPansouSearch((prev) => !prev);
+ }, 100);
+ } else if (typeParam === 'acg' && magnetSearchEnabled) {
+ setSearchQuery(query);
+ setShowResults(true);
+ setTimeout(() => {
+ setTriggerAcgSearch((prev) => !prev);
+ }, 100);
+ }
+ }, [searchParams, netdiskSearchEnabled, magnetSearchEnabled]);
// 组件卸载时,关闭可能存在的连接
useEffect(() => {
@@ -1553,8 +1564,9 @@ function SearchPageClient() {
setSearchQuery(trimmed);
setShowResults(true);
setShowSuggestions(false);
-
- router.push(`/search?q=${encodeURIComponent(trimmed)}`);
+ router.push(
+ `/search?q=${encodeURIComponent(trimmed)}&type=${activeTab}`
+ );
}}
/>
diff --git a/src/components/EpisodeSelector.tsx b/src/components/EpisodeSelector.tsx
index bf2f100..ddab291 100644
--- a/src/components/EpisodeSelector.tsx
+++ b/src/components/EpisodeSelector.tsx
@@ -14,6 +14,7 @@ import type { DanmakuComment,DanmakuSelection } from '@/lib/danmaku/types';
import { generateStorageKey, getCachedPlayRecordsSnapshot } from '@/lib/db.client';
import { isEpisodeHiddenByFilter } from '@/lib/episode-filter';
import { loadAllLocalEpisodeProgressRecords } from '@/lib/episode-progress';
+import { isNetdiskSource } from '@/lib/netdisk/source';
import { EpisodeFilterConfig,SearchResult } from '@/lib/types';
import { getVideoResolutionFromM3u8 } from '@/lib/utils';
@@ -1001,7 +1002,7 @@ const EpisodeSelector: React.FC = ({
{/* 源名称和集数信息 - 垂直居中 */}
diff --git a/src/components/PansouSearch.tsx b/src/components/PansouSearch.tsx
index 915b6db..a249f00 100644
--- a/src/components/PansouSearch.tsx
+++ b/src/components/PansouSearch.tsx
@@ -5,9 +5,10 @@ import { AlertCircle, Copy, ExternalLink, Loader2, RefreshCw } from 'lucide-reac
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
-import Toast, { ToastProps } from '@/components/Toast';
import { PansouLink, PansouSearchResult } from '@/lib/pansou.client';
+import Toast, { ToastProps } from '@/components/Toast';
+
interface PansouSearchProps {
keyword: string;
triggerSearch?: boolean; // 触发搜索的标志
@@ -108,7 +109,7 @@ export default function PansouSearch({
}
searchPansou();
- }, [triggerSearch, searchPansou]); // 依赖 triggerSearch 和 searchPansou
+ }, [triggerSearch]); // 只在触发标志变化时搜索,避免 keyword 变化自动搜索
const handleCopy = async (text: string, url: string) => {
try {
@@ -159,10 +160,10 @@ export default function PansouSearch({
}
};
- const handleQuarkInstantPlay = async (link: PansouLink) => {
+ const handleNetdiskInstantPlay = async (cloudType: string, link: PansouLink) => {
try {
setPlayingUrl(link.url);
- const response = await fetch('/api/netdisk/quark/instant-play', {
+ const response = await fetch(cloudType === 'mobile' ? '/api/netdisk/mobile/instant-play' : '/api/netdisk/quark/instant-play', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -180,7 +181,7 @@ export default function PansouSearch({
}
router.push(
- `/play?source=quark-temp&id=${encodeURIComponent(data.id)}&title=${encodeURIComponent(data.title || keyword)}`
+ `/play?source=${encodeURIComponent(data.source || (cloudType === 'mobile' ? 'netdisk-mobile' : 'netdisk-quark'))}&id=${encodeURIComponent(data.id)}&title=${encodeURIComponent(data.title || keyword)}`
);
} catch (err: any) {
setToast({
@@ -338,24 +339,26 @@ export default function PansouSearch({
{/* 操作按钮 */}
- {cloudType === 'quark' && (
+ {(cloudType === 'quark' || cloudType === 'mobile') && (
<>
-
+ {cloudType === 'quark' && (
+
+ )}
>
)}