fix: prevent duplicate chapters_idx records + add dedup endpoint
Some checks failed
Release / Test backend (push) Failing after 18s
Release / Docker / backend (push) Has been skipped
Release / Docker / runner (push) Has been skipped
Release / Check ui (push) Successful in 45s
Release / Docker / caddy (push) Successful in 38s
Release / Docker / ui (push) Successful in 2m45s
Release / Gitea Release (push) Has been skipped
Some checks failed
Release / Test backend (push) Failing after 18s
Release / Docker / backend (push) Has been skipped
Release / Docker / runner (push) Has been skipped
Release / Check ui (push) Successful in 45s
Release / Docker / caddy (push) Successful in 38s
Release / Docker / ui (push) Successful in 2m45s
Release / Gitea Release (push) Has been skipped
- Fix upsertChapterIdx race: use conflict-retry pattern (mirrors WriteMetadata)
so concurrent goroutines don't double-POST the same chapter number
- Add DeduplicateChapters to BookWriter interface and Store implementation;
keeps the latest record per (slug, number) and deletes extras
- Wire POST /api/admin/dedup-chapters/{slug} handler in server.go
This commit is contained in:
@@ -569,6 +569,30 @@ func (s *Server) handleReindex(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 0, map[string]any{"slug": slug, "indexed": count})
|
||||
}
|
||||
|
||||
// handleDedupChapters handles POST /api/admin/dedup-chapters/{slug}.
|
||||
// Removes duplicate chapters_idx records for a book, keeping the latest record
|
||||
// per chapter number. Returns the number of duplicate records deleted.
|
||||
func (s *Server) handleDedupChapters(w http.ResponseWriter, r *http.Request) {
|
||||
slug := r.PathValue("slug")
|
||||
if slug == "" {
|
||||
jsonError(w, http.StatusBadRequest, "missing slug")
|
||||
return
|
||||
}
|
||||
|
||||
deleted, err := s.deps.BookWriter.DeduplicateChapters(r.Context(), slug)
|
||||
if err != nil {
|
||||
s.deps.Log.Error("dedup-chapters failed", "slug", slug, "err", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{
|
||||
"error": err.Error(),
|
||||
"deleted": deleted,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
s.deps.Log.Info("dedup-chapters complete", "slug", slug, "deleted", deleted)
|
||||
writeJSON(w, 0, map[string]any{"slug": slug, "deleted": deleted})
|
||||
}
|
||||
|
||||
// ── Audio ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// handleAudioGenerate handles POST /api/audio/{slug}/{n}.
|
||||
|
||||
@@ -204,6 +204,9 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
|
||||
mux.HandleFunc("POST /api/admin/text-gen/description", s.handleAdminTextGenDescription)
|
||||
mux.HandleFunc("POST /api/admin/text-gen/description/apply", s.handleAdminTextGenApplyDescription)
|
||||
|
||||
// Admin data repair endpoints
|
||||
mux.HandleFunc("POST /api/admin/dedup-chapters/{slug}", s.handleDedupChapters)
|
||||
|
||||
// Voices list
|
||||
mux.HandleFunc("GET /api/voices", s.handleVoices)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user