Refactor AdminUserEndpoints and MediaEndpoints to include default parameter values for pagination and filtering, improving API usability. Update ManualInboxDialog to enhance show selection logic with auto-detection based on file names, and improve user feedback with new translations. Refactor PostgresFixture for better container management in integration tests.
build / backend (push) Successful in 2m26s
build / frontend (push) Successful in 1m5s
tests / backend-tests (push) Successful in 2m27s

This commit is contained in:
Leonid Pershin
2026-07-26 16:17:24 +03:00
parent 8fa4ea02fe
commit 2387223f0b
9 changed files with 297 additions and 82 deletions
@@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { ChevronDown, ChevronRight, Folder } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { listShows } from '@/features/admin/shows/api'
import { HttpError } from '@/shared/api/client'
@@ -22,6 +22,7 @@ import { toast } from '@/shared/ui/toast-store'
import { importManualInbox, listManualInbox } from './api'
import { compareParsed, formatSeasonEpisode, isValidRegex, parseEpisodeName } from './episode-parse'
import { buildEpisodeRegex, findNumbers, REGEX_PRESETS } from './episode-regex'
import { matchShowByName } from './match-show'
/** Байты → «1,4 ГБ»: в ручном разборе размер — главный ориентир, что это за файл. */
function formatSize(bytes: number): string {
@@ -48,6 +49,8 @@ export function ManualInboxDialog({ onClose }: { onClose: () => void }) {
const queryClient = useQueryClient()
const [selected, setSelected] = useState<string[]>([])
const [showId, setShowId] = useState('')
// Ручной выбор шоу отключает автоопределение: перебивать решение человека нельзя.
const [showPicked, setShowPicked] = useState(false)
const [query, setQuery] = useState('')
const [seasonStr, setSeasonStr] = useState('')
const [regexStr, setRegexStr] = useState('')
@@ -120,6 +123,25 @@ export function ManualInboxDialog({ onClose }: { onClose: () => void }) {
parts.push({ text: sample.name.slice(cursor), number: null })
return parts
}, [sample])
/**
* Автоопределение шоу по имени релиза — то же, что в загрузке в шоу. Сначала пробуем имя файла,
* затем имя папки: в раздачах название сериала обычно есть и там, и там («Mr.Pickles.S01.1080p»).
*/
const detectedShowId = useMemo(
() =>
shows && sample
? (matchShowByName(sample.name, shows) ?? matchShowByName(sample.folder, shows))
: undefined,
[shows, sample],
)
useEffect(() => {
if (showPicked || showId || !detectedShowId) return
setShowId(detectedShowId)
}, [detectedShowId, showPicked, showId])
const autoDetected = !showPicked && !!detectedShowId && showId === detectedShowId
const recognized = selectable.filter(
(f) => parsedByPath.get(f.relativePath)?.episode != null,
).length
@@ -180,7 +202,13 @@ export function ManualInboxDialog({ onClose }: { onClose: () => void }) {
{/* Шоу — первое решение в этом диалоге: остальные поля лишь помогают разложить файлы. */}
<div className="flex flex-col gap-1.5">
<Label>{t('admin.media.manualShow')}</Label>
<Select value={showId} onValueChange={setShowId}>
<Select
value={showId}
onValueChange={(value) => {
setShowPicked(true)
setShowId(value)
}}
>
<SelectTrigger>
<SelectValue placeholder={t('admin.media.manualPickShow')} />
</SelectTrigger>
@@ -192,6 +220,11 @@ export function ManualInboxDialog({ onClose }: { onClose: () => void }) {
))}
</SelectContent>
</Select>
{autoDetected && (
<p className="text-xs text-muted-foreground">
{t('admin.media.manualDetected')}
</p>
)}
</div>
<div className="grid gap-3 sm:grid-cols-3">
@@ -212,6 +212,7 @@ export function MediaPanel() {
sortKey="status"
sort={sort}
onToggle={sortColumn}
className="whitespace-nowrap"
/>
<SortHeader
label={t('admin.media.duration')}
@@ -274,7 +275,7 @@ function MediaRow({ asset, onDelete }: { asset: MediaAssetDto; onDelete: () => v
return (
<tr className="border-b border-border last:border-0">
<td className="px-4 py-2">{asset.originalFileName}</td>
<td className="px-4 py-2">
<td className="whitespace-nowrap px-4 py-2">
<Badge variant={statusVariant[asset.status]} title={asset.errorMessage ?? undefined}>
{t(`admin.media.statuses.${asset.status}`)}
</Badge>
+2
View File
@@ -226,6 +226,7 @@ const resources = {
'Файлы уйдут из папки, спутники (субтитры, nfo) и опустевший каталог будут удалены.',
manualTruncated: 'Показаны первые 500 файлов — в папке есть ещё.',
manualShow: 'Шоу',
manualDetected: 'Определено по имени релиза — проверьте и поправьте, если не то.',
manualPickShow: 'Выберите шоу',
manualImport: 'Забрать в шоу',
manualImported: 'Импортировано файлов: {{count}}',
@@ -917,6 +918,7 @@ const resources = {
'Files leave the folder; siblings (subtitles, nfo) and the emptied folder are removed.',
manualTruncated: 'Showing the first 500 files — there are more in the folder.',
manualShow: 'Show',
manualDetected: 'Detected from the release name — check it and change if wrong.',
manualPickShow: 'Pick a show',
manualImport: 'Import into show',
manualImported: 'Files imported: {{count}}',