Add original name support for shows: implement API endpoint to set original name, update Show and ShowDto models to include original name field, and enhance UI components for managing original names in show metadata. Update validation and database schema accordingly.

This commit is contained in:
Leonid Pershin
2026-07-25 11:10:33 +03:00
parent 84c2867062
commit 72451f89a8
18 changed files with 1052 additions and 7 deletions
@@ -15,6 +15,7 @@ import {
getMetadataProviders,
refreshEpisodesMetadata,
searchMetadata,
setShowOriginalName,
showPosterUrl,
updateMetadata,
uploadPoster,
@@ -25,7 +26,8 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
const posterInput = useRef<HTMLInputElement>(null)
const [bust, setBust] = useState(0)
const [provider, setProvider] = useState('')
const [query, setQuery] = useState(show.name)
const [originalName, setOriginalName] = useState(show.originalName ?? '')
const [query, setQuery] = useState(show.originalName || show.name)
const [results, setResults] = useState<MetadataCandidate[]>([])
const [searched, setSearched] = useState(false)
const [description, setDescription] = useState(show.description ?? '')
@@ -53,6 +55,15 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
},
onError,
})
const saveOriginal = useMutation({
mutationFn: () => setShowOriginalName(show.id, originalName.trim() || null),
onSuccess: () => {
toast.success(t('settings.saved'))
setQuery(originalName.trim() || show.name)
onChanged()
},
onError,
})
const apply = useMutation({
mutationFn: (externalId: string) => applyMetadata(show.id, effectiveProvider, externalId),
onSuccess: () => {
@@ -143,6 +154,28 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
{/* Поиск + ручная правка */}
<div className="flex min-w-0 flex-1 flex-col gap-4">
<div className="flex flex-col gap-1.5">
<Label>{t('admin.metadata.originalName')}</Label>
<div className="flex items-end gap-2">
<Input
className="min-w-40 flex-1"
value={originalName}
maxLength={256}
placeholder={t('admin.metadata.originalNamePlaceholder')}
onChange={(e) => setOriginalName(e.target.value)}
/>
<Button
size="sm"
variant="outline"
disabled={saveOriginal.isPending || originalName === (show.originalName ?? '')}
onClick={() => saveOriginal.mutate()}
>
{t('common.save')}
</Button>
</div>
<p className="text-xs text-muted-foreground">{t('admin.metadata.originalNameHint')}</p>
</div>
{providers && providers.length > 0 && (
<div className="flex flex-col gap-2">
<div className="flex flex-wrap items-end gap-2">
@@ -15,6 +15,7 @@ export function ShowsPanel() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [name, setName] = useState('')
const [originalName, setOriginalName] = useState('')
const [kind, setKind] = useState<ShowKind>('Series')
const { data, isLoading } = useQuery({ queryKey: ['admin', 'shows'], queryFn: listShows })
@@ -23,9 +24,11 @@ export function ShowsPanel() {
toast.error(error instanceof HttpError ? error.detail : t('common.error'))
const createMutation = useMutation({
mutationFn: () => createShow({ name: name.trim(), kind }),
mutationFn: () =>
createShow({ name: name.trim(), kind, originalName: originalName.trim() || undefined }),
onSuccess: () => {
setName('')
setOriginalName('')
invalidate()
},
onError,
@@ -43,6 +46,12 @@ export function ShowsPanel() {
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Input
className="max-w-xs"
placeholder={t('admin.shows.originalName')}
value={originalName}
onChange={(e) => setOriginalName(e.target.value)}
/>
<Select value={kind} onValueChange={(v) => setKind(v as ShowKind)}>
<SelectTrigger className="w-40">
<SelectValue />
+13 -1
View File
@@ -15,10 +15,22 @@ export function getShow(id: string) {
return apiRequest<ShowDto>(`/admin/shows/${id}`)
}
export function createShow(body: { name: string; kind: ShowKind; description?: string }) {
export function createShow(body: {
name: string
kind: ShowKind
description?: string
originalName?: string
}) {
return apiRequest<CreatedIdResponse>('/admin/shows', { method: 'POST', body })
}
export function setShowOriginalName(id: string, originalName: string | null) {
return apiRequest<void>(`/admin/shows/${id}/original-name`, {
method: 'PUT',
body: { originalName },
})
}
export function deleteShow(id: string) {
return apiRequest<void>(`/admin/shows/${id}`, { method: 'DELETE' })
}
+1
View File
@@ -102,6 +102,7 @@ export type EpisodeDto = {
export type ShowDto = {
id: string
name: string
originalName: string | null
kind: ShowKind
description: string | null
metadataProvider: string | null
+8
View File
@@ -152,6 +152,7 @@ const resources = {
shows: {
title: 'Шоу',
name: 'Название',
originalName: 'Оригинальное название (eng)',
kind: 'Тип',
kinds: { Series: 'Сериал', Single: 'Полнометражка' },
seasons: 'Сезоны',
@@ -268,6 +269,9 @@ const resources = {
},
metadata: {
title: 'Метаданные',
originalName: 'Оригинальное название (eng)',
originalNamePlaceholder: 'Например: Family Guy',
originalNameHint: 'По нему ищутся метаданные; на экранах показывается обычное название.',
source: 'Источник',
sourceLabel: 'Источник',
searchPlaceholder: 'Название для поиска',
@@ -437,6 +441,7 @@ const resources = {
shows: {
title: 'Shows',
name: 'Name',
originalName: 'Original name (eng)',
kind: 'Kind',
kinds: { Series: 'Series', Single: 'Movie' },
seasons: 'Seasons',
@@ -553,6 +558,9 @@ const resources = {
},
metadata: {
title: 'Metadata',
originalName: 'Original name (eng)',
originalNamePlaceholder: 'e.g. Family Guy',
originalNameHint: 'Metadata is looked up by this; screens still show the regular name.',
source: 'Source',
sourceLabel: 'Source',
searchPlaceholder: 'Title to search',