// Command seed-book-intro seeds a book's front-matter (intro-segments such as // Dedication, Front Flap, Preface, Introduction, Disciplic Succession) into the // `prose_chapters` MongoDB collection (volume=0, type="intro-segment"). // // The segment JSON is produced by the per-book extractor scripts, e.g. // extractor/scripts/bg_intro_extract.py, cc_intro_extract.py. // // Usage (from the spsays-bff directory, so .env is picked up): // // go run ./cmd/seed-book-intro --book cc // go run ./cmd/seed-book-intro --book bg --dir ../extractor/jsons/books/bg/intro // // It is idempotent: a book's existing intro-segments are removed before insert. package main import ( "context" "encoding/json" "flag" "fmt" "os" "path/filepath" "sort" "time" "go.uber.org/zap" "github.com/prabhupadasays/spsays-bff/internal/config" "github.com/prabhupadasays/spsays-bff/internal/repository" ) type introSegment struct { Slug string `json:"slug"` Title string `json:"title"` Type string `json:"type"` Book string `json:"book"` Volume int `json:"volume"` Order int `json:"order"` Paragraphs []string `json:"paragraphs"` } func main() { book := flag.String("book", "", "book code (e.g. bg, cc) — required") dir := flag.String("dir", "", "directory of intro-segment JSON files "+ "(default: ../extractor/jsons/books//intro)") flag.Parse() if *book == "" { fmt.Fprintln(os.Stderr, "--book is required") os.Exit(1) } segDir := *dir if segDir == "" { segDir = filepath.Join("..", "extractor", "jsons", "books", *book, "intro") } cfg, err := config.Load() if err != nil { fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err) os.Exit(1) } logger, _ := zap.NewDevelopment() defer logger.Sync() sugar := logger.Sugar() mongoDB, err := repository.NewMongoDB(cfg.MongoURI, cfg.MongoDB, logger) if err != nil { sugar.Fatalf("failed to connect to MongoDB: %v", err) } defer mongoDB.Close() entries, err := os.ReadDir(segDir) if err != nil { sugar.Fatalf("failed to read dir %s: %v", segDir, err) } var files []string for _, e := range entries { if !e.IsDir() && filepath.Ext(e.Name()) == ".json" { files = append(files, e.Name()) } } sort.Strings(files) var segments []introSegment for _, name := range files { raw, err := os.ReadFile(filepath.Join(segDir, name)) if err != nil { sugar.Fatalf("read %s: %v", name, err) } var seg introSegment if err := json.Unmarshal(raw, &seg); err != nil { sugar.Fatalf("parse %s: %v", name, err) } if seg.Book != *book || seg.Type != "intro-segment" { sugar.Warnf("skipping %s (book=%q type=%q)", name, seg.Book, seg.Type) continue } segments = append(segments, seg) } if len(segments) == 0 { sugar.Fatalf("no %s intro-segments found in %s", *book, segDir) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() col := mongoDB.Collection("prose_chapters") del, err := col.DeleteMany(ctx, map[string]any{ "book": *book, "type": "intro-segment", }) if err != nil { sugar.Fatalf("delete existing %s intro-segments: %v", *book, err) } sugar.Infof("removed %d existing %s intro-segment(s)", del.DeletedCount, *book) docs := make([]any, 0, len(segments)) for _, seg := range segments { // chapter is null for intro-segments (matches the ProseChapterDoc shape). docs = append(docs, map[string]any{ "book": *book, "volume": 0, "chapter": nil, "title": seg.Title, "type": "intro-segment", "slug": seg.Slug, "order": seg.Order, "paragraphs": seg.Paragraphs, }) sugar.Infof(" %s (order %d, %d paragraphs)", seg.Slug, seg.Order, len(seg.Paragraphs)) } res, err := col.InsertMany(ctx, docs) if err != nil { sugar.Fatalf("insert %s intro-segments: %v", *book, err) } sugar.Infof("inserted %d %s intro-segment(s)", len(res.InsertedIDs), *book) }