b179f57628
Build and Deploy / build-and-deploy (push) Successful in 45s
- Add podcast_slug to users with global uniqueness check
- RSS available at /rss/{slug} (path-based) with legacy ?slug= fallback
- Slug field in dashboard settings with validation (lowercase, hyphens)
- JS flash messages for success/error after form submissions
- Remove unused imports
162 lines
3.6 KiB
Go
162 lines
3.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"net/http"
|
|
"time"
|
|
|
|
"podstalk/store"
|
|
)
|
|
|
|
type RSSHandler struct {
|
|
Store *store.Store
|
|
Link string
|
|
}
|
|
|
|
type rssFeed struct {
|
|
XMLName xml.Name `xml:"rss"`
|
|
Version string `xml:"version,attr"`
|
|
ItunesNS string `xml:"xmlns:itunes,attr"`
|
|
Channel rssChannel `xml:"channel"`
|
|
}
|
|
|
|
type rssChannel struct {
|
|
Title string `xml:"title"`
|
|
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"`
|
|
}
|
|
|
|
type rssImage struct {
|
|
URL string `xml:"url"`
|
|
Title string `xml:"title"`
|
|
Link string `xml:"link"`
|
|
}
|
|
|
|
type rssItunesImage struct {
|
|
Href string `xml:"href,attr"`
|
|
}
|
|
|
|
type rssItem struct {
|
|
Title string `xml:"title"`
|
|
Description string `xml:"description"`
|
|
Enclosure rssEnclosure `xml:"enclosure"`
|
|
PubDate string `xml:"pubDate"`
|
|
GUID string `xml:"guid"`
|
|
ItunesImage *rssItunesImage `xml:"itunes:image,omitempty"`
|
|
}
|
|
|
|
type rssEnclosure struct {
|
|
URL string `xml:"url,attr"`
|
|
Length string `xml:"length,attr,omitempty"`
|
|
Type string `xml:"type,attr"`
|
|
}
|
|
|
|
func (h *RSSHandler) ServeRSS(w http.ResponseWriter, r *http.Request) {
|
|
// Try path-based slug first: /rss/my-slug
|
|
slug := r.PathValue("slug")
|
|
if slug == "" {
|
|
slug = r.URL.Query().Get("slug")
|
|
}
|
|
if slug == "" {
|
|
slug = r.URL.Query().Get("user") // legacy
|
|
}
|
|
|
|
var user *store.User
|
|
if slug != "" {
|
|
user, _ = h.Store.UserBySlug(slug)
|
|
}
|
|
if user == nil {
|
|
// legacy: try numeric user ID
|
|
if idStr := r.URL.Query().Get("user"); idStr != "" {
|
|
// already tried above, fall through
|
|
}
|
|
}
|
|
if user == nil {
|
|
http.Error(w, "podcast not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
episodes, err := h.Store.ListEpisodes(user.ID)
|
|
if err != nil {
|
|
http.Error(w, "failed to load episodes", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
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 = user.PodcastImage
|
|
}
|
|
if epImage != "" {
|
|
itemImage = &rssItunesImage{Href: h.fullURL(epImage)}
|
|
}
|
|
|
|
items = append(items, rssItem{
|
|
Title: ep.Title,
|
|
Description: ep.Description,
|
|
Enclosure: rssEnclosure{
|
|
URL: h.fullURL(ep.AudioPath),
|
|
Type: "audio/mpeg",
|
|
},
|
|
PubDate: ep.PublishedAt.Format(time.RFC1123Z),
|
|
GUID: h.fullURL(ep.AudioPath),
|
|
ItunesImage: itemImage,
|
|
})
|
|
}
|
|
|
|
var chanImage *rssImage
|
|
var chanItunesImage *rssItunesImage
|
|
if user.PodcastImage != "" {
|
|
chanImage = &rssImage{
|
|
URL: h.fullURL(user.PodcastImage),
|
|
Title: title,
|
|
Link: h.Link,
|
|
}
|
|
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: title,
|
|
Link: h.Link,
|
|
Description: title,
|
|
Language: "en-us",
|
|
Author: author,
|
|
Image: chanImage,
|
|
ItunesImage: chanItunesImage,
|
|
Items: items,
|
|
},
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
|
|
w.Write([]byte(xml.Header))
|
|
enc := xml.NewEncoder(w)
|
|
enc.Indent("", " ")
|
|
enc.Encode(feed)
|
|
}
|
|
|
|
func (h *RSSHandler) fullURL(path string) string {
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
if h.Link == "" {
|
|
return path
|
|
}
|
|
return h.Link + "/uploads/" + path
|
|
}
|