|
|
|
@ -7,22 +7,52 @@ export interface ParsedTitle {
|
|
|
|
export function parseTitleAndEpisode(rawTitle: string | null): ParsedTitle {
|
|
|
|
export function parseTitleAndEpisode(rawTitle: string | null): ParsedTitle {
|
|
|
|
if (!rawTitle) return { title: '', episode: null, fullCleanTitle: '' };
|
|
|
|
if (!rawTitle) return { title: '', episode: null, fullCleanTitle: '' };
|
|
|
|
|
|
|
|
|
|
|
|
// Remove site name suffixes like "- 마나토키 330", "- 뉴토끼", "- newtoki"
|
|
|
|
let clean = rawTitle.trim();
|
|
|
|
let clean = rawTitle
|
|
|
|
|
|
|
|
.replace(/\s*[-|_]\s*(마나토키|뉴토끼|newtoki|manatoki|웹툰|만화).*$/i, '')
|
|
|
|
// 1. Remove site name / brand suffixes & domain noise
|
|
|
|
|
|
|
|
// Matches: "- 마나토키 330", "> 뉴토끼 340", ":: manatoki 100", "| 뉴토끼", " - 웹툰", " - 만화", etc.
|
|
|
|
|
|
|
|
clean = clean
|
|
|
|
|
|
|
|
.replace(/\s*[-|_|>|:|=|~|/|\\]+\s*(마나토키|뉴토끼|newtoki|manatoki|웹툰|만화|toki|토끼)\s*\d*\s*$/i, '')
|
|
|
|
|
|
|
|
.replace(/\s*[-|_|>|:|=|~|/|\\]+\s*(https?:\/\/)?[\w.-]+\.(com|org|net|xyz|me|info|live)\s*$/i, '')
|
|
|
|
|
|
|
|
.replace(/\s*[\(\[]\s*(마나토키|뉴토끼|newtoki|manatoki|웹툰|만화)\s*\d*\s*[\)\]]\s*$/i, '')
|
|
|
|
|
|
|
|
.replace(/\s+(마나토키|뉴토끼|newtoki|manatoki)\s*\d+\s*$/i, '')
|
|
|
|
.trim();
|
|
|
|
.trim();
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Match explicit episode format like "150화", "150.5화", "EP.15"
|
|
|
|
// 2. Main Episode Pattern:
|
|
|
|
const epMatch = clean.match(/(\d+(?:\.\d+)?\s*화|ep\.?\s*\d+)/i);
|
|
|
|
// Group 1 (optional season/part): (시즌\s*\d+|[1-9]기|[1-9]부|[Pp]art\s*\d+)
|
|
|
|
|
|
|
|
// Group 2 (episode number with optional sub-part or comma): (\d{1,4}(?:,\d{3})*(?:[._-]\d+)?)
|
|
|
|
|
|
|
|
// Group 3 (unit): (화|회|화차|강|話)
|
|
|
|
|
|
|
|
const mainEpRegex = /(?:((?:시즌\s*\d+|[1-9]기|[1-9]부|[Pp]art\s*\d+))\s+)?(?:제\s*)?(\d{1,4}(?:,\d{3})*(?:[._-]\d+)?)\s*(화|회|화차|강|話)/i;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const epMatch = clean.match(mainEpRegex);
|
|
|
|
|
|
|
|
|
|
|
|
if (epMatch) {
|
|
|
|
if (epMatch) {
|
|
|
|
let episode = epMatch[1].replace(/\s+/g, '');
|
|
|
|
const season = epMatch[1] ? epMatch[1].trim() + ' ' : '';
|
|
|
|
if (!episode.endsWith('화') && !episode.toLowerCase().startsWith('ep')) {
|
|
|
|
const num = epMatch[2].replace(/\s+/g, '');
|
|
|
|
episode += '화';
|
|
|
|
const unit = epMatch[3] || '화';
|
|
|
|
|
|
|
|
const fullEpString = epMatch[0]; // e.g. "2기 150화" or "150-1화"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const episode = `${season}${num}${unit}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Remove the episode portion from clean to extract remaining title
|
|
|
|
|
|
|
|
let title = clean.replace(fullEpString, ' ');
|
|
|
|
|
|
|
|
title = cleanUpTitleString(title);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
title: title || clean,
|
|
|
|
|
|
|
|
episode,
|
|
|
|
|
|
|
|
fullCleanTitle: clean,
|
|
|
|
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Remove the matched episode part from the title
|
|
|
|
|
|
|
|
let title = clean.replace(epMatch[0], '').trim();
|
|
|
|
// 3. "EP.150", "Ep 150", "#150", "No.150" pattern
|
|
|
|
// Clean up trailing dashes/colons/dots left over
|
|
|
|
const epPrefixRegex = /(?:ep\.?|#|no\.?)\s*(\d+(?:[.-_]\d+)?)/i;
|
|
|
|
title = title.replace(/[-|_:]\s*$/, '').trim();
|
|
|
|
const prefixMatch = clean.match(epPrefixRegex);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (prefixMatch) {
|
|
|
|
|
|
|
|
const episode = `EP.${prefixMatch[1]}`;
|
|
|
|
|
|
|
|
let title = clean.replace(prefixMatch[0], ' ');
|
|
|
|
|
|
|
|
title = cleanUpTitleString(title);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
title: title || clean,
|
|
|
|
title: title || clean,
|
|
|
|
@ -31,12 +61,17 @@ export function parseTitleAndEpisode(rawTitle: string | null): ParsedTitle {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Match trailing numbers like "제목 150"
|
|
|
|
// 4. Trailing number without '화' (e.g. "원피스 1100", "원피스 1100-1")
|
|
|
|
const numMatch = clean.match(/(\d+(?:\.\d+)?)\s*$/);
|
|
|
|
const trailingNumRegex = /(?:((?:시즌\s*\d+|[1-9]기|[1-9]부|[Pp]art\s*\d+))\s+)?(\d{1,4}(?:[.-_]\d+)?)\s*$/i;
|
|
|
|
if (numMatch) {
|
|
|
|
const trailingMatch = clean.match(trailingNumRegex);
|
|
|
|
const episode = `${numMatch[1]}화`;
|
|
|
|
|
|
|
|
let title = clean.slice(0, numMatch.index).trim();
|
|
|
|
if (trailingMatch) {
|
|
|
|
title = title.replace(/[-|_:]\s*$/, '').trim();
|
|
|
|
const season = trailingMatch[1] ? trailingMatch[1].trim() + ' ' : '';
|
|
|
|
|
|
|
|
const num = trailingMatch[2];
|
|
|
|
|
|
|
|
const episode = `${season}${num}화`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let title = clean.slice(0, trailingMatch.index).trim();
|
|
|
|
|
|
|
|
title = cleanUpTitleString(title);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
title: title || clean,
|
|
|
|
title: title || clean,
|
|
|
|
@ -47,3 +82,12 @@ export function parseTitleAndEpisode(rawTitle: string | null): ParsedTitle {
|
|
|
|
|
|
|
|
|
|
|
|
return { title: clean, episode: null, fullCleanTitle: clean };
|
|
|
|
return { title: clean, episode: null, fullCleanTitle: clean };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function cleanUpTitleString(str: string): string {
|
|
|
|
|
|
|
|
let s = str.replace(/\s+/g, ' ').trim();
|
|
|
|
|
|
|
|
s = s
|
|
|
|
|
|
|
|
.replace(/^[\s\-_|:;=><~,/\\\\]+/, '')
|
|
|
|
|
|
|
|
.replace(/[\s\-_|:;=><~,/\\\\]+$/, '')
|
|
|
|
|
|
|
|
.trim();
|
|
|
|
|
|
|
|
return s;
|
|
|
|
|
|
|
|
}
|
|
|
|
|