From 0dc0384c78141092c5b9c29c00b38a55e07e6b21 Mon Sep 17 00:00:00 2001 From: mrleo1nid <43583385+mrleo1nid@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:25:48 +0300 Subject: [PATCH] fix(library): expose scan activity and failed analysis file details --- api/openapi.yaml | 60 ++ .../pages/admin/AnalysisErrorsDialog.test.tsx | 59 ++ .../src/pages/admin/AnalysisErrorsDialog.tsx | 40 + apps/web/src/pages/admin/LibraryPage.tsx | 20 +- apps/web/src/pages/admin/ScanStatus.test.tsx | 42 + apps/web/src/pages/admin/ScanStatus.tsx | 14 +- apps/web/src/pages/admin/library.module.css | 26 + docs/library-and-storage.md | 13 + packages/api-client/src/generated/schema.ts | 66 ++ packages/i18n/locales/en/admin.json | 16 +- packages/i18n/locales/ru/admin.json | 16 +- services/api/internal/api/gen/api.gen.go | 763 +++++++++++------- .../dbtest/library_diagnostics_test.go | 90 +++ .../internal/library/analysis_errors_test.go | 60 ++ services/api/internal/library/handler.go | 36 + services/api/internal/library/scanner.go | 35 +- services/api/internal/library/scanner_test.go | 36 +- services/api/internal/library/sources.go | 9 + services/api/internal/library/store.go | 37 +- services/api/internal/library/templates.go | 2 +- 20 files changed, 1114 insertions(+), 326 deletions(-) create mode 100644 apps/web/src/pages/admin/AnalysisErrorsDialog.test.tsx create mode 100644 apps/web/src/pages/admin/AnalysisErrorsDialog.tsx create mode 100644 apps/web/src/pages/admin/ScanStatus.test.tsx create mode 100644 services/api/internal/dbtest/library_diagnostics_test.go create mode 100644 services/api/internal/library/analysis_errors_test.go diff --git a/api/openapi.yaml b/api/openapi.yaml index d1cd2bc..6fe3b84 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -991,6 +991,23 @@ paths: $ref: "#/components/schemas/LibraryErrorPage" default: $ref: "#/components/responses/Problem" + /api/v1/admin/library/analysis/errors: + get: + operationId: listAnalysisErrors + tags: [library] + summary: Неудавшиеся задачи аудиоанализа с путями файлов (admin) + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Cursor" + responses: + "200": + description: Страница ошибок аудиоанализа, упорядоченная по ID трека + content: + application/json: + schema: + $ref: "#/components/schemas/AnalysisErrorPage" + default: + $ref: "#/components/responses/Problem" /api/v1/admin/library/host-dirs: get: operationId: listHostDirs @@ -2955,6 +2972,16 @@ components: type: object required: [running, full, started_at, finished_at, found, to_process, processed, added, updated, moved, missing, errors, error] properties: + phase: + type: string + enum: [walking, preparing, processing, finalizing] + description: Current scan stage; progress percentage is available only during processing. + walked_dirs: + type: integer + description: Directories visited so far, including the source root. + current_dir: + type: string + description: Current directory relative to the source root, using forward slashes. running: type: boolean full: @@ -3056,6 +3083,39 @@ components: next_cursor: type: string nullable: true + AnalysisError: + type: object + required: [track_id, title, source_id, source_name, rel_path, error] + properties: + track_id: + type: string + format: uuid + title: + type: string + source_id: + type: string + format: uuid + nullable: true + source_name: + type: string + nullable: true + rel_path: + type: string + nullable: true + description: Path relative to the source root; primary file if available, otherwise a known file of this track. + error: + type: string + AnalysisErrorPage: + type: object + required: [items, next_cursor] + properties: + items: + type: array + items: + $ref: "#/components/schemas/AnalysisError" + next_cursor: + type: string + nullable: true HostDirList: type: object required: [path, dirs] diff --git a/apps/web/src/pages/admin/AnalysisErrorsDialog.test.tsx b/apps/web/src/pages/admin/AnalysisErrorsDialog.test.tsx new file mode 100644 index 0000000..6f0a5c7 --- /dev/null +++ b/apps/web/src/pages/admin/AnalysisErrorsDialog.test.tsx @@ -0,0 +1,59 @@ +import { AuthSession } from '@pipu/core'; +import { createI18n } from '@pipu/i18n'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { cleanup, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { I18nextProvider } from 'react-i18next'; +import { afterEach, expect, it, vi } from 'vitest'; +import { SessionProvider } from '../../app/session'; +import { AnalysisErrorsDialog } from './AnalysisErrorsDialog'; + +afterEach(cleanup); + +async function fixture(mode: 'pages' | 'empty' | 'error' = 'pages') { + const cursors: (string | null)[] = []; + const session = new AuthSession({ baseUrl: 'http://localhost', clientType: 'web', fetch: async (input, init) => { + const req = new Request(input, init); + const cursor = new URL(req.url).searchParams.get('cursor'); + cursors.push(cursor); + if (mode === 'error') return new Response(JSON.stringify({ code: 'server.error' }), { status: 500 }); + return new Response(JSON.stringify({ + items: mode === 'empty' ? [] : cursor ? [{ track_id: 'second', title: 'Без файла', source_id: null, source_name: null, rel_path: null, error: '' }] + : [{ track_id: 'first', title: 'Короткий трек', source_id: 'source', source_name: 'Base', rel_path: 'Исполнитель/Альбом/track.mp3', error: 'audio is shorter than 1 s' }], + next_cursor: mode === 'pages' && !cursor ? 'next-page' : null, + }), { headers: { 'Content-Type': 'application/json' } }); + } }); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const i18n = await createI18n('ru'); + const close = vi.fn(); + render( + + ); + return { close, cursors, user: userEvent.setup() }; +} + +it('shows storage, path and error, loads the next page and closes with Escape', async () => { + const { user, cursors, close } = await fixture(); + expect(screen.getByRole('dialog', { name: 'Ошибки аудиоанализа' })).toBeVisible(); + expect(await screen.findByText('Исполнитель/Альбом/track.mp3')).toBeVisible(); + expect(screen.getByText('Хранилище: Base')).toBeVisible(); + expect(screen.getByText('audio is shorter than 1 s')).toBeVisible(); + await user.click(screen.getByRole('button', { name: 'Показать ещё' })); + expect(await screen.findByText('В библиотеке больше нет файла этого трека.')).toBeVisible(); + expect(screen.getByText('Короткий трек')).toBeVisible(); + expect(cursors).toEqual([null, 'next-page']); + expect(screen.queryByRole('button', { name: 'Показать ещё' })).not.toBeInTheDocument(); + await user.keyboard('{Escape}'); + expect(close).toHaveBeenCalledOnce(); +}); + +it('shows an empty state after failed jobs have been retried', async () => { + await fixture('empty'); + expect(await screen.findByText('Неудавшихся задач анализа нет.')).toBeVisible(); +}); + +it('shows a request failure without claiming the list is empty', async () => { + await fixture('error'); + expect(await screen.findByRole('alert')).toBeVisible(); + expect(screen.queryByText('Неудавшихся задач анализа нет.')).not.toBeInTheDocument(); +}); diff --git a/apps/web/src/pages/admin/AnalysisErrorsDialog.tsx b/apps/web/src/pages/admin/AnalysisErrorsDialog.tsx new file mode 100644 index 0000000..fdb9cfe --- /dev/null +++ b/apps/web/src/pages/admin/AnalysisErrorsDialog.tsx @@ -0,0 +1,40 @@ +import { unwrap } from '@pipu/core'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; + +import { useSession } from '../../app/session'; +import { useErrorText } from '../../shared/lib/errors'; +import { Button, Dialog, FormError } from '../../shared/ui/ui'; +import s from './library.module.css'; + +export function AnalysisErrorsDialog({ onClose }: { onClose: () => void }) { + const { t } = useTranslation(['admin', 'common']); + const session = useSession(); + const errorText = useErrorText(); + const errors = useInfiniteQuery({ + queryKey: ['analysis-errors'], + initialPageParam: undefined as string | undefined, + queryFn: async ({ pageParam }) => unwrap(await session.client.GET('/api/v1/admin/library/analysis/errors', { + params: { query: { limit: 50, cursor: pageParam } }, + })), + getNextPageParam: (page) => page.next_cursor ?? undefined, + }); + const items = errors.data?.pages.flatMap((page) => page.items) ?? []; + + return {t('common:form.close')}}> +

{t('library.analysis_errors_hint')}

+ {errors.isPending &&

{t('common:loading')}

} + {errors.error ? errorText(errors.error) : null} + {errors.isSuccess && items.length === 0 &&

{t('library.analysis_errors_empty')}

} + {items.length > 0 && } + {errors.hasNextPage && } +
; +} diff --git a/apps/web/src/pages/admin/LibraryPage.tsx b/apps/web/src/pages/admin/LibraryPage.tsx index eae8fb5..128b266 100644 --- a/apps/web/src/pages/admin/LibraryPage.tsx +++ b/apps/web/src/pages/admin/LibraryPage.tsx @@ -1,6 +1,6 @@ import { unwrap } from '@pipu/core'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useId } from 'react'; +import { useId, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSession, useUser } from '../../app/session'; @@ -10,6 +10,7 @@ import { Button, FormError } from '../../shared/ui/ui'; import s from '../page.module.css'; import l from './library.module.css'; import { ScanLine, StatusBadge } from './ScanStatus'; +import { AnalysisErrorsDialog } from './AnalysisErrorsDialog'; /** Состояние библиотеки: счётчики, хранилища и сканы (admin, moderator), ошибки чтения (admin). */ export function LibraryPage() { @@ -18,6 +19,7 @@ export function LibraryPage() { const user = useUser(); const errorText = useErrorText(); const admin = user.role === 'admin'; + const [showAnalysisErrors, setShowAnalysisErrors] = useState(false); const status = useQuery({ queryKey: ['library-status'], @@ -27,6 +29,7 @@ export function LibraryPage() { const errors = useQuery({ queryKey: ['library-errors'], enabled: admin, + refetchInterval: status.data?.sources.some((source) => source.scan?.running) ? 2000 : 15000, queryFn: async () => unwrap(await session.client.GET('/api/v1/admin/library/errors', { params: { query: { limit: 50 } } })), }); const reindex = useMutation({ @@ -35,7 +38,10 @@ export function LibraryPage() { 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'] }), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['library-status'] }); + void queryClient.invalidateQueries({ queryKey: ['analysis-errors'] }); + }, }); const d = status.data; const tiles = d @@ -89,13 +95,17 @@ export function LibraryPage() { {d.analysis.top_error && {d.analysis.top_error}} {admin && ( - +
+ + +
)} )} {d.analysis.analyzer_ver && {t('library.analyzer', { ver: d.analysis.analyzer_ver })}} + {showAnalysisErrors && setShowAnalysisErrors(false)} />} )} {d && ( diff --git a/apps/web/src/pages/admin/ScanStatus.test.tsx b/apps/web/src/pages/admin/ScanStatus.test.tsx new file mode 100644 index 0000000..ad5bac9 --- /dev/null +++ b/apps/web/src/pages/admin/ScanStatus.test.tsx @@ -0,0 +1,42 @@ +import type { components } from '@pipu/api-client'; +import { createI18n } from '@pipu/i18n'; +import { cleanup, render, screen } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { afterEach, expect, it } from 'vitest'; +import { ScanLine } from './ScanStatus'; + +afterEach(cleanup); +const scan: components['schemas']['ScanProgress'] = { + running: true, full: false, started_at: '2026-10-22T12:00:00Z', finished_at: null, + found: 12, to_process: 0, processed: 0, added: 0, updated: 0, moved: 0, missing: 0, errors: 0, error: null, + phase: 'walking', walked_dirs: 4, current_dir: 'Artist/Album', +}; + +async function fixture(overrides: Partial = {}) { + const i18n = await createI18n('ru'); + render(); +} + +it('shows changing folder and file counts during the walk without a misleading percentage', async () => { + await fixture(); + expect(screen.getByText(/Проверено папок: 4 · найдено аудиофайлов: 12/)).toBeVisible(); + expect(screen.getByText('Текущая папка: Artist/Album')).toBeVisible(); + expect(screen.getByRole('progressbar')).not.toHaveAttribute('aria-valuenow'); +}); + +it('shows a percentage only once file processing has a known total', async () => { + await fixture({ phase: 'processing', to_process: 12, processed: 3 }); + expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '25'); + expect(screen.queryByText(/Текущая папка/)).not.toBeInTheDocument(); +}); + +it.each([['preparing', 'Сопоставление файлов с библиотекой…'], ['finalizing', 'Завершение сканирования…']] as const)('labels the %s stage', async (phase, label) => { + await fixture({ phase }); + expect(screen.getByText(label)).toBeVisible(); + expect(screen.getByRole('progressbar')).not.toHaveAttribute('aria-valuenow'); +}); + +it('supports servers without the new progress fields', async () => { + await fixture({ phase: undefined, walked_dirs: undefined, current_dir: undefined, found: 0 }); + expect(screen.getByText('Обход папок…')).toBeVisible(); +}); diff --git a/apps/web/src/pages/admin/ScanStatus.tsx b/apps/web/src/pages/admin/ScanStatus.tsx index fd4f661..fd7a802 100644 --- a/apps/web/src/pages/admin/ScanStatus.tsx +++ b/apps/web/src/pages/admin/ScanStatus.tsx @@ -19,14 +19,22 @@ export function ScanLine({ scan, lastScanAt }: { scan: ScanProgress | null; last const { t, i18n } = useTranslation('admin'); const id = useId(); if (scan?.running) { + const phase = scan.phase ?? (scan.found === 0 ? 'walking' : scan.to_process > 0 ? 'processing' : 'preparing'); + const determinate = phase === 'processing' && scan.to_process > 0; const pct = scan.to_process > 0 ? Math.round((scan.processed / scan.to_process) * 100) : 0; + const state = phase === 'walking' + ? scan.walked_dirs === undefined ? t('sources.scan_walking') : t('sources.scan_walk_progress', { dirs: scan.walked_dirs.toLocaleString(i18n.language), found: scan.found.toLocaleString(i18n.language) }) + : phase === 'preparing' ? t('sources.scan_preparing') + : phase === 'finalizing' ? t('sources.scan_finalizing') + : t('sources.scanning', { processed: scan.processed, total: scan.to_process }); return (
- {scan.found === 0 ? t('sources.scan_walking') : t('sources.scanning', { processed: scan.processed, total: scan.to_process })} + {state}
-
- + {phase === 'walking' && scan.current_dir &&
{t('sources.scan_current_dir', { path: scan.current_dir })}
} +
+
); diff --git a/apps/web/src/pages/admin/library.module.css b/apps/web/src/pages/admin/library.module.css index 22a9a35..6fdc467 100644 --- a/apps/web/src/pages/admin/library.module.css +++ b/apps/web/src/pages/admin/library.module.css @@ -123,8 +123,14 @@ align-items: center; justify-content: space-between; gap: 16px; + flex-wrap: wrap; } +.analysisActions { display: flex; flex-wrap: wrap; gap: 8px; } +.analysisErrors { display: grid; gap: 16px; list-style: none; margin: 16px 0; padding: 0; } +.analysisErrors li { display: grid; gap: 6px; padding-bottom: 16px; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; } +.analysisPath { font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; } + .errorDetail { display: block; margin-top: 4px; @@ -146,6 +152,26 @@ transition: width 0.4s; } +.scanPath { + font-size: 12px; + color: var(--text3); + overflow-wrap: anywhere; +} + +.indeterminate span { + width: 35%; + animation: scanActivity 1.5s ease-in-out infinite alternate; +} + +@keyframes scanActivity { + from { transform: translateX(0); } + to { transform: translateX(185%); } +} + +@media (prefers-reduced-motion: reduce) { + .indeterminate span { animation: none; } +} + .test { font-size: 13px; } diff --git a/docs/library-and-storage.md b/docs/library-and-storage.md index 0221004..a4658eb 100644 --- a/docs/library-and-storage.md +++ b/docs/library-and-storage.md @@ -139,6 +139,19 @@ lower-case, `ё → е`, удаление пунктуации, схлопыва Ожидаемое время первого скана 11k файлов по NFS — 5–15 минут (bottleneck — чтение тегов). +Во время обхода админка показывает число посещённых папок (включая корень), найденных +аудиофайлов и текущую папку относительно корня хранилища. До завершения обхода общее +число файлов неизвестно, поэтому индикатор показывает активность без процента. +Затем отображаются этапы сопоставления, обработки файлов с известным общим числом +и завершения скана. API передаёт их в `ScanProgress.phase`, `walked_dirs`, `current_dir`. + +Ошибки в итогах скана относятся только к этому запуску. Общий счётчик и список +ошибок чтения учитывают сохранённые ошибки только присутствующих файлов: +после успешного скана пропавшие файлы исключаются из них, сохраняя запись на срок +`MissingRetention`. В блоке аудиоанализа администратор может открыть «Показать файлы»: +окно загружает ошибки анализа страницами через `GET /api/v1/admin/library/analysis/errors` +и показывает трек, хранилище, относительный путь из текущего каталога и причину сбоя. + ### Обложки Источники по приоритету: встроенная в файл → `cover.jpg|folder.jpg|front.*` в папке альбома → плейсхолдер, diff --git a/packages/api-client/src/generated/schema.ts b/packages/api-client/src/generated/schema.ts index b9eace6..4702847 100644 --- a/packages/api-client/src/generated/schema.ts +++ b/packages/api-client/src/generated/schema.ts @@ -860,6 +860,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/library/analysis/errors": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Неудавшиеся задачи аудиоанализа с путями файлов (admin) */ + get: operations["listAnalysisErrors"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/library/host-dirs": { parameters: { query?: never; @@ -2078,6 +2095,15 @@ export interface components { full?: boolean; }; ScanProgress: { + /** + * @description Current scan stage; progress percentage is available only during processing. + * @enum {string} + */ + phase?: "walking" | "preparing" | "processing" | "finalizing"; + /** @description Directories visited so far, including the source root. */ + walked_dirs?: number; + /** @description Current directory relative to the source root, using forward slashes. */ + current_dir?: string; running: boolean; full: boolean; /** Format: date-time */ @@ -2127,6 +2153,21 @@ export interface components { items: components["schemas"]["LibraryError"][]; next_cursor: string | null; }; + AnalysisError: { + /** Format: uuid */ + track_id: string; + title: string; + /** Format: uuid */ + source_id: string | null; + source_name: string | null; + /** @description Path relative to the source root; primary file if available, otherwise a known file of this track. */ + rel_path: string | null; + error: string; + }; + AnalysisErrorPage: { + items: components["schemas"]["AnalysisError"][]; + next_cursor: string | null; + }; HostDirList: { path: string; dirs: string[]; @@ -3760,6 +3801,31 @@ export interface operations { default: components["responses"]["Problem"]; }; }; + listAnalysisErrors: { + parameters: { + query?: { + limit?: components["parameters"]["Limit"]; + /** @description Непрозрачный курсор из `next_cursor` предыдущей страницы */ + cursor?: components["parameters"]["Cursor"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Страница ошибок аудиоанализа, упорядоченная по ID трека */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnalysisErrorPage"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; listHostDirs: { parameters: { query?: { diff --git a/packages/i18n/locales/en/admin.json b/packages/i18n/locales/en/admin.json index c58842a..185c67d 100644 --- a/packages/i18n/locales/en/admin.json +++ b/packages/i18n/locales/en/admin.json @@ -98,7 +98,11 @@ "delete_confirm": "Delete “{{name}}”? Tracks that exist only in this storage will disappear from the library along with their likes.", "scanning": "Scanning: {{processed}} of {{total}}", "scan_walking": "Walking folders…", - "scan_done": "Found {{found}} · new {{added}} · updated {{updated}} · moved {{moved}} · missing {{missing}} · errors {{errors}}", + "scan_walk_progress": "Walking folders… Directories checked: {{dirs}} · audio files found: {{found}}", + "scan_current_dir": "Current folder: {{path}}", + "scan_preparing": "Matching files with the library…", + "scan_finalizing": "Finishing scan…", + "scan_done": "Found {{found}} · new {{added}} · updated {{updated}} · moved {{moved}} · missing {{missing}} · errors in this scan {{errors}}", "scan_failed": "Scan stopped: {{error}}", "test_ok": "Reachable, audio files found: {{count}}", "test_fail": "Not reachable: {{message}}", @@ -172,7 +176,15 @@ "wave_likes": "Likes per 100", "wave_model": "Model version", "wave_by_model": "Wave model versions compared", - "analysis_retry": "Retry" + "analysis_retry": "Retry", + "analysis_show_errors": "Show files", + "analysis_errors_title": "Audio analysis errors", + "analysis_errors_hint": "Each track shows its storage, path relative to the storage root and failure reason. Paths come from the current library catalog.", + "analysis_errors_empty": "There are no failed analysis jobs.", + "analysis_errors_more": "Show more", + "analysis_error_source": "Storage: {{name}}", + "analysis_error_no_file": "This track no longer has a file in the library.", + "analysis_error_unknown": "No error reason was recorded." }, "write_template": { "title": "Uploads and path template", diff --git a/packages/i18n/locales/ru/admin.json b/packages/i18n/locales/ru/admin.json index c18a737..8975d81 100644 --- a/packages/i18n/locales/ru/admin.json +++ b/packages/i18n/locales/ru/admin.json @@ -98,7 +98,11 @@ "delete_confirm": "Удалить «{{name}}»? Треки, которые есть только в этом хранилище, пропадут из библиотеки вместе с лайками.", "scanning": "Сканирование: {{processed}} из {{total}}", "scan_walking": "Обход папок…", - "scan_done": "Найдено {{found}} · новых {{added}} · обновлено {{updated}} · перемещено {{moved}} · пропало {{missing}} · ошибок {{errors}}", + "scan_walk_progress": "Обход папок… Проверено папок: {{dirs}} · найдено аудиофайлов: {{found}}", + "scan_current_dir": "Текущая папка: {{path}}", + "scan_preparing": "Сопоставление файлов с библиотекой…", + "scan_finalizing": "Завершение сканирования…", + "scan_done": "Найдено {{found}} · новых {{added}} · обновлено {{updated}} · перемещено {{moved}} · пропало {{missing}} · ошибок в этом скане {{errors}}", "scan_failed": "Скан прервался: {{error}}", "test_ok": "Доступно, найдено аудиофайлов: {{count}}", "test_fail": "Нет доступа: {{message}}", @@ -172,7 +176,15 @@ "wave_likes": "Лайков на 100", "wave_model": "Версия модели", "wave_by_model": "Сравнение версий модели волны", - "analysis_retry": "Повторить" + "analysis_retry": "Повторить", + "analysis_show_errors": "Показать файлы", + "analysis_errors_title": "Ошибки аудиоанализа", + "analysis_errors_hint": "Для каждого трека показаны хранилище, путь относительно его корня и причина сбоя. Пути берутся из текущего каталога библиотеки.", + "analysis_errors_empty": "Неудавшихся задач анализа нет.", + "analysis_errors_more": "Показать ещё", + "analysis_error_source": "Хранилище: {{name}}", + "analysis_error_no_file": "В библиотеке больше нет файла этого трека.", + "analysis_error_unknown": "Причина ошибки не указана." }, "write_template": { "title": "Приём загрузок и шаблон пути", diff --git a/services/api/internal/api/gen/api.gen.go b/services/api/internal/api/gen/api.gen.go index e98a13d..2cbb2ad 100644 --- a/services/api/internal/api/gen/api.gen.go +++ b/services/api/internal/api/gen/api.gen.go @@ -129,6 +129,14 @@ const ( User Role = "user" ) +// Defines values for ScanProgressPhase. +const ( + ScanProgressPhaseFinalizing ScanProgressPhase = "finalizing" + ScanProgressPhasePreparing ScanProgressPhase = "preparing" + ScanProgressPhaseProcessing ScanProgressPhase = "processing" + ScanProgressPhaseWalking ScanProgressPhase = "walking" +) + // Defines values for SourceStatus. const ( Disabled SourceStatus = "disabled" @@ -200,9 +208,9 @@ const ( // Defines values for UploadPrecheckResultFilesResult. const ( - UploadPrecheckResultFilesResultAccepted UploadPrecheckResultFilesResult = "accepted" - UploadPrecheckResultFilesResultDuplicate UploadPrecheckResultFilesResult = "duplicate" - UploadPrecheckResultFilesResultTooLarge UploadPrecheckResultFilesResult = "too_large" + Accepted UploadPrecheckResultFilesResult = "accepted" + Duplicate UploadPrecheckResultFilesResult = "duplicate" + TooLarge UploadPrecheckResultFilesResult = "too_large" ) // Defines values for UploadReviewRequestDecision. @@ -325,6 +333,24 @@ type AlbumSummaryList struct { Items []AlbumSummary `json:"items"` } +// AnalysisError defines model for AnalysisError. +type AnalysisError struct { + Error string `json:"error"` + + // RelPath Path relative to the source root; primary file if available, otherwise a known file of this track. + RelPath *string `json:"rel_path"` + SourceId *openapi_types.UUID `json:"source_id"` + SourceName *string `json:"source_name"` + Title string `json:"title"` + TrackId openapi_types.UUID `json:"track_id"` +} + +// AnalysisErrorPage defines model for AnalysisErrorPage. +type AnalysisErrorPage struct { + Items []AnalysisError `json:"items"` + NextCursor *string `json:"next_cursor"` +} + // AnalysisStatus defines model for AnalysisStatus. type AnalysisStatus struct { AnalyzerVer *string `json:"analyzer_ver"` @@ -857,7 +883,10 @@ type Role string // ScanProgress defines model for ScanProgress. type ScanProgress struct { - Added int `json:"added"` + Added int `json:"added"` + + // CurrentDir Current directory relative to the source root, using forward slashes. + CurrentDir *string `json:"current_dir,omitempty"` Error *string `json:"error"` Errors int `json:"errors"` FinishedAt *time.Time `json:"finished_at"` @@ -865,13 +894,22 @@ type ScanProgress struct { Full bool `json:"full"` Missing int `json:"missing"` Moved int `json:"moved"` - Processed int `json:"processed"` - Running bool `json:"running"` - StartedAt time.Time `json:"started_at"` - ToProcess int `json:"to_process"` - Updated int `json:"updated"` + + // Phase Current scan stage; progress percentage is available only during processing. + Phase *ScanProgressPhase `json:"phase,omitempty"` + Processed int `json:"processed"` + Running bool `json:"running"` + StartedAt time.Time `json:"started_at"` + ToProcess int `json:"to_process"` + Updated int `json:"updated"` + + // WalkedDirs Directories visited so far, including the source root. + WalkedDirs *int `json:"walked_dirs,omitempty"` } +// ScanProgressPhase Current scan stage; progress percentage is available only during processing. +type ScanProgressPhase string + // ScanRequest defines model for ScanRequest. type ScanRequest struct { // Full Перечитать все файлы, а не только новые и изменённые @@ -1502,6 +1540,14 @@ type ControlGeniusJSONBody struct { // ControlGeniusJSONBodyAction defines parameters for ControlGenius. type ControlGeniusJSONBodyAction string +// ListAnalysisErrorsParams defines parameters for ListAnalysisErrors. +type ListAnalysisErrorsParams struct { + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + + // Cursor Непрозрачный курсор из `next_cursor` предыдущей страницы + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` +} + // ListLibraryErrorsParams defines parameters for ListLibraryErrors. type ListLibraryErrorsParams struct { Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` @@ -1726,6 +1772,9 @@ type ServerInterface interface { // (POST /api/v1/admin/genius) ControlGenius(w http.ResponseWriter, r *http.Request) + // Неудавшиеся задачи аудиоанализа с путями файлов (admin) + // (GET /api/v1/admin/library/analysis/errors) + ListAnalysisErrors(w http.ResponseWriter, r *http.Request, params ListAnalysisErrorsParams) // Вернуть в очередь треки, которые не удалось проанализировать (admin) // (POST /api/v1/admin/library/analysis/retry) RetryAnalysis(w http.ResponseWriter, r *http.Request) @@ -2018,6 +2067,12 @@ func (_ Unimplemented) ControlGenius(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Неудавшиеся задачи аудиоанализа с путями файлов (admin) +// (GET /api/v1/admin/library/analysis/errors) +func (_ Unimplemented) ListAnalysisErrors(w http.ResponseWriter, r *http.Request, params ListAnalysisErrorsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // Вернуть в очередь треки, которые не удалось проанализировать (admin) // (POST /api/v1/admin/library/analysis/retry) func (_ Unimplemented) RetryAnalysis(w http.ResponseWriter, r *http.Request) { @@ -2642,6 +2697,47 @@ func (siw *ServerInterfaceWrapper) ControlGenius(w http.ResponseWriter, r *http. handler.ServeHTTP(w, r) } +// ListAnalysisErrors operation middleware +func (siw *ServerInterfaceWrapper) ListAnalysisErrors(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params ListAnalysisErrorsParams + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", r.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + // ------------- Optional query parameter "cursor" ------------- + + err = runtime.BindQueryParameter("form", true, false, "cursor", r.URL.Query(), ¶ms.Cursor) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "cursor", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListAnalysisErrors(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // RetryAnalysis operation middleware func (siw *ServerInterfaceWrapper) RetryAnalysis(w http.ResponseWriter, r *http.Request) { @@ -5393,6 +5489,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/api/v1/admin/genius", wrapper.ControlGenius) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/admin/library/analysis/errors", wrapper.ListAnalysisErrors) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/api/v1/admin/library/analysis/retry", wrapper.RetryAnalysis) }) @@ -5731,6 +5830,35 @@ func (response ControlGenius200JSONResponse) VisitControlGeniusResponse(w http.R return json.NewEncoder(w).Encode(response) } +type ListAnalysisErrorsRequestObject struct { + Params ListAnalysisErrorsParams +} + +type ListAnalysisErrorsResponseObject interface { + VisitListAnalysisErrorsResponse(w http.ResponseWriter) error +} + +type ListAnalysisErrors200JSONResponse AnalysisErrorPage + +func (response ListAnalysisErrors200JSONResponse) VisitListAnalysisErrorsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListAnalysisErrorsdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response ListAnalysisErrorsdefaultApplicationProblemPlusJSONResponse) VisitListAnalysisErrorsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type RetryAnalysisRequestObject struct { } @@ -8363,6 +8491,9 @@ type StrictServerInterface interface { // (POST /api/v1/admin/genius) ControlGenius(ctx context.Context, request ControlGeniusRequestObject) (ControlGeniusResponseObject, error) + // Неудавшиеся задачи аудиоанализа с путями файлов (admin) + // (GET /api/v1/admin/library/analysis/errors) + ListAnalysisErrors(ctx context.Context, request ListAnalysisErrorsRequestObject) (ListAnalysisErrorsResponseObject, error) // Вернуть в очередь треки, которые не удалось проанализировать (admin) // (POST /api/v1/admin/library/analysis/retry) RetryAnalysis(ctx context.Context, request RetryAnalysisRequestObject) (RetryAnalysisResponseObject, error) @@ -8745,6 +8876,32 @@ func (sh *strictHandler) ControlGenius(w http.ResponseWriter, r *http.Request) { } } +// ListAnalysisErrors operation middleware +func (sh *strictHandler) ListAnalysisErrors(w http.ResponseWriter, r *http.Request, params ListAnalysisErrorsParams) { + var request ListAnalysisErrorsRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ListAnalysisErrors(ctx, request.(ListAnalysisErrorsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListAnalysisErrors") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ListAnalysisErrorsResponseObject); ok { + if err := validResponse.VisitListAnalysisErrorsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // RetryAnalysis operation middleware func (sh *strictHandler) RetryAnalysis(w http.ResponseWriter, r *http.Request) { var request RetryAnalysisRequestObject @@ -11220,297 +11377,303 @@ func (sh *strictHandler) GetReadyz(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "FISFPIV+v3Hv1r3/PwAA//8ZFRgBKHoBAA==", + "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", + "cf/O/f8/AAD//+0IjFXOgAEA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/services/api/internal/dbtest/library_diagnostics_test.go b/services/api/internal/dbtest/library_diagnostics_test.go new file mode 100644 index 0000000..ecf6133 --- /dev/null +++ b/services/api/internal/dbtest/library_diagnostics_test.go @@ -0,0 +1,90 @@ +//go:build integration + +package dbtest + +import ( + "context" + "log/slog" + "os" + "path/filepath" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "pipu/api/internal/library" + "pipu/api/internal/platform/db" +) + +func TestLibraryDiagnostics(t *testing.T) { + pool := connect(t) + ctx := context.Background() + require.NoError(t, db.Migrate(ctx, pool)) + store := library.NewStore(pool) + src, err := store.SaveSource(ctx, library.Source{ID: uuid.New(), Name: "diagnostics", Type: library.SourceLocal, Status: "mounted", TagsFirst: true, NFSOptions: "ro"}) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, store.DeleteSource(ctx, src.ID)) + require.NoError(t, store.CleanupOrphans(ctx)) + }) + before, err := store.Counts(ctx) + require.NoError(t, err) + badID, err := store.SaveFile(ctx, src.ID, uuid.Nil, library.FileData{RelPath: "broken.mp3", Mtime: time.Now(), ScanError: "invalid audio"}, nil) + require.NoError(t, err) + counts, err := store.Counts(ctx) + require.NoError(t, err) + require.Equal(t, before.Errors+1, counts.Errors) + require.NoError(t, store.MarkMissing(ctx, []uuid.UUID{badID}, time.Now())) + counts, err = store.Counts(ctx) + require.NoError(t, err) + require.Equal(t, before.Errors, counts.Errors) + fileErrors, err := store.FileErrors(ctx, "", 200) + require.NoError(t, err) + for _, item := range fileErrors { + require.NotEqual(t, badID, item.ID, "absent files must not remain in read errors") + } + // Failed analysis jobs retain their place in diagnostics even without a file. + root := t.TempDir() + scanner := library.NewScanner(store, fakeProber{}, library.NewProgressBoard(), slog.New(slog.DiscardHandler)) + tracks := map[uuid.UUID]bool{} + for _, name := range []string{"Artist - First.flac", "Artist - Second.flac"} { + require.NoError(t, os.WriteFile(filepath.Join(root, name), []byte(name), 0o644)) + fileID, err := scanner.ScanFile(ctx, src, root, name) + require.NoError(t, err) + var trackID uuid.UUID + require.NoError(t, pool.QueryRow(ctx, `SELECT track_id FROM media_files WHERE id = $1`, fileID).Scan(&trackID)) + tracks[trackID] = true + _, err = pool.Exec(ctx, `UPDATE analysis_jobs SET status = 'failed', error = 'audio is shorter than 1 s' WHERE track_id = $1`, trackID) + require.NoError(t, err) + if name == "Artist - Second.flac" { + _, err = pool.Exec(ctx, `DELETE FROM media_files WHERE id = $1`, fileID) + require.NoError(t, err) + } + } + var paths, missing int + after := uuid.Nil + for { + page, err := store.AnalysisErrors(ctx, after, 1) + require.NoError(t, err) + if len(page) == 0 { + break + } + item := page[0] + require.NotEqual(t, after, item.TrackID, "pagination must advance") + after = item.TrackID + if !tracks[item.TrackID] { + continue + } + require.Equal(t, "audio is shorter than 1 s", item.Error) + if item.RelPath == nil { + missing++ + } else { + paths++ + require.Equal(t, "Artist - First.flac", *item.RelPath) + require.Equal(t, src.ID, *item.SourceID) + } + } + require.Equal(t, 1, paths) + require.Equal(t, 1, missing) +} diff --git a/services/api/internal/library/analysis_errors_test.go b/services/api/internal/library/analysis_errors_test.go new file mode 100644 index 0000000..9a3b35c --- /dev/null +++ b/services/api/internal/library/analysis_errors_test.go @@ -0,0 +1,60 @@ +package library + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "pipu/api/internal/api/gen" + "pipu/api/internal/auth" +) + +type analysisErrorStore struct { + SourceStore + rows []AnalysisError + after uuid.UUID + limit int +} + +func (s *analysisErrorStore) AnalysisErrors(_ context.Context, after uuid.UUID, limit int) ([]AnalysisError, error) { + s.after, s.limit = after, limit + return s.rows, nil +} + +func TestAnalysisErrorsPermissionsAndPagination(t *testing.T) { + first, second := uuid.New(), uuid.New() + store := &analysisErrorStore{rows: []AnalysisError{{TrackID: first, Title: "One"}, {TrackID: second, Title: "Two"}}} + h := &Handler{svc: &Service{SourceService: &SourceService{store: store}}} + limit := 1 + req := gen.ListAnalysisErrorsRequestObject{Params: gen.ListAnalysisErrorsParams{Limit: &limit}} + for _, role := range []auth.Role{auth.RoleUser, auth.RoleModerator} { + _, err := h.ListAnalysisErrors(auth.WithPrincipal(context.Background(), auth.Principal{Role: role}), req) + require.Error(t, err) + require.Zero(t, store.limit, "unauthorized requests must not reach the store") + } + _, err := h.ListAnalysisErrors(context.Background(), req) + require.Error(t, err) + ctx := auth.WithPrincipal(context.Background(), auth.Principal{Role: auth.RoleAdmin}) + resp, err := h.ListAnalysisErrors(ctx, req) + require.NoError(t, err) + page := resp.(gen.ListAnalysisErrors200JSONResponse) + require.Len(t, page.Items, 1) + require.Equal(t, first, page.Items[0].TrackId) + require.Nil(t, page.Items[0].RelPath, "a missing file must not hide the failed job") + require.Equal(t, 2, store.limit) + require.NotNil(t, page.NextCursor) + store.rows = store.rows[1:] + req.Params.Cursor = page.NextCursor + resp, err = h.ListAnalysisErrors(ctx, req) + require.NoError(t, err) + require.Equal(t, first, store.after) + require.Nil(t, resp.(gen.ListAnalysisErrors200JSONResponse).NextCursor) + for _, cursor := range []string{"!invalid", base64.RawURLEncoding.EncodeToString([]byte("not-a-uuid"))} { + req.Params.Cursor = &cursor + _, err = h.ListAnalysisErrors(ctx, req) + require.Error(t, err) + } +} diff --git a/services/api/internal/library/handler.go b/services/api/internal/library/handler.go index 09b491c..7d487d7 100644 --- a/services/api/internal/library/handler.go +++ b/services/api/internal/library/handler.go @@ -11,6 +11,8 @@ import ( "os" "time" + "github.com/google/uuid" + "pipu/api/internal/api/gen" "pipu/api/internal/library/meta" "pipu/api/internal/platform/apperr" @@ -80,7 +82,9 @@ func (h *Handler) sourceDTO(s Source) gen.Source { } func progressDTO(p Progress) gen.ScanProgress { + phase := gen.ScanProgressPhase(p.Phase) out := gen.ScanProgress{ + Phase: &phase, WalkedDirs: &p.WalkedDirs, CurrentDir: &p.CurrentDir, Running: p.Running, Full: p.Full, StartedAt: p.StartedAt, FinishedAt: p.FinishedAt, Found: p.Found, ToProcess: p.ToProcess, Processed: p.Processed, Added: p.Added, Updated: p.Updated, Moved: p.Moved, Missing: p.Missing, Errors: p.Errors, @@ -365,6 +369,38 @@ func (h *Handler) ListLibraryErrors(ctx context.Context, req gen.ListLibraryErro return out, nil } +// ListAnalysisErrors implements the paginated analysis diagnostics endpoint. +func (h *Handler) ListAnalysisErrors(ctx context.Context, req gen.ListAnalysisErrorsRequestObject) (gen.ListAnalysisErrorsResponseObject, error) { + limit := 50 + if req.Params.Limit != nil { + limit = min(max(*req.Params.Limit, 1), 200) + } + after := uuid.Nil + if req.Params.Cursor != nil { + raw, err := base64.RawURLEncoding.DecodeString(*req.Params.Cursor) + if err == nil { + after, err = uuid.Parse(string(raw)) + } + if err != nil { + return nil, apperr.Validation("cursor", "validation.cursor_invalid", "malformed cursor", nil) + } + } + rows, err := h.svc.AnalysisErrors(ctx, after, limit+1) + if err != nil { + return nil, err + } + out := gen.ListAnalysisErrors200JSONResponse{Items: []gen.AnalysisError{}} + if len(rows) > limit { + rows = rows[:limit] + next := base64.RawURLEncoding.EncodeToString([]byte(rows[limit-1].TrackID.String())) + out.NextCursor = &next + } + for _, r := range rows { + out.Items = append(out.Items, gen.AnalysisError{TrackId: r.TrackID, Title: r.Title, SourceId: r.SourceID, SourceName: r.SourceName, RelPath: r.RelPath, Error: r.Error}) + } + return out, nil +} + // ListHostDirs реализует gen.StrictServerInterface. func (h *Handler) ListHostDirs(ctx context.Context, req gen.ListHostDirsRequestObject) (gen.ListHostDirsResponseObject, error) { rel := "" diff --git a/services/api/internal/library/scanner.go b/services/api/internal/library/scanner.go index a836963..32e9840 100644 --- a/services/api/internal/library/scanner.go +++ b/services/api/internal/library/scanner.go @@ -28,6 +28,9 @@ const MissingRetention = 30 * 24 * time.Hour // Progress — состояние скана одного источника (для админки). type Progress struct { + Phase string + WalkedDirs int + CurrentDir string SourceID uuid.UUID Running bool Full bool @@ -82,7 +85,7 @@ func (b *ProgressBoard) start(id uuid.UUID, full bool, now time.Time) bool { if p, ok := b.m[id]; ok && p.Running { return false } - b.m[id] = &Progress{SourceID: id, Running: true, Full: full, StartedAt: now} + b.m[id] = &Progress{SourceID: id, Running: true, Full: full, StartedAt: now, Phase: "walking"} return true } @@ -202,14 +205,18 @@ func (sc *Scanner) run(ctx context.Context, src Source, root string, full bool) if err != nil { return err } - found, err := walk(root, func(rel string) bool { return meta.MatchPath(patterns, rel).Skip }) + found, err := walk(root, func(rel string) bool { return meta.MatchPath(patterns, rel).Skip }, func(found, dirs int, currentDir string) { + sc.board.update(src.ID, func(p *Progress) { + p.Found, p.WalkedDirs, p.CurrentDir = found, dirs, currentDir + }) + }) if err != nil { return err } if len(found) == 0 && hasPresent(existing) { return ErrSourceEmpty } - sc.board.update(src.ID, func(p *Progress) { p.Found = len(found) }) + sc.board.update(src.ID, func(p *Progress) { p.Found, p.Phase = len(found), "preparing" }) var ( jobs []job @@ -256,11 +263,12 @@ func (sc *Scanner) run(ctx context.Context, src Source, root string, full bool) for _, w := range rest { jobs = append(jobs, job{walked: w}) } - sc.board.update(src.ID, func(p *Progress) { p.ToProcess, p.Moved = len(jobs), len(moves) }) + sc.board.update(src.ID, func(p *Progress) { p.ToProcess, p.Moved, p.Phase = len(jobs), len(moves), "processing" }) if err := sc.process(ctx, src, root, patterns, jobs); err != nil { return err } + sc.board.update(src.ID, func(p *Progress) { p.Phase = "finalizing" }) var goneIDs []uuid.UUID affected := map[uuid.UUID]bool{} @@ -462,11 +470,12 @@ func (sc *Scanner) apply(ctx context.Context, src Source, patterns []meta.Patter // walk обходит дерево источника и возвращает аудиофайлы. Скрытые файлы и каталоги пропускаются; // skip — пути, которые шаблоны велят игнорировать. Ошибки отдельных каталогов не прерывают обход. -func walk(root string, skip func(rel string) bool) ([]walked, error) { +func walk(root string, skip func(rel string) bool, report func(found, dirs int, currentDir string)) ([]walked, error) { if _, err := os.Stat(root); err != nil { return nil, fmt.Errorf("library: source root: %w", err) } var out []walked + dirs := 0 err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { if err != nil { if p == root { @@ -481,14 +490,21 @@ func walk(root string, skip func(rel string) bool) ([]walked, error) { } return nil } - if d.IsDir() || !IsAudio(name) { - return nil - } rel, err := filepath.Rel(root, p) if err != nil { return nil } rel = filepath.ToSlash(rel) + if d.IsDir() { + dirs++ + if report != nil { + report(len(out), dirs, rel) + } + return nil + } + if !IsAudio(name) { + return nil + } if skip(rel) { return nil } @@ -498,6 +514,9 @@ func walk(root string, skip func(rel string) bool) ([]walked, error) { } // PostgreSQL хранит время с точностью до микросекунд. out = append(out, walked{rel: rel, size: info.Size(), mtime: info.ModTime().UTC().Truncate(time.Microsecond)}) + if report != nil { + report(len(out), dirs, filepath.ToSlash(filepath.Dir(rel))) + } return nil }) if err != nil { diff --git a/services/api/internal/library/scanner_test.go b/services/api/internal/library/scanner_test.go index 9c527e5..ea7b726 100644 --- a/services/api/internal/library/scanner_test.go +++ b/services/api/internal/library/scanner_test.go @@ -55,7 +55,7 @@ func TestWalkAndHash(t *testing.T) { write("_incoming/new.mp3", 10) write("big.mp3", 3<<20) - found, err := walk(root, func(rel string) bool { return rel == "_incoming/new.mp3" }) + found, err := walk(root, func(rel string) bool { return rel == "_incoming/new.mp3" }, nil) require.NoError(t, err) assert.ElementsMatch(t, []string{"Artist/Album/01 - Song.FLAC", "big.mp3"}, rels(found)) for _, w := range found { @@ -69,10 +69,42 @@ func TestWalkAndHash(t *testing.T) { require.NoError(t, err) assert.Equal(t, h1, h2, "hash survives a rename") - _, err = walk(filepath.Join(root, "nope"), func(string) bool { return false }) + _, err = walk(filepath.Join(root, "nope"), func(string) bool { return false }, nil) assert.Error(t, err) } +func TestWalkReportsIncrementalProgress(t *testing.T) { + root := t.TempDir() + for _, dir := range []string{"a-empty", "b-album", ".hidden"} { + require.NoError(t, os.MkdirAll(filepath.Join(root, dir), 0o755)) + } + for _, rel := range []string{"b-album/1.mp3", "b-album/2.flac", "b-album/skip.mp3", ".hidden/3.mp3"} { + require.NoError(t, os.WriteFile(filepath.Join(root, filepath.FromSlash(rel)), []byte("audio"), 0o644)) + } + var counts, folders []int + var paths []string + found, err := walk(root, func(rel string) bool { return rel == "b-album/skip.mp3" }, func(n, dirs int, path string) { + counts, folders, paths = append(counts, n), append(folders, dirs), append(paths, path) + }) + require.NoError(t, err) + require.Len(t, found, 2) + assert.Equal(t, []int{0, 0, 0, 1, 2}, counts, "reports activity before any file is found and after each accepted audio file") + assert.Equal(t, []int{1, 2, 3, 3, 3}, folders) + assert.Equal(t, []string{".", "a-empty", "b-album", "b-album", "b-album"}, paths) +} + +func TestProgressDTOIncludesWalkActivity(t *testing.T) { + p := Progress{Phase: "walking", Running: true, Found: 7, WalkedDirs: 4, CurrentDir: "Artist/Album"} + dto := progressDTO(p) + require.NotNil(t, dto.Phase) + assert.Equal(t, "walking", string(*dto.Phase)) + assert.Equal(t, p.Found, dto.Found) + require.NotNil(t, dto.WalkedDirs) + assert.Equal(t, p.WalkedDirs, *dto.WalkedDirs) + require.NotNil(t, dto.CurrentDir) + assert.Equal(t, p.CurrentDir, *dto.CurrentDir) +} + func TestParseFFprobe(t *testing.T) { raw := []byte(`{ "streams": [ diff --git a/services/api/internal/library/sources.go b/services/api/internal/library/sources.go index b0b84cf..d6c838b 100644 --- a/services/api/internal/library/sources.go +++ b/services/api/internal/library/sources.go @@ -33,6 +33,7 @@ type SourceStore interface { AnalysisCounts(context.Context) (AnalysisCounts, error) WaveMetrics(context.Context) (WaveMetrics, error) FileErrors(context.Context, string, int) ([]FileError, error) + AnalysisErrors(context.Context, uuid.UUID, int) ([]AnalysisError, error) } // SourceService owns source configuration, connectivity and diagnostics. @@ -440,6 +441,14 @@ func (s *SourceService) FileErrors(ctx context.Context, after string, limit int) return s.store.FileErrors(ctx, after, limit) } +// AnalysisErrors lists failed analysis jobs for library administrators. +func (s *SourceService) AnalysisErrors(ctx context.Context, after uuid.UUID, limit int) ([]AnalysisError, error) { + if err := requirePerm(ctx, auth.PermManageLibrary); err != nil { + return nil, err + } + return s.store.AnalysisErrors(ctx, after, limit) +} + // HostDirs — подпапки каталога LIBRARY_HOST для выбора local-хранилища. func (s *SourceService) HostDirs(ctx context.Context, rel string) (string, []string, error) { if err := requirePerm(ctx, auth.PermManageLibrary); err != nil { diff --git a/services/api/internal/library/store.go b/services/api/internal/library/store.go index 1c216ad..066cf40 100644 --- a/services/api/internal/library/store.go +++ b/services/api/internal/library/store.go @@ -511,7 +511,7 @@ func (st *Store) Counts(ctx context.Context) (Counts, error) { err := st.pool.QueryRow(ctx, ` SELECT (SELECT count(*) FROM artists), (SELECT count(*) FROM albums), (SELECT count(*) FROM tracks), (SELECT count(*) FROM media_files), (SELECT count(*) FROM media_files WHERE missing_since IS NOT NULL), - (SELECT count(*) FROM media_files WHERE scan_error IS NOT NULL)`). + (SELECT count(*) FROM media_files WHERE scan_error IS NOT NULL AND missing_since IS NULL)`). Scan(&c.Artists, &c.Albums, &c.Tracks, &c.Files, &c.Missing, &c.Errors) if err != nil { return Counts{}, fmt.Errorf("library: counts: %w", err) @@ -531,7 +531,7 @@ type FileError struct { func (st *Store) FileErrors(ctx context.Context, after string, limit int) ([]FileError, error) { rows, err := st.pool.Query(ctx, ` SELECT id, source_id, rel_path, scan_error FROM media_files - WHERE scan_error IS NOT NULL AND rel_path > $1 ORDER BY rel_path LIMIT $2`, after, limit) + WHERE scan_error IS NOT NULL AND missing_since IS NULL AND rel_path > $1 ORDER BY rel_path LIMIT $2`, after, limit) if err != nil { return nil, fmt.Errorf("library: file errors: %w", err) } @@ -624,7 +624,38 @@ func (st *Store) AnalysisCounts(ctx context.Context) (AnalysisCounts, error) { return c, nil } -// RetryFailedAnalysis возвращает неудавшиеся задачи анализа в очередь с нуля попыток. +// AnalysisError is a failed analysis job with a known file path, when available. +type AnalysisError struct { + TrackID uuid.UUID + Title string + SourceID *uuid.UUID + SourceName *string + RelPath *string + Error string +} + +// AnalysisErrors includes failed jobs even when their file is no longer available. +func (st *Store) AnalysisErrors(ctx context.Context, after uuid.UUID, limit int) ([]AnalysisError, error) { + rows, err := st.pool.Query(ctx, ` + SELECT t.id, t.title, f.source_id, s.name, f.rel_path, COALESCE(a.error, '') + FROM analysis_jobs a JOIN tracks t ON t.id = a.track_id + LEFT JOIN LATERAL ( + SELECT mf.source_id, mf.rel_path FROM media_files mf WHERE mf.track_id = t.id + ORDER BY (mf.id = t.primary_file_id) DESC NULLS LAST, (mf.missing_since IS NULL) DESC, mf.id + LIMIT 1 + ) f ON true + LEFT JOIN library_sources s ON s.id = f.source_id + WHERE a.status = 'failed' AND t.id > $1 ORDER BY t.id LIMIT $2`, after, limit) + if err != nil { + return nil, fmt.Errorf("library: analysis errors: %w", err) + } + return pgx.CollectRows(rows, func(r pgx.CollectableRow) (AnalysisError, error) { + var e AnalysisError + return e, r.Scan(&e.TrackID, &e.Title, &e.SourceID, &e.SourceName, &e.RelPath, &e.Error) + }) +} + +// RetryFailedAnalysis returns failed jobs to the queue with attempts reset. func (st *Store) RetryFailedAnalysis(ctx context.Context) (int, error) { tag, err := st.pool.Exec(ctx, ` UPDATE analysis_jobs SET status = 'queued', attempts = 0, error = NULL, updated_at = now() diff --git a/services/api/internal/library/templates.go b/services/api/internal/library/templates.go index 4093393..ffcca3c 100644 --- a/services/api/internal/library/templates.go +++ b/services/api/internal/library/templates.go @@ -116,7 +116,7 @@ func (s *TemplateService) Preview(ctx context.Context, id uuid.UUID, patterns [] return nil, 0, err } if len(paths) == 0 { - found, err := walk(root, func(string) bool { return false }) + found, err := walk(root, func(string) bool { return false }, nil) if err != nil { return nil, 0, apperr.New(http.StatusConflict, CodeSourceOffline, err.Error()) }