diff --git a/src/utils/titleParser.ts b/src/utils/titleParser.ts index bd1d66b..6855e7f 100644 --- a/src/utils/titleParser.ts +++ b/src/utils/titleParser.ts @@ -7,22 +7,36 @@ export interface ParsedTitle { export function parseTitleAndEpisode(rawTitle: string | null): ParsedTitle { if (!rawTitle) return { title: '', episode: null, fullCleanTitle: '' }; - // Remove site name suffixes like "- 마나토키 330", "- 뉴토끼", "- newtoki" - let clean = rawTitle - .replace(/\s*[-|_]\s*(마나토키|뉴토끼|newtoki|manatoki|웹툰|만화).*$/i, '') + let clean = rawTitle.trim(); + + // 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(); - // 1. Match explicit episode format like "150화", "150.5화", "EP.15" - const epMatch = clean.match(/(\d+(?:\.\d+)?\s*화|ep\.?\s*\d+)/i); + // 2. Main Episode Pattern: + // 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) { - let episode = epMatch[1].replace(/\s+/g, ''); - if (!episode.endsWith('화') && !episode.toLowerCase().startsWith('ep')) { - episode += '화'; - } - // Remove the matched episode part from the title - let title = clean.replace(epMatch[0], '').trim(); - // Clean up trailing dashes/colons/dots left over - title = title.replace(/[-|_:]\s*$/, '').trim(); + const season = epMatch[1] ? epMatch[1].trim() + ' ' : ''; + const num = epMatch[2].replace(/\s+/g, ''); + 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, @@ -31,12 +45,33 @@ export function parseTitleAndEpisode(rawTitle: string | null): ParsedTitle { }; } - // 2. Match trailing numbers like "제목 150" - const numMatch = clean.match(/(\d+(?:\.\d+)?)\s*$/); - if (numMatch) { - const episode = `${numMatch[1]}화`; - let title = clean.slice(0, numMatch.index).trim(); - title = title.replace(/[-|_:]\s*$/, '').trim(); + // 3. "EP.150", "Ep 150", "#150", "No.150" pattern + const epPrefixRegex = /(?:ep\.?|#|no\.?)\s*(\d+(?:[.-_]\d+)?)/i; + const prefixMatch = clean.match(epPrefixRegex); + + if (prefixMatch) { + const episode = `EP.${prefixMatch[1]}`; + let title = clean.replace(prefixMatch[0], ' '); + title = cleanUpTitleString(title); + + return { + title: title || clean, + episode, + fullCleanTitle: clean, + }; + } + + // 4. Trailing number without '화' (e.g. "원피스 1100", "원피스 1100-1") + const trailingNumRegex = /(?:((?:시즌\s*\d+|[1-9]기|[1-9]부|[Pp]art\s*\d+))\s+)?(\d{1,4}(?:[.-_]\d+)?)\s*$/i; + const trailingMatch = clean.match(trailingNumRegex); + + if (trailingMatch) { + 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 { title: title || clean, @@ -47,3 +82,12 @@ export function parseTitleAndEpisode(rawTitle: string | null): ParsedTitle { 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; +}