修复无网盘权限可以直链播放网盘链接

This commit is contained in:
mtvpls
2026-05-15 10:19:58 +08:00
parent cef7f73ce0
commit ae3a054e56
2 changed files with 454 additions and 156 deletions
+298 -105
View File
@@ -6,6 +6,7 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config'; import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
import { getDetailFromApiV2 } from '@/lib/downstream'; import { getDetailFromApiV2 } from '@/lib/downstream';
import { getProxyToken } from '@/lib/emby-token'; import { getProxyToken } from '@/lib/emby-token';
import { hasFeaturePermission } from '@/lib/permissions';
import { import {
createBaiduNetdiskSession, createBaiduNetdiskSession,
getBaiduNetdiskSession, getBaiduNetdiskSession,
@@ -45,6 +46,7 @@ import {
NETDISK_QUARK_SOURCE, NETDISK_QUARK_SOURCE,
NETDISK_TIANYI_SOURCE, NETDISK_TIANYI_SOURCE,
NETDISK_UC_SOURCE, NETDISK_UC_SOURCE,
isNetdiskSource,
normalizeNetdiskSource, normalizeNetdiskSource,
} from '@/lib/netdisk/source'; } from '@/lib/netdisk/source';
import { import {
@@ -68,17 +70,19 @@ import {
export const runtime = 'nodejs'; export const runtime = 'nodejs';
function formatNetdiskEpisodeTitle(parsed: { function formatNetdiskEpisodeTitle(
season?: number; parsed: {
episode?: number; season?: number;
}, fallback: string) { episode?: number;
},
fallback: string
) {
if (parsed.season && parsed.episode) { if (parsed.season && parsed.episode) {
const season = String(Math.trunc(parsed.season)).padStart(2, '0'); const season = String(Math.trunc(parsed.season)).padStart(2, '0');
const episodeValue = parsed.episode; const episodeValue = parsed.episode;
const episode = const episode = Number.isInteger(episodeValue)
Number.isInteger(episodeValue) ? String(Math.trunc(episodeValue)).padStart(2, '0')
? String(Math.trunc(episodeValue)).padStart(2, '0') : String(episodeValue);
: String(episodeValue);
return `S${season}E${episode}`; return `S${season}E${episode}`;
} }
@@ -112,6 +116,19 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 }); return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
} }
if (isNetdiskSource(sourceCode)) {
const allowed = await hasFeaturePermission(
authInfo.username,
'netdisk_temp_play'
);
if (!allowed) {
return NextResponse.json(
{ error: '无权限使用临时播放' },
{ status: 403 }
);
}
}
const parsedScriptSource = parseScriptSourceValue(sourceCode); const parsedScriptSource = parseScriptSourceValue(sourceCode);
if (parsedScriptSource) { if (parsedScriptSource) {
try { try {
@@ -121,11 +138,12 @@ export async function GET(request: NextRequest) {
payload: {}, payload: {},
}); });
const sources = normalizeScriptSources(sourcesExecution.result); const sources = normalizeScriptSources(sourcesExecution.result);
const sourceInfo = const sourceInfo = sources.find(
sources.find((item) => item.id === parsedScriptSource.sourceId) || { (item) => item.id === parsedScriptSource.sourceId
id: parsedScriptSource.sourceId, ) || {
name: parsedScriptSource.sourceId, id: parsedScriptSource.sourceId,
}; name: parsedScriptSource.sourceId,
};
const detailExecution = await executeSavedSourceScript({ const detailExecution = await executeSavedSourceScript({
key: parsedScriptSource.scriptKey, key: parsedScriptSource.scriptKey,
@@ -161,7 +179,10 @@ export async function GET(request: NextRequest) {
const config = await getConfig(); const config = await getConfig();
// 检查是否有启用的 Emby 源 // 检查是否有启用的 Emby 源
if (!config.EmbyConfig?.Sources || config.EmbyConfig.Sources.length === 0) { if (
!config.EmbyConfig?.Sources ||
config.EmbyConfig.Sources.length === 0
) {
throw new Error('Emby 未配置或未启用'); throw new Error('Emby 未配置或未启用');
} }
@@ -174,13 +195,15 @@ export async function GET(request: NextRequest) {
// 使用 EmbyManager 获取客户端和配置 // 使用 EmbyManager 获取客户端和配置
const { embyManager } = await import('@/lib/emby-manager'); const { embyManager } = await import('@/lib/emby-manager');
const sources = await embyManager.getEnabledSources(); const sources = await embyManager.getEnabledSources();
const sourceConfig = sources.find(s => s.key === embyKey); const sourceConfig = sources.find((s) => s.key === embyKey);
const sourceName = sourceConfig?.name || 'Emby'; const sourceName = sourceConfig?.name || 'Emby';
const client = await embyManager.getClient(embyKey); const client = await embyManager.getClient(embyKey);
// 获取代理 token(如果启用了代理) // 获取代理 token(如果启用了代理)
const proxyToken = client.isProxyEnabled() ? await getProxyToken(request) : null; const proxyToken = client.isProxyEnabled()
? await getProxyToken(request)
: null;
// 获取媒体详情 // 获取媒体详情
const item = await client.getItem(id); const item = await client.getItem(id);
@@ -195,7 +218,12 @@ export async function GET(request: NextRequest) {
source_name: sourceName, source_name: sourceName,
id: item.Id, id: item.Id,
title: item.Name, title: item.Name,
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined), poster: client.getImageUrl(
item.Id,
'Primary',
undefined,
proxyToken || undefined
),
year: item.ProductionYear?.toString() || '', year: item.ProductionYear?.toString() || '',
douban_id: 0, douban_id: 0,
desc: item.Overview || '', desc: item.Overview || '',
@@ -229,15 +257,24 @@ export async function GET(request: NextRequest) {
source_name: sourceName, source_name: sourceName,
id: item.Id, id: item.Id,
title: item.Name, title: item.Name,
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined), poster: client.getImageUrl(
item.Id,
'Primary',
undefined,
proxyToken || undefined
),
year: item.ProductionYear?.toString() || '', year: item.ProductionYear?.toString() || '',
douban_id: 0, douban_id: 0,
desc: item.Overview || '', desc: item.Overview || '',
episodes: await Promise.all(allEpisodes.map((ep) => client.getStreamUrl(ep.Id))), episodes: await Promise.all(
allEpisodes.map((ep) => client.getStreamUrl(ep.Id))
),
episodes_titles: allEpisodes.map((ep) => { episodes_titles: allEpisodes.map((ep) => {
const seasonNum = ep.ParentIndexNumber || 1; const seasonNum = ep.ParentIndexNumber || 1;
const episodeNum = ep.IndexNumber || 1; const episodeNum = ep.IndexNumber || 1;
return `S${seasonNum.toString().padStart(2, '0')}E${episodeNum.toString().padStart(2, '0')}`; return `S${seasonNum.toString().padStart(2, '0')}E${episodeNum
.toString()
.padStart(2, '0')}`;
}), }),
subtitles: allEpisodes.map((ep) => client.getSubtitles(ep)), subtitles: allEpisodes.map((ep) => client.getSubtitles(ep)),
proxyMode: false, proxyMode: false,
@@ -261,16 +298,14 @@ export async function GET(request: NextRequest) {
const config = await getConfig(); const config = await getConfig();
const xiaoyaConfig = config.XiaoyaConfig; const xiaoyaConfig = config.XiaoyaConfig;
if ( if (!xiaoyaConfig || !xiaoyaConfig.Enabled || !xiaoyaConfig.ServerURL) {
!xiaoyaConfig ||
!xiaoyaConfig.Enabled ||
!xiaoyaConfig.ServerURL
) {
throw new Error('小雅未配置或未启用'); throw new Error('小雅未配置或未启用');
} }
const { XiaoyaClient } = await import('@/lib/xiaoya.client'); const { XiaoyaClient } = await import('@/lib/xiaoya.client');
const { getXiaoyaMetadata, getXiaoyaEpisodes } = await import('@/lib/xiaoya-metadata'); const { getXiaoyaMetadata, getXiaoyaEpisodes } = await import(
'@/lib/xiaoya-metadata'
);
const { base58Decode, base58Encode } = await import('@/lib/utils'); const { base58Decode, base58Encode } = await import('@/lib/utils');
const client = new XiaoyaClient( const client = new XiaoyaClient(
@@ -299,7 +334,9 @@ export async function GET(request: NextRequest) {
let clickedFilePath: string | undefined; let clickedFilePath: string | undefined;
if (fileName) { if (fileName) {
// 拼接目录路径和文件名 // 拼接目录路径和文件名
clickedFilePath = `${decodedDirPath}${decodedDirPath.endsWith('/') ? '' : '/'}${fileName}`; clickedFilePath = `${decodedDirPath}${
decodedDirPath.endsWith('/') ? '' : '/'
}${fileName}`;
console.log('[xiaoya] 用户点击的文件路径:', clickedFilePath); console.log('[xiaoya] 用户点击的文件路径:', clickedFilePath);
} }
@@ -319,7 +356,9 @@ export async function GET(request: NextRequest) {
// 如果有点击的文件路径,找到对应的集数索引 // 如果有点击的文件路径,找到对应的集数索引
let clickedFileIndex = -1; let clickedFileIndex = -1;
if (clickedFilePath) { if (clickedFilePath) {
clickedFileIndex = episodes.findIndex(ep => ep.path === clickedFilePath); clickedFileIndex = episodes.findIndex(
(ep) => ep.path === clickedFilePath
);
console.log('[xiaoya] 文件在集数列表中的索引:', clickedFileIndex); console.log('[xiaoya] 文件在集数列表中的索引:', clickedFileIndex);
} }
@@ -332,12 +371,16 @@ export async function GET(request: NextRequest) {
year: metadata.year || '', year: metadata.year || '',
douban_id: 0, douban_id: 0,
desc: metadata.plot || '', desc: metadata.plot || '',
episodes: episodes.map(ep => `/api/xiaoya/play?path=${encodeURIComponent(base58Encode(ep.path))}`), episodes: episodes.map(
episodes_titles: episodes.map(ep => ep.title), (ep) =>
`/api/xiaoya/play?path=${encodeURIComponent(base58Encode(ep.path))}`
),
episodes_titles: episodes.map((ep) => ep.title),
subtitles: [], subtitles: [],
proxyMode: false, proxyMode: false,
// 返回用户点击的文件索引(如果找到的话) // 返回用户点击的文件索引(如果找到的话)
initialEpisodeIndex: clickedFileIndex >= 0 ? clickedFileIndex : undefined, initialEpisodeIndex:
clickedFileIndex >= 0 ? clickedFileIndex : undefined,
// 返回元数据来源 // 返回元数据来源
metadataSource: metadata.source, metadataSource: metadata.source,
}; };
@@ -360,11 +403,17 @@ export async function GET(request: NextRequest) {
throw new Error('移动云盘未配置或未启用'); throw new Error('移动云盘未配置或未启用');
} }
let session = refreshMobileNetdiskSession(id) || getMobileNetdiskSession(id); let session =
refreshMobileNetdiskSession(id) || getMobileNetdiskSession(id);
if (!session) { if (!session) {
const payload = parseMobileNetdiskId(id); const payload = parseMobileNetdiskId(id);
const { listMobileShareVideos } = await import('@/lib/netdisk/mobile.client'); const { listMobileShareVideos } = await import(
const result = await listMobileShareVideos(payload.shareUrl, mobileConfig.Authorization); '@/lib/netdisk/mobile.client'
);
const result = await listMobileShareVideos(
payload.shareUrl,
mobileConfig.Authorization
);
session = createMobileNetdiskSession({ session = createMobileNetdiskSession({
title: title || result.title, title: title || result.title,
shareUrl: payload.shareUrl, shareUrl: payload.shareUrl,
@@ -377,8 +426,9 @@ export async function GET(request: NextRequest) {
} }
const mobileSession = session; const mobileSession = session;
const { parseVideoFileName } = await import('@/lib/video-parser'); const { parseVideoFileName } = await import('@/lib/video-parser');
const parsedFiles = mobileSession.files.map((file, index) => { const parsedFiles = mobileSession.files
const parsed = parseVideoFileName(file.name); .map((file, index) => {
const parsed = parseVideoFileName(file.name);
return { return {
...file, ...file,
originalIndex: index, originalIndex: index,
@@ -386,20 +436,24 @@ export async function GET(request: NextRequest) {
isOVA: parsed.isOVA, isOVA: parsed.isOVA,
displayTitle: formatNetdiskEpisodeTitle(parsed, file.name), displayTitle: formatNetdiskEpisodeTitle(parsed, file.name),
}; };
}).sort((a, b) => { })
if (a.isOVA && !b.isOVA) return 1; .sort((a, b) => {
if (!a.isOVA && b.isOVA) return -1; if (a.isOVA && !b.isOVA) return 1;
return a.sortEpisode !== b.sortEpisode if (!a.isOVA && b.isOVA) return -1;
? a.sortEpisode - b.sortEpisode return a.sortEpisode !== b.sortEpisode
: a.name.localeCompare(b.name, 'zh-Hans-CN', { ? a.sortEpisode - b.sortEpisode
numeric: true, : a.name.localeCompare(b.name, 'zh-Hans-CN', {
sensitivity: 'base', numeric: true,
}); sensitivity: 'base',
}); });
});
const episodes = parsedFiles.map((file) => ( const episodes = parsedFiles.map(
`/api/netdisk/mobile/play?id=${encodeURIComponent(mobileSession.id)}&episodeIndex=${file.originalIndex}` (file) =>
)); `/api/netdisk/mobile/play?id=${encodeURIComponent(
mobileSession.id
)}&episodeIndex=${file.originalIndex}`
);
return NextResponse.json({ return NextResponse.json({
source: NETDISK_MOBILE_SOURCE, source: NETDISK_MOBILE_SOURCE,
@@ -430,11 +484,18 @@ export async function GET(request: NextRequest) {
throw new Error('百度网盘未配置或未启用'); throw new Error('百度网盘未配置或未启用');
} }
let session = refreshBaiduNetdiskSession(id) || getBaiduNetdiskSession(id); let session =
refreshBaiduNetdiskSession(id) || getBaiduNetdiskSession(id);
if (!session) { if (!session) {
const payload = parseBaiduNetdiskId(id); const payload = parseBaiduNetdiskId(id);
const { listBaiduShareVideos } = await import('@/lib/netdisk/baidu.client'); const { listBaiduShareVideos } = await import(
const result = await listBaiduShareVideos(payload.shareUrl, baiduConfig.Cookie, payload.passcode || ''); '@/lib/netdisk/baidu.client'
);
const result = await listBaiduShareVideos(
payload.shareUrl,
baiduConfig.Cookie,
payload.passcode || ''
);
session = createBaiduNetdiskSession({ session = createBaiduNetdiskSession({
title: title || result.title, title: title || result.title,
shareUrl: payload.shareUrl, shareUrl: payload.shareUrl,
@@ -465,7 +526,10 @@ export async function GET(request: NextRequest) {
if (!a.isOVA && b.isOVA) return -1; if (!a.isOVA && b.isOVA) return -1;
return a.sortEpisode !== b.sortEpisode return a.sortEpisode !== b.sortEpisode
? a.sortEpisode - b.sortEpisode ? a.sortEpisode - b.sortEpisode
: a.name.localeCompare(b.name, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' }); : a.name.localeCompare(b.name, 'zh-Hans-CN', {
numeric: true,
sensitivity: 'base',
});
}); });
return NextResponse.json({ return NextResponse.json({
@@ -477,9 +541,12 @@ export async function GET(request: NextRequest) {
year: '', year: '',
douban_id: 0, douban_id: 0,
desc: `百度网盘分享:${baiduSession.shareUrl}`, desc: `百度网盘分享:${baiduSession.shareUrl}`,
episodes: parsedFiles.map((file) => ( episodes: parsedFiles.map(
`/api/netdisk/baidu/play?id=${encodeURIComponent(baiduSession.id)}&episodeIndex=${file.originalIndex}` (file) =>
)), `/api/netdisk/baidu/play?id=${encodeURIComponent(
baiduSession.id
)}&episodeIndex=${file.originalIndex}`
),
episodes_titles: parsedFiles.map((file) => file.displayTitle), episodes_titles: parsedFiles.map((file) => file.displayTitle),
proxyMode: false, proxyMode: false,
}); });
@@ -495,14 +562,21 @@ export async function GET(request: NextRequest) {
try { try {
const config = await getConfig(); const config = await getConfig();
const tianyiConfig = config.NetDiskConfig?.Tianyi; const tianyiConfig = config.NetDiskConfig?.Tianyi;
if (!tianyiConfig?.Enabled || !tianyiConfig.Account || !tianyiConfig.Password) { if (
!tianyiConfig?.Enabled ||
!tianyiConfig.Account ||
!tianyiConfig.Password
) {
throw new Error('天翼云盘未配置或未启用'); throw new Error('天翼云盘未配置或未启用');
} }
let session = refreshTianyiNetdiskSession(id) || getTianyiNetdiskSession(id); let session =
refreshTianyiNetdiskSession(id) || getTianyiNetdiskSession(id);
if (!session) { if (!session) {
const payload = parseTianyiNetdiskId(id); const payload = parseTianyiNetdiskId(id);
const { listTianyiShareVideos } = await import('@/lib/netdisk/tianyi.client'); const { listTianyiShareVideos } = await import(
'@/lib/netdisk/tianyi.client'
);
const result = await listTianyiShareVideos( const result = await listTianyiShareVideos(
payload.shareUrl, payload.shareUrl,
tianyiConfig.Account, tianyiConfig.Account,
@@ -542,7 +616,10 @@ export async function GET(request: NextRequest) {
if (!a.isOVA && b.isOVA) return -1; if (!a.isOVA && b.isOVA) return -1;
return a.sortEpisode !== b.sortEpisode return a.sortEpisode !== b.sortEpisode
? a.sortEpisode - b.sortEpisode ? a.sortEpisode - b.sortEpisode
: a.name.localeCompare(b.name, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' }); : a.name.localeCompare(b.name, 'zh-Hans-CN', {
numeric: true,
sensitivity: 'base',
});
}); });
return NextResponse.json({ return NextResponse.json({
@@ -554,9 +631,12 @@ export async function GET(request: NextRequest) {
year: '', year: '',
douban_id: 0, douban_id: 0,
desc: `天翼云盘分享:${tianyiSession.shareUrl}`, desc: `天翼云盘分享:${tianyiSession.shareUrl}`,
episodes: parsedFiles.map((file) => ( episodes: parsedFiles.map(
`/api/netdisk/tianyi/play?id=${encodeURIComponent(tianyiSession.id)}&episodeIndex=${file.originalIndex}` (file) =>
)), `/api/netdisk/tianyi/play?id=${encodeURIComponent(
tianyiSession.id
)}&episodeIndex=${file.originalIndex}`
),
episodes_titles: parsedFiles.map((file) => file.displayTitle), episodes_titles: parsedFiles.map((file) => file.displayTitle),
proxyMode: false, proxyMode: false,
}); });
@@ -572,15 +652,25 @@ export async function GET(request: NextRequest) {
try { try {
const config = await getConfig(); const config = await getConfig();
const pan123Config = config.NetDiskConfig?.Pan123; const pan123Config = config.NetDiskConfig?.Pan123;
if (!pan123Config?.Enabled || !pan123Config.Account || !pan123Config.Password) { if (
!pan123Config?.Enabled ||
!pan123Config.Account ||
!pan123Config.Password
) {
throw new Error('123网盘未配置或未启用'); throw new Error('123网盘未配置或未启用');
} }
let session = refreshPan123NetdiskSession(id) || getPan123NetdiskSession(id); let session =
refreshPan123NetdiskSession(id) || getPan123NetdiskSession(id);
if (!session) { if (!session) {
const payload = parsePan123NetdiskId(id); const payload = parsePan123NetdiskId(id);
const { listPan123ShareVideos } = await import('@/lib/netdisk/pan123.client'); const { listPan123ShareVideos } = await import(
const result = await listPan123ShareVideos(payload.shareUrl, payload.passcode || ''); '@/lib/netdisk/pan123.client'
);
const result = await listPan123ShareVideos(
payload.shareUrl,
payload.passcode || ''
);
session = createPan123NetdiskSession({ session = createPan123NetdiskSession({
title: title || result.title, title: title || result.title,
shareUrl: payload.shareUrl, shareUrl: payload.shareUrl,
@@ -610,7 +700,10 @@ export async function GET(request: NextRequest) {
if (!a.isOVA && b.isOVA) return -1; if (!a.isOVA && b.isOVA) return -1;
return a.sortEpisode !== b.sortEpisode return a.sortEpisode !== b.sortEpisode
? a.sortEpisode - b.sortEpisode ? a.sortEpisode - b.sortEpisode
: a.fileName.localeCompare(b.fileName, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' }); : a.fileName.localeCompare(b.fileName, 'zh-Hans-CN', {
numeric: true,
sensitivity: 'base',
});
}); });
return NextResponse.json({ return NextResponse.json({
@@ -622,9 +715,12 @@ export async function GET(request: NextRequest) {
year: '', year: '',
douban_id: 0, douban_id: 0,
desc: `123网盘分享:${pan123Session.shareUrl}`, desc: `123网盘分享:${pan123Session.shareUrl}`,
episodes: parsedFiles.map((file) => ( episodes: parsedFiles.map(
`/api/netdisk/123/play?id=${encodeURIComponent(pan123Session.id)}&episodeIndex=${file.originalIndex}` (file) =>
)), `/api/netdisk/123/play?id=${encodeURIComponent(
pan123Session.id
)}&episodeIndex=${file.originalIndex}`
),
episodes_titles: parsedFiles.map((file) => file.displayTitle), episodes_titles: parsedFiles.map((file) => file.displayTitle),
proxyMode: false, proxyMode: false,
}); });
@@ -645,11 +741,17 @@ export async function GET(request: NextRequest) {
throw new Error('115网盘未配置或未启用'); throw new Error('115网盘未配置或未启用');
} }
let session = refreshPan115NetdiskSession(id) || getPan115NetdiskSession(id); let session =
refreshPan115NetdiskSession(id) || getPan115NetdiskSession(id);
if (!session) { if (!session) {
const payload = parsePan115NetdiskId(id); const payload = parsePan115NetdiskId(id);
const { listPan115ShareVideos } = await import('@/lib/netdisk/pan115.client'); const { listPan115ShareVideos } = await import(
const result = await listPan115ShareVideos(payload.shareUrl, payload.passcode || ''); '@/lib/netdisk/pan115.client'
);
const result = await listPan115ShareVideos(
payload.shareUrl,
payload.passcode || ''
);
session = createPan115NetdiskSession({ session = createPan115NetdiskSession({
title: title || result.title, title: title || result.title,
shareUrl: payload.shareUrl, shareUrl: payload.shareUrl,
@@ -679,7 +781,10 @@ export async function GET(request: NextRequest) {
if (!a.isOVA && b.isOVA) return -1; if (!a.isOVA && b.isOVA) return -1;
return a.sortEpisode !== b.sortEpisode return a.sortEpisode !== b.sortEpisode
? a.sortEpisode - b.sortEpisode ? a.sortEpisode - b.sortEpisode
: a.name.localeCompare(b.name, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' }); : a.name.localeCompare(b.name, 'zh-Hans-CN', {
numeric: true,
sensitivity: 'base',
});
}); });
return NextResponse.json({ return NextResponse.json({
@@ -691,9 +796,12 @@ export async function GET(request: NextRequest) {
year: '', year: '',
douban_id: 0, douban_id: 0,
desc: `115网盘分享:${pan115Session.shareUrl}`, desc: `115网盘分享:${pan115Session.shareUrl}`,
episodes: parsedFiles.map((file) => ( episodes: parsedFiles.map(
`/api/netdisk/115/play?id=${encodeURIComponent(pan115Session.id)}&episodeIndex=${file.originalIndex}` (file) =>
)), `/api/netdisk/115/play?id=${encodeURIComponent(
pan115Session.id
)}&episodeIndex=${file.originalIndex}`
),
episodes_titles: parsedFiles.map((file) => file.displayTitle), episodes_titles: parsedFiles.map((file) => file.displayTitle),
proxyMode: false, proxyMode: false,
}); });
@@ -705,7 +813,10 @@ export async function GET(request: NextRequest) {
} }
} }
if (sourceCode === NETDISK_QUARK_SOURCE || sourceCode === LEGACY_QUARK_TEMP_SOURCE) { if (
sourceCode === NETDISK_QUARK_SOURCE ||
sourceCode === LEGACY_QUARK_TEMP_SOURCE
) {
try { try {
const config = await getConfig(); const config = await getConfig();
const quarkConfig = config.NetDiskConfig?.Quark; const quarkConfig = config.NetDiskConfig?.Quark;
@@ -714,11 +825,18 @@ export async function GET(request: NextRequest) {
} }
const { parseVideoFileName } = await import('@/lib/video-parser'); const { parseVideoFileName } = await import('@/lib/video-parser');
let session = refreshQuarkNetdiskSession(id) || getQuarkNetdiskSession(id); let session =
refreshQuarkNetdiskSession(id) || getQuarkNetdiskSession(id);
if (!session) { if (!session) {
const payload = parseQuarkNetdiskId(id); const payload = parseQuarkNetdiskId(id);
const { listQuarkShareVideos } = await import('@/lib/netdisk/quark.client'); const { listQuarkShareVideos } = await import(
const result = await listQuarkShareVideos(payload.shareUrl, quarkConfig.Cookie, payload.passcode || ''); '@/lib/netdisk/quark.client'
);
const result = await listQuarkShareVideos(
payload.shareUrl,
quarkConfig.Cookie,
payload.passcode || ''
);
session = createQuarkNetdiskSession({ session = createQuarkNetdiskSession({
title: title || result.title, title: title || result.title,
shareUrl: payload.shareUrl, shareUrl: payload.shareUrl,
@@ -761,9 +879,12 @@ export async function GET(request: NextRequest) {
year: '', year: '',
douban_id: 0, douban_id: 0,
desc: `夸克网盘分享:${quarkSession.shareUrl}`, desc: `夸克网盘分享:${quarkSession.shareUrl}`,
episodes: episodes.map((ep) => ( episodes: episodes.map(
`/api/netdisk/quark/play?id=${encodeURIComponent(quarkSession.id)}&episodeIndex=${ep.originalIndex}` (ep) =>
)), `/api/netdisk/quark/play?id=${encodeURIComponent(
quarkSession.id
)}&episodeIndex=${ep.originalIndex}`
),
episodes_titles: episodes.map((ep) => ep.title), episodes_titles: episodes.map((ep) => ep.title),
proxyMode: false, proxyMode: false,
}); });
@@ -788,7 +909,11 @@ export async function GET(request: NextRequest) {
if (!session) { if (!session) {
const payload = parseUCNetdiskId(id); const payload = parseUCNetdiskId(id);
const { listUCShareVideos } = await import('@/lib/netdisk/uc.client'); const { listUCShareVideos } = await import('@/lib/netdisk/uc.client');
const result = await listUCShareVideos(payload.shareUrl, ucConfig.Cookie, payload.passcode || ''); const result = await listUCShareVideos(
payload.shareUrl,
ucConfig.Cookie,
payload.passcode || ''
);
session = createUCNetdiskSession({ session = createUCNetdiskSession({
title: title || result.title, title: title || result.title,
shareUrl: payload.shareUrl, shareUrl: payload.shareUrl,
@@ -831,9 +956,12 @@ export async function GET(request: NextRequest) {
year: '', year: '',
douban_id: 0, douban_id: 0,
desc: `UC网盘分享:${ucSession.shareUrl}`, desc: `UC网盘分享:${ucSession.shareUrl}`,
episodes: episodes.map((ep) => ( episodes: episodes.map(
`/api/netdisk/uc/play?id=${encodeURIComponent(ucSession.id)}&episodeIndex=${ep.originalIndex}` (ep) =>
)), `/api/netdisk/uc/play?id=${encodeURIComponent(
ucSession.id
)}&episodeIndex=${ep.originalIndex}`
),
episodes_titles: episodes.map((ep) => ep.title), episodes_titles: episodes.map((ep) => ep.title),
proxyMode: false, proxyMode: false,
}); });
@@ -867,7 +995,9 @@ export async function GET(request: NextRequest) {
let metaInfo: any = null; let metaInfo: any = null;
let folderMeta: any = null; let folderMeta: any = null;
try { try {
const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache'); const { getCachedMetaInfo, setCachedMetaInfo } = await import(
'@/lib/openlist-cache'
);
const { db } = await import('@/lib/db'); const { db } = await import('@/lib/db');
metaInfo = getCachedMetaInfo(); metaInfo = getCachedMetaInfo();
@@ -891,11 +1021,15 @@ export async function GET(request: NextRequest) {
// 使用 folderName 构建实际路径 // 使用 folderName 构建实际路径
const folderName = folderMeta.folderName; const folderName = folderMeta.folderName;
const folderPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}${folderName}`; const folderPath = `${rootPath}${
rootPath.endsWith('/') ? '' : '/'
}${folderName}`;
// 2. 直接调用 OpenList 客户端获取视频列表 // 2. 直接调用 OpenList 客户端获取视频列表
const { OpenListClient } = await import('@/lib/openlist.client'); const { OpenListClient } = await import('@/lib/openlist.client');
const { getCachedVideoInfo, setCachedVideoInfo } = await import('@/lib/openlist-cache'); const { getCachedVideoInfo, setCachedVideoInfo } = await import(
'@/lib/openlist-cache'
);
const { parseVideoFileName } = await import('@/lib/video-parser'); const { parseVideoFileName } = await import('@/lib/video-parser');
const client = new OpenListClient( const client = new OpenListClient(
@@ -914,7 +1048,11 @@ export async function GET(request: NextRequest) {
let hasMore = true; let hasMore = true;
while (hasMore) { while (hasMore) {
const listResponse = await client.listDirectory(folderPath, currentPage, pageSize); const listResponse = await client.listDirectory(
folderPath,
currentPage,
pageSize
);
if (listResponse.code !== 200) { if (listResponse.code !== 200) {
throw new Error('OpenList 列表获取失败4'); throw new Error('OpenList 列表获取失败4');
@@ -927,10 +1065,35 @@ export async function GET(request: NextRequest) {
currentPage++; currentPage++;
} }
const videoExtensions = ['.mp4', '.mkv', '.avi', '.m3u8', '.flv', '.ts', '.mov', '.wmv', '.webm', '.rmvb', '.rm', '.mpg', '.mpeg', '.3gp', '.f4v', '.m4v', '.vob']; const videoExtensions = [
'.mp4',
'.mkv',
'.avi',
'.m3u8',
'.flv',
'.ts',
'.mov',
'.wmv',
'.webm',
'.rmvb',
'.rm',
'.mpg',
'.mpeg',
'.3gp',
'.f4v',
'.m4v',
'.vob',
];
const videoFiles = allFiles.filter((item) => { const videoFiles = allFiles.filter((item) => {
if (item.is_dir || item.name.startsWith('.') || item.name.endsWith('.json')) return false; if (
return videoExtensions.some(ext => item.name.toLowerCase().endsWith(ext)); item.is_dir ||
item.name.startsWith('.') ||
item.name.endsWith('.json')
)
return false;
return videoExtensions.some((ext) =>
item.name.toLowerCase().endsWith(ext)
);
}); });
if (!videoInfo) { if (!videoInfo) {
@@ -940,7 +1103,7 @@ export async function GET(request: NextRequest) {
const file = videoFiles[i]; const file = videoFiles[i];
const parsed = parseVideoFileName(file.name); const parsed = parseVideoFileName(file.name);
videoInfo.episodes[file.name] = { videoInfo.episodes[file.name] = {
episode: parsed.episode || (i + 1), episode: parsed.episode || i + 1,
season: parsed.season, season: parsed.season,
title: parsed.title, title: parsed.title,
parsed_from: 'filename', parsed_from: 'filename',
@@ -955,25 +1118,46 @@ export async function GET(request: NextRequest) {
const parsed = parseVideoFileName(file.name); const parsed = parseVideoFileName(file.name);
let episodeInfo; let episodeInfo;
if (parsed.episode) { if (parsed.episode) {
episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename', isOVA: parsed.isOVA }; episodeInfo = {
episode: parsed.episode,
season: parsed.season,
title: parsed.title,
parsed_from: 'filename',
isOVA: parsed.isOVA,
};
} else { } else {
episodeInfo = videoInfo!.episodes[file.name] || { episode: index + 1, season: undefined, title: undefined, parsed_from: 'filename' }; episodeInfo = videoInfo!.episodes[file.name] || {
episode: index + 1,
season: undefined,
title: undefined,
parsed_from: 'filename',
};
} }
let displayTitle = episodeInfo.title; let displayTitle = episodeInfo.title;
if (!displayTitle && episodeInfo.episode) { if (!displayTitle && episodeInfo.episode) {
displayTitle = episodeInfo.isOVA ? `OVA ${episodeInfo.episode}` : `${episodeInfo.episode}`; displayTitle = episodeInfo.isOVA
? `OVA ${episodeInfo.episode}`
: `${episodeInfo.episode}`;
} }
if (!displayTitle) { if (!displayTitle) {
displayTitle = file.name; displayTitle = file.name;
} }
return { fileName: file.name, episode: episodeInfo.episode || 0, season: episodeInfo.season, title: displayTitle, isOVA: episodeInfo.isOVA }; return {
fileName: file.name,
episode: episodeInfo.episode || 0,
season: episodeInfo.season,
title: displayTitle,
isOVA: episodeInfo.isOVA,
};
}) })
.sort((a, b) => { .sort((a, b) => {
// OVA 排在最后 // OVA 排在最后
if (a.isOVA && !b.isOVA) return 1; if (a.isOVA && !b.isOVA) return 1;
if (!a.isOVA && b.isOVA) return -1; if (!a.isOVA && b.isOVA) return -1;
// 都是 OVA 或都不是 OVA,按集数排序 // 都是 OVA 或都不是 OVA,按集数排序
return a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName); return a.episode !== b.episode
? a.episode - b.episode
: a.fileName.localeCompare(b.fileName);
}); });
// 3. 从 metainfo 中获取元数据 // 3. 从 metainfo 中获取元数据
@@ -984,11 +1168,20 @@ export async function GET(request: NextRequest) {
source_name: '私人影库', source_name: '私人影库',
id: id, id: id,
title: folderMeta?.title || folderName, title: folderMeta?.title || folderName,
poster: folderMeta?.poster_path ? getTMDBImageUrl(folderMeta.poster_path) : '', poster: folderMeta?.poster_path
year: folderMeta?.release_date ? folderMeta.release_date.split('-')[0] : '', ? getTMDBImageUrl(folderMeta.poster_path)
: '',
year: folderMeta?.release_date
? folderMeta.release_date.split('-')[0]
: '',
douban_id: 0, douban_id: 0,
desc: folderMeta?.overview || '', desc: folderMeta?.overview || '',
episodes: episodes.map((ep) => `/api/openlist/play?folder=${encodeURIComponent(folderName)}&fileName=${encodeURIComponent(ep.fileName)}`), episodes: episodes.map(
(ep) =>
`/api/openlist/play?folder=${encodeURIComponent(
folderName
)}&fileName=${encodeURIComponent(ep.fileName)}`
),
episodes_titles: episodes.map((ep) => ep.title), episodes_titles: episodes.map((ep) => ep.title),
proxyMode: false, // openlist 源不使用代理模式 proxyMode: false, // openlist 源不使用代理模式
}; };
+156 -51
View File
@@ -2,7 +2,15 @@
'use client'; 'use client';
import { BookMarked, BookOpen, Bot, ChevronRight, Link as LinkIcon, ListVideo, Music } from 'lucide-react'; import {
BookMarked,
BookOpen,
Bot,
ChevronRight,
Link as LinkIcon,
ListVideo,
Music,
} from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { Suspense, useEffect, useState } from 'react'; import { Suspense, useEffect, useState } from 'react';
@@ -56,23 +64,29 @@ function HomeClient() {
{ id: 'upcomingContent', name: '即将上映', enabled: true, order: 5 }, { id: 'upcomingContent', name: '即将上映', enabled: true, order: 5 },
]); ]);
const [homeBannerEnabled, setHomeBannerEnabled] = useState(true); const [homeBannerEnabled, setHomeBannerEnabled] = useState(true);
const [homeContinueWatchingEnabled, setHomeContinueWatchingEnabled] = useState(true); const [homeContinueWatchingEnabled, setHomeContinueWatchingEnabled] =
useState(true);
const [showAnnouncement, setShowAnnouncement] = useState(false); const [showAnnouncement, setShowAnnouncement] = useState(false);
const [showHttpWarning, setShowHttpWarning] = useState(true); const [showHttpWarning, setShowHttpWarning] = useState(true);
const [showAIChat, setShowAIChat] = useState(false); const [showAIChat, setShowAIChat] = useState(false);
const [aiEnabled, setAiEnabled] = useState(false); const [aiEnabled, setAiEnabled] = useState(false);
const [aiDefaultMessageNoVideo, setAiDefaultMessageNoVideo] = useState('你好!我是MoonTVPlus的AI影视助手。想看什么电影或剧集?需要推荐吗?'); const [aiDefaultMessageNoVideo, setAiDefaultMessageNoVideo] = useState(
'你好!我是MoonTVPlus的AI影视助手。想看什么电影或剧集?需要推荐吗?'
);
const [sourceSearchEnabled, setSourceSearchEnabled] = useState(true); const [sourceSearchEnabled, setSourceSearchEnabled] = useState(true);
const [musicEnabled, setMusicEnabled] = useState(false); const [musicEnabled, setMusicEnabled] = useState(false);
const [mangaEnabled, setMangaEnabled] = useState(false); const [mangaEnabled, setMangaEnabled] = useState(false);
const [booksEnabled, setBooksEnabled] = useState(false); const [booksEnabled, setBooksEnabled] = useState(false);
const [netdiskTempPlayEnabled, setNetdiskTempPlayEnabled] = useState(false);
const [showDirectPlayDialog, setShowDirectPlayDialog] = useState(false); const [showDirectPlayDialog, setShowDirectPlayDialog] = useState(false);
const [directPlayUrl, setDirectPlayUrl] = useState(''); const [directPlayUrl, setDirectPlayUrl] = useState('');
const [directPlaySubmitting, setDirectPlaySubmitting] = useState(false); const [directPlaySubmitting, setDirectPlaySubmitting] = useState(false);
const [toast, setToast] = useState<ToastProps | null>(null); const [toast, setToast] = useState<ToastProps | null>(null);
const detectNetdiskLink = (url: string): { const detectNetdiskLink = (
url: string
): {
provider: 'quark' | 'mobile' | 'baidu' | 'tianyi' | '123' | 'uc' | '115'; provider: 'quark' | 'mobile' | 'baidu' | 'tianyi' | '123' | 'uc' | '115';
shareUrl: string; shareUrl: string;
passcode?: string; passcode?: string;
@@ -84,11 +98,17 @@ function HomeClient() {
const inlinePasscode = (text: string) => const inlinePasscode = (text: string) =>
pickPasscode( pickPasscode(
text.match(/(?:提取码|访问码|密码)\s*[:=]?\s*([a-zA-Z0-9]{4,8})/i)?.[1], text.match(
/(?:提取码|访问码|密码)\s*[:=]?\s*([a-zA-Z0-9]{4,8})/i
)?.[1],
text.match(/[?&](?:pwd|passcode|accessCode)=([^&\s]+)/i)?.[1] text.match(/[?&](?:pwd|passcode|accessCode)=([^&\s]+)/i)?.[1]
); );
if (/https:\/\/(?:www\.)?123(?:684|865|912|pan)\.(?:com|cn)\/s\//i.test(trimmed)) { if (
/https:\/\/(?:www\.)?123(?:684|865|912|pan)\.(?:com|cn)\/s\//i.test(
trimmed
)
) {
return { return {
provider: '123', provider: '123',
shareUrl: trimmed, shareUrl: trimmed,
@@ -99,7 +119,10 @@ function HomeClient() {
}; };
} }
if (/https:\/\/cloud\.189\.cn\/(web\/share\?code=|t\/)/i.test(trimmed) || /https:\/\/h5\.cloud\.189\.cn\/share\.html#\/t\//i.test(trimmed)) { if (
/https:\/\/cloud\.189\.cn\/(web\/share\?code=|t\/)/i.test(trimmed) ||
/https:\/\/h5\.cloud\.189\.cn\/share\.html#\/t\//i.test(trimmed)
) {
return { return {
provider: 'tianyi', provider: 'tianyi',
shareUrl: trimmed, shareUrl: trimmed,
@@ -172,21 +195,25 @@ function HomeClient() {
setDirectPlaySubmitting(true); setDirectPlaySubmitting(true);
try { try {
const netdisk = detectNetdiskLink(trimmed); const netdisk = detectNetdiskLink(trimmed);
if (netdisk && !netdiskTempPlayEnabled) {
throw new Error('无权限使用临时播放');
}
if (netdisk) { if (netdisk) {
const source = const source =
netdisk.provider === 'mobile' netdisk.provider === 'mobile'
? 'netdisk-mobile' ? 'netdisk-mobile'
: netdisk.provider === 'baidu' : netdisk.provider === 'baidu'
? 'netdisk-baidu' ? 'netdisk-baidu'
: netdisk.provider === 'tianyi' : netdisk.provider === 'tianyi'
? 'netdisk-tianyi' ? 'netdisk-tianyi'
: netdisk.provider === '115' : netdisk.provider === '115'
? 'netdisk-115' ? 'netdisk-115'
: netdisk.provider === 'uc' : netdisk.provider === 'uc'
? 'netdisk-uc' ? 'netdisk-uc'
: netdisk.provider === '123' : netdisk.provider === '123'
? 'netdisk-123' ? 'netdisk-123'
: 'netdisk-quark'; : 'netdisk-quark';
const id = base58Encode( const id = base58Encode(
JSON.stringify({ JSON.stringify({
shareUrl: netdisk.shareUrl, shareUrl: netdisk.shareUrl,
@@ -196,7 +223,11 @@ function HomeClient() {
if (!id) { if (!id) {
throw new Error('网盘链接编码失败'); throw new Error('网盘链接编码失败');
} }
const targetUrl = `/play?source=${encodeURIComponent(source)}&id=${encodeURIComponent(id)}&title=${encodeURIComponent('网盘直链播放')}`; const targetUrl = `/play?source=${encodeURIComponent(
source
)}&id=${encodeURIComponent(id)}&title=${encodeURIComponent(
'网盘直链播放'
)}`;
setShowDirectPlayDialog(false); setShowDirectPlayDialog(false);
setDirectPlayUrl(''); setDirectPlayUrl('');
window.location.assign(targetUrl); window.location.assign(targetUrl);
@@ -205,7 +236,9 @@ function HomeClient() {
const encoded = base58Encode(trimmed); const encoded = base58Encode(trimmed);
if (!encoded) return; if (!encoded) return;
const targetUrl = `/play?source=directplay&id=${encodeURIComponent(encoded)}`; const targetUrl = `/play?source=directplay&id=${encodeURIComponent(
encoded
)}`;
setShowDirectPlayDialog(false); setShowDirectPlayDialog(false);
setDirectPlayUrl(''); setDirectPlayUrl('');
window.location.assign(targetUrl); window.location.assign(targetUrl);
@@ -237,9 +270,13 @@ function HomeClient() {
setHomeBannerEnabled(savedHomeBannerEnabled === 'true'); setHomeBannerEnabled(savedHomeBannerEnabled === 'true');
} }
const savedHomeContinueWatchingEnabled = localStorage.getItem('homeContinueWatchingEnabled'); const savedHomeContinueWatchingEnabled = localStorage.getItem(
'homeContinueWatchingEnabled'
);
if (savedHomeContinueWatchingEnabled !== null) { if (savedHomeContinueWatchingEnabled !== null) {
setHomeContinueWatchingEnabled(savedHomeContinueWatchingEnabled === 'true'); setHomeContinueWatchingEnabled(
savedHomeContinueWatchingEnabled === 'true'
);
} }
}; };
@@ -256,7 +293,10 @@ function HomeClient() {
window.addEventListener('homeModulesUpdated', handleHomeModulesUpdated); window.addEventListener('homeModulesUpdated', handleHomeModulesUpdated);
return () => { return () => {
window.removeEventListener('homeModulesUpdated', handleHomeModulesUpdated); window.removeEventListener(
'homeModulesUpdated',
handleHomeModulesUpdated
);
}; };
}, []); }, []);
@@ -269,7 +309,8 @@ function HomeClient() {
setAiEnabled(enabled); setAiEnabled(enabled);
// 加载AI默认消息配置 // 加载AI默认消息配置
const defaultMsg = (window as any).RUNTIME_CONFIG?.AI_DEFAULT_MESSAGE_NO_VIDEO; const defaultMsg = (window as any).RUNTIME_CONFIG
?.AI_DEFAULT_MESSAGE_NO_VIDEO;
if (defaultMsg) { if (defaultMsg) {
setAiDefaultMessageNoVideo(defaultMsg); setAiDefaultMessageNoVideo(defaultMsg);
} }
@@ -279,7 +320,8 @@ function HomeClient() {
// 检查源站寻片功能是否启用 // 检查源站寻片功能是否启用
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const enabled = (window as any).RUNTIME_CONFIG?.ENABLE_SOURCE_SEARCH !== false; const enabled =
(window as any).RUNTIME_CONFIG?.ENABLE_SOURCE_SEARCH !== false;
setSourceSearchEnabled(enabled); setSourceSearchEnabled(enabled);
} }
}, []); }, []);
@@ -308,6 +350,15 @@ function HomeClient() {
} }
}, []); }, []);
// 检查网盘临时播放权限,仅有权限时在直链播放弹窗展示网盘在线播放提示
useEffect(() => {
if (typeof window !== 'undefined') {
const enabled = !!(window as any).RUNTIME_CONFIG
?.NETDISK_TEMP_PLAY_ENABLED;
setNetdiskTempPlayEnabled(enabled);
}
}, []);
// 检查公告弹窗状态 // 检查公告弹窗状态
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined' && announcement) { if (typeof window !== 'undefined' && announcement) {
@@ -336,7 +387,10 @@ function HomeClient() {
const setCache = (key: string, data: any) => { const setCache = (key: string, data: any) => {
try { try {
localStorage.setItem(key, JSON.stringify({ data, timestamp: Date.now() })); localStorage.setItem(
key,
JSON.stringify({ data, timestamp: Date.now() })
);
} catch { } catch {
// Ignore localStorage errors // Ignore localStorage errors
} }
@@ -356,18 +410,43 @@ function HomeClient() {
if (duanjuCache?.data) setHotDuanju(duanjuCache.data); if (duanjuCache?.data) setHotDuanju(duanjuCache.data);
if (upcomingCache?.data) setUpcomingContent(upcomingCache.data); if (upcomingCache?.data) setUpcomingContent(upcomingCache.data);
const hasCache = moviesCache || tvShowsCache || varietyCache || bangumiCache || duanjuCache || upcomingCache; const hasCache =
moviesCache ||
tvShowsCache ||
varietyCache ||
bangumiCache ||
duanjuCache ||
upcomingCache;
if (hasCache) setLoading(false); if (hasCache) setLoading(false);
const needsRefresh = !moviesCache || moviesCache.expired || !tvShowsCache || tvShowsCache.expired || const needsRefresh =
!varietyCache || varietyCache.expired || !bangumiCache || bangumiCache.expired || !moviesCache ||
!duanjuCache || duanjuCache.expired || !upcomingCache || upcomingCache.expired; moviesCache.expired ||
!tvShowsCache ||
tvShowsCache.expired ||
!varietyCache ||
varietyCache.expired ||
!bangumiCache ||
bangumiCache.expired ||
!duanjuCache ||
duanjuCache.expired ||
!upcomingCache ||
upcomingCache.expired;
if (needsRefresh) { if (needsRefresh) {
(async () => { (async () => {
try { try {
const [moviesData, tvShowsData, varietyShowsData, bangumiCalendarData] = await Promise.all([ const [
getDoubanCategories({ kind: 'movie', category: '热门', type: '全部' }), moviesData,
tvShowsData,
varietyShowsData,
bangumiCalendarData,
] = await Promise.all([
getDoubanCategories({
kind: 'movie',
category: '热门',
type: '全部',
}),
getDoubanCategories({ kind: 'tv', category: 'tv', type: 'tv' }), getDoubanCategories({ kind: 'tv', category: 'tv', type: 'tv' }),
getDoubanCategories({ kind: 'tv', category: 'show', type: 'show' }), getDoubanCategories({ kind: 'tv', category: 'show', type: 'show' }),
GetBangumiCalendarData(), GetBangumiCalendarData(),
@@ -400,7 +479,11 @@ function HomeClient() {
const duanjuResponse = await fetch('/api/duanju/recommends'); const duanjuResponse = await fetch('/api/duanju/recommends');
if (duanjuResponse.ok) { if (duanjuResponse.ok) {
const duanjuResult = await duanjuResponse.json(); const duanjuResult = await duanjuResponse.json();
if (duanjuResult.code === 200 && duanjuResult.data && duanjuResult.data.length > 0) { if (
duanjuResult.code === 200 &&
duanjuResult.data &&
duanjuResult.data.length > 0
) {
setHotDuanju(duanjuResult.data); setHotDuanju(duanjuResult.data);
setCache('homepage_duanju', duanjuResult.data); setCache('homepage_duanju', duanjuResult.data);
} }
@@ -413,10 +496,18 @@ function HomeClient() {
const response = await fetch('/api/tmdb/upcoming'); const response = await fetch('/api/tmdb/upcoming');
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
if (result.code === 200 && result.data && result.data.length > 0) { if (
result.code === 200 &&
result.data &&
result.data.length > 0
) {
const sorted = [...result.data].sort((a, b) => { const sorted = [...result.data].sort((a, b) => {
const dateA = new Date(a.release_date || '9999-12-31').getTime(); const dateA = new Date(
const dateB = new Date(b.release_date || '9999-12-31').getTime(); a.release_date || '9999-12-31'
).getTime();
const dateB = new Date(
b.release_date || '9999-12-31'
).getTime();
return dateA - dateB; return dateA - dateB;
}); });
setUpcomingContent(sorted); setUpcomingContent(sorted);
@@ -436,8 +527,6 @@ function HomeClient() {
} }
}, []); }, []);
const handleCloseAnnouncement = (announcement: string) => { const handleCloseAnnouncement = (announcement: string) => {
setShowAnnouncement(false); setShowAnnouncement(false);
localStorage.setItem('hasSeenAnnouncement', announcement); // 记录已查看弹窗 localStorage.setItem('hasSeenAnnouncement', announcement); // 记录已查看弹窗
@@ -448,7 +537,7 @@ function HomeClient() {
switch (moduleId) { switch (moduleId) {
case 'hotMovies': case 'hotMovies':
return ( return (
<section key="hotMovies" className='mb-8'> <section key='hotMovies' className='mb-8'>
<div className='mb-4 flex items-center justify-between'> <div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'> <h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
@@ -496,7 +585,7 @@ function HomeClient() {
case 'hotDuanju': case 'hotDuanju':
if (hotDuanju.length === 0) return null; if (hotDuanju.length === 0) return null;
return ( return (
<section key="hotDuanju" className='mb-8'> <section key='hotDuanju' className='mb-8'>
<div className='mb-4 flex items-center justify-between'> <div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'> <h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
@@ -550,7 +639,7 @@ function HomeClient() {
case 'bangumiCalendar': case 'bangumiCalendar':
return ( return (
<section key="bangumiCalendar" className='mb-8'> <section key='bangumiCalendar' className='mb-8'>
<div className='mb-4 flex items-center justify-between'> <div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'> <h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
@@ -578,7 +667,15 @@ function HomeClient() {
)) ))
: (() => { : (() => {
const today = new Date(); const today = new Date();
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const weekdays = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
const currentWeekday = weekdays[today.getDay()]; const currentWeekday = weekdays[today.getDay()];
const todayAnimes = const todayAnimes =
bangumiCalendarData bangumiCalendarData
@@ -615,7 +712,7 @@ function HomeClient() {
case 'hotTvShows': case 'hotTvShows':
return ( return (
<section key="hotTvShows" className='mb-8'> <section key='hotTvShows' className='mb-8'>
<div className='mb-4 flex items-center justify-between'> <div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'> <h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
@@ -662,7 +759,7 @@ function HomeClient() {
case 'hotVarietyShows': case 'hotVarietyShows':
return ( return (
<section key="hotVarietyShows" className='mb-8'> <section key='hotVarietyShows' className='mb-8'>
<div className='mb-4 flex items-center justify-between'> <div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'> <h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
@@ -699,7 +796,9 @@ function HomeClient() {
rate={varietyShow.rate} rate={varietyShow.rate}
type='tv' type='tv'
from='douban' from='douban'
douban_id={varietyShow.id ? parseInt(varietyShow.id) : undefined} douban_id={
varietyShow.id ? parseInt(varietyShow.id) : undefined
}
/> />
</div> </div>
))} ))}
@@ -710,7 +809,7 @@ function HomeClient() {
case 'upcomingContent': case 'upcomingContent':
if (upcomingContent.length === 0) return null; if (upcomingContent.length === 0) return null;
return ( return (
<section key="upcomingContent" className='mb-8'> <section key='upcomingContent' className='mb-8'>
<div className='mb-4 flex items-center justify-between'> <div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'> <h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
@@ -762,7 +861,11 @@ function HomeClient() {
{/* 首页内容 */} {/* 首页内容 */}
<> <>
{/* 源站寻片和AI问片入口 */} {/* 源站寻片和AI问片入口 */}
<div className={`flex items-center justify-end gap-2 mb-4 ${homeBannerEnabled ? '' : 'mt-[30px]'}`}> <div
className={`flex items-center justify-end gap-2 mb-4 ${
homeBannerEnabled ? '' : 'mt-[30px]'
}`}
>
<button <button
onClick={handleDirectPlay} onClick={handleDirectPlay}
className='p-1.5 rounded-lg text-blue-500 hover:text-blue-600 transition-colors' className='p-1.5 rounded-lg text-blue-500 hover:text-blue-600 transition-colors'
@@ -833,9 +936,9 @@ function HomeClient() {
{/* 根据配置动态渲染首页模块 */} {/* 根据配置动态渲染首页模块 */}
{homeModules {homeModules
.filter(module => module.enabled) .filter((module) => module.enabled)
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
.map(module => renderModule(module.id))} .map((module) => renderModule(module.id))}
</> </>
</div> </div>
</div> </div>
@@ -899,9 +1002,11 @@ function HomeClient() {
<div className='text-sm text-gray-600 dark:text-gray-300'> <div className='text-sm text-gray-600 dark:text-gray-300'>
</div> </div>
<div className='text-xs text-gray-500 dark:text-gray-400'> {netdiskTempPlayEnabled && (
UC123115 线 <div className='text-xs text-gray-500 dark:text-gray-400'>
</div> UC123115 线
</div>
)}
<input <input
value={directPlayUrl} value={directPlayUrl}
onChange={(event) => setDirectPlayUrl(event.target.value)} onChange={(event) => setDirectPlayUrl(event.target.value)}