Files
jbrechtel 9e9124ecb0
Build and Deploy / build-and-deploy (push) Successful in 56s
Add podcast email field for RSS itunes:owner tag
Enables users to set a podcast contact email from the dashboard
settings. This email is included in the RSS feed as
itunes:owner/itunes:email, which podcast directories like Spotify
and Apple Podcasts require for indexing and verification.
2026-07-12 17:29:10 -04:00

182 lines
4.1 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"`
ItunesOwner *rssItunesOwner `xml:"itunes:owner,omitempty"`
Image *rssImage `xml:"image,omitempty"`
ItunesImage *rssItunesImage `xml:"itunes:image,omitempty"`
ItunesType string `xml:"itunes:type,omitempty"`
Items []rssItem `xml:"item"`
}
type rssItunesOwner struct {
Name string `xml:"itunes:name"`
Email string `xml:"itunes:email"`
}
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)}
}
pubStr := ep.PublishedAt
if pt, err := time.Parse("2006-01-02 15:04:05", ep.PublishedAt); err == nil {
pubStr = pt.Format(time.RFC1123Z)
}
items = append(items, rssItem{
Title: ep.Title,
Description: ep.Description,
Enclosure: rssEnclosure{
URL: h.fullURL(ep.AudioPath),
Type: "audio/mpeg",
},
PubDate: pubStr,
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)}
}
var owner *rssItunesOwner
if user.PodcastEmail != "" || author != "" {
owner = &rssItunesOwner{
Name: author,
Email: user.PodcastEmail,
}
}
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,
ItunesOwner: owner,
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
}