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 }} - + + + - + + + + {{ 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)
{{ $pref.Title }}