Files
spsays-bff/internal/repository/transcription_repo.go
T

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
}