私人影库支持解析ova
This commit is contained in:
@@ -121,6 +121,7 @@ export async function GET(request: NextRequest) {
|
||||
season: parsed.season,
|
||||
title: parsed.title,
|
||||
parsed_from: 'filename',
|
||||
isOVA: parsed.isOVA,
|
||||
};
|
||||
}
|
||||
setCachedVideoInfo(folderPath, videoInfo);
|
||||
@@ -131,20 +132,26 @@ export async function GET(request: NextRequest) {
|
||||
const parsed = parseVideoFileName(file.name);
|
||||
let episodeInfo;
|
||||
if (parsed.episode) {
|
||||
episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename' };
|
||||
episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename', isOVA: parsed.isOVA };
|
||||
} 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}集`;
|
||||
displayTitle = episodeInfo.isOVA ? `OVA ${episodeInfo.episode}` : `第${episodeInfo.episode}集`;
|
||||
}
|
||||
if (!displayTitle) {
|
||||
displayTitle = file.name;
|
||||
}
|
||||
return { fileName: file.name, episode: episodeInfo.episode || 0, season: episodeInfo.season, title: displayTitle };
|
||||
return { fileName: file.name, episode: episodeInfo.episode || 0, season: episodeInfo.season, title: displayTitle, isOVA: episodeInfo.isOVA };
|
||||
})
|
||||
.sort((a, b) => a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName));
|
||||
.sort((a, b) => {
|
||||
// OVA 排在最后
|
||||
if (a.isOVA && !b.isOVA) return 1;
|
||||
if (!a.isOVA && b.isOVA) return -1;
|
||||
// 都是 OVA 或都不是 OVA,按集数排序
|
||||
return a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName);
|
||||
});
|
||||
|
||||
// 3. 从 metainfo 中获取元数据
|
||||
const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
|
||||
|
||||
@@ -315,6 +315,7 @@ async function handleDetail(
|
||||
season: parsed.season,
|
||||
title: parsed.title,
|
||||
parsed_from: 'filename',
|
||||
isOVA: parsed.isOVA,
|
||||
};
|
||||
}
|
||||
setCachedVideoInfo(folderPath, videoInfo);
|
||||
@@ -332,13 +333,13 @@ async function handleDetail(
|
||||
const parsed = parseVideoFileName(file.name);
|
||||
let episodeInfo;
|
||||
if (parsed.episode) {
|
||||
episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename' };
|
||||
episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename', isOVA: parsed.isOVA };
|
||||
} 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}集`;
|
||||
displayTitle = episodeInfo.isOVA ? `OVA ${episodeInfo.episode}` : `第${episodeInfo.episode}集`;
|
||||
}
|
||||
if (!displayTitle) {
|
||||
displayTitle = file.name;
|
||||
@@ -353,9 +354,16 @@ async function handleDetail(
|
||||
season: episodeInfo.season,
|
||||
title: displayTitle,
|
||||
playUrl,
|
||||
isOVA: episodeInfo.isOVA,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName));
|
||||
.sort((a, b) => {
|
||||
// OVA 排在最后
|
||||
if (a.isOVA && !b.isOVA) return 1;
|
||||
if (!a.isOVA && b.isOVA) return -1;
|
||||
// 都是 OVA 或都不是 OVA,按集数排序
|
||||
return a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName);
|
||||
});
|
||||
|
||||
// 转换为 CMS vod_play_url 格式
|
||||
// 格式:第1集$url1#第2集$url2#第3集$url3
|
||||
|
||||
@@ -127,6 +127,7 @@ export async function GET(request: NextRequest) {
|
||||
season: parsed.season,
|
||||
title: parsed.title,
|
||||
parsed_from: 'filename',
|
||||
isOVA: parsed.isOVA,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,6 +175,7 @@ export async function GET(request: NextRequest) {
|
||||
season: parsed.season,
|
||||
title: parsed.title,
|
||||
parsed_from: 'filename',
|
||||
isOVA: parsed.isOVA,
|
||||
};
|
||||
} else {
|
||||
// 如果解析失败,尝试从 videoInfo 获取
|
||||
@@ -192,8 +194,7 @@ export async function GET(request: NextRequest) {
|
||||
// 优先使用解析出的标题,其次是"第X集"格式,最后才是文件名
|
||||
let displayTitle = episodeInfo.title;
|
||||
if (!displayTitle && episodeInfo.episode) {
|
||||
// 支持小数集数显示
|
||||
displayTitle = `第${episodeInfo.episode}集`;
|
||||
displayTitle = episodeInfo.isOVA ? `OVA ${episodeInfo.episode}` : `第${episodeInfo.episode}集`;
|
||||
}
|
||||
if (!displayTitle) {
|
||||
displayTitle = file.name;
|
||||
@@ -205,9 +206,13 @@ export async function GET(request: NextRequest) {
|
||||
season: episodeInfo.season,
|
||||
title: displayTitle,
|
||||
size: file.size,
|
||||
isOVA: episodeInfo.isOVA,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// OVA 排在最后
|
||||
if (a.isOVA && !b.isOVA) return 1;
|
||||
if (!a.isOVA && b.isOVA) return -1;
|
||||
// 确保排序稳定,即使 episode 相同也按文件名排序
|
||||
if (a.episode !== b.episode) {
|
||||
return a.episode - b.episode;
|
||||
|
||||
@@ -214,6 +214,7 @@ export async function GET(request: NextRequest) {
|
||||
season: parsed.season,
|
||||
title: parsed.title,
|
||||
parsed_from: 'filename',
|
||||
isOVA: parsed.isOVA,
|
||||
};
|
||||
}
|
||||
setCachedVideoInfo(folderPath, videoInfo);
|
||||
@@ -224,20 +225,26 @@ export async function GET(request: NextRequest) {
|
||||
const parsed = parseVideoFileName(file.name);
|
||||
let episodeInfo;
|
||||
if (parsed.episode) {
|
||||
episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename' };
|
||||
episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename', isOVA: parsed.isOVA };
|
||||
} 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}集`;
|
||||
displayTitle = episodeInfo.isOVA ? `OVA ${episodeInfo.episode}` : `第${episodeInfo.episode}集`;
|
||||
}
|
||||
if (!displayTitle) {
|
||||
displayTitle = file.name;
|
||||
}
|
||||
return { fileName: file.name, episode: episodeInfo.episode || 0, season: episodeInfo.season, title: displayTitle };
|
||||
return { fileName: file.name, episode: episodeInfo.episode || 0, season: episodeInfo.season, title: displayTitle, isOVA: episodeInfo.isOVA };
|
||||
})
|
||||
.sort((a, b) => a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName));
|
||||
.sort((a, b) => {
|
||||
// OVA 排在最后
|
||||
if (a.isOVA && !b.isOVA) return 1;
|
||||
if (!a.isOVA && b.isOVA) return -1;
|
||||
// 都是 OVA 或都不是 OVA,按集数排序
|
||||
return a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName);
|
||||
});
|
||||
|
||||
// 3. 从 metainfo 中获取元数据
|
||||
const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
|
||||
|
||||
@@ -291,6 +291,10 @@ const DownloadEpisodeSelector: React.FC<DownloadEpisodeSelectorProps> = ({
|
||||
if (!title) {
|
||||
return episodeNumber;
|
||||
}
|
||||
// 如果是 OVA 格式,直接返回完整标题
|
||||
if (title.match(/^OVA\s+\d+/i)) {
|
||||
return title;
|
||||
}
|
||||
// 如果匹配"第X集"、"第X话"、"X集"、"X话"格式,提取中间的数字
|
||||
const match = title.match(/(?:第)?(\d+)(?:集|话)/);
|
||||
if (match) {
|
||||
|
||||
@@ -676,6 +676,10 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
if (!title) {
|
||||
return episodeNumber;
|
||||
}
|
||||
// 如果是 OVA 格式,直接返回完整标题
|
||||
if (title.match(/^OVA\s+\d+/i)) {
|
||||
return title;
|
||||
}
|
||||
// 如果匹配"第X集"、"第X话"、"X集"、"X话"格式,提取中间的数字(支持小数)
|
||||
const match = title.match(/(?:第)?(\d+(?:\.\d+)?)(?:集|话)/);
|
||||
if (match) {
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface VideoInfo {
|
||||
season?: number;
|
||||
title?: string;
|
||||
parsed_from: 'videoinfo' | 'filename';
|
||||
isOVA?: boolean;
|
||||
};
|
||||
};
|
||||
last_updated: number;
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface ParsedVideoInfo {
|
||||
episode?: number;
|
||||
season?: number;
|
||||
title?: string;
|
||||
isOVA?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,27 +28,29 @@ export function parseVideoFileName(fileName: string): ParsedVideoInfo {
|
||||
|
||||
// 降级方案:使用多种正则模式提取集数
|
||||
// 按优先级排序:更具体的模式优先
|
||||
const patterns = [
|
||||
const patterns: Array<{ pattern: RegExp; isOVA?: boolean }> = [
|
||||
// OVA01, OVA 01, ova01, ova 01 (OVA特殊处理) - 最优先
|
||||
{ pattern: /OVA\s*(\d+(?:\.\d+)?)/i, isOVA: true },
|
||||
// S01E01, s01e01, S01E01.5 (支持小数) - 最具体
|
||||
/[Ss]\d+[Ee](\d+(?:\.\d+)?)/,
|
||||
// [01], (01), [01.5], (01.5) (支持小数) - 很具体
|
||||
/[\[\(](\d+(?:\.\d+)?)[\]\)]/,
|
||||
{ pattern: /[Ss]\d+[Ee](\d+(?:\.\d+)?)/ },
|
||||
// [01], (01), [01.5], (01.5) (支持小数,但要排除中文括号内容) - 很具体
|
||||
{ pattern: /[\[\(](\d+(?:\.\d+)?)[\]\)]/ },
|
||||
// E01, E1, e01, e1, E01.5 (支持小数)
|
||||
/[Ee](\d+(?:\.\d+)?)/,
|
||||
{ pattern: /[Ee](\d+(?:\.\d+)?)/ },
|
||||
// 第01集, 第1集, 第01话, 第1话, 第1.5集 (支持小数)
|
||||
/第(\d+(?:\.\d+)?)[集话]/,
|
||||
{ pattern: /第(\d+(?:\.\d+)?)[集话]/ },
|
||||
// _01_, -01-, _01.5_, -01.5- (支持小数)
|
||||
/[_\-](\d+(?:\.\d+)?)[_\-]/,
|
||||
{ pattern: /[_\-](\d+(?:\.\d+)?)[_\-]/ },
|
||||
// 01.mp4, 001.mp4, 01.5.mp4 (纯数字开头,支持小数) - 最不具体
|
||||
/^(\d+(?:\.\d+)?)[^\d.]/,
|
||||
{ pattern: /^(\d+(?:\.\d+)?)[^\d.]/ },
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
for (const { pattern, isOVA } of patterns) {
|
||||
const match = fileName.match(pattern);
|
||||
if (match && match[1]) {
|
||||
const episode = parseFloat(match[1]);
|
||||
if (episode > 0 && episode < 10000) { // 合理的集数范围
|
||||
return { episode };
|
||||
return { episode, isOVA };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user