From be4b1636c03333f8ddc27f4bfcb4f5e674e58e8b Mon Sep 17 00:00:00 2001 From: Krispire Admin Date: Wed, 17 Jun 2026 12:40:19 +0530 Subject: [PATCH] feat: add book categorisation in transcripts --- internal/handler/transcription.go | 24 ++++++++ internal/repository/transcription_repo.go | 74 +++++++++++++++++++++++ internal/server/server.go | 2 + 3 files changed, 100 insertions(+) diff --git a/internal/handler/transcription.go b/internal/handler/transcription.go index 0f4228b..f00a103 100644 --- a/internal/handler/transcription.go +++ b/internal/handler/transcription.go @@ -3,6 +3,7 @@ package handler import ( "net/http" "strconv" + "strings" "github.com/go-chi/chi/v5" "github.com/prabhupadasays/spsays-bff/internal/repository" @@ -70,6 +71,29 @@ func (h *TranscriptionHandler) GetByType(w http.ResponseWriter, r *http.Request) response.JSON(w, http.StatusOK, docs) } +func (h *TranscriptionHandler) GetBooks(w http.ResponseWriter, r *http.Request) { + docs, err := h.repo.GetBooks(r.Context()) + if err != nil { + response.Error(w, http.StatusInternalServerError, "failed to fetch books") + return + } + response.JSON(w, http.StatusOK, docs) +} + +func (h *TranscriptionHandler) GetByBook(w http.ResponseWriter, r *http.Request) { + code := strings.ToUpper(chi.URLParam(r, "book")) + if h.repo.BookTitle(code) == "" { + response.Error(w, http.StatusNotFound, "unknown book") + return + } + docs, err := h.repo.GetByBook(r.Context(), code) + if err != nil { + response.Error(w, http.StatusInternalServerError, "failed to fetch transcriptions") + return + } + response.JSON(w, http.StatusOK, docs) +} + func (h *TranscriptionHandler) GetYears(w http.ResponseWriter, r *http.Request) { docs, err := h.repo.GetYears(r.Context()) if err != nil { diff --git a/internal/repository/transcription_repo.go b/internal/repository/transcription_repo.go index 3f275a5..0d79c6c 100644 --- a/internal/repository/transcription_repo.go +++ b/internal/repository/transcription_repo.go @@ -180,6 +180,80 @@ func (r *TranscriptionRepo) GetPlaces(ctx context.Context) ([]bson.M, error) { return results, nil } +// TranscriptionBooks is the canonical set of scriptures that appear in the +// free-text `reference` field of transcriptions (Prabhupāda's lectures are +// commentaries on specific verses). Browsing by "book" is derived from the +// reference string — there are no structured book/chapter columns. Ordered for +// display fallbacks; the listing endpoints filter out books with no transcripts. +var TranscriptionBooks = []struct{ Code, Title string }{ + {"SB", "Śrīmad-Bhāgavatam"}, + {"BG", "Bhagavad-gītā"}, + {"CC", "Caitanya-caritāmṛta"}, + {"BS", "Brahma-saṁhitā"}, + {"NOD", "The Nectar of Devotion"}, + {"ISO", "Śrī Īśopaniṣad"}, +} + +// bookTitle returns the display title for a known book code, or "" if unknown. +func bookTitle(code string) string { + for _, b := range TranscriptionBooks { + if b.Code == code { + return b.Title + } + } + return "" +} + +// bookRefFilter matches published transcriptions whose reference begins with the +// given book code. The trailing "([^A-Za-z]|$)" is a boundary so e.g. "BS" can't +// match "BG"; no code is a prefix of another, so this is unambiguous. +func bookRefFilter(code string) bson.M { + return bson.M{ + "isPublished": true, + "reference": bson.M{"$regex": "^" + code + "([^A-Za-z]|$)"}, + } +} + +// GetBooks returns the books that have at least one transcript, as +// {_id, title, count} — mirroring the {_id, count} shape the frontend already +// consumes for years/places. One count query per book (like GetTypeDistribution). +func (r *TranscriptionRepo) GetBooks(ctx context.Context) ([]bson.M, error) { + results := make([]bson.M, 0, len(TranscriptionBooks)) + for _, b := range TranscriptionBooks { + count, err := r.col.CountDocuments(ctx, bookRefFilter(b.Code)) + if err != nil { + return nil, err + } + if count > 0 { + results = append(results, bson.M{"_id": b.Code, "title": b.Title, "count": count}) + } + } + return results, nil +} + +// GetByBook lists the transcript summaries for a book code, sorted +// chronologically (same projection/sort as GetByType). +func (r *TranscriptionRepo) GetByBook(ctx context.Context, code string) ([]TranscriptionSummaryDoc, error) { + opts := options.Find(). + SetSort(bson.D{{Key: "year", Value: 1}, {Key: "month", Value: 1}, {Key: "day", Value: 1}}). + SetProjection(transcriptionSummaryProjection) + cursor, err := r.col.Find(ctx, bookRefFilter(code), opts) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + var results []TranscriptionSummaryDoc + if err := cursor.All(ctx, &results); err != nil { + return nil, err + } + return results, nil +} + +// BookTitle exposes the registry lookup to the handler for validation. +func (r *TranscriptionRepo) BookTitle(code string) string { + return bookTitle(code) +} + func (r *TranscriptionRepo) GetTypeDistribution(ctx context.Context) (int64, int64, error) { lectureCount, err := r.col.CountDocuments(ctx, bson.M{"type": "lecture", "isPublished": true}) if err != nil { diff --git a/internal/server/server.go b/internal/server/server.go index 1c5eacf..a0cbbf7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -626,9 +626,11 @@ func (s *Server) setupRouter() { r.Get("/years", s.transcriptionH.GetYears) r.Get("/places", s.transcriptionH.GetPlaces) r.Get("/types", s.transcriptionH.GetTypeDistribution) + r.Get("/books", s.transcriptionH.GetBooks) r.Get("/year/{year}", s.transcriptionH.GetByYear) r.Get("/place/{place}", s.transcriptionH.GetByPlace) r.Get("/type/{type}", s.transcriptionH.GetByType) + r.Get("/book/{book}", s.transcriptionH.GetByBook) r.Get("/adjacent/{slug}", s.transcriptionH.GetAdjacent) r.Get("/{slug}", s.transcriptionH.GetBySlug) })