This commit is contained in:
mtvpls
2025-12-24 19:40:33 +08:00
4 changed files with 87 additions and 47 deletions

View File

@@ -507,6 +507,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
enabledApis?: string[]; enabledApis?: string[];
tags?: string[]; tags?: string[];
created_at?: number; created_at?: number;
oidcSub?: string;
}> = !hasOldUserData && usersV2 ? usersV2 : (config?.UserConfig?.Users || []); }> = !hasOldUserData && usersV2 ? usersV2 : (config?.UserConfig?.Users || []);
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算 // 使用 useMemo 计算全选状态,避免每次渲染都重新计算

View File

@@ -161,27 +161,6 @@ export async function POST(request: NextRequest) {
// 使用新版本创建用户带SHA256加密和OIDC绑定 // 使用新版本创建用户带SHA256加密和OIDC绑定
await db.createUserV2(username, randomPassword, 'user', defaultTags, oidcSession.sub); await db.createUserV2(username, randomPassword, 'user', defaultTags, oidcSession.sub);
// 同时在旧版本存储中创建(保持兼容性)
await db.registerUser(username, randomPassword);
// 将用户添加到配置中(保持兼容性)
const newUser: any = {
username: username,
role: 'user',
banned: false,
oidcSub: oidcSession.sub, // 保存OIDC标识符
};
// 如果配置了默认用户组,分配给新用户
if (defaultTags) {
newUser.tags = defaultTags;
}
config.UserConfig.Users.push(newUser);
// 保存配置
await db.saveAdminConfig(config);
// 设置认证cookie // 设置认证cookie
const response = NextResponse.json({ ok: true, message: '注册成功' }); const response = NextResponse.json({ ok: true, message: '注册成功' });
const cookieValue = await generateAuthCookie(username, 'user'); const cookieValue = await generateAuthCookie(username, 'user');

View File

@@ -31,12 +31,12 @@ export async function GET(request: NextRequest) {
} }
const rootPath = openListConfig.RootPath || '/'; const rootPath = openListConfig.RootPath || '/';
const folderPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}${id}`;
// 1. 读取 metainfo 获取元数据 // 1. 读取 metainfo 获取元数据
let metaInfo: any = null; let metaInfo: any = null;
try { try {
const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache'); const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache');
const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
const { db } = await import('@/lib/db'); const { db } = await import('@/lib/db');
metaInfo = getCachedMetaInfo(rootPath); metaInfo = getCachedMetaInfo(rootPath);
@@ -49,51 +49,105 @@ export async function GET(request: NextRequest) {
} }
} }
} catch (error) { } catch (error) {
console.error('[Detail] 从数据库读取 metainfo 失败:', error); // 忽略错误
} }
// 2. 调用 openlist detail API // 2. 直接调用 OpenList 客户端获取视频列表
const openlistResponse = await fetch( const { OpenListClient } = await import('@/lib/openlist.client');
`${request.headers.get('x-forwarded-proto') || 'http'}://${request.headers.get('host')}/api/openlist/detail?folder=${encodeURIComponent(id)}`, const { getCachedVideoInfo, setCachedVideoInfo } = await import('@/lib/openlist-cache');
{ const { parseVideoFileName } = await import('@/lib/video-parser');
headers: {
Cookie: request.headers.get('cookie') || '', const client = new OpenListClient(
}, openListConfig.URL,
} openListConfig.Username,
openListConfig.Password
); );
if (!openlistResponse.ok) { let videoInfo = getCachedVideoInfo(folderPath);
throw new Error('获取 OpenList 视频详情失败');
if (!videoInfo) {
try {
const videoinfoPath = `${folderPath}/videoinfo.json`;
const fileResponse = await client.getFile(videoinfoPath);
if (fileResponse.code === 200 && fileResponse.data.raw_url) {
const contentResponse = await fetch(fileResponse.data.raw_url);
const content = await contentResponse.text();
videoInfo = JSON.parse(content);
if (videoInfo) {
setCachedVideoInfo(folderPath, videoInfo);
}
}
} catch (error) {
// 忽略错误
}
} }
const openlistData = await openlistResponse.json(); const listResponse = await client.listDirectory(folderPath);
if (!openlistData.success) { if (listResponse.code !== 200) {
throw new Error(openlistData.error || '获取视频详情失败'); throw new Error('OpenList 列表获取失败');
} }
const videoExtensions = ['.mp4', '.mkv', '.avi', '.m3u8', '.flv', '.ts', '.mov', '.wmv', '.webm', '.rmvb', '.rm', '.mpg', '.mpeg', '.3gp', '.f4v', '.m4v', '.vob'];
const videoFiles = listResponse.data.content.filter((item) => {
if (item.is_dir || item.name.startsWith('.') || item.name.endsWith('.json')) return false;
return videoExtensions.some(ext => item.name.toLowerCase().endsWith(ext));
});
if (!videoInfo) {
videoInfo = { episodes: {}, last_updated: Date.now() };
videoFiles.sort((a, b) => a.name.localeCompare(b.name));
for (let i = 0; i < videoFiles.length; i++) {
const file = videoFiles[i];
const parsed = parseVideoFileName(file.name);
videoInfo.episodes[file.name] = {
episode: parsed.episode || (i + 1),
season: parsed.season,
title: parsed.title,
parsed_from: 'filename',
};
}
setCachedVideoInfo(folderPath, videoInfo);
}
const episodes = videoFiles
.map((file, index) => {
const parsed = parseVideoFileName(file.name);
let episodeInfo;
if (parsed.episode) {
episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename' };
} else {
episodeInfo = videoInfo!.episodes[file.name] || { episode: index + 1, season: undefined, title: undefined, parsed_from: 'filename' };
}
let displayTitle = episodeInfo.title;
if (!displayTitle && episodeInfo.episode) {
displayTitle = `${episodeInfo.episode}`;
}
if (!displayTitle) {
displayTitle = file.name;
}
return { fileName: file.name, episode: episodeInfo.episode || 0, season: episodeInfo.season, title: displayTitle };
})
.sort((a, b) => a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName));
// 3. 从 metainfo 中获取元数据 // 3. 从 metainfo 中获取元数据
const folderMeta = metaInfo?.folders?.[id]; const folderMeta = metaInfo?.folders?.[id];
const { getTMDBImageUrl } = await import('@/lib/tmdb.search'); const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
// 转换为标准格式(使用懒加载 URL
const result = { const result = {
source: 'openlist', source: 'openlist',
source_name: '私人影库', source_name: '私人影库',
id: openlistData.folder, id: id,
title: folderMeta?.title || openlistData.folder, title: folderMeta?.title || id,
poster: folderMeta?.poster_path ? getTMDBImageUrl(folderMeta.poster_path) : '', poster: folderMeta?.poster_path ? getTMDBImageUrl(folderMeta.poster_path) : '',
year: folderMeta?.release_date ? folderMeta.release_date.split('-')[0] : '', year: folderMeta?.release_date ? folderMeta.release_date.split('-')[0] : '',
douban_id: 0, douban_id: 0,
desc: folderMeta?.overview || '', desc: folderMeta?.overview || '',
episodes: openlistData.episodes.map((ep: any) => episodes: episodes.map((ep) => `/api/openlist/play?folder=${encodeURIComponent(id)}&fileName=${encodeURIComponent(ep.fileName)}`),
`/api/openlist/play?folder=${encodeURIComponent(openlistData.folder)}&fileName=${encodeURIComponent(ep.fileName)}` episodes_titles: episodes.map((ep) => ep.title),
),
episodes_titles: openlistData.episodes.map((ep: any) => ep.title || `${ep.episode}`),
}; };
console.log('[Detail] result.episodes_titles:', result.episodes_titles);
return NextResponse.json(result); return NextResponse.json(result);
} catch (error) { } catch (error) {
return NextResponse.json( return NextResponse.json(

View File

@@ -266,7 +266,8 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
if ( if (
!optimizationEnabled || // 若关闭测速则直接退出 !optimizationEnabled || // 若关闭测速则直接退出
activeTab !== 'sources' || activeTab !== 'sources' ||
availableSources.length === 0 availableSources.length === 0 ||
currentSource === 'openlist' // 私人影库不进行测速
) )
return; return;
@@ -293,7 +294,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
fetchVideoInfosInBatches(); fetchVideoInfosInBatches();
// 依赖项保持与之前一致 // 依赖项保持与之前一致
}, [activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted]); }, [activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted, currentSource]);
// 升序分页标签 // 升序分页标签
const categoriesAsc = useMemo(() => { const categoriesAsc = useMemo(() => {
@@ -848,6 +849,11 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
</div> </div>
{/* 重新测试按钮 */} {/* 重新测试按钮 */}
{(() => { {(() => {
// 私人影库不显示重新测试按钮
if (source.source === 'openlist') {
return null;
}
const sourceKey = `${source.source}-${source.id}`; const sourceKey = `${source.source}-${source.id}`;
const isTesting = retestingSources.has(sourceKey); const isTesting = retestingSources.has(sourceKey);
const videoInfo = videoInfoMap.get(sourceKey); const videoInfo = videoInfoMap.get(sourceKey);