Add upload to show functionality in MediaPanel: introduce file selection for uploading media files directly to a show, enhancing the upload store to support show associations. Update translations for new upload options and UI elements.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user