feat(podcasts+ops): Apple launcher-scheme feeds + visible build stamp
Release / build-and-push (push) Successful in 3m29s

Apple scheme normalization (follow-up to the Scribl discussion): Add
Feed accepts the launcher spellings podcast pages ship next to plain
RSS — itpc://, podcasts://, feed:// — and rewrites them to the https
feed URL they wrap (scheme match case-insensitive). http stays and the
guard still rejects it. Unit-pinned.

Build stamp (stale-container drift guard): the 2026-10-29 podcast
session lost an hour to a container running a build that predated the
feature. version.Commit rides ldflags from a GIT_COMMIT build arg; the
dev compose passes git rev-parse --short HEAD via the Makefile; the
app logs 'Bookhoard <ver> (commit <hash>)' at boot and /api/health
reports it — the UI and the repo can no longer silently diverge.
This commit is contained in:
John O'Keefe committed 2026-10-29 20:12:39 -04:00
1 parent acbcc61ae4
commit 253d8d0e8e
8 files changed
+49 -2

No files matched your search

+18 -1
View File
@@ -156,7 +156,7 @@ func (s *PodcastService) AddFeed(ctx context.Context, userID pgtype.UUID, isAdmi
}
}
feedURL := strings.TrimSpace(rawFeedURL)
feedURL := normalizeFeedURL(rawFeedURL)
if feedURL == "" {
return nil, errBadRequest("feed URL is required")
}
@@ -711,6 +711,23 @@ func sortedStringSet(set map[string]bool) []string {
// classifyFetchError maps guard/HTTP errors to user-facing text (the
// add-feed dialog and the download_error column both render it).
// normalizeFeedURL accepts the launcher-scheme spellings podcast
// pages hand out alongside plain RSS (issue follow-up: "one says RSS,
// the other says Apple Podcast") and rewrites them to the https URL
// they wrap: itpc:// and podcasts:// are Apple's subscribe-launcher
// aliases for the SAME feed document; feed:// is the generic form.
// Anything else keeps its scheme — guardFetch enforces https.
func normalizeFeedURL(raw string) string {
u := strings.TrimSpace(raw)
lowered := strings.ToLower(u)
for _, prefix := range []string{"itpc://", "podcasts://", "feed://"} {
if strings.HasPrefix(lowered, prefix) {
return "https://" + u[len(prefix):]
}
}
return u
}
func classifyFetchError(err error) error {
if errors.Is(err, ErrBlockedPrivateAddress) {
return fmt.Errorf("blocked: private/loopback addresses are not fetchable")