Add podcast email field for RSS itunes:owner tag
Build and Deploy / build-and-deploy (push) Successful in 56s

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.
This commit is contained in:
2026-07-12 17:29:10 -04:00
parent 9011a00475
commit 9e9124ecb0
5 changed files with 39 additions and 10 deletions
+1
View File
@@ -84,6 +84,7 @@ func (h *EpisodeHandler) ServeDashboard(w http.ResponseWriter, r *http.Request)
"PodcastSlug": user.PodcastSlug, "PodcastSlug": user.PodcastSlug,
"PodcastImageURL": podcastImageURL, "PodcastImageURL": podcastImageURL,
"PodcastAuthor": user.PodcastAuthor, "PodcastAuthor": user.PodcastAuthor,
"PodcastEmail": user.PodcastEmail,
"RSSURL": template.URL(h.BaseURL + "/rss/" + user.PodcastSlug), "RSSURL": template.URL(h.BaseURL + "/rss/" + user.PodcastSlug),
}) })
} }
+16
View File
@@ -26,11 +26,18 @@ type rssChannel struct {
Description string `xml:"description"` Description string `xml:"description"`
Language string `xml:"language"` Language string `xml:"language"`
Author string `xml:"itunes:author,omitempty"` Author string `xml:"itunes:author,omitempty"`
ItunesOwner *rssItunesOwner `xml:"itunes:owner,omitempty"`
Image *rssImage `xml:"image,omitempty"` Image *rssImage `xml:"image,omitempty"`
ItunesImage *rssItunesImage `xml:"itunes:image,omitempty"` ItunesImage *rssItunesImage `xml:"itunes:image,omitempty"`
ItunesType string `xml:"itunes:type,omitempty"`
Items []rssItem `xml:"item"` Items []rssItem `xml:"item"`
} }
type rssItunesOwner struct {
Name string `xml:"itunes:name"`
Email string `xml:"itunes:email"`
}
type rssImage struct { type rssImage struct {
URL string `xml:"url"` URL string `xml:"url"`
Title string `xml:"title"` Title string `xml:"title"`
@@ -132,6 +139,14 @@ func (h *RSSHandler) ServeRSS(w http.ResponseWriter, r *http.Request) {
chanItunesImage = &rssItunesImage{Href: h.fullURL(user.PodcastImage)} chanItunesImage = &rssItunesImage{Href: h.fullURL(user.PodcastImage)}
} }
var owner *rssItunesOwner
if user.PodcastEmail != "" || author != "" {
owner = &rssItunesOwner{
Name: author,
Email: user.PodcastEmail,
}
}
feed := rssFeed{ feed := rssFeed{
Version: "2.0", Version: "2.0",
ItunesNS: "http://www.itunes.com/dtds/podcast-1.0.dtd", ItunesNS: "http://www.itunes.com/dtds/podcast-1.0.dtd",
@@ -141,6 +156,7 @@ func (h *RSSHandler) ServeRSS(w http.ResponseWriter, r *http.Request) {
Description: title, Description: title,
Language: "en-us", Language: "en-us",
Author: author, Author: author,
ItunesOwner: owner,
Image: chanImage, Image: chanImage,
ItunesImage: chanItunesImage, ItunesImage: chanItunesImage,
Items: items, Items: items,
+6 -1
View File
@@ -64,6 +64,11 @@ func (h *SettingsHandler) ServeSettings(w http.ResponseWriter, r *http.Request)
author = user.PodcastAuthor author = user.PodcastAuthor
} }
email := r.FormValue("podcast_email")
if email == "" {
email = user.PodcastEmail
}
image := user.PodcastImage image := user.PodcastImage
// Handle podcast image upload // Handle podcast image upload
@@ -90,6 +95,6 @@ func (h *SettingsHandler) ServeSettings(w http.ResponseWriter, r *http.Request)
} }
} }
h.Store.UpdateUserPodcast(userID, title, slug, image, author) h.Store.UpdateUserPodcast(userID, title, slug, image, author, email)
http.Redirect(w, r, "/?success=podcast+settings+saved", http.StatusSeeOther) http.Redirect(w, r, "/?success=podcast+settings+saved", http.StatusSeeOther)
} }
+11 -9
View File
@@ -14,6 +14,7 @@ type User struct {
PodcastSlug string PodcastSlug string
PodcastImage string PodcastImage string
PodcastAuthor string PodcastAuthor string
PodcastEmail string
CreatedAt string CreatedAt string
} }
@@ -56,6 +57,7 @@ func migrate(db *sql.DB) error {
podcast_slug TEXT NOT NULL DEFAULT '', podcast_slug TEXT NOT NULL DEFAULT '',
podcast_image TEXT NOT NULL DEFAULT '', podcast_image TEXT NOT NULL DEFAULT '',
podcast_author TEXT NOT NULL DEFAULT '', podcast_author TEXT NOT NULL DEFAULT '',
podcast_email TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
@@ -96,8 +98,8 @@ func (s *Store) CreateUser(email, passwordHash string) (int64, error) {
func (s *Store) UserByEmail(email string) (*User, error) { func (s *Store) UserByEmail(email string) (*User, error) {
u := &User{} u := &User{}
err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_slug, podcast_image, podcast_author, created_at FROM users WHERE email = ?", email). err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_slug, podcast_image, podcast_author, podcast_email, created_at FROM users WHERE email = ?", email).
Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastSlug, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt) Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastSlug, &u.PodcastImage, &u.PodcastAuthor, &u.PodcastEmail, &u.CreatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -106,8 +108,8 @@ func (s *Store) UserByEmail(email string) (*User, error) {
func (s *Store) GetUser(id int64) (*User, error) { func (s *Store) GetUser(id int64) (*User, error) {
u := &User{} u := &User{}
err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_slug, podcast_image, podcast_author, created_at FROM users WHERE id = ?", id). err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_slug, podcast_image, podcast_author, podcast_email, created_at FROM users WHERE id = ?", id).
Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastSlug, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt) Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastSlug, &u.PodcastImage, &u.PodcastAuthor, &u.PodcastEmail, &u.CreatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -116,8 +118,8 @@ func (s *Store) GetUser(id int64) (*User, error) {
func (s *Store) UserBySlug(slug string) (*User, error) { func (s *Store) UserBySlug(slug string) (*User, error) {
u := &User{} u := &User{}
err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_slug, podcast_image, podcast_author, created_at FROM users WHERE podcast_slug = ?", slug). err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_slug, podcast_image, podcast_author, podcast_email, created_at FROM users WHERE podcast_slug = ?", slug).
Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastSlug, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt) Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastSlug, &u.PodcastImage, &u.PodcastAuthor, &u.PodcastEmail, &u.CreatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -136,9 +138,9 @@ func (s *Store) SlugTaken(slug string, excludeUserID int64) (bool, error) {
return true, nil return true, nil
} }
func (s *Store) UpdateUserPodcast(id int64, title, slug, image, author string) error { func (s *Store) UpdateUserPodcast(id int64, title, slug, image, author, email string) error {
_, err := s.db.Exec("UPDATE users SET podcast_title=?, podcast_slug=?, podcast_image=?, podcast_author=? WHERE id=?", _, err := s.db.Exec("UPDATE users SET podcast_title=?, podcast_slug=?, podcast_image=?, podcast_author=?, podcast_email=? WHERE id=?",
title, slug, image, author, id) title, slug, image, author, email, id)
return err return err
} }
+5
View File
@@ -42,6 +42,11 @@
<input type="text" id="podcast_author" name="podcast_author" class="nb-input blue" <input type="text" id="podcast_author" name="podcast_author" class="nb-input blue"
value="{{.PodcastAuthor}}" placeholder="Podcast Host" /> value="{{.PodcastAuthor}}" placeholder="Podcast Host" />
</div> </div>
<div class="form-group form-flex-1">
<label for="podcast_email">Podcast Email</label>
<input type="email" id="podcast_email" name="podcast_email" class="nb-input blue"
value="{{.PodcastEmail}}" placeholder="podcast@example.com" />
</div>
<div class="form-group"> <div class="form-group">
<label for="podcast_image">Podcast Image</label> <label for="podcast_image">Podcast Image</label>
<div class="file-upload"> <div class="file-upload">