fix(podcast): address CodeRabbit review feedback

- Remove total HTTP client timeout from episode downloads (use transport
  phase timeouts instead) and delete partial files on failure
- Keep SSE progress listener subscribed while the show view is mounted
- Return podcastEpisode (with streamId) from getNewestPodcasts
- Clean up dependent rows and MediaFiles in DeleteChannel within a tx
- Limit RSS feed body size to 32 MiB
- Block additional reserved IP ranges in the SSRF guard
- Fall back to enclosure URL when an item has no <guid>
- Handle clipboard write failure in the podcast list
This commit is contained in:
Jiho Andrew Lee 2026-09-30 15:42:14 +09:00
commit 4bebfad0ed
10 changed files with 126 additions and 52 deletions

View file

@ -225,25 +225,7 @@ func (api *Router) GetNewestPodcasts(r *http.Request) (*responses.Subsonic, erro
resp := newResponse()
resp.NewestPodcasts = &responses.NewestPodcasts{}
for _, ep := range eps {
child := responses.Child{
Id: ep.ID,
Title: ep.Title,
IsDir: false,
Parent: ep.ChannelID,
Duration: int32(ep.Duration),
Size: ep.Size,
BitRate: int32(ep.BitRate),
Suffix: ep.Suffix,
ContentType: ep.ContentType,
Type: "podcast",
ChannelId: ep.ChannelID,
Description: ep.Description,
Status: string(ep.Status),
}
if !ep.PublishDate.IsZero() {
child.PublishDate = ep.PublishDate.UTC().Format(time.RFC3339)
}
resp.NewestPodcasts.Episode = append(resp.NewestPodcasts.Episode, child)
resp.NewestPodcasts.Episode = append(resp.NewestPodcasts.Episode, buildPodcastEpisode(ep))
}
return resp, nil
}

View file

@ -118,12 +118,12 @@ var _ = Describe("Podcasts", func() {
BeforeEach(func() {
now := time.Now()
episodeRepo.Data = map[string]*model.PodcastEpisode{
"ep-1": {ID: "ep-1", Title: "Ep1", ChannelID: "ch-1", PublishDate: now.Add(-time.Hour), Status: model.PodcastStatusCompleted},
"ep-2": {ID: "ep-2", Title: "Ep2", ChannelID: "ch-1", PublishDate: now, Status: model.PodcastStatusNew},
"ep-1": {ID: "ep-1", Title: "Ep1", ChannelID: "ch-1", StreamID: "stream-ep-1", PublishDate: now.Add(-time.Hour), Status: model.PodcastStatusCompleted},
"ep-2": {ID: "ep-2", Title: "Ep2", ChannelID: "ch-1", StreamID: "stream-ep-2", PublishDate: now, Status: model.PodcastStatusNew},
}
})
It("returns episodes in Child format", func() {
It("returns episodes in podcastEpisode format", func() {
r := httptest.NewRequest("GET", "/rest/getNewestPodcasts", nil)
r = r.WithContext(userCtx)
@ -132,14 +132,14 @@ var _ = Describe("Podcasts", func() {
Expect(resp.NewestPodcasts.Episode).To(HaveLen(2))
})
It("sets type to podcast", func() {
It("includes streamId so clients can play the episode", func() {
r := httptest.NewRequest("GET", "/rest/getNewestPodcasts", nil)
r = r.WithContext(userCtx)
resp, err := api.GetNewestPodcasts(r)
Expect(err).ToNot(HaveOccurred())
for _, ep := range resp.NewestPodcasts.Episode {
Expect(ep.Type).To(Equal("podcast"))
Expect(ep.StreamId).To(Equal("stream-" + ep.ID))
}
})

View file

@ -169,12 +169,7 @@ type Child struct {
SongCount int32 `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
IsVideo bool `xml:"isVideo,attr,omitempty" json:"isVideo,omitempty"`
BookmarkPosition int64 `xml:"bookmarkPosition,attr,omitempty" json:"bookmarkPosition,omitempty"`
// Podcast-specific fields (used in getNewestPodcasts)
ChannelId string `xml:"channelId,attr,omitempty" json:"channelId,omitempty"`
Description string `xml:"description,attr,omitempty" json:"description,omitempty"`
Status string `xml:"status,attr,omitempty" json:"status,omitempty"`
PublishDate string `xml:"publishDate,attr,omitempty" json:"publishDate,omitempty"`
*OpenSubsonicChild `xml:",omitempty" json:",omitempty"`
*OpenSubsonicChild `xml:",omitempty" json:",omitempty"`
}
type OpenSubsonicChild struct {
@ -706,7 +701,7 @@ type Podcasts struct {
}
type NewestPodcasts struct {
Episode []Child `xml:"episode,omitempty" json:"episode,omitempty"`
Episode []PodcastEpisode `xml:"episode,omitempty" json:"episode,omitempty"`
}
type PodcastFundingResp struct {