Refactor regex handling and improve null checks across various components
Updated regex patterns in episode parsing and metadata handling to use String.raw for better readability and maintainability. Enhanced null checks in BlockBuilder, MediaPanel, and ShowMetadataCard components to prevent potential runtime errors. Consolidated imports in UsersPanel for cleaner code structure.
This commit is contained in:
@@ -47,7 +47,7 @@ export function BlockBuilder({
|
||||
setOver(false)
|
||||
const item = readDragItem(event)
|
||||
// Блок из блоков собрать нельзя: коллекция хранит шоу, а не вложенные коллекции.
|
||||
if (!item || item.kind !== 'clip') return
|
||||
if (item?.kind !== 'clip') return
|
||||
setItems((current) => [...current, item])
|
||||
}
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ export function MediaPanel() {
|
||||
onDelete={() => deleteMutation.mutate(asset.id)}
|
||||
/>
|
||||
))}
|
||||
{data && data.items.length === 0 && !isLoading && (
|
||||
{data?.items.length === 0 && !isLoading && (
|
||||
<tr>
|
||||
<td className="px-4 py-3 text-muted-foreground" colSpan={6}>
|
||||
{t('admin.media.empty')}
|
||||
|
||||
@@ -9,14 +9,14 @@ export type ParsedEpisode = { season: number | null; episode: number | null }
|
||||
|
||||
/** Встроенные шаблоны: SxxEyy, NxNN, ведущий номер серии. */
|
||||
function parseBuiltin(name: string): ParsedEpisode {
|
||||
const se = name.match(/[Ss](\d{1,2})[ ._-]*[Ee](\d{1,3})/)
|
||||
const se = /[Ss](\d{1,2})[ ._-]*[Ee](\d{1,3})/.exec(name)
|
||||
if (se) return { season: Number(se[1]), episode: Number(se[2]) }
|
||||
|
||||
const nx = name.match(/(?:^|[^\d])(\d{1,2})x(\d{1,3})(?:[^\d]|$)/i)
|
||||
const nx = /(?:^|[^\d])(\d{1,2})x(\d{1,3})(?:[^\d]|$)/i.exec(name)
|
||||
if (nx) return { season: Number(nx[1]), episode: Number(nx[2]) }
|
||||
|
||||
// Ведущий номер серии: «01. Название», «02 - Название», «03_Название», «4) Название».
|
||||
const lead = name.match(/^\s*(\d{1,3})[\s._)\]-]/)
|
||||
const lead = /^\s*(\d{1,3})[\s._)\]-]/.exec(name)
|
||||
return { season: null, episode: lead ? Number(lead[1]) : null }
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ function parseBuiltin(name: string): ParsedEpisode {
|
||||
* при одной группе означает «сезон не трогаем».
|
||||
*/
|
||||
function parseCustom(name: string, pattern: string): ParsedEpisode | null {
|
||||
let match: RegExpMatchArray | null
|
||||
let match: RegExpExecArray | null
|
||||
try {
|
||||
match = name.match(new RegExp(pattern, 'i'))
|
||||
match = new RegExp(pattern, 'i').exec(name)
|
||||
} catch {
|
||||
return null // невалидный regex — просто игнорируем
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** Готовые шаблоны для частых раскладок имён. Подпись переводится в UI по ключу. */
|
||||
export const REGEX_PRESETS: { key: string; pattern: string }[] = [
|
||||
{ key: 'seriesWord', pattern: '[Сс]ерия\\s*(\\d{1,3})' },
|
||||
{ key: 'episodeWord', pattern: '[Ээ]пизод\\s*(\\d{1,3})' },
|
||||
{ key: 'seasonEpisode', pattern: '[Ss](\\d{1,2})[Ee](\\d{1,3})' },
|
||||
{ key: 'afterDash', pattern: '[-–—]\\s*(\\d{1,3})' },
|
||||
{ key: 'firstNumber', pattern: '(?:^|\\D)(\\d{1,3})(?:\\D|$)' },
|
||||
{ key: 'seriesWord', pattern: String.raw`[Сс]ерия\s*(\d{1,3})` },
|
||||
{ key: 'episodeWord', pattern: String.raw`[Ээ]пизод\s*(\d{1,3})` },
|
||||
{ key: 'seasonEpisode', pattern: String.raw`[Ss](\d{1,2})[Ee](\d{1,3})` },
|
||||
{ key: 'afterDash', pattern: String.raw`[-–—]\s*(\d{1,3})` },
|
||||
{ key: 'firstNumber', pattern: String.raw`(?:^|\D)(\d{1,3})(?:\D|$)` },
|
||||
]
|
||||
|
||||
/** Числа в имени файла: позиция и текст — по ним строится кликабельный образец. */
|
||||
@@ -16,7 +16,7 @@ export function findNumbers(fileName: string): { index: number; start: number; t
|
||||
}))
|
||||
}
|
||||
|
||||
const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&')
|
||||
const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\/-]/g, String.raw`\$&`)
|
||||
|
||||
const isLetter = (char: string) => /\p{L}/u.test(char)
|
||||
const isLetterOrDigit = (char: string) => /[\p{L}\p{N}]/u.test(char)
|
||||
@@ -44,9 +44,9 @@ export function buildEpisodeRegex(fileName: string, occurrenceIndex: number): st
|
||||
|
||||
// Всегда до трёх цифр — как во встроенных шаблонах: правило строится по одному файлу,
|
||||
// а применяется ко всей папке, где рядом может лежать и «Серия 100».
|
||||
const digits = '(\\d{1,3})'
|
||||
const digits = String.raw`(\d{1,3})`
|
||||
const before = fileName.slice(0, target.start)
|
||||
if (!before.trim()) return `^\\s*${digits}`
|
||||
if (!before.trim()) return String.raw`^\s*${digits}`
|
||||
|
||||
// Разделители между якорем и числом описываем классом, а не буквально: в соседних файлах
|
||||
// там встречается то пробел, то точка, то подчёркивание.
|
||||
@@ -56,9 +56,9 @@ export function buildEpisodeRegex(fileName: string, occurrenceIndex: number): st
|
||||
// к первому сезону, и на «S02E05» оно бы уже не сработало.
|
||||
const anchor = trailingRun(anchorSource, isLetter)
|
||||
|
||||
if (anchor) return `${escapeRegex(anchor)}${gap ? '[\\s._-]*' : ''}${digits}`
|
||||
if (anchor) return `${escapeRegex(anchor)}${gap ? String.raw`[\s._-]*` : ''}${digits}`
|
||||
|
||||
// Слова перед числом нет — цепляемся за последний разделитель («- 05», «(05)»).
|
||||
const punctuation = gap.trim().slice(-1)
|
||||
return punctuation ? `${escapeRegex(punctuation)}\\s*${digits}` : `\\s${digits}`
|
||||
return punctuation ? String.raw`${escapeRegex(punctuation)}\s*${digits}` : String.raw`\s${digits}`
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ export function ShowMetadataCard({
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admin.metadata.missingTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{missing && missing.seasons.length === 0 && (
|
||||
{missing?.seasons.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{t('admin.metadata.missingNoSeasons')}</p>
|
||||
)}
|
||||
{missing && missing.seasons.length > 0 && (
|
||||
|
||||
@@ -14,8 +14,7 @@ import { useTableSort } from '@/shared/lib/table-sort'
|
||||
import { SortHeader } from '@/shared/ui/sortable'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import type { UserSummaryDto } from '@/shared/api/types'
|
||||
import { changeUserRole } from '@/features/admin/roles/api'
|
||||
import { listRoles } from '@/features/admin/roles/api'
|
||||
import { changeUserRole, listRoles } from '@/features/admin/roles/api'
|
||||
import { blockUser, createUser, deleteUser, listUsers, resetUserPassword, unblockUser } from './api'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
@@ -306,8 +306,8 @@ function buildGuide(entries: PublicEpgEntryDto[]): {
|
||||
const blocks: GuideBlock[] = []
|
||||
for (const entry of entries) {
|
||||
if (entry.kind !== 'Program') continue
|
||||
const last = blocks[blocks.length - 1]
|
||||
if (last && last.showId === entry.showId) {
|
||||
const last = blocks.at(-1)
|
||||
if (last?.showId === entry.showId) {
|
||||
last.endsAtUtc = entry.endsAtUtc
|
||||
} else {
|
||||
blocks.push({
|
||||
|
||||
Reference in New Issue
Block a user