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:
Leonid Pershin
2026-07-25 08:16:05 +03:00
parent 4bebe64ff0
commit 2dd8bf9724
5 changed files with 311 additions and 6 deletions
@@ -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)
}