diff --git a/server/assets/pages/home.tmpl b/server/assets/pages/home.tmpl
index 57ab2ab..66ef8c9 100644
--- a/server/assets/pages/home.tmpl
+++ b/server/assets/pages/home.tmpl
@@ -179,9 +179,22 @@
{{ range $pref := .Podcasts }}
-
+
+
+
| {{ $pref.Title }} |
- |
+ |
+ |
+ |
+ |
{{ end }}
diff --git a/server/ctrladmin/ctrl.go b/server/ctrladmin/ctrl.go
index eddc4e8..085e335 100644
--- a/server/ctrladmin/ctrl.go
+++ b/server/ctrladmin/ctrl.go
@@ -128,7 +128,7 @@ type templateData struct {
DefaultListenBrainzURL string
SelectedUser *db.User
//
- Podcasts []*db.Podcast
+ Podcasts []*db.Podcast
}
type Response struct {
diff --git a/server/ctrladmin/handlers.go b/server/ctrladmin/handlers.go
index c28153e..9ccc7b1 100644
--- a/server/ctrladmin/handlers.go
+++ b/server/ctrladmin/handlers.go
@@ -442,6 +442,47 @@ func (c *Controller) ServePodcastAddDo(r *http.Request) *Response {
}
}
+func (c *Controller) ServePodcastDownloadDo(r *http.Request) *Response {
+ id, err := strconv.Atoi(r.URL.Query().Get("id"))
+ if err != nil {
+ return &Response{
+ err: "please provide a valid podcast id",
+ code: 400,
+ }
+ }
+ if err := c.Podcasts.DownloadPodcastAll(id); err != nil {
+ return &Response{
+ err: "please provide a valid podcast id",
+ code: 400,
+ }
+ }
+ return &Response{
+ redirect: "/admin/home",
+ flashN: []string{"started downloading podcast episodes"},
+ }
+}
+
+func (c *Controller) ServePodcastUpdateDo(r *http.Request) *Response {
+ id, err := strconv.Atoi(r.URL.Query().Get("id"))
+ if err != nil {
+ return &Response{
+ err: "please provide a valid podcast id",
+ code: 400,
+ }
+ }
+ autoDlSetting := r.FormValue("auto_dl")
+ if err := c.Podcasts.SetAutoDownload(id, autoDlSetting); err != nil {
+ return &Response{
+ err: "please provide a valid podcast id and dl setting",
+ code: 400,
+ }
+ }
+ return &Response{
+ redirect: "/admin/home",
+ flashN: []string{"future podcast episodes will be automatically downloaded"},
+ }
+}
+
func (c *Controller) ServePodcastDeleteDo(r *http.Request) *Response {
user := r.Context().Value(CtxUser).(*db.User)
id, err := strconv.Atoi(r.URL.Query().Get("id"))
diff --git a/server/db/db.go b/server/db/db.go
index 0886c7e..defa5f4 100644
--- a/server/db/db.go
+++ b/server/db/db.go
@@ -81,6 +81,7 @@ func New(path string) (*DB, error) {
migrateListenBrainz(),
migratePodcast(),
migrateBookmarks(),
+ migratePodcastAutoDownload(),
))
if err = migr.Migrate(); err != nil {
return nil, fmt.Errorf("migrating to latest version: %w", err)
diff --git a/server/db/migrations.go b/server/db/migrations.go
index 141c51b..162dd6c 100644
--- a/server/db/migrations.go
+++ b/server/db/migrations.go
@@ -250,3 +250,15 @@ func migrateBookmarks() gormigrate.Migration {
},
}
}
+
+func migratePodcastAutoDownload() gormigrate.Migration {
+ return gormigrate.Migration{
+ ID: "202102191448",
+ Migrate: func(tx *gorm.DB) error {
+ return tx.AutoMigrate(
+ Podcast{},
+ ).
+ Error
+ },
+ }
+}
diff --git a/server/db/model.go b/server/db/model.go
index 3f13dc0..7e60c69 100644
--- a/server/db/model.go
+++ b/server/db/model.go
@@ -290,17 +290,18 @@ type AlbumGenre struct {
}
type Podcast struct {
- ID int `gorm:"primary_key"`
- UpdatedAt time.Time
- ModifiedAt time.Time
- UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
- URL string
- Title string
- Description string
- ImageURL string
- ImagePath string
- Error string
- Episodes []*PodcastEpisode
+ ID int `gorm:"primary_key"`
+ UpdatedAt time.Time
+ ModifiedAt time.Time
+ UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
+ URL string
+ Title string
+ Description string
+ ImageURL string
+ ImagePath string
+ Error string
+ Episodes []*PodcastEpisode
+ AutoDownload string
}
func (p *Podcast) Fullpath(podcastPath string) string {
diff --git a/server/podcasts/podcasts.go b/server/podcasts/podcasts.go
index c98607c..8bde1ab 100644
--- a/server/podcasts/podcasts.go
+++ b/server/podcasts/podcasts.go
@@ -33,6 +33,10 @@ const (
episodeDownloading = "downloading"
episodeSkipped = "skipped"
episodeDeleted = "deleted"
+ episodeCompleted = "completed"
+
+ autoDlLatest = "dl_latest"
+ autoDlNone = "dl_none"
)
func (p *Podcasts) GetPodcastOrAll(userID int, id int, includeEpisodes bool) ([]*db.Podcast, error) {
@@ -87,7 +91,7 @@ func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed,
if err := p.DB.Save(&podcast).Error; err != nil {
return &podcast, err
}
- if err := p.AddNewEpisodes(podcast.ID, feed.Items); err != nil {
+ if err := p.AddNewEpisodes(&podcast, feed.Items); err != nil {
return nil, err
}
go func() {
@@ -98,6 +102,28 @@ func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed,
return &podcast, nil
}
+var errNoSuchAutoDlOption = errors.New("invalid autodownload setting")
+
+func (p *Podcasts) SetAutoDownload(podcastID int, setting string) error {
+ podcast := db.Podcast{}
+ err := p.DB.
+ Where("id=?", podcastID).
+ First(&podcast).
+ Error
+ if err != nil {
+ return err
+ }
+ switch setting {
+ case autoDlLatest:
+ podcast.AutoDownload = autoDlLatest
+ return p.DB.Save(&podcast).Error
+ case autoDlNone:
+ podcast.AutoDownload = autoDlNone
+ return p.DB.Save(&podcast).Error
+ }
+ return errNoSuchAutoDlOption
+}
+
func getEntriesAfterDate(feed []*gofeed.Item, after time.Time) []*gofeed.Item {
items := []*gofeed.Item{}
for _, item := range feed {
@@ -109,10 +135,10 @@ func getEntriesAfterDate(feed []*gofeed.Item, after time.Time) []*gofeed.Item {
return items
}
-func (p *Podcasts) AddNewEpisodes(podcastID int, items []*gofeed.Item) error {
+func (p *Podcasts) AddNewEpisodes(podcast *db.Podcast, items []*gofeed.Item) error {
podcastEpisode := db.PodcastEpisode{}
err := p.DB.
- Where("podcast_id=?", podcastID).
+ Where("podcast_id=?", podcast.ID).
Order("publish_date DESC").
First(&podcastEpisode).Error
itemFound := true
@@ -123,16 +149,23 @@ func (p *Podcasts) AddNewEpisodes(podcastID int, items []*gofeed.Item) error {
}
if !itemFound {
for _, item := range items {
- if err := p.AddEpisode(podcastID, item); err != nil {
+ if _, err := p.AddEpisode(podcast.ID, item); err != nil {
return err
}
}
return nil
}
for _, item := range getEntriesAfterDate(items, *podcastEpisode.PublishDate) {
- if err := p.AddEpisode(podcastID, item); err != nil {
+ episode, err := p.AddEpisode(podcast.ID, item)
+ if err != nil {
return err
}
+ if podcast.AutoDownload == autoDlLatest &&
+ (episode.Status != episodeCompleted && episode.Status != episodeDownloading) {
+ if err := p.DownloadEpisode(episode.ID); err != nil {
+ return err
+ }
+ }
}
return nil
}
@@ -157,7 +190,7 @@ func getSecondsFromString(time string) int {
return 0
}
-func (p *Podcasts) AddEpisode(podcastID int, item *gofeed.Item) error {
+func (p *Podcasts) AddEpisode(podcastID int, item *gofeed.Item) (*db.PodcastEpisode, error) {
duration := 0
// if it has the media extension use it
for _, content := range item.Extensions["media"]["content"] {
@@ -174,19 +207,19 @@ func (p *Podcasts) AddEpisode(podcastID int, item *gofeed.Item) error {
if episode, ok := p.findEnclosureAudio(podcastID, duration, item); ok {
if err := p.DB.Save(episode).Error; err != nil {
- return err
+ return nil, err
}
- return nil
+ return episode, nil
}
if episode, ok := p.findMediaAudio(podcastID, duration, item); ok {
if err := p.DB.Save(episode).Error; err != nil {
- return err
+ return nil, err
}
- return nil
+ return episode, nil
}
// hopefully shouldnt reach here
log.Println("failed to find audio in feed item, skipping")
- return nil
+ return nil, nil
}
func isAudio(mediaType, url string) bool {
@@ -275,7 +308,7 @@ func (p *Podcasts) refreshPodcasts(podcasts []*db.Podcast) error {
errs.Add(fmt.Errorf("refreshing podcast with url %q: %w", podcast.URL, err))
continue
}
- if err = p.AddNewEpisodes(podcast.ID, feed.Items); err != nil {
+ if err = p.AddNewEpisodes(podcast, feed.Items); err != nil {
errs.Add(fmt.Errorf("adding episodes: %w", err))
continue
}
@@ -283,6 +316,30 @@ func (p *Podcasts) refreshPodcasts(podcasts []*db.Podcast) error {
return errs
}
+func (p *Podcasts) DownloadPodcastAll(podcastID int) error {
+ podcastEpisodes := []db.PodcastEpisode{}
+ err := p.DB.
+ Where("podcast_id=?", podcastID).
+ Find(&podcastEpisodes).
+ Error
+ if err != nil {
+ return fmt.Errorf("get episodes by podcast id: %w", err)
+ }
+ go func() {
+ for _, episode := range podcastEpisodes {
+ if episode.Status == episodeDownloading || episode.Status == episodeCompleted {
+ log.Println("skipping episode is in progress or already downloaded")
+ continue
+ }
+ if err := p.DownloadEpisode(episode.ID); err != nil {
+ log.Println(err)
+ }
+ log.Printf("Finished downloading episode: \"%s\"", episode.Title)
+ }
+ }()
+ return nil
+}
+
func (p *Podcasts) DownloadEpisode(episodeID int) error {
podcastEpisode := db.PodcastEpisode{}
podcast := db.Podcast{}
@@ -412,13 +469,13 @@ func (p *Podcasts) doPodcastDownload(podcastEpisode *db.PodcastEpisode, file *os
podcastPath := path.Join(p.PodcastBasePath, podcastEpisode.Path)
podcastTags, err := tags.New(podcastPath)
if err != nil {
- log.Printf("error parsing podcast: %e", err)
+ log.Printf("error parsing podcast audio: %e", err)
podcastEpisode.Status = "error"
p.DB.Save(podcastEpisode)
return nil
}
podcastEpisode.Bitrate = podcastTags.Bitrate()
- podcastEpisode.Status = "completed"
+ podcastEpisode.Status = episodeCompleted
podcastEpisode.Length = podcastTags.Length()
podcastEpisode.Size = int(stat.Size())
return p.DB.Save(podcastEpisode).Error
diff --git a/server/server.go b/server/server.go
index b9e93c6..c096048 100644
--- a/server/server.go
+++ b/server/server.go
@@ -145,10 +145,10 @@ func setupAdmin(r *mux.Router, ctrl *ctrladmin.Controller) {
routUser.Handle("/delete_playlist_do", ctrl.H(ctrl.ServeDeletePlaylistDo))
routUser.Handle("/create_transcode_pref_do", ctrl.H(ctrl.ServeCreateTranscodePrefDo))
routUser.Handle("/delete_transcode_pref_do", ctrl.H(ctrl.ServeDeleteTranscodePrefDo))
- if ctrl.Podcasts.PodcastBasePath != "" {
- routUser.Handle("/add_podcast_do", ctrl.H(ctrl.ServePodcastAddDo))
- routUser.Handle("/delete_podcast_do", ctrl.H(ctrl.ServePodcastDeleteDo))
- }
+ routUser.Handle("/add_podcast_do", ctrl.H(ctrl.ServePodcastAddDo))
+ routUser.Handle("/delete_podcast_do", ctrl.H(ctrl.ServePodcastDeleteDo))
+ routUser.Handle("/download_podcast_do", ctrl.H(ctrl.ServePodcastDownloadDo))
+ routUser.Handle("/update_podcast_do", ctrl.H(ctrl.ServePodcastUpdateDo))
// ** begin admin routes (if session is valid, and is admin)
routAdmin := routUser.NewRoute().Subrouter()
routAdmin.Use(ctrl.WithAdminSession)