私人影库扫描增加季度支持

This commit is contained in:
mtvpls
2025-12-25 23:34:54 +08:00
parent 636e6042c8
commit 76b3349aa2
11 changed files with 699 additions and 34 deletions
+2
View File
@@ -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;
+171
View File
@@ -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('');
});
}
+123
View File
@@ -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
*/