diff --git a/db/db.go b/db/db.go index ab5a9485..2622c161 100644 --- a/db/db.go +++ b/db/db.go @@ -508,6 +508,9 @@ type PodcastEpisode struct { Status PodcastEpisodeStatus Error string Podcast *Podcast + Artist string + Album string + Image string } func (pe *PodcastEpisode) AudioLength() int { return pe.Length } diff --git a/db/migrations.go b/db/migrations.go index b532bfad..f702cba5 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -75,6 +75,7 @@ func (db *DB) Migrate(ctx MigrationContext) error { construct(ctx, "202405301140", migrateAddReplayGainFields), construct(ctx, "202501152035", migrateTrackAddIndexOnAlbumID), construct(ctx, "202501152036", migrateAlbumAddIndexOnParentID), + construct(ctx, "202502012036", migratePodcastEpisode), } return gormigrate. @@ -832,3 +833,9 @@ func migrateAlbumAddIndexOnParentID(tx *gorm.DB, _ MigrationContext) error { CREATE INDEX idx_albums_parent_id ON "albums" (parent_id); `).Error } + +func migratePodcastEpisode(tx *gorm.DB, _ MigrationContext) error { + return tx.AutoMigrate( + PodcastEpisode{}, + ).Error +} diff --git a/podcast/podcast.go b/podcast/podcast.go index 59786c90..643a9bc6 100644 --- a/podcast/podcast.go +++ b/podcast/podcast.go @@ -169,7 +169,7 @@ func (p *Podcasts) RefreshPodcast(podcast *db.Podcast, items []*gofeed.Item) err var episodeErrs []error for _, item := range items { - podcastEpisode, err := p.addEpisode(podcast.ID, item) + podcastEpisode, err := p.addEpisode(podcast.ID, item, podcast.Title) if err != nil { episodeErrs = append(episodeErrs, err) continue @@ -186,7 +186,7 @@ func (p *Podcasts) RefreshPodcast(podcast *db.Podcast, items []*gofeed.Item) err return errors.Join(episodeErrs...) } -func (p *Podcasts) addEpisode(podcastID int, item *gofeed.Item) (*db.PodcastEpisode, error) { +func (p *Podcasts) addEpisode(podcastID int, item *gofeed.Item, podcastTitle string) (*db.PodcastEpisode, error) { var duration int // if it has the media extension use it for _, content := range item.Extensions["media"]["content"] { @@ -200,14 +200,13 @@ func (p *Podcasts) addEpisode(podcastID int, item *gofeed.Item) (*db.PodcastEpis if duration == 0 && item.ITunesExt != nil { duration = getSecondsFromString(item.ITunesExt.Duration) } - - if episode, ok := p.findEnclosureAudio(podcastID, duration, item); ok { + if episode, ok := p.findEnclosureAudio(podcastID, duration, item, podcastTitle); ok { if err := p.db.Save(episode).Error; err != nil { return nil, err } return episode, nil } - if episode, ok := p.findMediaAudio(podcastID, duration, item); ok { + if episode, ok := p.findMediaAudio(podcastID, duration, item, podcastTitle); ok { if err := p.db.Save(episode).Error; err != nil { return nil, err } @@ -224,7 +223,7 @@ func (p *Podcasts) isAudio(rawItemURL string) (bool, error) { return p.tagReader.CanRead(itemURL.Path), nil } -func itemToEpisode(podcastID, size, duration int, audio string, item *gofeed.Item) *db.PodcastEpisode { +func itemToEpisode(podcastID, size, duration int, audio string, item *gofeed.Item, podcastTitle string) *db.PodcastEpisode { return &db.PodcastEpisode{ PodcastID: podcastID, Description: item.Description, @@ -234,21 +233,24 @@ func itemToEpisode(podcastID, size, duration int, audio string, item *gofeed.Ite PublishDate: item.PublishedParsed, AudioURL: audio, Status: db.PodcastEpisodeStatusSkipped, + Artist: item.ITunesExt.Author, + Album: podcastTitle, + Image: item.ITunesExt.Image, } } -func (p *Podcasts) findEnclosureAudio(podcastID, duration int, item *gofeed.Item) (*db.PodcastEpisode, bool) { +func (p *Podcasts) findEnclosureAudio(podcastID, duration int, item *gofeed.Item, podcastTitle string) (*db.PodcastEpisode, bool) { for _, enc := range item.Enclosures { if t, err := p.isAudio(enc.URL); !t || err != nil { continue } size, _ := strconv.Atoi(enc.Length) - return itemToEpisode(podcastID, size, duration, enc.URL, item), true + return itemToEpisode(podcastID, size, duration, enc.URL, item, podcastTitle), true } return nil, false } -func (p *Podcasts) findMediaAudio(podcastID, duration int, item *gofeed.Item) (*db.PodcastEpisode, bool) { +func (p *Podcasts) findMediaAudio(podcastID, duration int, item *gofeed.Item, podcastTitle string) (*db.PodcastEpisode, bool) { extensions, ok := item.Extensions["media"]["content"] if !ok { return nil, false @@ -257,7 +259,7 @@ func (p *Podcasts) findMediaAudio(podcastID, duration int, item *gofeed.Item) (* if t, err := p.isAudio(ext.Attrs["url"]); !t || err != nil { continue } - return itemToEpisode(podcastID, 0, duration, ext.Attrs["url"], item), true + return itemToEpisode(podcastID, 0, duration, ext.Attrs["url"], item, podcastTitle), true } return nil, false } diff --git a/server/ctrlsubsonic/handlers_bookmark.go b/server/ctrlsubsonic/handlers_bookmark.go index 7c6822b4..d1e44644 100644 --- a/server/ctrlsubsonic/handlers_bookmark.go +++ b/server/ctrlsubsonic/handlers_bookmark.go @@ -48,6 +48,18 @@ func (c *Controller) ServeGetBookmarks(r *http.Request) *spec.Response { return spec.NewError(10, "finding entry: %v", err) } respBookmark.Entry = spec.NewTrackByTags(&track, track.Album) + case specid.PodcastEpisode: + var podcastEpisode db.PodcastEpisode + err := c.dbc. + Preload("Podcast"). + Find(&podcastEpisode, "id=?", bookmark.EntryID). + Error + if err != nil { + return spec.NewError(10, "finding entry: %v", err) + } + respBookmark.Entry = spec.NewTCPodcastEpisode(&podcastEpisode) + default: + continue } sub.Bookmarks.List = append(sub.Bookmarks.List, respBookmark) diff --git a/server/ctrlsubsonic/spec/construct_by_folder.go b/server/ctrlsubsonic/spec/construct_by_folder.go index c39b695e..a720f189 100644 --- a/server/ctrlsubsonic/spec/construct_by_folder.go +++ b/server/ctrlsubsonic/spec/construct_by_folder.go @@ -127,6 +127,9 @@ func NewTCPodcastEpisode(pe *db.PodcastEpisode) *TrackChild { IsDir: false, Type: "podcastepisode", CreatedAt: pe.CreatedAt, + Album: pe.Album, + Artist: pe.Artist, + CoverID: pe.SID(), } if pe.Podcast != nil { trCh.ParentID = pe.Podcast.SID() @@ -136,7 +139,7 @@ func NewTCPodcastEpisode(pe *db.PodcastEpisode) *TrackChild { } func NewArtistByFolder(f *db.Album) *Artist { - // the db is structued around "browse by tags", and where + // the db is structured around "browse by tags", and where // an album is also a folder. so we're constructing an artist // from an "album" where // maybe TODO: rename the Album model to Folder diff --git a/server/ctrlsubsonic/spec/construct_podcast.go b/server/ctrlsubsonic/spec/construct_podcast.go index bff2555d..4a0d22d0 100644 --- a/server/ctrlsubsonic/spec/construct_podcast.go +++ b/server/ctrlsubsonic/spec/construct_podcast.go @@ -26,22 +26,25 @@ func NewPodcastEpisode(pe *db.PodcastEpisode) *PodcastEpisode { return nil } r := &PodcastEpisode{ - ID: pe.SID(), - StreamID: pe.SID(), - ContentType: pe.MIME(), - ChannelID: pe.PodcastSID(), - Title: pe.Title, - Description: CleanExternalText(pe.Description), - Status: string(pe.Status), - CoverArt: pe.PodcastSID(), - PublishDate: *pe.PublishDate, - Genre: "Podcast", - Duration: pe.Length, - Year: pe.PublishDate.Year(), - Suffix: formatExt(pe.Ext()), - BitRate: pe.Bitrate, - IsDir: false, - Size: pe.Size, + ID: pe.SID(), + StreamID: pe.SID(), + ContentType: pe.MIME(), + ChannelID: pe.PodcastSID(), + Title: pe.Title, + Description: CleanExternalText(pe.Description), + Status: string(pe.Status), + CoverArt: pe.PodcastSID(), + PublishDate: *pe.PublishDate, + Genre: "Podcast", + Duration: pe.Length, + Year: pe.PublishDate.Year(), + Suffix: formatExt(pe.Ext()), + BitRate: pe.Bitrate, + IsDir: false, + Size: pe.Size, + Album: pe.Album, + Artist: pe.Artist, + OriginalImageURL: pe.Image, } if pe.Podcast != nil { r.Path = pe.AbsPath() diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index 48055b08..8e00b745 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -394,24 +394,27 @@ type PodcastChannel struct { } type PodcastEpisode struct { - ID *specid.ID `xml:"id,attr" json:"id"` - StreamID *specid.ID `xml:"streamId,attr" json:"streamId"` - ChannelID *specid.ID `xml:"channelId,attr" json:"channelId"` - Title string `xml:"title,attr" json:"title"` - Description string `xml:"description,attr" json:"description"` - PublishDate time.Time `xml:"publishDate,attr" json:"publishDate"` - Status string `xml:"status,attr" json:"status"` - Parent string `xml:"parent,attr" json:"parent"` - IsDir bool `xml:"isDir,attr" json:"isDir"` - Year int `xml:"year,attr" json:"year"` - Genre string `xml:"genre,attr" json:"genre"` - CoverArt *specid.ID `xml:"coverArt,attr" json:"coverArt"` - Size int `xml:"size,attr" json:"size"` - ContentType string `xml:"contentType,attr" json:"contentType"` - Suffix string `xml:"suffix,attr" json:"suffix"` - Duration int `xml:"duration,attr" json:"duration"` - BitRate int `xml:"bitRate,attr" json:"bitrate"` - Path string `xml:"path,attr" json:"path"` + ID *specid.ID `xml:"id,attr" json:"id"` + StreamID *specid.ID `xml:"streamId,attr" json:"streamId"` + ChannelID *specid.ID `xml:"channelId,attr" json:"channelId"` + Title string `xml:"title,attr" json:"title"` + Description string `xml:"description,attr" json:"description"` + PublishDate time.Time `xml:"publishDate,attr" json:"publishDate"` + Status string `xml:"status,attr" json:"status"` + Parent string `xml:"parent,attr" json:"parent"` + IsDir bool `xml:"isDir,attr" json:"isDir"` + Year int `xml:"year,attr" json:"year"` + Genre string `xml:"genre,attr" json:"genre"` + CoverArt *specid.ID `xml:"coverArt,attr" json:"coverArt"` + Size int `xml:"size,attr" json:"size"` + ContentType string `xml:"contentType,attr" json:"contentType"` + Suffix string `xml:"suffix,attr" json:"suffix"` + Duration int `xml:"duration,attr" json:"duration"` + BitRate int `xml:"bitRate,attr" json:"bitrate"` + Path string `xml:"path,attr" json:"path"` + Album string `xml:"album,attr" json:"album"` + Artist string `xml:"artist,attr" json:"artist"` + OriginalImageURL string `xml:"originalImageUrl,attr" json:"originalImageUrl"` } type Bookmarks struct {