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[];
tags?: string[];
created_at?: number;
oidcSub?: string;
}> = !hasOldUserData && usersV2 ? usersV2 : (config?.UserConfig?.Users || []);
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算

View File

@@ -161,27 +161,6 @@ export async function POST(request: NextRequest) {
// 使用新版本创建用户带SHA256加密和OIDC绑定
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
const response = NextResponse.json({ ok: true, message: '注册成功' });
const cookieValue = await generateAuthCookie(username, 'user');

View File

@@ -31,12 +31,12 @@ export async function GET(request: NextRequest) {
}
const rootPath = openListConfig.RootPath || '/';
const folderPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}${id}`;
// 1. 读取 metainfo 获取元数据
let metaInfo: any = null;
try {
const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache');
const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
const { db } = await import('@/lib/db');
metaInfo = getCachedMetaInfo(rootPath);
@@ -49,51 +49,105 @@ export async function GET(request: NextRequest) {
}
}
} catch (error) {
console.error('[Detail] 从数据库读取 metainfo 失败:', error);
// 忽略错误
}
// 2. 调用 openlist detail API
const openlistResponse = await fetch(
`${request.headers.get('x-forwarded-proto') || 'http'}://${request.headers.get('host')}/api/openlist/detail?folder=${encodeURIComponent(id)}`,
{
headers: {
Cookie: request.headers.get('cookie') || '',
},
}
// 2. 直接调用 OpenList 客户端获取视频列表
const { OpenListClient } = await import('@/lib/openlist.client');
const { getCachedVideoInfo, setCachedVideoInfo } = await import('@/lib/openlist-cache');
const { parseVideoFileName } = await import('@/lib/video-parser');
const client = new OpenListClient(
openListConfig.URL,
openListConfig.Username,
openListConfig.Password
);
if (!openlistResponse.ok) {
throw new Error('获取 OpenList 视频详情失败');
let videoInfo = getCachedVideoInfo(folderPath);
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) {
throw new Error(openlistData.error || '获取视频详情失败');
if (listResponse.code !== 200) {
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 中获取元数据
const folderMeta = metaInfo?.folders?.[id];
const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
// 转换为标准格式(使用懒加载 URL
const result = {
source: 'openlist',
source_name: '私人影库',
id: openlistData.folder,
title: folderMeta?.title || openlistData.folder,
id: id,
title: folderMeta?.title || id,
poster: folderMeta?.poster_path ? getTMDBImageUrl(folderMeta.poster_path) : '',
year: folderMeta?.release_date ? folderMeta.release_date.split('-')[0] : '',
douban_id: 0,
desc: folderMeta?.overview || '',
episodes: openlistData.episodes.map((ep: any) =>
`/api/openlist/play?folder=${encodeURIComponent(openlistData.folder)}&fileName=${encodeURIComponent(ep.fileName)}`
),
episodes_titles: openlistData.episodes.map((ep: any) => ep.title || `${ep.episode}`),
episodes: episodes.map((ep) => `/api/openlist/play?folder=${encodeURIComponent(id)}&fileName=${encodeURIComponent(ep.fileName)}`),
episodes_titles: episodes.map((ep) => ep.title),
};
console.log('[Detail] result.episodes_titles:', result.episodes_titles);
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(

View File

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