watch-party/backend/internal/repo/episode_repo.go

169 lines
3.9 KiB
Go

package repo
import (
"context"
"errors"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var ErrNotFound = errors.New("not found")
type Episode struct {
EpNum int `json:"ep_num"`
EpTitle string `json:"ep_title"`
SeasonName string `json:"season_name"`
StartTime string `json:"start_time"`
PlaybackLength string `json:"playback_length"`
DateCreated time.Time `json:"date_created"`
}
type MoveResult struct {
MovedIDs []int64 `json:"moved_ids"`
DeletedIDs []int64 `json:"deleted_ids"`
SkippedIDs []int64 `json:"skipped_ids"`
}
type EpisodeRepository interface {
GetCurrent(ctx context.Context) (Episode, error)
SetCurrent(ctx context.Context, id int64, startHHMMSS string) error
MoveToArchive(ctx context.Context, ids []int64) (MoveResult, error)
}
type pgxEpisodeRepo struct {
pool *pgxpool.Pool
}
func NewEpisodeRepo(pool *pgxpool.Pool) EpisodeRepository {
return &pgxEpisodeRepo{pool: pool}
}
func (r *pgxEpisodeRepo) GetCurrent(ctx context.Context) (Episode, error) {
const q = `
SELECT
ep_num,
ep_title,
season_name,
to_char(start_time, 'HH24:MI:SS') AS start_time,
to_char(playback_length, 'HH24:MI:SS') AS playback_length,
date_created
FROM current
WHERE current_ep = true
ORDER BY id DESC
LIMIT 1;
`
var e Episode
err := r.pool.QueryRow(ctx, q).Scan(
&e.EpNum, &e.EpTitle, &e.SeasonName, &e.StartTime, &e.PlaybackLength, &e.DateCreated,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return Episode{}, ErrNotFound
}
return Episode{}, err
}
return e, nil
}
func (r *pgxEpisodeRepo) SetCurrent(ctx context.Context, id int64, startHHMMSS string) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
// 1) clear any current episode flag
if _, err := tx.Exec(ctx, `UPDATE current SET current_ep = false WHERE current_ep = true`); err != nil {
return err
}
// 2) set start_time and current_ep for the id
ct, err := tx.Exec(ctx, `UPDATE current SET start_time = $1::time, current_ep = true WHERE id = $2`, startHHMMSS, id)
if err != nil {
return err
}
if ct.RowsAffected() == 0 {
return ErrNotFound
}
return tx.Commit(ctx)
}
func (r *pgxEpisodeRepo) MoveToArchive(ctx context.Context, ids []int64) (MoveResult, error) {
res := MoveResult{}
if len(ids) == 0 {
return res, nil
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return res, err
}
defer func() { _ = tx.Rollback(ctx) }()
// 1) Insert into archive from current (skip duplicates by id)
insRows, err := tx.Query(ctx, `
INSERT INTO current_archive (
id, ep_num, ep_title, season_name, start_time, playback_length, current_ep, date_created
)
SELECT id, ep_num, ep_title, season_name, start_time, playback_length, current_ep, date_created
FROM current
WHERE id = ANY($1::bigint[])
ON CONFLICT (id) DO NOTHING
RETURNING id
`, ids)
if err != nil {
return res, err
}
for insRows.Next() {
var id int64
if err := insRows.Scan(&id); err != nil {
return res, err
}
res.MovedIDs = append(res.MovedIDs, id)
}
if err := insRows.Err(); err != nil {
return res, err
}
// 2) Delete from current only those actually inserted to archive
if len(res.MovedIDs) > 0 {
delRows, err := tx.Query(ctx, `
DELETE FROM current
WHERE id = ANY($1::bigint[])
RETURNING id
`, res.MovedIDs)
if err != nil {
return res, err
}
for delRows.Next() {
var id int64
if err := delRows.Scan(&id); err != nil {
return res, err
}
res.DeletedIDs = append(res.DeletedIDs, id)
}
if err := delRows.Err(); err != nil {
return res, err
}
}
// 3) Compute skipped = requested - moved
sel := make(map[int64]struct{}, len(res.MovedIDs))
for _, id := range res.MovedIDs {
sel[id] = struct{}{}
}
for _, id := range ids {
if _, ok := sel[id]; !ok {
res.SkippedIDs = append(res.SkippedIDs, id)
}
}
if err := tx.Commit(ctx); err != nil {
return res, err
}
return res, nil
}