私人影库扫描增加季度支持
This commit is contained in:
@@ -28,6 +28,8 @@ export interface MetaInfo {
|
||||
media_type: 'movie' | 'tv';
|
||||
last_updated: number;
|
||||
failed?: boolean; // 标记是否搜索失败
|
||||
season_number?: number; // 季度编号(仅电视剧)
|
||||
season_name?: string; // 季度名称(仅电视剧)
|
||||
};
|
||||
};
|
||||
last_refresh: number;
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 季度标识解析工具
|
||||
* 用于从文件夹名称中识别和提取季度信息
|
||||
*/
|
||||
|
||||
export interface SeasonInfo {
|
||||
/** 清理后的标题(移除季度标识) */
|
||||
cleanTitle: string;
|
||||
/** 季度编号,如果未识别则为 null */
|
||||
seasonNumber: number | null;
|
||||
/** 原始标题 */
|
||||
originalTitle: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件夹名称中提取季度信息
|
||||
* 支持多种格式:
|
||||
* - S01, S1, s01, s1
|
||||
* - [S01], [S1]
|
||||
* - Season 1, Season 01
|
||||
* - 第一季, 第1季, 第01季
|
||||
* - [第一季], [第1季]
|
||||
* - 第一部, 第1部
|
||||
*/
|
||||
export function parseSeasonFromTitle(title: string): SeasonInfo {
|
||||
const originalTitle = title;
|
||||
let cleanTitle = title;
|
||||
let seasonNumber: number | null = null;
|
||||
|
||||
// 定义季度匹配模式(按优先级排序)
|
||||
const patterns = [
|
||||
// [S01], [S1], [s01], [s1] 格式(方括号包裹)
|
||||
{
|
||||
regex: /\[([Ss]\d{1,2})\]/,
|
||||
extract: (match: RegExpMatchArray) => {
|
||||
const seasonMatch = match[1].match(/[Ss](\d{1,2})/);
|
||||
return seasonMatch ? parseInt(seasonMatch[1], 10) : null;
|
||||
},
|
||||
},
|
||||
// S01, S1, s01, s1 格式
|
||||
{
|
||||
regex: /\b[Ss](\d{1,2})\b/,
|
||||
extract: (match: RegExpMatchArray) => parseInt(match[1], 10),
|
||||
},
|
||||
// [Season 1], [Season 01] 格式(方括号包裹)
|
||||
{
|
||||
regex: /\[Season\s+(\d{1,2})\]/i,
|
||||
extract: (match: RegExpMatchArray) => parseInt(match[1], 10),
|
||||
},
|
||||
// Season 1, Season 01 格式
|
||||
{
|
||||
regex: /\bSeason\s+(\d{1,2})\b/i,
|
||||
extract: (match: RegExpMatchArray) => parseInt(match[1], 10),
|
||||
},
|
||||
// [第一季], [第1季], [第01季] 格式(方括号包裹)
|
||||
{
|
||||
regex: /\[第([一二三四五六七八九十\d]{1,2})季\]/,
|
||||
extract: (match: RegExpMatchArray) => chineseNumberToInt(match[1]),
|
||||
},
|
||||
// 第一季, 第1季, 第01季 格式
|
||||
{
|
||||
regex: /第([一二三四五六七八九十\d]{1,2})季/,
|
||||
extract: (match: RegExpMatchArray) => chineseNumberToInt(match[1]),
|
||||
},
|
||||
// [第一部], [第1部] 格式(方括号包裹)
|
||||
{
|
||||
regex: /\[第([一二三四五六七八九十\d]{1,2})部\]/,
|
||||
extract: (match: RegExpMatchArray) => chineseNumberToInt(match[1]),
|
||||
},
|
||||
// 第一部, 第1部, 第01部 格式
|
||||
{
|
||||
regex: /第([一二三四五六七八九十\d]{1,2})部/,
|
||||
extract: (match: RegExpMatchArray) => chineseNumberToInt(match[1]),
|
||||
},
|
||||
];
|
||||
|
||||
// 尝试匹配每个模式
|
||||
for (const pattern of patterns) {
|
||||
const match = title.match(pattern.regex);
|
||||
if (match) {
|
||||
const extracted = pattern.extract(match);
|
||||
if (extracted !== null) {
|
||||
seasonNumber = extracted;
|
||||
// 移除匹配到的季度标识
|
||||
cleanTitle = title.replace(pattern.regex, '').trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理标题:移除空的方括号和多余的空格
|
||||
cleanTitle = cleanTitle
|
||||
.replace(/\[\s*\]/g, '') // 移除空方括号
|
||||
.replace(/\s+/g, ' ') // 合并多个空格
|
||||
.replace(/[·\-_\s]+$/, '') // 移除末尾的特殊字符
|
||||
.trim();
|
||||
|
||||
return {
|
||||
cleanTitle,
|
||||
seasonNumber,
|
||||
originalTitle,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将中文数字转换为阿拉伯数字
|
||||
*/
|
||||
function chineseNumberToInt(str: string): number {
|
||||
// 如果已经是数字,直接返回
|
||||
if (/^\d+$/.test(str)) {
|
||||
return parseInt(str, 10);
|
||||
}
|
||||
|
||||
const chineseNumbers: Record<string, number> = {
|
||||
'一': 1, '二': 2, '三': 3, '四': 4, '五': 5,
|
||||
'六': 6, '七': 7, '八': 8, '九': 9, '十': 10,
|
||||
};
|
||||
|
||||
// 处理"十"的特殊情况
|
||||
if (str === '十') {
|
||||
return 10;
|
||||
}
|
||||
|
||||
// 处理"十X"的情况(如"十一")
|
||||
if (str.startsWith('十')) {
|
||||
const unit = str.substring(1);
|
||||
return 10 + (chineseNumbers[unit] || 0);
|
||||
}
|
||||
|
||||
// 处理"X十"的情况(如"二十")
|
||||
if (str.endsWith('十')) {
|
||||
const tens = str.substring(0, str.length - 1);
|
||||
return (chineseNumbers[tens] || 0) * 10;
|
||||
}
|
||||
|
||||
// 处理"X十Y"的情况(如"二十一")
|
||||
const tenIndex = str.indexOf('十');
|
||||
if (tenIndex !== -1) {
|
||||
const tens = str.substring(0, tenIndex);
|
||||
const units = str.substring(tenIndex + 1);
|
||||
return (chineseNumbers[tens] || 0) * 10 + (chineseNumbers[units] || 0);
|
||||
}
|
||||
|
||||
// 单个中文数字
|
||||
return chineseNumbers[str] || parseInt(str, 10) || 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试示例
|
||||
*/
|
||||
export function testSeasonParser() {
|
||||
const testCases = [
|
||||
'权力的游戏 第一季',
|
||||
'Breaking Bad S01',
|
||||
'Game of Thrones Season 1',
|
||||
'绝命毒师 第1季',
|
||||
'权力的游戏 S1',
|
||||
'权力的游戏',
|
||||
'绝命毒师 第二部',
|
||||
'Stranger Things S03',
|
||||
];
|
||||
|
||||
console.log('Season Parser Test Results:');
|
||||
testCases.forEach((title) => {
|
||||
const result = parseSeasonFromTitle(title);
|
||||
console.log(`Input: "${title}"`);
|
||||
console.log(` Clean Title: "${result.cleanTitle}"`);
|
||||
console.log(` Season: ${result.seasonNumber}`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
@@ -79,6 +79,129 @@ export async function searchTMDB(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TMDB 季度信息
|
||||
*/
|
||||
export interface TMDBSeasonInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
season_number: number;
|
||||
episode_count: number;
|
||||
air_date: string | null;
|
||||
poster_path: string | null;
|
||||
overview: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TMDB 电视剧详情(包含季度列表)
|
||||
*/
|
||||
interface TMDBTVDetails {
|
||||
id: number;
|
||||
name: string;
|
||||
seasons: TMDBSeasonInfo[];
|
||||
number_of_seasons: number;
|
||||
poster_path: string | null;
|
||||
first_air_date: string;
|
||||
overview: string;
|
||||
vote_average: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电视剧的季度列表
|
||||
*/
|
||||
export async function getTVSeasons(
|
||||
apiKey: string,
|
||||
tvId: number,
|
||||
proxy?: string
|
||||
): Promise<{ code: number; seasons: TMDBSeasonInfo[] | null }> {
|
||||
try {
|
||||
if (!apiKey) {
|
||||
return { code: 400, seasons: null };
|
||||
}
|
||||
|
||||
const url = `https://api.themoviedb.org/3/tv/${tvId}?api_key=${apiKey}&language=zh-CN`;
|
||||
|
||||
const fetchOptions: any = proxy
|
||||
? {
|
||||
agent: new HttpsProxyAgent(proxy, {
|
||||
timeout: 30000,
|
||||
keepAlive: false,
|
||||
}),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
: {
|
||||
signal: AbortSignal.timeout(15000),
|
||||
};
|
||||
|
||||
const response = await nodeFetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('TMDB 获取电视剧详情失败:', response.status, response.statusText);
|
||||
return { code: response.status, seasons: null };
|
||||
}
|
||||
|
||||
const data: TMDBTVDetails = await response.json() as TMDBTVDetails;
|
||||
|
||||
// 过滤掉特殊季度(如 Season 0 通常是特别篇)
|
||||
const validSeasons = data.seasons.filter((season) => season.season_number > 0);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
seasons: validSeasons,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('TMDB 获取季度列表异常:', error);
|
||||
return { code: 500, seasons: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电视剧特定季度的详细信息
|
||||
*/
|
||||
export async function getTVSeasonDetails(
|
||||
apiKey: string,
|
||||
tvId: number,
|
||||
seasonNumber: number,
|
||||
proxy?: string
|
||||
): Promise<{ code: number; season: TMDBSeasonInfo | null }> {
|
||||
try {
|
||||
if (!apiKey) {
|
||||
return { code: 400, season: null };
|
||||
}
|
||||
|
||||
const url = `https://api.themoviedb.org/3/tv/${tvId}/season/${seasonNumber}?api_key=${apiKey}&language=zh-CN`;
|
||||
|
||||
const fetchOptions: any = proxy
|
||||
? {
|
||||
agent: new HttpsProxyAgent(proxy, {
|
||||
timeout: 30000,
|
||||
keepAlive: false,
|
||||
}),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
: {
|
||||
signal: AbortSignal.timeout(15000),
|
||||
};
|
||||
|
||||
const response = await nodeFetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('TMDB 获取季度详情失败:', response.status, response.statusText);
|
||||
return { code: response.status, season: null };
|
||||
}
|
||||
|
||||
const data: TMDBSeasonInfo = await response.json() as TMDBSeasonInfo;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
season: data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('TMDB 获取季度详情异常:', error);
|
||||
return { code: 500, season: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 TMDB 图片完整 URL
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user