mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-05 09:48:37 +01:00
Adds full support for the Podcasting 2.0 namespace (https://podcastindex.org/namespace/1.0) across RSS parsing, persistence, Subsonic API responses, and OpenSubsonic extensions. - podcast:guid: channel-level UUIDv5 for stable identity across feed URL changes - podcast:chapters: per-episode chapters URL + MIME type (chaptersUrl in API) - podcast:transcript: multiple transcripts per episode stored in a dedicated podcast_transcript table; attributes: url, type, language, rel - podcast:season: season number + optional name per episode - podcast:episode: episode number (decimal string) + optional display label - podcast:person: host/guest entries at both channel and episode level stored in a dedicated podcast_person table; attributes: name, role, group, img, href; role defaults to "host" and group defaults to "cast" per spec - podcast:locked: feed lock flag + optional owner email on channel - podcast:funding: first funding entry URL + display text on channel - podcast:medium: content type classification on channel - podcast:soundbite: startTime (float), duration (float), title per episode - podcast:updateFrequency: display text + rrule + complete flag on channel - podcast:podroll: creator-recommended feed list stored in podcast_podroll table (feedGuid, feedUrl, title, sort_order); returned as podroll[] in GetPodcasts - podcast:liveItem: live stream detection stored in podcast_live_item table (one row per channel, unique index); stores status, start/end times, enclosure URL/type, and contentLink for fallback playback; returned as liveItem object in GetPodcasts; Upsert preserves created_at on updates - podcast:podping: usesPodping boolean on channel; RefreshChannels skips channels with usesPodping=true (they receive updates via Podping WebSocket) - 20260428000000_add_podcast20.go: ALTER TABLE adds 9 columns to podcast_channel, 9 columns to podcast_episode; CREATE TABLE podcast_transcript (episode_id FK, url, mime_type, language, rel) and podcast_person (no FK constraints — put() serialises "" not NULL) - 20260428120000_add_podcast_tier3.go: ALTER TABLE adds uses_podping to podcast_channel; CREATE TABLE podcast_podroll and podcast_live_item (UNIQUE INDEX on channel_id) GetPodcasts response (PodcastChannel) gains: podcastGuid, locked, medium, fundingUrl, fundingText, updateFrequency, complete, usesPodping, person[], podroll[], liveItem{} GetPodcastEpisode response (PodcastEpisode) gains: season, seasonName, episode, episodeDisplay, chaptersUrl, soundbiteStart, soundbiteDur, transcript[], person[] GetPodcastEpisode now loads transcripts and persons from their repositories (previously only read the base episode row). New OpenSubsonic extensions declared: podcastChapters, podcastTranscripts, podcastSeason, podcastPerson, podcastFunding, podcastMedium, podcastPodroll, podcastLiveItem, podcastPodping - core/podcasts/rss_test.go: 41 new specs covering all namespace tags, default value handling (role→"host", group→"cast"), backward compatibility - core/podcasts/podcasts_test.go: 26 new service specs covering AddChannel field persistence, transcript/person saving, podroll/liveItem saving, RefreshChannels podping skip behaviour - persistence/podcast_transcript_repository_test.go: 10 specs - persistence/podcast_person_repository_test.go: 12 specs - persistence/podcast_podroll_repository_test.go: 10 specs - persistence/podcast_live_item_repository_test.go: 8 specs - server/subsonic/podcasts_test.go: 15 new handler specs for Tier 2 and Tier 3 fields in GetPodcasts and GetPodcastEpisode responses
59 lines
1.6 KiB
Go
59 lines
1.6 KiB
Go
package persistence
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
. "github.com/Masterminds/squirrel"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/id"
|
|
"github.com/pocketbase/dbx"
|
|
)
|
|
|
|
type podcastTranscriptRepository struct {
|
|
sqlRepository
|
|
}
|
|
|
|
func NewPodcastTranscriptRepository(ctx context.Context, db dbx.Builder) model.PodcastTranscriptRepository {
|
|
r := &podcastTranscriptRepository{}
|
|
r.ctx = ctx
|
|
r.db = db
|
|
r.registerModel(&model.PodcastTranscript{}, nil)
|
|
return r
|
|
}
|
|
|
|
func (r *podcastTranscriptRepository) GetByEpisode(episodeID string) (model.PodcastTranscripts, error) {
|
|
sel := r.newSelect().Columns("*").Where(Eq{"episode_id": episodeID})
|
|
var result model.PodcastTranscripts
|
|
err := r.queryAll(sel, &result)
|
|
return result, err
|
|
}
|
|
|
|
func (r *podcastTranscriptRepository) GetByEpisodes(episodeIDs []string) (model.PodcastTranscripts, error) {
|
|
if len(episodeIDs) == 0 {
|
|
return nil, nil
|
|
}
|
|
sel := r.newSelect().Columns("*").Where(Eq{"episode_id": episodeIDs})
|
|
var result model.PodcastTranscripts
|
|
err := r.queryAll(sel, &result)
|
|
return result, err
|
|
}
|
|
|
|
func (r *podcastTranscriptRepository) Save(transcripts []model.PodcastTranscript) error {
|
|
for i := range transcripts {
|
|
if transcripts[i].ID == "" {
|
|
transcripts[i].ID = id.NewRandom()
|
|
}
|
|
transcripts[i].CreatedAt = time.Now()
|
|
if _, err := r.put(transcripts[i].ID, &transcripts[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *podcastTranscriptRepository) DeleteByEpisode(episodeID string) error {
|
|
return r.delete(Eq{"episode_id": episodeID})
|
|
}
|
|
|
|
var _ model.PodcastTranscriptRepository = (*podcastTranscriptRepository)(nil)
|