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
+37 -33
View File
@@ -3,35 +3,15 @@ package handler
import (
"encoding/xml"
"net/http"
"strconv"
"time"
"podstalk/store"
)
type RSSHandler struct {
Store *store.Store
BaseTitle string
Link string
Author string
}
func (h *RSSHandler) resolveTitle() string {
title, err := h.Store.GetSetting("podcast_title")
if err == nil && title != "" {
return title
}
if h.BaseTitle != "" {
return h.BaseTitle
}
return "Podstalk"
}
func (h *RSSHandler) resolveImage() string {
img, err := h.Store.GetSetting("podcast_image")
if err == nil && img != "" {
return img
}
return ""
Store *store.Store
Link string
}
type rssFeed struct {
@@ -46,6 +26,7 @@ type rssChannel struct {
Link string `xml:"link"`
Description string `xml:"description"`
Language string `xml:"language"`
Author string `xml:"itunes:author,omitempty"`
Image *rssImage `xml:"image,omitempty"`
ItunesImage *rssItunesImage `xml:"itunes:image,omitempty"`
Items []rssItem `xml:"item"`
@@ -77,21 +58,43 @@ type rssEnclosure struct {
}
func (h *RSSHandler) ServeRSS(w http.ResponseWriter, r *http.Request) {
episodes, err := h.Store.ListEpisodes()
// Resolve user: check query param, fall back to first user.
var user *store.User
idStr := r.URL.Query().Get("user")
if idStr != "" {
id, err := strconv.ParseInt(idStr, 10, 64)
if err == nil {
user, _ = h.Store.GetUser(id)
}
}
if user == nil {
// Fall back to first user with any episodes
// Simplest: just get user ID 1
user, _ = h.Store.GetUser(1)
}
if user == nil {
http.Error(w, "no podcast found", http.StatusNotFound)
return
}
episodes, err := h.Store.ListEpisodes(user.ID)
if err != nil {
http.Error(w, "failed to load episodes", http.StatusInternalServerError)
return
}
podcastTitle := h.resolveTitle()
podcastImage := h.resolveImage()
title := user.PodcastTitle
if title == "" {
title = "Podstalk"
}
author := user.PodcastAuthor
items := make([]rssItem, 0, len(episodes))
for _, ep := range episodes {
var itemImage *rssItunesImage
epImage := ep.ImagePath
if epImage == "" {
epImage = podcastImage
epImage = user.PodcastImage
}
if epImage != "" {
itemImage = &rssItunesImage{Href: h.fullURL(epImage)}
@@ -112,23 +115,24 @@ func (h *RSSHandler) ServeRSS(w http.ResponseWriter, r *http.Request) {
var chanImage *rssImage
var chanItunesImage *rssItunesImage
if podcastImage != "" {
if user.PodcastImage != "" {
chanImage = &rssImage{
URL: h.fullURL(podcastImage),
Title: podcastTitle,
URL: h.fullURL(user.PodcastImage),
Title: title,
Link: h.Link,
}
chanItunesImage = &rssItunesImage{Href: h.fullURL(podcastImage)}
chanItunesImage = &rssItunesImage{Href: h.fullURL(user.PodcastImage)}
}
feed := rssFeed{
Version: "2.0",
ItunesNS: "http://www.itunes.com/dtds/podcast-1.0.dtd",
Channel: rssChannel{
Title: podcastTitle,
Title: title,
Link: h.Link,
Description: podcastTitle,
Description: title,
Language: "en-us",
Author: author,
Image: chanImage,
ItunesImage: chanItunesImage,
Items: items,