Files

207 lines
6.7 KiB
Go

package database
import (
"database/sql"
"errors"
"time"
"git.noroutes.net/NoRoutes/vibetunes/internal/idgen"
)
type PodcastChannel struct {
ID string
URL string
Title string
Description string
CoverArtID sql.NullString
Status string // new, downloading, completed, error
ErrorMessage string
CreatedAt time.Time
UpdatedAt time.Time
}
const podcastChannelSelect = `SELECT id, url, title, description, cover_art_id, status, error_message, created_at, updated_at FROM podcast_channels`
func CreatePodcastChannel(db *sql.DB, url string) (*PodcastChannel, error) {
id := idgen.New("pc")
_, err := db.Exec(`INSERT INTO podcast_channels (id, url) VALUES (?, ?)`, id, url)
if err != nil {
return nil, err
}
return GetPodcastChannel(db, id)
}
func GetPodcastChannel(db *sql.DB, id string) (*PodcastChannel, error) {
return scanPodcastChannel(db.QueryRow(podcastChannelSelect+` WHERE id = ?`, id))
}
func scanPodcastChannel(row *sql.Row) (*PodcastChannel, error) {
var c PodcastChannel
var createdAt, updatedAt string
err := row.Scan(&c.ID, &c.URL, &c.Title, &c.Description, &c.CoverArtID, &c.Status, &c.ErrorMessage, &createdAt, &updatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
c.CreatedAt, _ = parseTime(createdAt)
c.UpdatedAt, _ = parseTime(updatedAt)
return &c, nil
}
func ListPodcastChannels(db *sql.DB) ([]*PodcastChannel, error) {
rows, err := db.Query(podcastChannelSelect + ` ORDER BY title COLLATE NOCASE`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*PodcastChannel
for rows.Next() {
var c PodcastChannel
var createdAt, updatedAt string
if err := rows.Scan(&c.ID, &c.URL, &c.Title, &c.Description, &c.CoverArtID, &c.Status, &c.ErrorMessage, &createdAt, &updatedAt); err != nil {
return nil, err
}
c.CreatedAt, _ = parseTime(createdAt)
c.UpdatedAt, _ = parseTime(updatedAt)
out = append(out, &c)
}
return out, rows.Err()
}
func UpdatePodcastChannelMetadata(db *sql.DB, id, title, description string) error {
_, err := db.Exec(`UPDATE podcast_channels SET title=?, description=?, updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?`,
title, description, id)
return err
}
func SetPodcastChannelStatus(db *sql.DB, id, status, errMsg string) error {
_, err := db.Exec(`UPDATE podcast_channels SET status=?, error_message=?, updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?`,
status, errMsg, id)
return err
}
func DeletePodcastChannel(db *sql.DB, id string) error {
_, err := db.Exec(`DELETE FROM podcast_channels WHERE id = ?`, id)
return err
}
// --- Episodes ---
type PodcastEpisode struct {
ID string
ChannelID string
GUID string
Title string
Description string
PublishDate string
DurationSeconds float64
EnclosureURL string
MediaFileID sql.NullString
Status string // new, downloading, completed, error, skipped
CreatedAt time.Time
}
const podcastEpisodeSelect = `SELECT id, channel_id, guid, title, description, publish_date, duration_seconds, enclosure_url, media_file_id, status, created_at FROM podcast_episodes`
// UpsertPodcastEpisode creates or updates an episode by (channel, guid),
// keeping its ID stable across feed refreshes — mirrors the scanner's
// natural-key approach for media files.
func UpsertPodcastEpisode(db *sql.DB, channelID, guid, title, description, publishDate, enclosureURL string, duration float64) (*PodcastEpisode, error) {
var id string
err := db.QueryRow(`SELECT id FROM podcast_episodes WHERE channel_id = ? AND guid = ?`, channelID, guid).Scan(&id)
if err == nil {
_, err = db.Exec(`UPDATE podcast_episodes SET title=?, description=?, publish_date=?, enclosure_url=?, duration_seconds=? WHERE id=?`,
title, description, publishDate, enclosureURL, duration, id)
if err != nil {
return nil, err
}
return GetPodcastEpisode(db, id)
}
if !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
id = idgen.New("pe")
_, err = db.Exec(`INSERT INTO podcast_episodes (id, channel_id, guid, title, description, publish_date, enclosure_url, duration_seconds, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'new')`, id, channelID, guid, title, description, publishDate, enclosureURL, duration)
if err != nil {
return nil, err
}
return GetPodcastEpisode(db, id)
}
func GetPodcastEpisode(db *sql.DB, id string) (*PodcastEpisode, error) {
return scanPodcastEpisode(db.QueryRow(podcastEpisodeSelect+` WHERE id = ?`, id))
}
func scanPodcastEpisode(row *sql.Row) (*PodcastEpisode, error) {
var e PodcastEpisode
var createdAt string
err := row.Scan(&e.ID, &e.ChannelID, &e.GUID, &e.Title, &e.Description, &e.PublishDate, &e.DurationSeconds, &e.EnclosureURL, &e.MediaFileID, &e.Status, &createdAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
e.CreatedAt, _ = parseTime(createdAt)
return &e, nil
}
func ListPodcastEpisodes(db *sql.DB, channelID string) ([]*PodcastEpisode, error) {
rows, err := db.Query(podcastEpisodeSelect+` WHERE channel_id = ? ORDER BY publish_date DESC, created_at DESC`, channelID)
if err != nil {
return nil, err
}
defer rows.Close()
return scanPodcastEpisodes(rows)
}
func ListNewestPodcastEpisodes(db *sql.DB, limit int) ([]*PodcastEpisode, error) {
rows, err := db.Query(podcastEpisodeSelect+` WHERE status = 'completed' ORDER BY publish_date DESC, created_at DESC LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
return scanPodcastEpisodes(rows)
}
func ListPodcastEpisodesByStatus(db *sql.DB, status string) ([]*PodcastEpisode, error) {
rows, err := db.Query(podcastEpisodeSelect+` WHERE status = ?`, status)
if err != nil {
return nil, err
}
defer rows.Close()
return scanPodcastEpisodes(rows)
}
func scanPodcastEpisodes(rows *sql.Rows) ([]*PodcastEpisode, error) {
var out []*PodcastEpisode
for rows.Next() {
var e PodcastEpisode
var createdAt string
if err := rows.Scan(&e.ID, &e.ChannelID, &e.GUID, &e.Title, &e.Description, &e.PublishDate, &e.DurationSeconds, &e.EnclosureURL, &e.MediaFileID, &e.Status, &createdAt); err != nil {
return nil, err
}
e.CreatedAt, _ = parseTime(createdAt)
out = append(out, &e)
}
return out, rows.Err()
}
func SetPodcastEpisodeStatus(db *sql.DB, id, status string) error {
_, err := db.Exec(`UPDATE podcast_episodes SET status = ? WHERE id = ?`, status, id)
return err
}
func SetPodcastEpisodeMediaFile(db *sql.DB, id, mediaFileID string) error {
_, err := db.Exec(`UPDATE podcast_episodes SET media_file_id = ?, status = 'completed' WHERE id = ?`, mediaFileID, id)
return err
}
func DeletePodcastEpisode(db *sql.DB, id string) error {
_, err := db.Exec(`DELETE FROM podcast_episodes WHERE id = ?`, id)
return err
}