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
+18 -17
View File
@@ -23,28 +23,36 @@ type EpisodeHandler struct {
}
func (h *EpisodeHandler) ServeDashboard(w http.ResponseWriter, r *http.Request) {
episodes, err := h.Store.ListEpisodes()
userID := UserIDFromContext(r)
user, err := h.Store.GetUser(userID)
if err != nil {
http.Error(w, "user not found", http.StatusInternalServerError)
return
}
episodes, err := h.Store.ListEpisodes(userID)
if err != nil {
http.Error(w, "failed to load episodes", http.StatusInternalServerError)
return
}
title, _ := h.Store.GetSetting("podcast_title")
title := user.PodcastTitle
if title == "" {
title = "Podstalk"
}
podcastImage, _ := h.Store.GetSetting("podcast_image")
h.Tpl.Render(w, "dashboard", map[string]any{
"Episodes": episodes,
"PodcastTitle": title,
"PodcastImage": podcastImage,
"BaseURL": h.BaseURL,
"Episodes": episodes,
"PodcastTitle": title,
"PodcastImage": user.PodcastImage,
"PodcastAuthor": user.PodcastAuthor,
"BaseURL": h.BaseURL,
"UserID": user.ID,
})
}
func (h *EpisodeHandler) ServeNew(w http.ResponseWriter, r *http.Request) {
userID := UserIDFromContext(r)
if r.Method == http.MethodGet {
h.Tpl.Render(w, "episode_form", map[string]any{"Editing": false})
return
@@ -89,7 +97,7 @@ func (h *EpisodeHandler) ServeNew(w http.ResponseWriter, r *http.Request) {
imagePath, _ := h.saveUploadedFile(r, "image_file", "images")
_, err = h.Store.CreateEpisode(title, description, audioPath, imagePath, publishedAt)
_, err = h.Store.CreateEpisode(userID, title, description, audioPath, imagePath, publishedAt)
if err != nil {
h.Tpl.Render(w, "episode_form", map[string]any{
"Editing": false,
@@ -151,7 +159,6 @@ func (h *EpisodeHandler) ServeEdit(w http.ResponseWriter, r *http.Request) {
return
}
// Load existing episode to know which files to replace
existing, err := h.Store.GetEpisode(id)
if err != nil {
http.Error(w, "episode not found", http.StatusNotFound)
@@ -191,7 +198,6 @@ func (h *EpisodeHandler) ServeDelete(w http.ResponseWriter, r *http.Request) {
return
}
// Delete associated files
ep, err := h.Store.GetEpisode(id)
if err == nil {
h.removeUploadedFile(ep.AudioPath)
@@ -205,9 +211,7 @@ func (h *EpisodeHandler) ServeDelete(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// ServeUpload serves files from the upload directory.
func (h *EpisodeHandler) ServeUpload(w http.ResponseWriter, r *http.Request) {
// Strip /uploads/ prefix to get the relative path
relPath := strings.TrimPrefix(r.URL.Path, "/uploads/")
if relPath == "" || strings.Contains(relPath, "..") {
http.Error(w, "invalid path", http.StatusBadRequest)
@@ -216,8 +220,6 @@ func (h *EpisodeHandler) ServeUpload(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, filepath.Join(h.UploadDir, relPath))
}
// saveUploadedFile saves a file from a multipart form field to the upload directory.
// Returns the relative path (category/filename) or empty string if no file uploaded.
func (h *EpisodeHandler) saveUploadedFile(r *http.Request, field, category string) (string, error) {
file, header, err := r.FormFile(field)
if err != nil {
@@ -260,7 +262,6 @@ func (h *EpisodeHandler) removeUploadedFile(relPath string) {
}
func sanitizeFilename(name string) string {
// Keep only the base name, lowercased, alphanumeric + dots + dashes
name = filepath.Base(name)
name = strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '.' || r == '-' {