fix(library): expose scan activity and failed analysis file details
ci / api / api (push) Successful in 55s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m6s
ci / db / db (push) Successful in 52s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m38s
release / android (APK в релиз) (push) Successful in 3m50s
ci / ts / ts (packages + web) (push) Successful in 3m20s
release / release (версия и заметки) (push) Successful in 5s
release / desktop (Linux и Windows в релиз) (push) Successful in 44m45s
release / prune (старые файлы и прогоны) (push) Successful in 7s

This commit is contained in:
mrleo1nid committed 2026-10-22 15:25:48 +03:00
1 parent 17b6dd2041
commit 0dc0384c78
20 files changed
+1105 -317

No files matched your search

+60
View File
@@ -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]
@@ -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(<I18nextProvider i18n={i18n}><SessionProvider session={session}><QueryClientProvider client={client}>
<AnalysisErrorsDialog onClose={close} />
</QueryClientProvider></SessionProvider></I18nextProvider>);
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();
});
@@ -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 <Dialog title={t('library.analysis_errors_title')} onClose={onClose}
footer={<Button onClick={onClose}>{t('common:form.close')}</Button>}>
<p>{t('library.analysis_errors_hint')}</p>
{errors.isPending && <p role="status">{t('common:loading')}</p>}
<FormError>{errors.error ? errorText(errors.error) : null}</FormError>
{errors.isSuccess && items.length === 0 && <p>{t('library.analysis_errors_empty')}</p>}
{items.length > 0 && <ul className={s.analysisErrors}>
{items.map((item) => <li key={item.track_id}>
<strong>{item.title}</strong>
{item.source_name && <span className={s.scanSummary}>{t('library.analysis_error_source', { name: item.source_name })}</span>}
<code className={s.analysisPath}>{item.rel_path ?? t('library.analysis_error_no_file')}</code>
<span className={s.scanError}>{item.error || t('library.analysis_error_unknown')}</span>
</li>)}
</ul>}
{errors.hasNextPage && <Button busy={errors.isFetchingNextPage} onClick={() => void errors.fetchNextPage()}>{t('library.analysis_errors_more')}</Button>}
</Dialog>;
}
+12 -2
View File
@@ -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 && <span className={l.errorDetail}>{d.analysis.top_error}</span>}
</span>
{admin && (
<div className={l.analysisActions}>
<Button onClick={() => setShowAnalysisErrors(true)}>{t('library.analysis_show_errors')}</Button>
<Button variant="secondary" icon={refreshIcon} busy={retry.isPending} onClick={() => retry.mutate()}>
{t('library.analysis_retry')}
</Button>
</div>
)}
</div>
)}
{d.analysis.analyzer_ver && <span className={l.scanSummary}>{t('library.analyzer', { ver: d.analysis.analyzer_ver })}</span>}
{showAnalysisErrors && <AnalysisErrorsDialog onClose={() => setShowAnalysisErrors(false)} />}
</section>
)}
{d && (
@@ -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<typeof scan> = {}) {
const i18n = await createI18n('ru');
render(<I18nextProvider i18n={i18n}><ScanLine scan={{ ...scan, ...overrides }} lastScanAt={null} /></I18nextProvider>);
}
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();
});
+11 -3
View File
@@ -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 (
<div className={s.scan}>
<div className={s.scanText} id={id}>
{scan.found === 0 ? t('sources.scan_walking') : t('sources.scanning', { processed: scan.processed, total: scan.to_process })}
{state}
</div>
<div className={s.bar} role="progressbar" aria-labelledby={id} aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100}>
<span style={{ width: `${pct}%` }} />
{phase === 'walking' && scan.current_dir && <div className={s.scanPath} title={scan.current_dir}>{t('sources.scan_current_dir', { path: scan.current_dir })}</div>}
<div className={cx(s.bar, !determinate && s.indeterminate)} role="progressbar" aria-labelledby={id} aria-valuenow={determinate ? pct : undefined} aria-valuemin={0} aria-valuemax={100}>
<span style={determinate ? { width: `${pct}%` } : undefined} />
</div>
</div>
);
@@ -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;
}
+13
View File
@@ -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.*` в папке альбома → плейсхолдер,
@@ -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?: {
+14 -2
View File
@@ -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",
+14 -2
View File
@@ -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": "Приём загрузок и шаблон пути",
+457 -294
View File
@@ -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"`
@@ -858,6 +884,9 @@ type Role string
// ScanProgress defines model for ScanProgress.
type ScanProgress struct {
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"`
// 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(), &params.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(), &params.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{
"H4sIAAAAAAAC/+y9/24cx5U/+iqN2e8FyOyIpGTLSSQsLmT5564VM6J8fS8i3ZnmTJHsqKd73N1DiTEE",
"iGJs2ZeKuPZmd4Nkbcebu9gL5J8RxbFGFH8AfoLuV8iTXNQ5VdVV3VXdPWQPZQMLBLE409NVderUqfPz",
"cz5udPxe3/eIF4WNSx83+nZg90hEAvjr6iAI/YD+q0vCTuD0I8f3Gpca8VfxKD5O7sdH8bPkfjxMHsaH",
"yXb83Ir3k63kfrIZHyX3rXgcP7PaHrkbtTrworYFPxrFe8l2vJdsJZ/Ho/i5lWwmD+hb4sN4nHyabDea",
"DYeO8tGABBuNZsOze6RxqYHvaDQbYWeN9Gw6q2ijT78Jo8DxVhv37jUbb3qRE22826Xfwkv6drSWvsPp",
"NpqNgHw0cALSbVyKggGR37fiBz07alxqDAbwZP797zk9JxIvz8zQhS/lF3bJij1wo8aliwvNRs++6/QG",
"vcalCwv0L8fDv86LcRwvIqskgIGW/EHQIdNbyAd917e7S5EdDcK3HDcium3+NnkQD5MHyVaymWxbycN4",
"BNv3zIqfxcP4ONmB7x43mg1yt+/6XdK4tGK7IdFvYAiDKfRxItIDTvtfAVlpXGr83XzKjvP4WDgvT7Vx",
"TyzGDgJ7g/4dRhsu/YAumv79QUiCaRHuHv1x2Pe9kMC8FwN/2SU9+s+O70XEA+aw+33X6diUjPN9fOLv",
"fx1Smn4sjVW0Zv5eGDGzK18nn8Xj+Em8Hw+tmetvXbV+/urFn87CrrKf07df6fYcj5ICjnXg90kQOTjp",
"TkDsiHRbdqQsuWtH5Fzk9Eh+3c1G1wn7rr3RQjJ+3PAGrmsvU7ojCXM/cLoV6NlsOGFr2fU7t0lXOtDL",
"vu8S26Pfu3YYtVx/1fGKpls6G9fv2C7RyIxmozcIo1ZnzfZWSasTkC7xIsd2Q/105If7dhje8QPDxAOy",
"7pA7YWsA3BtqTtd/xEfxd/EoeWCBDN1MNuMDOG7343G8C/96ZMVH4tjt0T8P46EV78VH8ZN4GO/GL+IR",
"FZvxaM6iQtlKNuGjvWQLXgwiGOT0i3hs0f8diEHjJ8k2DJFs0deOks14nz6YPIr343HyiRW/SB7He1xE",
"UwH9LNlK91Beq4+0LeLo6/SZe83GICQBZ6K8WEpP5q/wqIrHMyzIBs3TWey1wluGnTPvflM+JVk2vCWI",
"4C//mnQiui5x3hbtVZI/c0LSVRJ56eHVyDvpRq1wErNEheHVl2iX4y4Pevll2EHkhFHp9OGp62SFvqnj",
"r5Og1fFdP1AJkDuJ2YXiLweBW0ngdAcBSNyW8nZxp1aWSJETGSRFFNid29U38QZ9XLeuDWIX7ZysBuTO",
"A86OvUMmUYbQTb5VKmHEIox7Tnctf2Oc0R6eeovOgrRG0i0Nej072PhRnZp6DkWr4w9Q9ckfuzPmdnk+",
"ZTv1HtuVU4lqeedz26GVvtppeba7ETohU3PzLES//w0JWuskqCYOfY/oN2TFdlxF3ZK++2hABqbvgoHn",
"0Zdrv4z8fosEgdZa/DYegmazQ00Iqug8gD/iI0mRBQ1jFO9RHYUqI1vxHlVw6BPJJ8lmsmP97f7vrfg4",
"Por3QFMZxs/o/zet5GHyID6irx6DBfkgeVSuEWb2hS07XSOjnyBWU90Aeb363ewGvtO9TlxihxptoOvf",
"8ai6wo9pXil1vFY46Pf9IMLtyJD0S6oTJpvxGOk1pjqdRbXCZAdss8NkG/6MD+OjeBepQqnYaGr2zvMj",
"ErZ6unH+G2jL3nIUP42PrJlrdnCbLkBolyOqXdJhZ3WiI1yzL1x8TbvK0PkNUcSP40Wvvaqd4zoJQgct",
"KHLX7vXB4luYe23uvG5M9nSrAzbpx2Wyhr8880OJMmyuYjVNdQuzG6bliX7/quswC5F4VLv6VcNGRmk0",
"G3ccr+vfAf3V8QZ36Svtji8Li3R54lWvDxy3ewL2WnFcYlDCp7BhxWcvJb6YlaC2spBCor7luJpz1hEE",
"LxThYmfuNU9JO9/tonyudneo+6jzbZztXjB6nW4rQtM+hJMTBrZVZwNJIst0+ou+q1My8MVpiSI0v8x1",
"TrWGIq2pqvukZ68SI6sa2bREadOpYowb5IlXULaESpvXsqotcAJXATyqncWg60RXOpEfnHgaJ/VaGKfz",
"phdpLYVOlLvl6Mvm0Buhm5rNF2a77vsrjUu/KjlaKTHu3cr6FqnWBIoWVS1AURtRzY3e9C/iscW0shfJ",
"F/Eh9/gfM5/RM1ARhvCTFxoV7F7zRH7HivvTtzeocAIydLsOXY7tLkqkRS3QoBWmexPZwSqJWvpRS3Vu",
"9mv8fGLPDGiZsDFNzgfqK+XppUtW6Grkt1q8UinjvjS31CBau+HfJp7OROp0SBi2Ivq1VuqRu30nIGHL",
"8bSxjvvxUbxvxd+BLnsYjy184TmqAsf74Gil5sZmPIr3k634MN6DWJXGUCIrAQnX0plkhvp9/CLZsdp4",
"b8DWXrKYIti24nHmmy4Jb0d+v601y+kIgt+4Uvk6sQMSaDXHAQsIFG3zNZLbHoW0yrAKVdn7dRv3uu9H",
"YRTY/aupk/U6+WhAdLejR+7ovOvSrUDutKqLZOXppvp23Vyvgjt4kT1inGVnEAR0m0pnWvCAZqbFUwPO",
"uJHZ8DtkGSxUbkswltFywFUQFh+EJDAuLBvr6dl33yPearTWuPTaq5pXloc6vuXhAyveTbbpJYLBjSHc",
"KOPkAUYUnyb3ky24SfbnLCk+Al/yH8FxxCjFkQWfH1DL/wwDExIvwQDajUKZrA/ARaTX9wM72FBYI0Oz",
"b0DqUKpt04uVEgL8IPERJVl8aCFNGyc85VKIQbM8+tb8JHULfXOd2i921FnLr5OsT6T1L7r2BrwOPCD2",
"3XfxRxcXFkrcamyc4uldJyGE4nXXRj8yOb26AwzlEkNQISB0JK2P5luqMyWfQVTtwGJur/vJdjyyZm4O",
"FhZeIdYrC1a8Fx/Go/j5rFCyNsEDBrfQbjyC31Fd68CC0OAI39codaCKdSmLkGasI9dbDnG7b3I/XjYI",
"0CUGi5i4FYQbPtbE9+gGf5t4ziC84nl+ZHM1OLNXg2iNOcTzswjs1R5RDBrpoiR39V+YTKd1Ejgrjj4q",
"nV0XH5gNgy+VXtEU8zav+mqaPZB1/HJyVD9IOUJq1DV0mLeKDUiFozXfuxuB09FvR+h7qyVvDyHHxfy1",
"cIUX85XIK2GzUV6cm4hm5eo6mwrJzTu2FNlRWLpfeYnR8b0VZ3UQmDIeOr7vdv07XmvgRY5b3VICb7Qp",
"9EnXavgOQtvCdV++xdJPe1SumiRn3x6EpjX2idc1hhLycQbpl5Ef2a7+dwOvYD5ZR1e6CWKmsvcfh0kn",
"mq5VYjN5qwWN5XmITVGonNtiHY+9Q2w3WjNFg9KjwVVA/7ZG29MfFO1wThj5wQa9cU/sI6E644TWPfiP",
"KsbRdeZyOiR/V8Hi6jCBZTq9LBv4HT+M3nACffCy60waL4b8uFIhy7Lo4PW6Sb3nLFNd0aA6mMXLiuOS",
"VkUGC4jbMsxWyPxKr8opJjgH+SXSaOwUly67Dv5SyPiyGIxNwhiKdpcHphwbm4WxS60PNdwt9BHDa2GH",
"ii44+oDpknLC0PFWWwWP4LZX3yVMzsW5m0RBmiyUH++OvV5qln5or5NrJIKrJqfaM1o1+V6I4TglsstW",
"SZiuWNoxNi0tQ/irjmf2g6TeqrJFSe4L0C7XnQ4RboZcjvkQ0sXH8UE8Su5b3//16lrg94g1b32IsdLv",
"XzSt7/+66NwlrvWz71/AJS08Feclq1H2Exf4ak7kAZByCWVC6Mh4jfywc3H/J0n2B5Mk67T6AVkpT413",
"FuGxs82rFbM7XVat7oi87y37dsBtA10WnSqlp5KmmM23j5/EL4AjMFEJspkwFyZ+gsyQfMY/GAJv0I8x",
"bpZsYoQMU5MgNrZTJZR0uvDoaSK88i8nzILMUXXNdzqlTJ/u+VV8/l6zsUq8gBRtdU0L16y5ysLSV5R6",
"h8S/m9K1zZZXfAKuCuplzsEqaa0G/qCv4dX/xOBU8jlm2OH9+YTlZ3EGfEQFJP16VxRwgZMRP0Qn/HF8",
"HA/nrPhbkKu7cAMnn4gyrQcWZfd4D/35T+Ij+o0FIeLkIaSbCac1zuIo+QxE7mdUDrMfJ9tzVLlmFuzA",
"65Kgdf6VRrNx/pXW+Z83mo0LC60L9L+vLLReof99daH1Kv3vxYXWRfrf1xZafRct2tIDJajW4gmpGdL9",
"C67nOD7i9wO4YeFUK4vMUu0ofi5RDRcVELv7vudumJJdUy9UV+Xy0hMv+8XzjLlKPJaAxMnaQ5m9Qtg/",
"/GgNQ3SlFNOcwVL9Kp3dK2VOe4kAhQdi0Y7WFu0oIoHGIeyELeLRVRgVjFVyNz/zhTJTCX/XlN9vmBud",
"l94U7+OXE8Q/pJWW7HTeOsehCia5iDf6aS1U9hqT2TOJE47bo+lvyqdvtEKmSe1mI2TJOKcp6ay+YSIO",
"ptFwvIha76gelB5H/ng2QwAsPWY9ysnzIbGDzhpc9a5LeA7KGjq9Giw9XBtWXgn8XknljSpvP/jg3TfW",
"f0pV9wdWvA831Cg+TB7EQ8z03qeadfKQaud7kIVEBfMD9swhFdLJg3hMJVuZxPQ7EKmfzMbyV1ZcR8mg",
"l93X6HY0Lbfvh05hJRKqG1VLLzJ7B7p6GNlBxBygLeKB7+q204ctJLe5TxsuonAAK3Sd26C7e+wfXScU",
"H/F/6zaWskorJCEkP57Eu5Y+Ii1c3RXTIVi2O7evIgubvNJ5I9ZeJm6Bzv9xTUs0TngpsiNiPLnVE/Wy",
"BLh3S5NSx1woOOnSG72UMfF8T6SPZCUlvKLleF28d3UR8z6xo1aPRZNFAGNlhXKFh3LJ1fJiuDZYWXEN",
"h7LoUJVSZtDvlrlhSt+x7rsDxSjxBr1ljeyXDoG8IZz6KgmFDG+ky1dpKAaWuUFZ0a0ybn3X6w+iQpad",
"iFEzbFnulFPZUlyjC816WLQwlSTHscXD/yC5N+U8oZCcb+oWUgdHmrjJZUpwdelGf5FWzuVinGt2wFko",
"l0A5ip+DArCbbCWPmbWbbCabyXb8It6Ph5eZmRbvgqH2uZQ/peaP7cYvIIcM7OLk02RLhrmY75H5Ppto",
"qFE1shtjsPrqriDW7Z5Ok7wl7YzhiGcSPKSjemFBe1Y1OYHnL6DuK/42Lfw0xq4yHaQD1VycjwaEPQE3",
"ot69o6Z3pHMpYuU6akPzTH7S8lDxJnooNPkA0lmZTDsTvywc1VTYfKLoRUlOUaaOv0J502l9pdSyzBQM",
"5ZT1glKdCrpD1eqVLKuKmWXdsmpNvwJVUeHip3v6ATw2ucBGQZIX1zIRJzCExc8M4kvyNpizCsoVXzta",
"a0H+YWguETGHCVLqMbs91RZyQcsjFq5MNqEg5phVMaMjMfksHrJoAhQTiHobqDfgmb3mK19RRAqSIQKR",
"61p6vVwjkQ3ayW2n3ze50SJ79VSEy226SK1QCZpOQ90zNgOxsKKUDAmSSZ+/moswDsErLSqa9tERnFan",
"j614D0o2nPM/85oQXlQi0217EK3NOd667TpdOd7V1su/yHZcfcJ3spNsgtf5EN3kOCzwy1PKTtYM/VZ8",
"fsxTxOOhqRDr8WxxrqARW2oMrhmuGVHaJJ+wQSDaySDA7oO7fNhoVrsRpfxibSpSYPfCsiKu3I7nEkVl",
"6c2MfuG2a9jL/iC6tOza3u1Gs2pqqTFn+TqxOxHp3uCJbKe/H0+RE6fcA+Z8OHnOdSQtKTQ4ddJSQY6n",
"wX/MkzXL8ppgnry8khluGW+Yzm7jPzOo0IH01lIqQTJ2ThqyLwxz7hozsTprhJkTJsGsHu+2f7vNywza",
"K7bjtq0ZlGQMPQN9rxkLicsfSJzAaOCYnvnZUu2uEf+Z2lHJFhhXgCXIooEYUUQxQyXYd5CuwaQKvp9K",
"19TLW3DopRRYOv5qYHeVUELp2UYq6qkPlXzGoENJpR+DAbHiMa/iA7t0FD+hkpQlrMTD5IvkgZVWGVKS",
"d3z/tkOqOR6vs8QSgSnR7UEtXs/vksDGetJMXV66X0sd21sM/NWAhLpUv27XlN5dXfkqztvznHDtlI63",
"FX/gGWa5MnBdQ5IS5scZcgb9dWNae+B3SBhWB8mRxgSv/aTXgd9iQxZaHxXEZZrdDlRRpqPuBCepMrq8",
"9CbjjHR4TrOUsFLyu1lZo+xnPF1883J6EpwczDPgGWC7VDZZyW/jYfw8fpFsNy2qE1FlSZVmgGQDNViQ",
"6xU/ozpcfCgq2kea3C3doVuCSBnWlBXmyNYAItXUpj5VhhLL5zoFAVSAtRCgNU/hf09VO5D/e5Dc8BQk",
"NCY+QMRuGO9RidW0IC8OkueOKY3HyWbyCRgzlNoHmDpHrxiWTQEq7CGo3S9AxD+qcspPpEXoddxPQcPe",
"pxxyFD8BGOKnDEIKFjKUrbc9po4/xxNQNRG6KJ3ZnBtcOdFXm6U0De9iUa4xI3Gepcp1sSUSrJPgXW/F",
"1xyevtPS4j2ta8GelnlpeUtOy9IKjV208HaRPaXqYUS7AN5+wPN8RsnnyReMW1EtGSX3k8+oqBAhah61",
"VnJGJbMMhVMGtcyQINpzvBar4W5lYWpyNivMOT5g+YZoubIfXfW7xGKqxzlmqGL+IjBystMEG5ct9Igu",
"DyZIOT25D/gHPHVLKHyj5D6U+rOdOL+w8POfa6HEmI8t3bO+0x+c6w1Cp6MFB+OQWS3MLg2VH9M7CzLE",
"KAtVT+aUeMcEmwbhgnRt8dCaWSK9/4MEs/IyGwtz5+cWSq1E5rpLsYJkBtatsWCvteysP0BhqK2RPXE6",
"vmqsZikHyuoBZA0y4T6UIyKFOgxDStDWSI8gpxu5chw/U1nzOd0lcEHEQ6oYSz4H7RHKVBTUlhhvh1Fr",
"EJ7axytPTy0X0MEN8wFT+hXwQR3hCs5SJw9TYGGMqca+KM//GxBTXKaxtHxM+rnPc/TlNH/EPdSn+Vsz",
"v3hrCSXyITUfQXsR8AlDasrCF0fxgZDy/HXAYXAbPJrVstgUQYxg18OOXQPUuXANa0Al9kCtOWb6D8+m",
"UrKDj6z33n39+pXr/1frnfeXbjSp0b4FpvgR3nyIS/AIbHeqg6OHoJIWZwrCeCthi9ylgrLSyaWP+/1s",
"mbX6fQg6RqXXUapXj4UoRrM2Lwd2kd6LwbrttnqON4h0Dpn4n+NdZESgI+jIoCeMUfRRheFQChNkPDOU",
"saH3BjZhKA8apE6TquVt6a9aPRKGzFtYAQlrtdUPHD9woo1SPdReXeSPSh7b8hnyi+tO4ESkFZFe32Uh",
"rQyV/7806oKs/ADPO5iCySfCbEQXe9fvhPNMUs31un/301k1UDMSeDRFoZoKsT4m+SVWVY6ByuTKwc4Q",
"2MRvzZzgzRGrKbXlULc5I48yNxQcGPMVYPCVau4BTcsHRYBVykDQpyooEuWUEkQrMbQnXAQ/L1Y5kS/5",
"oEhEvFieFS/zrXnva1FFUJE4rSayqMclKldHvkW3EMbVUOVAG4mlJTMd5RdvLZ1Lq2IA7+dzqn8EdwA1",
"LnmIl2yqcgz1SoVUw6BOBNrpoOChNzUffYw1iZqRBUbQmCEOMTG3w9WfQ9CewLEOBSuHyfZs06LsKSFs",
"ZwfaZAjQQ+3sf+QHVqX4gonaEMgvu6sbNZ/xWq+2y1k9LtmKn6DPI3mEtbHMHEZ/AgRhgGvjfQQEhV+1",
"X71wwYJYN6SgzPHZtVgEvH3TSzatNgZz5wJih77XnrNAxU8DP8z3hh4XKdbDblgBZS7XrPHANzQY48zP",
"nBq4bSOqyx/nkdOsNlDyHJ/rPAR92nM3PbVq3CAEDeJlKReDkvBi/IEXpRgwGGPMVhJJcZgspMBJUVjq",
"MSLM4OB1K8pno5EWaGGV9Z8SlecGCSMjxtyg6/gpAkXustlXIA0RFDE+UtRSyDp5zrzRRyxuqx4nIfDR",
"Gh6rsIH0/JxfWFiY1cooiZT5WpjbekUNhdMkZgH+At+XIcJfEFoY4wHof3+WAkWm2MNMGrfnwLHIT3UY",
"tendJ5MRRUXmqr9sYZybCm9Q6mfNp8FU0Qtx5pRL5L0t4I5MFY+3IlR6vTyIAmL3fjmwXXZpiOjuIPIb",
"zYYfOKuOB2kPa87qGsyo60A1l+vf0b5SvlqkF0b2athacQIoAGNJV/QP7Ss4JuSiBOtQBduy+DhWhJo0",
"JNnYvFVVaYSNBcRqjabZ67bDuCfH0/8KWtYjK6tw0buVnW1rRqPFZZQwOPHN9Cd4FR4wbMiRXqXsOmGn",
"5flVOu9U6J21QuxoEHB6VxH9sFlv8Z/locbjf+ZijoUonoHgoorrTFqEbTF5xhuM4C3P0t+G3F+YfIqN",
"UHYRaSHZBn8eVYDugyw8ZLIQFYX4KUjOA/A08/yS3MWkqX4uDTxUxrFKM4aqETPNHdLfoVRWGNAruMcP",
"UnbiQ1blL9dKMH4S1441c/6ClTycvSz7ndCqyJsPoyo+kPL+UdUYtahFlJoRnYlaphXm6cGQhpZPsUJO",
"aa+kM2CUTW9Jp0SVUcv9XsECeVkONQRJsLpR6VHXH3Q9EoYtd4BAMSW/yBCPzkiMl32bcYXvdkOdZhrW",
"WJCV2+SC6dThaqgWBjd7GtIsavPNlEejgDYaaV+4/APmrygDVxPrJ5FhJSe12sAn7Pkm6tAV8qRnmJ91",
"cZZZXzicGaOMbo84TlJ+h3rLjooZrIR6/DBcsbukFSq1da81S4oE1/weaVG5se7oEzGz4N6oJYAYfgqx",
"zl2OKwLZgoA6wiFILNbgawecEDt4W1L1+AC034fsnnsMPh29bgDz6/l+t/apHSIqCuYVsPv2BBPs+cuO",
"S1pQZ6+gmQAEUR6qTjJcqexxWWskTQYepG21Or7r2v2QaI2xXUwAgZTYXTmFg16h+5AQtQ80OI73ky3A",
"634BdyFQ5QlUDia/pVRMtuilayYAePf0FGAT5eWYWryaPaof7WJQ6JhuBb2kpX1J52ycQYd4EUt64BSG",
"T+iNQFbo2Quc1TW9MRCtkZ5i03Tt4DYgDdBfNBvhRhiRnhlUYN0OHFtth+YHy/CCjwaO3k1yhyxruGIZ",
"pIATurZnSPzNSwTIW7xGKjdByMWrt0QjQ8Hx+zyBehM8WyyqCvZrfAC4X2WtFAog+CbGhDMve6LuD2ez",
"8LI+4OVIgV/KjSUwxx0kpfBwVm83gSmhO0XtJpri5B2keYMjC+QBPX9yq8UaGlJoysxYLhvm5o/jw4mX",
"kM15m2RJxUeLbhH0XGBdMDT6MP22KgKK8JuZYP8wkcYACFLQoeGPvNLrgSijolzMY79Y9b4ycN2m+Ixu",
"SuujgR/ZmS8i32+5drBK0o9E2wXp516aDcbWXcV6Og04QDQIK3cDxmm2CkCZJ/OzpjuTe3c6MWmLpKWW",
"w/BlDRrOUpxhbhXzpogN6gUc5ohjUd0OC0rwQ/U4eSClms9ZCIy4bIfktVfRaIaImZTLdJRiSCZb9LVD",
"iA2O5/RJxRVCs7KJUPqoPMH8kv9x8c23m9biL97mcvNDsrzYZGtOES5H1s+oiPnCkt0ze6whCRWr6I9m",
"bpYqrDmBmVC0k2ZkYr/HO4KU4b9NKGXKY5n5oOeFixdLQM1zieqa3CzWdS/ZYnUPIkANd/LnoHUzBZ2n",
"eY1yyMgLU+rTW3D81W6ibDDdEc04B3qOx/48XyIAyk69vkUstmYpVapAYwIHMMoZibVK2Xyiy27ZiVpd",
"0keGKLe0l50oYJHh8ofZfsgsVzr5jt8lnWpPngQMgl+RLZesI24XV+pFp+UeieyuHdlYpLRKgn7geFGj",
"SSdUBccyHcNfaZ3qKs24ycsJzkeoLxWZISW20EOiRcf5Kg0UMmNVytGMR6DifP/X+Akow3A7ZXNoj6x4",
"nyr3W1SeKDg7IyznkVCOky1AXp+YlmIhHAb0hCtJoyLpKsY85lm8Ch048+MTrYVzaGWHPkokxF7QufT5",
"G1uk60Qmm4jHAFvGaH3fD0Nn2SUtcQb0b/oI44ytQR+qZ7XAwgDU+B0U1GEwADSEI3B+JA8FxAVs0kNO",
"cI7m+F28h1Wu9KL6TCpEEZVjuzzcBfU1tuv6d1rZaWktKtIhzjrptpY3WD5PBcQYtCZPWXaKmKCt6uKX",
"3n0TTbNacgTyUi45AhNvqmU/nzaAxHEgKCt8pgbbxvEzK4zsVcdbxYCStOkiwWFoJVvAW4Cx/6Bar93V",
"U2K5sGa6BQrYN6mWZYjSWmqCljWTSRQ+ZqEyqhqP4xdUKjWtTMl9vGspWzY7Qavhwq43k5iG+eMuewx2",
"0UMAQlPkV0mid5e3N3wALs8Rs3cxE6oO8MUK6TyS/aeqaLz9npzro4rOvGakHNackBFXBFePUj1MFQuy",
"OpcNUTJEG0m7yQp93SWp0QBMGk5eu9JeCHnpr+Gu/GnJyhlVqJoxqWS7X5I7xWp7HSglkhHwshorSbe+",
"OVZYbvfTJycw/id4dMLQYiVLWAQUS0c/6wBjcXxxkvAi7Ksh2VxWDquqhNXke2Vf7GJAIN/VjLRQ4vs4",
"iVMqbx9O5EApTUmqw19hDncXeDImdmGcymuR7p0+tfR0znG9gZALiXIsBFDalGjCATqesBoKETQyZhlD",
"SqOqVFOKPnBTYAzqnk6v5+sVmY+ahsYAQsAc7/ogpWTvZ5eF0EP7dHZKb2UZ/eGFNQONop7QJYu4ElXr",
"PgF37pj5hbNEmJ3cjixgOQF7pyL1ZrduYp95GfP9cuDr7irX6TnRRJZMz76Lnv9JftQnAfTtb5WNV35f",
"QC31JGPDuBP+KkNcedbKDAwryw+ao5t5p64Xd8foko5A5xTnqd8P/HUi4i/6vg4YrjldgleBDxcy7EJt",
"gsM3qpXEKnIAOjNf87ELfSIgtCvMlQpub30nUEgT5BQrp7leMOe1UwN6aUX8VQZrqXUXVXQe6hP5dcYU",
"5LibYZtOmCrHPBUkihxvVatm6Lw+lZxRiFMnPFEHWT/UCBqJgA8K2EnqJnjE3gHYASluFAIfbWH3LAPa",
"BGBKntIIf9miEaPck4r0zJ7nX2IUcpn16sjYNHBCOU+Zgrp/gmDsmO++ASx1BwpNDJ4eTfaUAuL3mDud",
"AYosfko/lP1Bu8l2pqJwPltNHh/N6gLChkNRkSFVSqTzYYlq2dUOTas9mZO/lLeLsKLNfG5aVJ7IUDpQ",
"Yevl5dV9eIqyRbONofSHJr+zBWchV5Uoe+MYiB/7A8sVW6w1WJPrBF3oZ9/3gwifw39LH7dWbMfNqeKs",
"AFTN6LjjRGvdwL7jAbJSPwNolPIKD7GetPDxpA1QdYT80F4nr5tqxyGtd6OSY33Fdt1l5tTQIKPuI1p4",
"fIiJaxji19ZY8NrsISspBwvlCTu64D1HF/znCGQIBYIvwALbj8dN7FL7BMwwhD7MR77isSkV16/WXGii",
"BkpTAquT5tBMd4otQm6SzffFtPnGrlMTbX9F2t0zTOItQrrL2pI3fxB1fDX5ltLMJeiBZp3Abjv9FrED",
"dyNt/1WEczxBVzJz/xg+MxNheUtzHabdiLV13s/Ucj2Lh9ZPLdaPMw/DuLwB7Xj0YYwRqolooqdvP8be",
"LwwNjvd8RqSxsQqsPrN+3pq3rry3ZM1b7MB9x5Odks/pC2dZ7quoNhP47dRWwWN4WBUhHYhElyOav+dN",
"JtjUFuxvSfRRKhdybqexypx2hJKC1xlb5xcWZI/IrvkulAcII+K1orXAH6yuVZ8YQDYY0HTZea4Clcle",
"o59HnmQyQaRxmik3GTlY3py8VXfarZkanWFVLSM6hnEXMnROX9M8Dc1N1P2FVuquSJKw8ikS4rO8l6p2",
"Ku+LHjUZfaSjdVf8AdERx3BiDkHMXLMjEji2ay1t9JZ9QErNEf422VABM1OY5mKhS3/YxMncKlxBaLzF",
"sp6KMooyimgkkigmOvW7shGbdKZ8FNNql6An5pnc2NBms3tiVTXb0pMHmDL9WEsh7BmAmNYa0HJ14ETk",
"BstVuLpGdKpFzwkxwjzBdsqvvcZ+r56685q+e1oYiC8hfUAg2cCV+izVdak6kMLGHGILfmrHplDeHK6Z",
"2vJ/+/y/rJ8v/G+KQx+QkSD6IIHnHhhcPIE/8LqtKHD6hgvK7pEs8pEcXarYyQLcbbyNhTSmPEBT3ppb",
"lXbX6A4+OQSY+GXpDAQjnKJvU9qySZOU9EJFMgbLBrb1afI7ACw6TrZ1/g2GYH4MCCMMXEBAxQ/j/ctW",
"m3UlaqeZPAoHHUn9mg5Zf3gY5juu+jUmwSsuyED6s8KlUuJ3mjqlRxjwusQAhM1zmo4z/aiSrRzMoSZ7",
"aeIwlpQwImYlNrbIx6ww09JgdZWE+vtYZuYTMy+I9M4gcKKNJSrWmGlB7IAEVwa4N/jXW1y6/+OHNwAV",
"CMsg2bfpbqxFUb9xj77YYSjn+Z1A5zJAeNLdvf7m0g3ryuK71qLTH1jXBqHTmbPiPyBiF32KaheIr/8A",
"oLm2JUQsBczat/vOuY7fJavEm73pYZuVtMc24Fz6feLRx+h8cWKzcxYEe6HQN03TBI4AVEy778z1uoia",
"xSLnjXSudOoSDjaHz6aiHodqXGq8Mrcw9woDmgES03fOr5+fh64l8/ag64C8WiXwH7rPkAbwbrdxqfGe",
"E0ZX4AnWK4pEJAiNoZP0kfn3nJ6DXetKHryK2Tv3bkHQte97IXLChYUF0RIX0/7tPjq+HN+b/zWLYOGN",
"WIonQ5cAKU3AHtmwNNbkAek/jYdW/F2yldxngmoouXlNw4h5pzEjQFpnPRwb8b+lL8QCHtZLFW7NGdiI",
"2abcKiPZZLbyJwhdD6mXvNHMLfp2dRdXieegB1K7jW+T6G14YimyI9bVbTqUlofR0HqRciq1XCKLeIHT",
"WevRf4aRTdUvpxMyYli+527MokqVWTqE4TVLvOp7UeC7OH4DpRAJo9f97sZEi8ursmoMV/SBF+3eo2Cj",
"XHE0NpxSH2TRxZe1PVis3JU2RLsHOfZzneXADjbmbc92N0KHnoiIdS/VbtZ1+vUV9vBp2THbIAoiBJXa",
"8/BH9btSjBnHQkjJQwmrEyKYCmR6HcLjSwYQmdafSUNAsTEHIM0pZ6hIbSlAbwzGTkZ5EoWEMAKTR5LY",
"YdtbtPNpyynjPfIePvom71H0I75P5KVUv1Z4f8ujeL8Ovvh/pc5LVbddaeF0oo1e88PoXNcp2et3/DB6",
"w9Fts0k5NsHiw9Io++/kQfHxK6pgPYJYWONSQ3TDgbhUgydOi13NSulpcgkjAkAm6RhEbQ1Qi6zIvDJb",
"wDVUugykAVyGJSP9DpVcAMc4p7HoTsQ6aXGNSUlh52qJFw1M+wDzEh7t6X2YfJE8gGwooMhYQA4n23Vs",
"1bcMC+8o2eHBiycgHl4AEukDhimNlG5aordgRaIjnM18QETjZNNVDA9gd7U8yS+Y28FBmhhUoyebiAiY",
"7PCGMVusLnpYD1NjufVm6ohSAIKkBnPpjJJtDZMiUfTkwnSWeQnuxMSlmaSqKbJpZiQdn34l4T89r0uM",
"5F6a7QSTJy2n2y3s2a3NCfoLw3N4xC1dumciWwTTwMYYPUwRt454C7tMPzt+vyU7UFLLfCiCFWS4GVjA",
"d/Eey4jm1VQZPQoun10oaUV0Bcgumms0MxzAEH3yTHAyg6P6/mOq1RlbDC+LBf8gvMtjAYh2cp7Mn3dI",
"6ylWYpbYM1OkrtS/QW9zYBXoUbyf86vWQeT/muhiT03vTJNjUBPacjcDqd+RpopcUUJUR9plq+2thOxd",
"uX5SVhj5gb1KztmrxIvmbnq5FYwUqHqUDawGdA/dxWrvzGznBdGyIaM/X1LqLZk2cCSaWaIsQ30Kddld",
"qFAYoecu46qAQj3WcGM6gkPuCVNJYJyveWgtO+s3C1HHqeyuTwMub9sxiQLLhMX8x073HrI/5P1osl9S",
"SzxFK1Kss+QTK34CZdzjXNfv5HccCyw336Z03aV5r6x7n6Lh5++rN2C2ErspG/9qATL8qK5N+Trf2yOH",
"Lz/hJjWN6plppQsvhcWnIabLKDOZZ4XJii64TEyq2x8ywV7ACd9javnQgvZ/olRLWJFFnXzy+21Stc5A",
"Ur4U1eqs2SirUtUpFudZALfQcBK0xienSFo2hlGxkpr4UAENyYnY+XKHMmqyVQe9s4NAhsUojfhM4fQO",
"Ii2qGfodQR3DDOhjiC7vMBh/XY8htp+ixZAldRgCj0GbyvBsz6HcCV4c6Ha9/mOc2/CzO8ZlvPatpI+K",
"pvdqIkAtDqV/j4fq8f7sJPxX7ZTP91kBw6WPT8WvWpfUIr77LPmGDcnTd14OB7FJaJnoz5CVxfy0wmWL",
"XeekpkW1aNDMouYQQ6xej2eGPeHd2lN+4pVNuiZhJ+c13viqBv7ShPKgu7UoUNxF/PDDeHSZB8ieonLD",
"WrDFu1b77TdvWFqXdnvOiv+CtiV/I4rUhZ9b7Mk56GrFMk01cnKpY3vT1XM6tiex9z29p7e2odIeZIZQ",
"qtRdnDmL65KBPEgkOiniYCdnxIhn9NUt6G6Q8MysFalZml6+jOJnyRYAz4Kbw8q0OxvXJVr4K8epF19t",
"vDQ+lR6q6zo4HSmSaQQJmBsIV5y2gNu3Lkp1HUOdbT9s5pJvn0BEXipwZW7vTNJjshMfYFKklJ6bFy2Q",
"mKrk9k1JxJjTYc/4PtVkXet4/g8YXTgjTpfVMVYYq2nHOWlYvoj3Q0zhnA73f5lNwNVytpTEuwtBnxF0",
"7ziMhywTAoGuxzIA/lF8oCgXirbDsPxG8S5Q7nO53+KuQuO5m178VT4PYgYBD9UO5Nx1kTtgIi0X/Ri7",
"VFjxzr6zqd3EL3nRmHXgif5WbZ0LmGXX5k/lWZwIKbVXm5bA9D9ETx8xi+W5Qtz6PLVySFc9IqK3LuRN",
"61Fln7OOGMlWvA+u+fuTXh5l8V5q1n0gerlPdo7k6vC3HDeCEoWPtYkqKWCi2M/SilD9mwCKsY4XaZEL",
"c5k0EqjJjyuHK4N1WDGDq+bcvq/VCDTruKxiQ+v1oYLYJvtqfhlLV0TYYjLmfdOLnGijzC2sAXR+1FTg",
"qgHA9Cn2/VC0pFS92ZeKK+gPdi0VJoG+QkFBmLPir9P+DtgRi3VWyTbhhSGhKw5z8D0wO5qlDgdTDehL",
"3TCmoBlNC7KzIuiQ5hzRvcIU2ReyB0yKkIn2HHXpXkOAtx5qkOKSTyymNAv+GbOTR5nosZhMsnWyYwcQ",
"b9M8dKUgySxdigXbtuUDwdwbQrrgTjwAC4O1b4KUA9aK7rGkW6n4ghMdqameppeYFoMHRcPxLDe3Tm5m",
"9rIO/lGPoXRK5pVy+E/Bwub0f6TguyDWGy99p5Qs/lQvHdWn6jKIPKlNGz89cnsf5Zo79RYi3LOk2+YU",
"HAYpn2xBfgy4LtspSHRbBoxXtZ8567rtrRLuwNmDb75Tb1+NmxNeLYTCSTnLoLGSu/1GVghoFWEzxpr+",
"xaGzWvjiE2SVD7qOP/8TlY3F/JYdz4bxsy++Z27qrQD/n5xpWUlm49KvbuUdGqLhwFApiVWGxtaXvIEB",
"aIMA+opt7RgkzEE8lNxiEzK4FHoyiBf6fWq0Te/quf4yo0YaSMxiLxfrFRJ/B+LtgO4HV8FqiXd/LcSm",
"QNuTIUTHCs9o87OLNj8kSs2JJsh6H1MFQYrvc1i9tF1jsjVntT9qW3g9MrGrfG9xtWaE1eYsrLWFpQBY",
"yA6KEjc28gIO3AUw1Zx00wmWjxSTurhlp0k4QYfLZkWmwVaXPzZb/QplAUrW6qa6FnQQ0Kxqus/zLy8p",
"1shXsObqKobAvQBJ9xSTreAw8exXOVXW4pBdz1RXqPWTn2D6OgJ+D+NnP/nJZaGzH7MXgF2+C4ja0NOY",
"59VDU5hD8LfJuQRzNz1odYq3POteKiVvYqchOZ2xTc9rW4EfhzAqwOJx4IgjSjRzhizd8CmJ73SAiYT3",
"+Zon0IUlmmrD8piZj5SE2ZqqkNgWimicHqqztAYYBLQmRVaXjCp2tmoqavJFPcv9i5JyagSlzd9M0uGd",
"zAUckqDEnP8zP/JS9NDUQRjgIvBgDsUJsvCuegFlLcoFeBA/ZVYNa1KJN/Kz1LrHgfbRFWAkCD3/33Ab",
"hcECPsWJ7UP0ClvUP1W7HSOsM74PXHGoBkDBca5ndGb6yI8iiwKYa861w6gFO6ENqqSttqem9HVPIjWm",
"cA9WdrM9N+3po6kU6UDz8abFr7Gmhi+rAVqk0oTOgkTn+nYY3vGD7uQOCfkI6i9eqbn3Lqh4B1LYC9yB",
"4lrW3LjoIFPv3MsMlBqv7E0OeWkEwX4GLp5Rch9sq8cm8/k6JQXqQYwaU2S2G6TX9wM72BCD6Zjuy0KC",
"1XNFZTrcy+8/CSsFBBuVYaZrOA2OymMNY6YtVLGxBgvCuQoRcci92WFlRpn0D8zE+y3k7AIP7kPTt6E1",
"AxIVg+PAfvHTpnX1rXMg8PdFk/knoBhDSeNs86bHK5kErFFRR0GEdUvxWektIZBiL1v5CaTPfidOR+pc",
"ozfJnySY5D24DwRwMlpgrC76Pkbgj9h8+anQSH5xLq5ntraSjsEZ7LP6Cl6yPJu2ulEZooR/3eVBL9Wp",
"TGn1VxiI4fSuHRhA632S4nzJppLpVE8sp3iAlHodO7Jdf/UEGprkr5ZJ3+/P21438J3ufNe/47m+3Z3/",
"mAFtmXfjDfboFfzplf5tgx+AAVMwE54DeBX5GFnqdeNS4/++ebP79zdvzkn/mTkXdNg/Z//3/6VxHE5m",
"na973Tm2+rm+3bkNJZdBZ81ZJ6f2WF5Z/KfavZNXFv9JBZeeifeT30GvK2Y787rOoVpvOmoaHdjcv41I",
"GODa2udRSsUy6Pczp1ZiHdfmyaMmt1Vq0EO31wMqUSFCJ4QwncvbTkRsi9WnY/csrraLGHtmxVIZKoeY",
"tlAxT8vgecVhssVj82nSzzh5fNPD9Cd0C0BPaAkl34r3cV5zVvyVGEbUaI/411zPh5OLgEN0Fyy6aUzB",
"f9Wy+/057FlZmjtFJR6S9z2k7jQlHw50nbjEDonJUpfsInpfpgudghteHi3ZgdAs5/tkx2LzPafZzR1r",
"Btz00jYcxbucs3hk55lyPkr5HFumFfhl5cZBYmActGklDwHLJ8WbYdO/RHmjabE87iGDdQDkd+tDx+v6",
"d8KmNdcly03rjWtvU5XiL7JnG1TQF2BC/hbUiINk22r7bpcEvEidpdxsAybE55i+uCnVVMzQF1k/yxCr",
"aTG4EWGYj1jOJr2apBSWeDzL0g/LT4WSh5iFjDzp+ej3r7KdmebhSEfRHYzfSw01DjmqoSBx/Ufjjynt",
"mCtDPpfJJ1nOn2HcxvhvRPcWyH5cyvapRoAH4N4kqgGnWiXNoMMfNSsGlbZICh/8sFUPvxOR6FwauD5d",
"dPQvGgnCDt409BCRTaAc5GF96kgxZwKIeQV7gXfhnZ5kwBH04UhNxmAtvijti6doIUjUZpZaMYwuPH8F",
"H5y2sbaEdDHWpEpmFZWVmu5EVF/g+iaDO0HZfcgA0ZJNHjZSjbLZms2++ud3RjwR+f1zac+lEr644fdv",
"8F5JFeLG0KtMCfoKip9fgDYA2P7s4kJxv7epRm1hQUYWlLFSwPLhKgLLMIlHvHcWC1qypgE1hW+P0eEG",
"0JLbUltL5oDSsRuDCdD7yZIdDKlScf5JQfh5quw3iNbmXX/V8eTkmKzDNh7FT6yZNut2TDniknWHLLdn",
"+ay3BEZHQFYCEq6dS61NK961Or5/2yFWu+/0B60gat/0ZtrvRFH/fc/duGwt0buRXLaW7B5ZciLyD0tR",
"4HSiy9aiHa39gzzV9myTa/wCOgnJPrLabOhW5N8mEO6h2jS7JpmfWWRuyNAjUuhhyNoLYGWu4tnnCL65",
"sJPsxUaLCKWKHDDnuJmi6eRNr/3qwnmLrmmO4SW0OgHpEi9ybBfKg79hRUvb6GVUmiQeAztuI5FBbsnQ",
"KqzI6Of4+sj3Wz3b22hRlazXj0KtCfAe8MB0gmDw7pcV/xpEazcoQ4QmbQ+27TO+h2zD6tfzvmScoE8h",
"StnwsayvDaIs9CQ7rz7Cduiz2d7D7yt6slmwKRtTgoBxM3dyLYYIXCNY55fJtnxKRLgZk8z3WbH3cyUw",
"VpVI52zXLSPUFdetRqsvTxqfG9UF2JGSSpQfwOWBfg8EOmR9AobJJ+VEYiLTLPuva8T5E1jVFzKkWI5L",
"ZiCe9GRWyFnwI+GVNhR2NPSygEjNfawXFKPwkBMLAo3FYPyeTUnO5O/BZZGTdCzlTtPfj1L3zf+T7ChV",
"UnJyA7pesKIDJPVB8oj+U/ab7upjSUAkJmb02hhSSCqVQ0qVI03XL47ZdAuhHc5MAH/Fzs0Ol4HDDMHr",
"l8R/hk3n8MN5hQXb9RgPTsdfl9OWytPkk0+ZE2kfdQKRNQ9vwqR5ufwNrH8pIRqB95spZgqc8/14eEmt",
"SQIXuVTlcWC119uYUJM23jqMh7OXb3qyt0GpH2TJRO3Q+Q2hWohwT1Ca8Zl7q453V/YA/5/nrnQ6xD13",
"nXSdgHQig5Pxqo89CWtO4l+fKNO+poR942t+Q/RW1isLC03eE+S1V5v0z/MXFhZuncjEcnr2Kpm/Q5b7",
"p/d4/RHkLzQrolxU/4HLFF5mWP1DsryoN3GkQ0fWedDAmAmryHwWnAJ3blr1xPDAtyGRYceagedZrMHp",
"znIUV7kKLk2p2IM74rt4L9nWgJb5YfTmunCf1y+04eWvv4y6tXTkgpKBbyB4xOC36zG4M220YdOwu5rB",
"mmYhmIsLCzI79V17A9tIy/yEfZlN7s5rU0VWuEaMJBQpO3WQ8D+F9jwuSupLSdUjhVjsrJ+f1R44rX5A",
"VsK2hbDIyjlJNlU8ZZ5WeBCPL2ewMZSCxGP2dnwkU/BtqhW9Rqaat3mNvCSr1cAipnTNujnHmKS5A7ry",
"flOLrT6G0AW1DX6L/coURQqYSzmB88u+H4VRYPfPSa6PAhn/n3IcFcsT2r1BGLU6a7a3SnIOFDkLcw+8",
"RCwlgeoy2NNvmCJ4H2Fqc2pxPo2P5m56abdT1gwnZwJm8qMZdvhuss31JR66KdF0tagvJHqdU+mqRKTp",
"cL1uqB+q3wYLrJjwUCTJM4FqCXZ3swLp6+s5wpKCOUzdZryf6zMhOyEzucJpJrwoGGM6TclR6jqh69wm",
"hVi6b/BnftSNtK4TuxORLkQKqtZ21bG9v1fTTqv0Xuz4rktYF7/Mfq05YeRjrbyp7uWqS+zgHfZcJf/U",
"15Jn7qgurJmxhIyo5Nk+hmiarE9n827Txqe57GE1pibTyYzSntLiR8y+bBHVqxIlktfUJkXNlTYmcZ+S",
"w0vF0Xv/I4tOuoV/qkkK+d6ybwddx1sFrWug2agle528nz42Hb0jHeDqmu9wgPsyZeNVQ1G3jCd8VFff",
"NQGWwnB00hZ4uXIG8N/x8CLc7YomwMBm+ZYds45kikS8Y6/n73ilnknfVqczCALiRS3+aBsRI3eoPiIC",
"yodNK97HAGUaPn8I6pGiTqcvmRFxTIxeZQKjLBs4DWHNpnpzqap8WVW6AWMgF82g769Hob4Ka1OKoaZQ",
"K6wM8sPUoBXrSPiHqVl5Vgrzt6qNqa/R0ui63KNzLoxYR3nTFbPInlyKpgyWqQ5kgvRXejdezvUmPYbT",
"CYX6o+Tz5AvmCpFMCXCdbiY7NWW0KMW5rA1Wpr+kmBMVajzhhaUcPoMLbQzWrtIXz+B+a+pvmMWBZp+m",
"ANsvjzFBm6uzZhLFbwbBy/y+TOdSK91/1w6jc4gYfMfxwipeVjisrhNGxRlti+KpKdPfha7S+vyypUGn",
"A/jzJySuwZwxNJ8HNAc+pSnyPH39S+nqJtZ2VqQ2MF5FgAllK8r0TPxJt25WKbrH3CknYJ/5dlWpZXC6",
"1dDZDDDFtwxXDnr3z+jw4WAv47L5wZy++XDNDkjRGbxO1v3bYkeW4PFKBt/v5epCkW07AkwiBvbNMjF4",
"tg9rHFEXPszXoBg/EwAk6YDJFkaYpYaN4D/cQzss+XT2cjZXCTXuZIv19N4XWSoZ4CRm9W9BplI6XuPl",
"HjXtRQd7eZYCDJlHW0gCpMf2sL9l5OW4VIJLRGUhMm1XsPL8x/DBu917l0WJL/ATNOlEDyjTkJ/IJTun",
"zig1Qi/xFgQsw3krz38q+zFv7EMwuA/TFAgzg1ZzIwXEhi/CeSxeKL1xwbUNXrPr7KdVk1VrzDBgL+NQ",
"C8N4H87a48qHaCJUW61vjUR5Ikwj3w9fP4EWeGauNebJFLysgHtIZTjVGDEkYcjxWYy2xrWNJf7YNFso",
"4RjV+nSnPrICr3stNVICe4u/P3XOzeiSiGeLXTGc4KUnHu93QfrG2dxIp85/r6vBGH+pMLT5oI9ZSWee",
"9KMCyqt+e20GapsTTmqyrkMHZBk5EAT6hEFSvbAQd2iU8V/HQ/rVENIE6fW2hTUpT1QMt0zkGsrsOLJN",
"/MKIazN302t3wPvfRm1H1LMqwBTJNoK6KJUBDLvgCSvVFrWccJSeMwgumNImtryiHz6FytpjoMzzyxho",
"QwowPSx9YCh1pEgbxZtrcg3pr5k4ypQkjzSKFn0L3KxjWAJUvY+n5GD6Z84nHAU3v+nPWV2UOqWmPoFs",
"pHIBY1GIb3DGTTtisbYEWJDHlJ3C+EpI7ACz3vTHiUqXf7BdNz1OMHwG3QHToeWmeBykgSNNYFe2eExp",
"wDO6odnVUwbxBYcBM7SPKAWhqusBb6hEpfcxTEF0L4G4Ih6LtkfuRq0ORD71TaRwlZWBhs3SVwIevrCw",
"UBl5GB7Tpkc3bCjDYQnS7C9WHUv/xaugWX3sLe2QP6bgMe4F5tSG1Tos1lNEpAU+xiCkrGexE6E5JXKb",
"OH35w79iYig7JxjBgCuA3hmHkHnH8YMBLQj0vT1UK5sZ0GLJQmbFCaK5oRXvgYx+ChIdBTxUNgyhQeTI",
"Yn/Q8zbEZl/J9k1v5vu/rq4tdyPv+xfW3z790vr+r+wCAdny/YtZI1Iy1h/J3VB4Dqa0Ruj82vGDgHQi",
"0m3BCSg4jay32lkcypfLztC9jekRz+rq3PNlsg3icw8iypjqLKLqVLmWuRtl9158ZL2iLdmmykSTfieX",
"KACKz8XUFjmKd0tPSbBOgnOOt+Kbjwid+DO10464D4aslaHqqoCb4ZKFlXWURQUajdSNEnOmZYinx6py",
"MgQ4R3j8gDXHUy5OFYaElzjm4EV48T3LCAb74XdwW0GbXk2P1mRn1qAXLQG93qXkmiqDilH0BtkuxEJF",
"G8ejjFJXf11M6ZDCz5QBeZL5byOMSC/Df6rjKkw9V0XBcvCbdV92qCH+Bo8h5GdvAoK96p9hHWzHss9u",
"iJ+hIs77hQh/Vyan25JbkjCw8rrat2LlUAqNri4Fb8M9TI7PN6VM/dQzBX7E2dM4eRkfnM6uljkNQJfq",
"qIVUOggJt0+2M0xBvfVYFEZa5y9YycPZy5Za5HiAyHZqvSMa5O2PAC7iZLWOlyyWnck0eN7cJt/cVgC4",
"Mf2LGQ97VGF4vz8I8XcM+Gmo1RiASuA0rL+McnAqtvihN1hqVmmnUnh/AO1/ObBdKstfTsem+u+gtBmU",
"5tRVgBTTJ36wOMAyryTTh4beZv7vcJr9M2GEd7vhWQdgK0IZcQgbMG6Y0SLZEvHwsmhHLXtCUr/c5/Wh",
"S0iTwgbKPDk12bKcLmA6HME9lQIzKEgKUq+30oLeTKTIpJRwUTfdbSraohoJO0UcJ4mc86vEcwZhKVXf",
"xsemSFsc4Sp7lYbGV+3OGula7kbgdELL9rqW7Xl+hEjsly2PrJPA6tm3Cf3SGvRRSbACkVA7PYJKTb4N",
"1Yi/R73jOK18hk6xCjorIDFFgxD1mploEFrn5xbmFppqZPadN6+8Yc1bi1duXH0HkrPR3T5GW/W1V634",
"P+IvZhE0GVWg1HTbhaqNEeiIf7v/+5tem0197qMBGZDWysB129a8JT4ehCRofTTwIxu/a1rxHs84wAnz",
"J0UX77YAnxADDy30a+6CsjQqaMl0lh2SX1JrJmkGrEuTMeVaqmlnPZnqiTLlujLxhsSsXQ4qr8y4VFvm",
"iD09smZEG7KmVa3Vnr6FcRoGVEkgMzq7ZCQ8cQBdE0BmyVaKvDxOvogPlM6i9Lf0OM1AIyHw9B1B+OA+",
"6zA0tN7vE+/K4ruz+eLxD51orRvYd6RWwxWK60SeD3opJbLBTb0LRsUR+PlrrMDLZhfBiftCIBrJgN3o",
"MMAoBItgqmGPdAdrFZLzPccjJTH3tLfmhI1T4HdLkR0NwrccN6ImwY/M0Z+2E56gF6Fmd2vpQ/gfiH6q",
"eX+yc4rj3w9IZ410bhfcl19l3f5ZkYTOjjlr6Z0r5y5cfA1MetFmXDbleZ9yZpOwLggzXb8T8gnN9bp/",
"96rm5C+yeZ5Fq1c+1ktt9ppO4gfQ7vUbxnWSvxohgbgCAsH8PR4zwgD8M/Qun5g1QdUp0oaRUL+Ex6a+",
"HziMbhv+xOE9WXD7Gc8yQAQ7cdvUsRF/ZHpbtkU3gxWU8MDEzXLyDQgJ8cxOgGt2wE/jEn2w8mVMCfKw",
"3psWO+WPpZvWLCuFhX5AKYmN3znQTVXKDDxOGy1vfgBfI3HeIB2nlqS1fkAHiRz8dccf4I8E5POCzgUn",
"ia1fsd+kAXh/+dekoxct/80guGpK2MymWnAlh8Ebyd1u9LuGLaHY1n3GkDhYQgbmDQ1PwOh37HUy7/ej",
"wtTDt0n0ob1O3u9H0049lIepkAFUV44byqox+uIzHUw4PhcPCIoka+f8zzzWQ5vlu5QVQgOx5URPQ9p5",
"ZAdA8Cld8fTVMIYeP/N8rQNJuG9F6YtSt2ZRuvvwdJB+6RZ/BS9DK+T7v1I9Mnmc9vXb+v5F9V0rzRZ9",
"0+uKrSu9C/79ZSRtin6G+VWfLk+ce8x1BUtTZmdw1N09cz21mMEz+LC1sXOm+DxVtqiuM6+RXyxPyYLK",
"iAfCOSFcOhzVDjFxsB8nD95OeDTmPboNE/cYVRlJKxV/Qe5OWyj+grHQy2OZb+rlEyiRZ5F6lQ8hN2GL",
"IZFBu0kpUYHqJAyje1vBJ0vbRujZYo3YbrT2myJF4h32yDRximAI9LsUwFV+ikLRwiBhcajx3yCOKBKN",
"juUXqJmHck5LQOzuRiE1ruMT08X56W4U0IJBBD5jKQWbaUYV8+HLXc0oV15ceOXMJvc1ZrSLEpt81rJ2",
"4iNk4FF++kV7/C9gVh5hd3e+00fxE8y54aFkKUWbpYVuWzMVyTerZRR1Th83lokdkODKIFqjU6QCEZPy",
"UJAOArdxqTEPgpK962ORV4DvvNcUnwAEuPR3jyjfQhNe6QPXWQ4oNaSPeHBK+ojlDUqfiEi6/MM0wUf6",
"FISFPIV+v3Hv1r3/PwAA//8ZFRgBKHoBAA==",
"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",
"cf/O/f8/AAD//+0IjFXOgAEA",
}
// GetSwagger returns the content of the embedded swagger specification file
@@ -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)
}
@@ -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)
}
}
+36
View File
@@ -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 := ""
+27 -8
View File
@@ -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 {
+34 -2
View File
@@ -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": [
+9
View File
@@ -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 {
+34 -3
View File
@@ -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()
+1 -1
View File
@@ -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())
}