Files
2026-05-09 23:19:19 +05:30

634 lines
18 KiB
Go

package handler
import (
"math"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/prabhupadasays/spsays-bff/internal/repository"
"github.com/prabhupadasays/spsays-bff/pkg/response"
"go.mongodb.org/mongo-driver/v2/bson"
)
type BookHandler struct {
repo *repository.BookRepo
}
func NewBookHandler(repo *repository.BookRepo) *BookHandler {
return &BookHandler{repo: repo}
}
// ---------------------------------------------------------------------------
// GET /books — list all books (lightweight summaries only)
// ---------------------------------------------------------------------------
func (h *BookHandler) ListBooks(w http.ResponseWriter, r *http.Request) {
docs, err := h.repo.GetAllBookMetas(r.Context())
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch books")
return
}
response.JSON(w, http.StatusOK, docs)
}
// ---------------------------------------------------------------------------
// GET /books/{code} — full book metadata (chapters, cantos, līlās, etc.)
// ---------------------------------------------------------------------------
func (h *BookHandler) GetBook(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
doc, err := h.repo.GetBookMeta(r.Context(), code)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch book")
return
}
if doc == nil {
response.Error(w, http.StatusNotFound, "book not found")
return
}
response.JSON(w, http.StatusOK, doc)
}
// ---------------------------------------------------------------------------
// VERSE BOOKS WITHOUT DIVISIONS (BG, SB1962, BS, MM, NBS)
//
// GET /books/{code}/{chapter} → verse index for sidebar
// GET /books/{code}/{chapter}/{verse} → single verse with full content
// GET /books/{code}/{chapter}/verses → paginated verses for the chapter
// ---------------------------------------------------------------------------
// GetChapterVerseIndex returns the verse index for a chapter (for sidebar).
// GET /books/{code}/{chapter}
func (h *BookHandler) GetChapterVerseIndex(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
chapterStr := chi.URLParam(r, "chapter")
chapter, err := strconv.Atoi(chapterStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid chapter number")
return
}
verses, err := h.repo.GetVerseIndex(r.Context(), code, nil, chapter)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch chapter info")
return
}
response.JSON(w, http.StatusOK, map[string]interface{}{
"book": code,
"chapter": chapter,
"total_verses": len(verses),
"verses": verses,
})
}
// GetVerse returns a single verse with full content.
// GET /books/{code}/{chapter}/{verse}
func (h *BookHandler) GetVerse(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
chapterStr := chi.URLParam(r, "chapter")
verseStr := chi.URLParam(r, "verse")
chapter, err := strconv.Atoi(chapterStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid chapter number")
return
}
verse, err := strconv.Atoi(verseStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid verse number")
return
}
doc, err := h.repo.GetSingleVerse(r.Context(), code, nil, chapter, verse)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch verse")
return
}
if doc == nil {
response.Error(w, http.StatusNotFound, "verse not found")
return
}
response.JSON(w, http.StatusOK, doc)
}
// GetChapterVerses returns paginated verses for a chapter.
// GET /books/{code}/{chapter}/verses?page=&limit=
func (h *BookHandler) GetChapterVerses(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
chapterStr := chi.URLParam(r, "chapter")
chapter, err := strconv.Atoi(chapterStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid chapter number")
return
}
page, limit := parsePagination(r)
docs, total, err := h.repo.GetVerses(r.Context(), code, nil, chapter, page, limit)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch verses")
return
}
totalPages := int(math.Ceil(float64(total) / float64(limit)))
response.JSONWithMeta(w, http.StatusOK, docs, &response.Meta{
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
})
}
// ---------------------------------------------------------------------------
// VERSE BOOKS WITH DIVISIONS
//
// SB: division = canto → /books/sb/{canto}/{chapter}/{verse}
// CC: division = līlā → /books/cc/{lila}/{chapter}/{verse}
//
// GET /books/{code}/{division} → chapter list for that division
// GET /books/{code}/{division}/{chapter} → verse index for sidebar
// GET /books/{code}/{division}/{chapter}/{verse} → single verse
// GET /books/{code}/{division}/{chapter}/verses → paginated verses
// ---------------------------------------------------------------------------
// GetDivisionInfo returns the chapter listing for a specific division (canto/līlā).
// It extracts the relevant division from the book_meta data blob.
// GET /books/sb/{division} or /books/cc/{division}
func (h *BookHandler) GetDivisionInfo(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
division := chi.URLParam(r, "division")
doc, err := h.repo.GetBookMeta(r.Context(), code)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch book")
return
}
if doc == nil {
response.Error(w, http.StatusNotFound, "book not found")
return
}
// Extract the division's chapter list from the data blob.
// SB data: { cantos: [{ number, title, totalChapters, chapters: [{number, title, totalVerses}] }] }
// CC data: { lilas: [{ key, name, totalChapters, chapters: [{number, title, totalVerses}] }] }
result := extractDivisionFromMeta(code, division, doc.Data)
if result == nil {
response.Error(w, http.StatusNotFound, "division not found")
return
}
response.JSON(w, http.StatusOK, result)
}
// GetDivisionChapterVerseIndex returns the verse index for a chapter within a division.
// GET /books/{code}/{division}/{chapter}
func (h *BookHandler) GetDivisionChapterVerseIndex(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
division := chi.URLParam(r, "division")
chapterStr := chi.URLParam(r, "chapter")
chapter, err := strconv.Atoi(chapterStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid chapter number")
return
}
verses, err := h.repo.GetVerseIndex(r.Context(), code, &division, chapter)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch chapter info")
return
}
response.JSON(w, http.StatusOK, map[string]interface{}{
"book": code,
"division": division,
"chapter": chapter,
"total_verses": len(verses),
"verses": verses,
})
}
// GetDivisionVerse returns a single verse within a division.
// GET /books/{code}/{division}/{chapter}/{verse}
func (h *BookHandler) GetDivisionVerse(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
division := chi.URLParam(r, "division")
chapterStr := chi.URLParam(r, "chapter")
verseStr := chi.URLParam(r, "verse")
chapter, err := strconv.Atoi(chapterStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid chapter number")
return
}
verse, err := strconv.Atoi(verseStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid verse number")
return
}
doc, err := h.repo.GetSingleVerse(r.Context(), code, &division, chapter, verse)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch verse")
return
}
if doc == nil {
response.Error(w, http.StatusNotFound, "verse not found")
return
}
response.JSON(w, http.StatusOK, doc)
}
// GetDivisionChapterVerses returns paginated verses within a division.
// GET /books/{code}/{division}/{chapter}/verses?page=&limit=
func (h *BookHandler) GetDivisionChapterVerses(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
division := chi.URLParam(r, "division")
chapterStr := chi.URLParam(r, "chapter")
chapter, err := strconv.Atoi(chapterStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid chapter number")
return
}
page, limit := parsePagination(r)
docs, total, err := h.repo.GetVerses(r.Context(), code, &division, chapter, page, limit)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch verses")
return
}
totalPages := int(math.Ceil(float64(total) / float64(limit)))
response.JSONWithMeta(w, http.StatusOK, docs, &response.Meta{
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
})
}
// ---------------------------------------------------------------------------
// PROSE BOOKS — simple chapters (BBD, EJ, EK, MG, NOD, TLC, SSR, DS, etc.)
//
// GET /books/{code}/chapters → chapter list (sidebar)
// GET /books/{code}/chapters/{slug} → single chapter content
// ---------------------------------------------------------------------------
// ListProseChapters returns chapter listing for a prose book.
// GET /books/{code}/chapters?volume=&type=
//
// For books with verses (ISO, BS, NOI), also appends verse entries
// (mantras/texts) from the book metadata so the sidebar is complete.
func (h *BookHandler) ListProseChapters(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
volumeStr := r.URL.Query().Get("volume")
chapterType := r.URL.Query().Get("type")
volume := 0
if v, e := strconv.Atoi(volumeStr); e == nil {
volume = v
}
var typePtr *string
if chapterType != "" {
typePtr = &chapterType
}
docs, err := h.repo.GetProseChapters(r.Context(), code, volume, typePtr)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch chapters")
return
}
// Strip paragraphs from list response — only return sidebar info
type ChapterSummary struct {
ID interface{} `json:"id"`
Book string `json:"book"`
Volume int `json:"volume"`
Chapter *int `json:"chapter,omitempty"`
Title string `json:"title"`
Type string `json:"type"`
Slug string `json:"slug"`
Order int `json:"order"`
Mantra *int `json:"mantra,omitempty"`
Text *int `json:"text,omitempty"`
}
summaries := make([]ChapterSummary, 0, len(docs))
for _, d := range docs {
summaries = append(summaries, ChapterSummary{
ID: d.ID,
Book: d.Book,
Volume: d.Volume,
Chapter: d.Chapter,
Title: d.Title,
Type: d.Type,
Slug: d.Slug,
Order: d.Order,
})
}
// For verse+prose books (ISO, BS, NOI), append verse entries from book meta
// so the sidebar includes mantras/texts alongside intro segments.
if chapterType == "" || chapterType == "verse" {
meta, metaErr := h.repo.GetBookMeta(r.Context(), code)
if metaErr == nil && meta != nil {
if dataMap, ok := meta.Data.(bson.D); ok {
for _, elem := range dataMap {
if elem.Key == "mantras" {
// ISO: mantras array with {number, slug, title}
if arr, ok := elem.Value.(bson.A); ok {
baseOrder := len(summaries) + 1
for i, item := range arr {
if m, ok := item.(bson.D); ok {
num := 0
slug := ""
title := ""
for _, f := range m {
switch f.Key {
case "number":
if n, ok := f.Value.(int32); ok {
num = int(n)
} else if n, ok := f.Value.(int64); ok {
num = int(n)
}
case "slug":
if s, ok := f.Value.(string); ok {
slug = s
}
case "title":
if s, ok := f.Value.(string); ok {
title = s
}
}
}
if slug == "" {
slug = strconv.Itoa(num)
}
mantraNum := num
summaries = append(summaries, ChapterSummary{
Book: code,
Title: title,
Type: "verse",
Slug: slug,
Order: baseOrder + i,
Mantra: &mantraNum,
})
}
}
}
} else if elem.Key == "texts" {
// BS, NOI: texts array with {number, slug, title}
if arr, ok := elem.Value.(bson.A); ok {
baseOrder := len(summaries) + 1
for i, item := range arr {
if m, ok := item.(bson.D); ok {
num := 0
slug := ""
title := ""
for _, f := range m {
switch f.Key {
case "number":
if n, ok := f.Value.(int32); ok {
num = int(n)
} else if n, ok := f.Value.(int64); ok {
num = int(n)
}
case "slug":
if s, ok := f.Value.(string); ok {
slug = s
}
case "title":
if s, ok := f.Value.(string); ok {
title = s
}
}
}
if slug == "" {
slug = strconv.Itoa(num)
}
textNum := num
summaries = append(summaries, ChapterSummary{
Book: code,
Title: title,
Type: "verse",
Slug: slug,
Order: baseOrder + i,
Text: &textNum,
})
}
}
}
}
}
}
}
}
response.JSON(w, http.StatusOK, summaries)
}
// GetProseChapter returns a single prose chapter with full content.
// GET /books/{code}/chapters/{slug}?volume=
func (h *BookHandler) GetProseChapter(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
slug := chi.URLParam(r, "slug")
volumeStr := r.URL.Query().Get("volume")
volume := 0
if v, e := strconv.Atoi(volumeStr); e == nil {
volume = v
}
doc, err := h.repo.GetProseChapter(r.Context(), code, volume, slug)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch chapter")
return
}
if doc == nil {
response.Error(w, http.StatusNotFound, "chapter not found")
return
}
response.JSON(w, http.StatusOK, doc)
}
// ---------------------------------------------------------------------------
// VOLUME-BASED PROSE BOOKS (KB)
//
// GET /books/{code}/volumes/{volume} → chapter list for volume
// GET /books/{code}/volumes/{volume}/{slug} → single chapter content
// ---------------------------------------------------------------------------
// ListVolumeChapters returns chapters for a specific volume.
// GET /books/{code}/volumes/{volume}
func (h *BookHandler) ListVolumeChapters(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
volumeStr := chi.URLParam(r, "volume")
volume, err := strconv.Atoi(volumeStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid volume number")
return
}
docs, err := h.repo.GetProseChapters(r.Context(), code, volume, nil)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch chapters")
return
}
// Strip paragraphs from list response
type ChapterSummary struct {
ID interface{} `json:"id"`
Book string `json:"book"`
Volume int `json:"volume"`
Chapter *int `json:"chapter,omitempty"`
Title string `json:"title"`
Type string `json:"type"`
Slug string `json:"slug"`
Order int `json:"order"`
}
summaries := make([]ChapterSummary, len(docs))
for i, d := range docs {
summaries[i] = ChapterSummary{
ID: d.ID,
Book: d.Book,
Volume: d.Volume,
Chapter: d.Chapter,
Title: d.Title,
Type: d.Type,
Slug: d.Slug,
Order: d.Order,
}
}
response.JSON(w, http.StatusOK, summaries)
}
// GetVolumeChapter returns a single chapter within a volume.
// GET /books/{code}/volumes/{volume}/{slug}
func (h *BookHandler) GetVolumeChapter(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
volumeStr := chi.URLParam(r, "volume")
slug := chi.URLParam(r, "slug")
volume, err := strconv.Atoi(volumeStr)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid volume number")
return
}
doc, err := h.repo.GetProseChapter(r.Context(), code, volume, slug)
if err != nil {
response.Error(w, http.StatusInternalServerError, "failed to fetch chapter")
return
}
if doc == nil {
response.Error(w, http.StatusNotFound, "chapter not found")
return
}
response.JSON(w, http.StatusOK, doc)
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
func parsePagination(r *http.Request) (page, limit int) {
page = 1
limit = 20
if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && p > 0 {
page = p
}
if l, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && l > 0 && l <= 100 {
limit = l
}
return
}
// extractDivisionFromMeta pulls the relevant division (canto/līlā) from the
// book_meta data blob and returns it as a map, or nil if not found.
func extractDivisionFromMeta(code, division string, data interface{}) map[string]interface{} {
dataMap, ok := data.(bson.D)
if !ok {
return nil
}
divNum, numErr := strconv.Atoi(division)
switch code {
case "sb":
// SB: data.cantos is an array, division is canto number (1-indexed)
for _, elem := range dataMap {
if elem.Key != "cantos" {
continue
}
cantos, ok := elem.Value.(bson.A)
if !ok {
return nil
}
if numErr != nil || divNum < 1 || divNum > len(cantos) {
return nil
}
canto, ok := cantos[divNum-1].(bson.D)
if !ok {
return nil
}
return bsonDToMap(canto)
}
case "cc":
// CC: data.lilas is an array, division is līlā key ("adi", "madhya", "antya")
for _, elem := range dataMap {
if elem.Key != "lilas" {
continue
}
lilas, ok := elem.Value.(bson.A)
if !ok {
return nil
}
for _, l := range lilas {
lila, ok := l.(bson.D)
if !ok {
continue
}
for _, field := range lila {
if field.Key == "key" && field.Value == division {
return bsonDToMap(lila)
}
}
}
}
}
return nil
}
// bsonDToMap converts a bson.D to a map[string]interface{} for JSON serialization.
func bsonDToMap(d bson.D) map[string]interface{} {
m := make(map[string]interface{}, len(d))
for _, elem := range d {
switch v := elem.Value.(type) {
case bson.D:
m[elem.Key] = bsonDToMap(v)
case bson.A:
arr := make([]interface{}, len(v))
for i, item := range v {
if sub, ok := item.(bson.D); ok {
arr[i] = bsonDToMap(sub)
} else {
arr[i] = item
}
}
m[elem.Key] = arr
default:
m[elem.Key] = v
}
}
return m
}