199 lines
5.6 KiB
TypeScript
199 lines
5.6 KiB
TypeScript
/**
|
||
* 季度标识解析工具
|
||
* 用于从文件夹名称中识别和提取季度信息
|
||
*/
|
||
|
||
export interface SeasonInfo {
|
||
/** 清理后的标题(移除季度标识和年份) */
|
||
cleanTitle: string;
|
||
/** 季度编号,如果未识别则为 null */
|
||
seasonNumber: number | null;
|
||
/** 年份,如果未识别则为 null */
|
||
year: number | null;
|
||
/** 原始标题 */
|
||
originalTitle: string;
|
||
}
|
||
|
||
/**
|
||
* 从文件夹名称中提取季度信息
|
||
* 支持多种格式:
|
||
* - S01, S1, s01, s1
|
||
* - [S01], [S1]
|
||
* - Season 1, Season 01
|
||
* - 第一季, 第1季, 第01季
|
||
* - [第一季], [第1季]
|
||
* - 第一部, 第1部
|
||
* - 年份: 2023, [2023], (2023)
|
||
*/
|
||
export function parseSeasonFromTitle(title: string): SeasonInfo {
|
||
const originalTitle = title;
|
||
// 先将下划线替换成空格,方便后续解析和搜索
|
||
let cleanTitle = title.replace(/_/g, ' ');
|
||
let seasonNumber: number | null = null;
|
||
let year: 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 = cleanTitle.match(pattern.regex);
|
||
if (match) {
|
||
const extracted = pattern.extract(match);
|
||
if (extracted !== null) {
|
||
seasonNumber = extracted;
|
||
// 移除匹配到的季度标识
|
||
cleanTitle = cleanTitle.replace(pattern.regex, '').trim();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 提取年份(支持多种格式)
|
||
const yearPatterns = [
|
||
/\[(\d{4})\]/, // [2023]
|
||
/\((\d{4})\)/, // (2023)
|
||
/\b(\d{4})\b/, // 2023
|
||
];
|
||
|
||
for (const yearPattern of yearPatterns) {
|
||
const yearMatch = cleanTitle.match(yearPattern);
|
||
if (yearMatch) {
|
||
const extractedYear = parseInt(yearMatch[1], 10);
|
||
// 验证年份合理性(1900-2100)
|
||
if (extractedYear >= 1900 && extractedYear <= 2100) {
|
||
year = extractedYear;
|
||
cleanTitle = cleanTitle.replace(yearPattern, '').trim();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清理标题:移除空的方括号和多余的空格
|
||
cleanTitle = cleanTitle
|
||
.replace(/\[\s*\]/g, '') // 移除空方括号
|
||
.replace(/\(\s*\)/g, '') // 移除空圆括号
|
||
.replace(/\s+/g, ' ') // 合并多个空格
|
||
.replace(/[·\-_\s]+$/, '') // 移除末尾的特殊字符
|
||
.trim();
|
||
|
||
return {
|
||
cleanTitle,
|
||
seasonNumber,
|
||
year,
|
||
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('');
|
||
});
|
||
}
|