Per-user podcasts, file uploads, RSS images, Docker support
Build and Deploy / build-and-deploy (push) Failing after 13s

- Each user owns a podcast: podcast title/author/image on users table
- Episodes scoped by user_id with context-based auth
- Audio file upload replaces URL linking, served from /uploads/
- Podcast and per-episode images with itunes:image in RSS
- RSS per-user via ?user=N, dashboard shows user-specific feed URL
- Settings form for title + author + image per user
- Docker multi-stage build (golang:1.25-alpine / alpine:3.21)
- Removed PODSTALK_TITLE/AUTHOR env vars
This commit is contained in:
2026-07-08 09:20:19 -04:00
parent f43c98b56f
commit 0861f7510b
8 changed files with 151 additions and 105 deletions
+39 -35
View File
@@ -8,14 +8,18 @@ import (
)
type User struct {
ID int64
Email string
PasswordHash string
CreatedAt time.Time
ID int64
Email string
PasswordHash string
PodcastTitle string
PodcastImage string
PodcastAuthor string
CreatedAt time.Time
}
type Episode struct {
ID int64
UserID int64
Title string
Description string
AudioPath string
@@ -48,11 +52,15 @@ func migrate(db *sql.DB) error {
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
podcast_title TEXT NOT NULL DEFAULT '',
podcast_image TEXT NOT NULL DEFAULT '',
podcast_author TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS episodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
audio_path TEXT NOT NULL DEFAULT '',
@@ -60,11 +68,6 @@ func migrate(db *sql.DB) error {
published_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT ''
);
`
_, err := db.Exec(schema)
return err
@@ -92,17 +95,34 @@ func (s *Store) CreateUser(email, passwordHash string) (int64, error) {
func (s *Store) UserByEmail(email string) (*User, error) {
u := &User{}
err := s.db.QueryRow("SELECT id, email, password_hash, created_at FROM users WHERE email = ?", email).
Scan(&u.ID, &u.Email, &u.PasswordHash, &u.CreatedAt)
err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_image, podcast_author, created_at FROM users WHERE email = ?", email).
Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt)
if err != nil {
return nil, err
}
return u, nil
}
func (s *Store) GetUser(id int64) (*User, error) {
u := &User{}
err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_image, podcast_author, created_at FROM users WHERE id = ?", id).
Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt)
if err != nil {
return nil, err
}
return u, nil
}
func (s *Store) UpdateUserPodcast(id int64, title, image, author string) error {
_, err := s.db.Exec("UPDATE users SET podcast_title=?, podcast_image=?, podcast_author=? WHERE id=?",
title, image, author, id)
return err
}
// --- Episodes ---
func (s *Store) ListEpisodes() ([]Episode, error) {
rows, err := s.db.Query("SELECT id, title, description, audio_path, image_path, published_at, created_at FROM episodes ORDER BY published_at DESC")
func (s *Store) ListEpisodes(userID int64) ([]Episode, error) {
rows, err := s.db.Query("SELECT id, user_id, title, description, audio_path, image_path, published_at, created_at FROM episodes WHERE user_id = ? ORDER BY published_at DESC", userID)
if err != nil {
return nil, err
}
@@ -111,7 +131,7 @@ func (s *Store) ListEpisodes() ([]Episode, error) {
var eps []Episode
for rows.Next() {
var e Episode
if err := rows.Scan(&e.ID, &e.Title, &e.Description, &e.AudioPath, &e.ImagePath, &e.PublishedAt, &e.CreatedAt); err != nil {
if err := rows.Scan(&e.ID, &e.UserID, &e.Title, &e.Description, &e.AudioPath, &e.ImagePath, &e.PublishedAt, &e.CreatedAt); err != nil {
return nil, err
}
eps = append(eps, e)
@@ -121,17 +141,17 @@ func (s *Store) ListEpisodes() ([]Episode, error) {
func (s *Store) GetEpisode(id int64) (*Episode, error) {
e := &Episode{}
err := s.db.QueryRow("SELECT id, title, description, audio_path, image_path, published_at, created_at FROM episodes WHERE id = ?", id).
Scan(&e.ID, &e.Title, &e.Description, &e.AudioPath, &e.ImagePath, &e.PublishedAt, &e.CreatedAt)
err := s.db.QueryRow("SELECT id, user_id, title, description, audio_path, image_path, published_at, created_at FROM episodes WHERE id = ?", id).
Scan(&e.ID, &e.UserID, &e.Title, &e.Description, &e.AudioPath, &e.ImagePath, &e.PublishedAt, &e.CreatedAt)
if err != nil {
return nil, err
}
return e, nil
}
func (s *Store) CreateEpisode(title, description, audioPath, imagePath string, publishedAt time.Time) (int64, error) {
res, err := s.db.Exec("INSERT INTO episodes (title, description, audio_path, image_path, published_at) VALUES (?, ?, ?, ?, ?)",
title, description, audioPath, imagePath, publishedAt)
func (s *Store) CreateEpisode(userID int64, title, description, audioPath, imagePath string, publishedAt time.Time) (int64, error) {
res, err := s.db.Exec("INSERT INTO episodes (user_id, title, description, audio_path, image_path, published_at) VALUES (?, ?, ?, ?, ?, ?)",
userID, title, description, audioPath, imagePath, publishedAt)
if err != nil {
return 0, err
}
@@ -148,19 +168,3 @@ func (s *Store) DeleteEpisode(id int64) error {
_, err := s.db.Exec("DELETE FROM episodes WHERE id = ?", id)
return err
}
// --- Settings ---
func (s *Store) GetSetting(key string) (string, error) {
var val string
err := s.db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&val)
if err != nil {
return "", err
}
return val, nil
}
func (s *Store) SetSetting(key, value string) error {
_, err := s.db.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", key, value)
return err
}