93 lines
3.5 KiB
TypeScript
93 lines
3.5 KiB
TypeScript
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)
|
|
}
|