Merge branch 'dev' of https://github.com/mtvpls/MoonTVPlus into dev
This commit is contained in:
@@ -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 计算全选状态,避免每次渲染都重新计算
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user