- {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())
}