export type ParseOptions = { /** Ручной сезон — перебивает распознанный/дефолтный. */ seasonOverride?: number | null /** Пользовательский regex для номера серии: 1 группа = серия, 2 группы = (сезон, серия). */ episodeRegex?: string | null } export type ParsedEpisode = { season: number | null; episode: number | null } /** * Пытается распознать сезон/серию из имени файла. Сначала встроенные шаблоны (SxxEyy, NxNN), затем — * пользовательский regex (перебивает серию, а при двух группах и сезон), в конце — ручной сезон. * Если серия распознана, а сезон нет — сезон считается первым. */ export function parseEpisodeName(name: string, opts?: ParseOptions): ParsedEpisode { let season: number | null = null let episode: number | null = null const se = name.match(/[Ss](\d{1,2})[ ._-]*[Ee](\d{1,3})/) if (se) { season = Number(se[1]) episode = Number(se[2]) } else { const nx = name.match(/(?:^|[^\d])(\d{1,2})x(\d{1,3})(?:[^\d]|$)/i) if (nx) { season = Number(nx[1]) episode = Number(nx[2]) } else { // Ведущий номер серии: «01. Название», «02 - Название», «03_Название», «4) Название». const lead = name.match(/^\s*(\d{1,3})[\s._)\]-]/) if (lead) episode = Number(lead[1]) } } const rawRegex = opts?.episodeRegex?.trim() if (rawRegex) { try { const match = name.match(new RegExp(rawRegex, 'i')) if (match) { if (match.length >= 3 && match[1] != null && match[2] != null) { season = Number(match[1]) episode = Number(match[2]) } else if (match[1] != null) { episode = Number(match[1]) } } } catch { // невалидный regex — просто игнорируем } } if (opts?.seasonOverride != null) season = opts.seasonOverride if (episode != null && season == null) season = 1 if (episode != null && !Number.isFinite(episode)) episode = null if (season != null && !Number.isFinite(season)) season = null return { season, episode } } const pad2 = (n: number) => String(n).padStart(2, '0') /** «S14E17» либо null, если серия не распознана. */ export function formatSeasonEpisode(parsed: ParsedEpisode): string | null { if (parsed.episode == null) return null return `S${pad2(parsed.season ?? 1)}E${pad2(parsed.episode)}` } /** Проверяет корректность пользовательского regex (для подсветки ошибки в UI). */ export function isValidRegex(pattern: string): boolean { if (!pattern.trim()) return true try { new RegExp(pattern) return true } catch { return false } } /** Сортировка по (сезон, серия); нераспознанные — в конец по имени. */ export function compareParsed( a: { name: string; parsed: ParsedEpisode }, b: { name: string; parsed: ParsedEpisode }, ): number { const ae = a.parsed.episode const be = b.parsed.episode if (ae != null && be != null) { return (a.parsed.season ?? 1) - (b.parsed.season ?? 1) || ae - be } if (ae != null) return -1 if (be != null) return 1 return a.name.localeCompare(b.name) }