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

88 lines
2.7 KiB
Go

package repository
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/prabhupadasays/spsays-bff/internal/model"
)
// AudioProgressRepo wraps Postgres access for audio_progress — the audio
// player's per-file resume points. One row per (user, audio_key).
type AudioProgressRepo struct{ pg *Postgres }
func NewAudioProgressRepo(pg *Postgres) *AudioProgressRepo {
return &AudioProgressRepo{pg: pg}
}
// ListByUser returns all of the user's saved resume points, newest first.
// Used to hydrate the player's in-memory map on sign-in.
func (r *AudioProgressRepo) ListByUser(ctx context.Context, userID string) ([]model.AudioProgress, error) {
rows, err := r.pg.Pool.Query(ctx, `
SELECT audio_key, position_seconds, duration_seconds, updated_at
FROM audio_progress
WHERE user_id = $1
ORDER BY updated_at DESC`,
userID,
)
if err != nil {
return nil, fmt.Errorf("list audio progress: %w", err)
}
defer rows.Close()
var out []model.AudioProgress
for rows.Next() {
var p model.AudioProgress
if err := rows.Scan(&p.Key, &p.Position, &p.Duration, &p.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan audio progress: %w", err)
}
out = append(out, p)
}
return out, rows.Err()
}
// Upsert writes a batch of resume points. Each row is inserted or updated
// only when the incoming updated_at is at least as new as the stored one
// (newest-wins), so concurrent tabs/devices and the on-login sync of
// anonymous IndexedDB entries never clobber a fresher position.
func (r *AudioProgressRepo) Upsert(ctx context.Context, userID string, items []model.AudioProgress) error {
if len(items) == 0 {
return nil
}
batch := &pgx.Batch{}
for _, it := range items {
batch.Queue(`
INSERT INTO audio_progress
(user_id, audio_key, position_seconds, duration_seconds, updated_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_id, audio_key) DO UPDATE
SET position_seconds = EXCLUDED.position_seconds,
duration_seconds = EXCLUDED.duration_seconds,
updated_at = EXCLUDED.updated_at
WHERE EXCLUDED.updated_at >= audio_progress.updated_at`,
userID, it.Key, it.Position, it.Duration, it.UpdatedAt)
}
br := r.pg.Pool.SendBatch(ctx, batch)
defer br.Close()
for range items {
if _, err := br.Exec(); err != nil {
return fmt.Errorf("upsert audio progress: %w", err)
}
}
return nil
}
// Delete removes a single resume point (called when a track plays to
// completion so it restarts from the beginning next time).
func (r *AudioProgressRepo) Delete(ctx context.Context, userID, key string) error {
_, err := r.pg.Pool.Exec(ctx,
`DELETE FROM audio_progress WHERE user_id = $1 AND audio_key = $2`,
userID, key,
)
if err != nil {
return fmt.Errorf("delete audio progress: %w", err)
}
return nil
}