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.
build / backend (push) Successful in 3m5s
build / frontend (push) Successful in 44s
tests / backend-tests (push) Successful in 2m38s

This commit is contained in:
Leonid Pershin
2026-07-26 02:15:47 +03:00
parent 8962840654
commit 44e954920c
11 changed files with 344 additions and 2 deletions
@@ -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>
</>
)
}
+6
View File
@@ -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`)
}