package server import ( "encoding/json" "fmt" "net/http" "strconv" "time" "github.com/libnovel/scraper/internal/storage" ) // ─── Reading progress API ───────────────────────────────────────────────────── // handleGetProgress handles GET /api/progress. // Returns JSON: {"slug": chapterNum, ...} merged with {"slug_ts": timestampMs, ...} func (s *Server) handleGetProgress(w http.ResponseWriter, r *http.Request) { sid := ensureSession(w, r) entries, err := s.store.AllProgress(r.Context(), sid) if err != nil { s.log.Error("AllProgress failed", "err", err) entries = nil } progress := make(map[string]interface{}, len(entries)*2) for _, p := range entries { progress[p.Slug] = p.Chapter progress[p.Slug+"_ts"] = p.UpdatedAt.UnixMilli() } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(progress) } // handleSetProgress handles POST /api/progress/{slug}. // Body: {"chapter": N} func (s *Server) handleSetProgress(w http.ResponseWriter, r *http.Request) { sid := ensureSession(w, r) slug := r.PathValue("slug") if slug == "" { http.Error(w, `{"error":"missing slug"}`, http.StatusBadRequest) return } var body struct { Chapter int `json:"chapter"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Chapter < 1 { http.Error(w, `{"error":"invalid body"}`, http.StatusBadRequest) return } p := storage.ReadingProgress{ Slug: slug, Chapter: body.Chapter, UpdatedAt: time.Now(), } if err := s.store.SetProgress(r.Context(), sid, p); err != nil { s.log.Error("SetProgress failed", "slug", slug, "err", err) http.Error(w, `{"error":"store error"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{}) } // handleDeleteProgress handles DELETE /api/progress/{slug}. func (s *Server) handleDeleteProgress(w http.ResponseWriter, r *http.Request) { sid := ensureSession(w, r) slug := r.PathValue("slug") if slug == "" { http.Error(w, `{"error":"missing slug"}`, http.StatusBadRequest) return } if err := s.store.DeleteProgress(r.Context(), sid, slug); err != nil { s.log.Error("DeleteProgress failed", "slug", slug, "err", err) // Non-fatal — treat as success. } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{}) } // handleChapterText returns the plain text of a chapter (markdown stripped) // for server-side audio generation. Called by handleAudioGenerate internally. func (s *Server) handleChapterText(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") n, err := strconv.Atoi(r.PathValue("n")) if err != nil || n < 1 { http.NotFound(w, r) return } raw, err := s.store.ReadChapter(r.Context(), slug, n) if err != nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Cache-Control", "no-store") fmt.Fprint(w, stripMarkdown(raw)) }