Add missing episodes feature: implement FindMissingEpisodes endpoint, update metadata providers to retrieve season episode counts, and enhance frontend components for displaying missing episodes report. Update translations for new UI elements.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { HttpError } from '@/shared/api/client'
|
||||
import type { MetadataCandidate, ShowDto } from '@/shared/api/types'
|
||||
import type { MetadataCandidate, MissingEpisodesReport, ShowDto } from '@/shared/api/types'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Label } from '@/shared/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
@@ -14,6 +16,7 @@ import { ImageGallery } from '@/features/admin/images/ImageGallery'
|
||||
import {
|
||||
applyMetadata,
|
||||
clearMetadata,
|
||||
findMissingEpisodes,
|
||||
getMetadataProviders,
|
||||
refreshEpisodesMetadata,
|
||||
renameShow,
|
||||
@@ -34,6 +37,14 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
||||
const [description, setDescription] = useState(show.description ?? '')
|
||||
const [year, setYear] = useState(show.year != null ? String(show.year) : '')
|
||||
|
||||
// После «Применить»/обновления с сервера показанные значения меняются — подхватываем их в поля формы
|
||||
// (иначе описание/год оставались бы пустыми, хотя в БД уже записаны). Реагируем только на смену
|
||||
// серверных значений, так что ручной ввод между сохранениями не затирается.
|
||||
useEffect(() => {
|
||||
setDescription(show.description ?? '')
|
||||
setYear(show.year != null ? String(show.year) : '')
|
||||
}, [show.description, show.year])
|
||||
|
||||
const { data: providers } = useQuery({
|
||||
queryKey: ['admin', 'metadata', 'providers'],
|
||||
queryFn: getMetadataProviders,
|
||||
@@ -110,11 +121,18 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
||||
},
|
||||
onError,
|
||||
})
|
||||
const [missing, setMissing] = useState<MissingEpisodesReport | null>(null)
|
||||
const findMissing = useMutation({
|
||||
mutationFn: () => findMissingEpisodes(show.id),
|
||||
onSuccess: (report) => setMissing(report),
|
||||
onError,
|
||||
})
|
||||
|
||||
const linked =
|
||||
!!show.metadataExternalId && !!show.metadataProvider && show.metadataProvider !== 'manual'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('admin.metadata.title')}</CardTitle>
|
||||
@@ -260,11 +278,23 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
||||
disabled={refreshEpisodes.isPending}
|
||||
onClick={() => refreshEpisodes.mutate()}
|
||||
>
|
||||
{refreshEpisodes.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{refreshEpisodes.isPending
|
||||
? t('admin.metadata.refreshing')
|
||||
: t('admin.metadata.refreshEpisodes')}
|
||||
</Button>
|
||||
)}
|
||||
{linked && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={findMissing.isPending}
|
||||
onClick={() => findMissing.mutate()}
|
||||
>
|
||||
{findMissing.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{t('admin.metadata.findMissing')}
|
||||
</Button>
|
||||
)}
|
||||
{(show.metadataProvider || show.posterImageId) && (
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -285,5 +315,50 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={missing != null} onOpenChange={(open) => !open && setMissing(null)}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admin.metadata.missingTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{missing && missing.seasons.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{t('admin.metadata.missingNoSeasons')}</p>
|
||||
)}
|
||||
{missing && missing.seasons.length > 0 && (
|
||||
<div className="flex max-h-[60vh] flex-col gap-3 overflow-y-auto">
|
||||
{missing.seasons.map((s) => (
|
||||
<div key={s.season} className="rounded-md border border-border p-3 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">
|
||||
{t('admin.metadata.seasonN', { n: s.season })}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('admin.metadata.loadedOf', {
|
||||
loaded: s.loaded,
|
||||
total: s.expected ?? '?',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{s.expected == null ? (
|
||||
<p className="mt-1 text-xs text-amber-500">
|
||||
{t('admin.metadata.missingUnknown')}
|
||||
</p>
|
||||
) : s.missing.length === 0 ? (
|
||||
<p className="mt-1 text-xs text-emerald-500">
|
||||
{t('admin.metadata.missingNone')}
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{t('admin.metadata.missingList')}:{' '}
|
||||
<span className="text-foreground">{s.missing.join(', ')}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { apiRequest } from '@/shared/api/client'
|
||||
import type {
|
||||
CreatedIdResponse,
|
||||
MetadataCandidate,
|
||||
MissingEpisodesReport,
|
||||
ShowAudience,
|
||||
ShowDto,
|
||||
ShowKind,
|
||||
@@ -94,3 +95,8 @@ export function setShowPoster(showId: string, imageId: string | null) {
|
||||
export function refreshEpisodesMetadata(showId: string) {
|
||||
return apiRequest<number>(`/admin/metadata/shows/${showId}/refresh-episodes`, { method: 'POST' })
|
||||
}
|
||||
|
||||
/** Отчёт: каких серий не хватает в загруженных сезонах (по данным источника). */
|
||||
export function findMissingEpisodes(showId: string) {
|
||||
return apiRequest<MissingEpisodesReport>(`/admin/metadata/shows/${showId}/missing-episodes`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user