import { unwrap } from '@pipu/core'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useId } from 'react'; import { useTranslation } from 'react-i18next'; import { useSession, useUser } from '../../app/session'; import { useErrorText } from '../../shared/lib/errors'; import { refreshIcon } from '../../shared/ui/icons'; import { Button, FormError } from '../../shared/ui/ui'; import s from '../page.module.css'; import l from './library.module.css'; import { ScanLine, StatusBadge } from './ScanStatus'; /** Состояние библиотеки: счётчики, хранилища и сканы (admin, moderator), ошибки чтения (admin). */ export function LibraryPage() { const { t } = useTranslation(['admin', 'common']); const session = useSession(); const user = useUser(); const errorText = useErrorText(); const admin = user.role === 'admin'; const status = useQuery({ queryKey: ['library-status'], queryFn: async () => unwrap(await session.client.GET('/api/v1/admin/library/status')), refetchInterval: (q) => (q.state.data?.sources.some((x) => x.scan?.running) ? 2000 : 15000), }); const errors = useQuery({ queryKey: ['library-errors'], enabled: admin, queryFn: async () => unwrap(await session.client.GET('/api/v1/admin/library/errors', { params: { query: { limit: 50 } } })), }); const reindex = useMutation({ mutationFn: async () => unwrap(await session.client.POST('/api/v1/admin/search/reindex')), }); const queryClient = useQueryClient(); const retry = useMutation({ mutationFn: async () => unwrap(await session.client.POST('/api/v1/admin/library/analysis/retry')), onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['library-status'] }), }); const d = status.data; const tiles = d ? ([ ['artists', d.artists], ['albums', d.albums], ['tracks', d.tracks], ['files', d.files], ['missing', d.missing_files], ['errors', d.file_errors], ] as const) : []; return (

{t('library.title')}

{t('library.subtitle')}

{admin && ( )}
{status.error || reindex.error ? errorText(status.error ?? reindex.error) : null}
{tiles.map(([k, v]) => (
{v.toLocaleString()} {t(`library.${k}`)}
))}
{d && (

{t('library.analysis')}

{d.analysis.failed > 0 && (
{t('library.analysis_failed', { count: d.analysis.failed })} {d.analysis.top_error && {d.analysis.top_error}} {admin && ( )}
)} {d.analysis.analyzer_ver && {t('library.analyzer', { ver: d.analysis.analyzer_ver })}}
)} {d && (

{t('library.wave')}

{( [ ['wave_served', d.wave.served.toLocaleString()], ['wave_sessions', d.wave.sessions.toLocaleString()], ['wave_listen', pct(d.wave.listen_through_rate)], ['wave_early_skip', pct(d.wave.early_skip_rate)], ['wave_likes', d.wave.like_rate === null ? '—' : d.wave.like_rate.toFixed(1)], ] as const ).map(([k, v]) => (
{v} {t(`library.${k}`)}
))}
{d.wave.by_model.length > 1 && (
{d.wave.by_model.map((m) => ( ))}
{t('library.wave_by_model')}
{t('library.wave_model')} {t('library.wave_served')} {t('library.wave_listen')} {t('library.wave_early_skip')} {t('library.wave_likes')}
{m.model_ver || '—'} {m.served.toLocaleString()} {pct(m.listen_through_rate)} {pct(m.early_skip_rate)} {m.like_rate === null ? '—' : m.like_rate.toFixed(1)}
)}
)} {d && d.sources.length > 0 && (

{t('library.sources')}

{d.sources.map((src) => (
{src.name}
))}
)} {admin && errors.data && errors.data.items.length > 0 && (

{t('library.error_files')}

{errors.data.items.map((e) => (
{e.rel_path} {e.error}
))}
)}
); } const pct = (v: number | null) => (v === null ? '—' : `${Math.round(v * 100)}%`); function ScanLineBar({ done, total, label }: { done: number; total: number; label: string }) { const value = total > 0 ? Math.round((done / total) * 100) : 0; const id = useId(); return (
{label}
); }