308 lines
11 KiB
Go
308 lines
11 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
|
)
|
|
|
|
type TranscriptionRepo struct {
|
|
col *mongo.Collection
|
|
}
|
|
|
|
func NewTranscriptionRepo(db *MongoDB) *TranscriptionRepo {
|
|
return &TranscriptionRepo{col: db.Collection("transcriptions")}
|
|
}
|
|
|
|
type ContentBlockDoc struct {
|
|
Text string `bson:"text" json:"text"`
|
|
Format string `bson:"format" json:"format"`
|
|
Alignment string `bson:"alignment" json:"alignment"`
|
|
Speaker *string `bson:"speaker,omitempty" json:"speaker,omitempty"`
|
|
}
|
|
|
|
type TranscriptionDoc struct {
|
|
ID bson.ObjectID `bson:"_id" json:"id"`
|
|
Slug string `bson:"slug" json:"slug"`
|
|
Name string `bson:"name" json:"name"`
|
|
Type string `bson:"type" json:"type"`
|
|
Date *string `bson:"date,omitempty" json:"date,omitempty"`
|
|
Year *int `bson:"year,omitempty" json:"year,omitempty"`
|
|
Month *int `bson:"month,omitempty" json:"month,omitempty"`
|
|
Day *int `bson:"day,omitempty" json:"day,omitempty"`
|
|
Place *string `bson:"place,omitempty" json:"place,omitempty"`
|
|
Context *string `bson:"context,omitempty" json:"context,omitempty"`
|
|
Reference *string `bson:"reference,omitempty" json:"reference,omitempty"`
|
|
DurationMinutes *int `bson:"durationMinutes,omitempty" json:"duration_minutes,omitempty"`
|
|
AudioUrl *string `bson:"audioUrl,omitempty" json:"audio_url,omitempty"`
|
|
Participants []string `bson:"participants" json:"participants"`
|
|
Content []ContentBlockDoc `bson:"content" json:"content"`
|
|
ContentJson *string `bson:"contentJson,omitempty" json:"-"`
|
|
Tags []string `bson:"tags" json:"tags"`
|
|
Language string `bson:"language" json:"language"`
|
|
Source *string `bson:"source,omitempty" json:"source,omitempty"`
|
|
IsPublished bool `bson:"isPublished" json:"is_published"`
|
|
}
|
|
|
|
type TranscriptionSummaryDoc struct {
|
|
ID bson.ObjectID `bson:"_id" json:"id"`
|
|
Slug string `bson:"slug" json:"slug"`
|
|
Name string `bson:"name" json:"name"`
|
|
Type string `bson:"type" json:"type"`
|
|
Date *string `bson:"date,omitempty" json:"date,omitempty"`
|
|
Year *int `bson:"year,omitempty" json:"year,omitempty"`
|
|
Month *int `bson:"month,omitempty" json:"month,omitempty"`
|
|
Day *int `bson:"day,omitempty" json:"day,omitempty"`
|
|
Place *string `bson:"place,omitempty" json:"place,omitempty"`
|
|
Context *string `bson:"context,omitempty" json:"context,omitempty"`
|
|
Reference *string `bson:"reference,omitempty" json:"reference,omitempty"`
|
|
DurationMinutes *int `bson:"durationMinutes,omitempty" json:"duration_minutes,omitempty"`
|
|
AudioUrl *string `bson:"audioUrl,omitempty" json:"audio_url,omitempty"`
|
|
Participants []string `bson:"participants" json:"participants"`
|
|
}
|
|
|
|
var publishedFilter = bson.M{"isPublished": true}
|
|
|
|
var transcriptionSummaryProjection = bson.M{
|
|
"slug": 1, "name": 1, "type": 1, "date": 1,
|
|
"year": 1, "month": 1, "day": 1, "place": 1,
|
|
"context": 1, "reference": 1, "durationMinutes": 1,
|
|
"audioUrl": 1, "participants": 1,
|
|
}
|
|
|
|
func (r *TranscriptionRepo) GetBySlug(ctx context.Context, slug string) (*TranscriptionDoc, error) {
|
|
filter := bson.M{"slug": slug, "isPublished": true}
|
|
var doc TranscriptionDoc
|
|
err := r.col.FindOne(ctx, filter).Decode(&doc)
|
|
if err != nil {
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
if len(doc.Content) == 0 && doc.ContentJson != nil && *doc.ContentJson != "" {
|
|
var blocks []ContentBlockDoc
|
|
if err := json.Unmarshal([]byte(*doc.ContentJson), &blocks); err == nil {
|
|
doc.Content = blocks
|
|
}
|
|
}
|
|
doc.ContentJson = nil
|
|
return &doc, nil
|
|
}
|
|
|
|
func (r *TranscriptionRepo) GetByYear(ctx context.Context, year int) ([]TranscriptionSummaryDoc, error) {
|
|
filter := bson.M{"year": year, "isPublished": true}
|
|
opts := options.Find().
|
|
SetSort(bson.D{{Key: "month", Value: 1}, {Key: "day", Value: 1}}).
|
|
SetProjection(transcriptionSummaryProjection)
|
|
cursor, err := r.col.Find(ctx, filter, 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
|
|
}
|
|
|
|
func (r *TranscriptionRepo) GetByPlace(ctx context.Context, place string) ([]TranscriptionSummaryDoc, error) {
|
|
filter := bson.M{"place": place, "isPublished": true}
|
|
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, filter, 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
|
|
}
|
|
|
|
func (r *TranscriptionRepo) GetByType(ctx context.Context, docType string) ([]TranscriptionSummaryDoc, error) {
|
|
filter := bson.M{"type": docType, "isPublished": true}
|
|
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, filter, 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
|
|
}
|
|
|
|
func (r *TranscriptionRepo) GetYears(ctx context.Context) ([]bson.M, error) {
|
|
pipeline := bson.A{
|
|
bson.M{"$match": bson.M{"isPublished": true, "year": bson.M{"$ne": nil}}},
|
|
bson.M{"$group": bson.M{"_id": "$year", "count": bson.M{"$sum": 1}}},
|
|
bson.M{"$sort": bson.M{"_id": 1}},
|
|
}
|
|
cursor, err := r.col.Aggregate(ctx, pipeline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer cursor.Close(ctx)
|
|
var results []bson.M
|
|
if err := cursor.All(ctx, &results); err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (r *TranscriptionRepo) GetPlaces(ctx context.Context) ([]bson.M, error) {
|
|
pipeline := bson.A{
|
|
bson.M{"$match": bson.M{"isPublished": true, "place": bson.M{"$ne": nil}}},
|
|
bson.M{"$group": bson.M{"_id": "$place", "count": bson.M{"$sum": 1}}},
|
|
bson.M{"$sort": bson.M{"count": -1}},
|
|
}
|
|
cursor, err := r.col.Aggregate(ctx, pipeline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer cursor.Close(ctx)
|
|
var results []bson.M
|
|
if err := cursor.All(ctx, &results); err != nil {
|
|
return nil, err
|
|
}
|
|
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 {
|
|
return 0, 0, err
|
|
}
|
|
conversationCount, err := r.col.CountDocuments(ctx, bson.M{"type": "conversation", "isPublished": true})
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return lectureCount, conversationCount, nil
|
|
}
|
|
|
|
func (r *TranscriptionRepo) Count(ctx context.Context) (int64, error) {
|
|
return r.col.CountDocuments(ctx, publishedFilter)
|
|
}
|
|
|
|
func (r *TranscriptionRepo) GetAdjacent(ctx context.Context, slug string, year int) (*TranscriptionSummaryDoc, *TranscriptionSummaryDoc, error) {
|
|
filter := bson.M{"year": year, "isPublished": true}
|
|
opts := options.Find().
|
|
SetSort(bson.D{{Key: "month", Value: 1}, {Key: "day", Value: 1}}).
|
|
SetProjection(transcriptionSummaryProjection)
|
|
cursor, err := r.col.Find(ctx, filter, opts)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer cursor.Close(ctx)
|
|
var summaries []TranscriptionSummaryDoc
|
|
if err := cursor.All(ctx, &summaries); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
idx := -1
|
|
for i, s := range summaries {
|
|
if s.Slug == slug {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx < 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
var prev, next *TranscriptionSummaryDoc
|
|
if idx > 0 {
|
|
prev = &summaries[idx-1]
|
|
}
|
|
if idx < len(summaries)-1 {
|
|
next = &summaries[idx+1]
|
|
}
|
|
return prev, next, nil
|
|
}
|