diff --git a/handlers.go b/handlers.go index 03d8792..e5d75a3 100644 --- a/handlers.go +++ b/handlers.go @@ -1,6 +1,8 @@ package main import ( + "encoding/json" + "errors" "fmt" "net/http" "os" @@ -121,6 +123,10 @@ func downloadHandler(c echo.Context) error { }) } +func isPlaylistUrl(url string) bool { + return strings.Contains(strings.ToLower(url), "playlist") +} + func downloadPostHandler(c echo.Context) error { url := c.FormValue("url") userID := c.Get("user_id").(uint) @@ -135,15 +141,28 @@ func downloadPostHandler(c echo.Context) error { return c.Redirect(http.StatusSeeOther, "/download") } - original := Original{ - URL: url, - UserID: userID, - Status: Pending, - Audio: audioOnly, - Video: !audioOnly, + if isPlaylistUrl(url) { + playlist := Playlist{ + URL: url, + UserID: userID, + Audio: audioOnly, + Video: !audioOnly, + } + db.Create(&playlist) + go startPlaylist(playlist.ID, url, audioOnly) + + } else { + original := Original{ + URL: url, + UserID: userID, + Status: Pending, + Audio: audioOnly, + Video: !audioOnly, + } + db.Create(&original) + go startDownload(original.ID, url, audioOnly) } - db.Create(&original) - go startDownload(original.ID, url, audioOnly) + return c.Redirect(http.StatusSeeOther, "/videos") } @@ -162,6 +181,26 @@ func getYtdlpTitle(url string, args []string) (string, error) { return strings.TrimSpace(string(stdout)), nil } +func getYtdlpPlaylistTitle(url string) (string, error) { + stdout, _, err := runYtdlp("--flat-playlist", "--dump-single-json", url) + if err != nil { + log.Errorln(err) + return "", err + } + + var data map[string]interface{} + err = json.Unmarshal(stdout, &data) + if err != nil { + return "", err + } + title, ok := data["title"].(string) + if !ok { + return "", errors.New("title field not found or not a string") + } + + return title, nil +} + func getYtdlpArtist(url string, args []string) (string, error) { args = append(args, "--simulate", "--print", "%(uploader)s", url) stdout, _, err := runYtdlp(args...) @@ -643,14 +682,61 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) { processOriginal(originalID) } +func startPlaylist(id uint, url string, audioOnly bool) { + // retrieve playlist metadata + title, err := getYtdlpPlaylistTitle(url) + if err != nil { + SetPlaylistStatus(id, Failed) + return + } + err = db.Model(&Playlist{}).Where("id = ?", id).Updates(map[string]interface{}{ + "title": title, + }).Error + if err != nil { + SetPlaylistStatus(id, Failed) + return + } + + // populate playlist entries + stdout, _, err := runYtdlp("--get-id", "--flat-playlist", url) + if err != nil { + SetPlaylistStatus(id, Failed) + return + } + for _, line := range strings.Split(string(stdout), "\n") { + line = strings.TrimSpace(line) + if line != "" { + original := Original{ + URL: fmt.Sprintf("https://www.youtube.com/watch?v=%s", line), + Status: Pending, + Video: !audioOnly, + Audio: audioOnly, + Playlist: true, + PlaylistID: id, + } + err = db.Create(&original).Error + if err != nil { + SetPlaylistStatus(id, Failed) + return + } + } + } + SetPlaylistStatus(id, Completed) +} + func videosHandler(c echo.Context) error { userID := c.Get("user_id").(uint) var origs []Original db.Where("user_id = ?", userID).Find(&origs) + + var playlists []Playlist + db.Where("user_id = ?", userID).Find(&playlists) + return c.Render(http.StatusOK, "videos.html", map[string]interface{}{ - "videos": origs, - "Footer": makeFooter(), + "videos": origs, + "playlists": playlists, + "Footer": makeFooter(), }) } @@ -1000,3 +1086,19 @@ func processHandler(c echo.Context) error { return c.Redirect(http.StatusSeeOther, "/videos") } + +func playlistHandler(c echo.Context) error { + referrer := c.Request().Referer() + if referrer == "" { + referrer = "/videos" + } + return c.Redirect(http.StatusSeeOther, referrer) +} + +func deletePlaylistHandler(c echo.Context) error { + referrer := c.Request().Referer() + if referrer == "" { + referrer = "/videos" + } + return c.Redirect(http.StatusSeeOther, referrer) +} diff --git a/main.go b/main.go index c7bf832..e540ba6 100644 --- a/main.go +++ b/main.go @@ -80,7 +80,7 @@ func main() { sqlDB.SetMaxOpenConns(1) // Migrate the schema - db.AutoMigrate(&Original{}, &media.Video{}, &media.Audio{}, &User{}, &TempURL{}, &Transcode{}) + db.AutoMigrate(&Original{}, &Playlist{}, &media.Video{}, &media.Audio{}, &User{}, &TempURL{}, &Transcode{}) go PeriodicCleanup() // create a user @@ -129,6 +129,9 @@ func main() { e.POST("/transcode_to_video/:id", transcodeToVideoHandler, authMiddleware) e.POST("/transcode_to_audio/:id", transcodeToAudioHandler, authMiddleware) + e.GET("/p/:id", playlistHandler, authMiddleware) + e.POST("/p/:id/delete", deletePlaylistHandler, authMiddleware) + dataGroup := e.Group("/data") dataGroup.Use(authMiddleware) dataGroup.Static("/", getDataDir()) diff --git a/models.go b/models.go index 92fa763..52c23da 100644 --- a/models.go +++ b/models.go @@ -25,13 +25,17 @@ const ( type Original struct { gorm.Model - UserID uint - URL string - Title string - Artist string - Status OriginalStatus - Audio bool // video download requested - Video bool // audio download requested + UserID uint + URL string + Title string + Artist string + Status OriginalStatus + Audio bool // video download requested + Video bool // audio download requested + Watched bool + + Playlist bool // part of a playlist + PlaylistID uint // Playlist.ID (if part of a playlist) } type Transcode struct { @@ -53,6 +57,16 @@ type Transcode struct { Rate uint } +type Playlist struct { + gorm.Model + UserID uint + URL string + Title string + Status OriginalStatus + Audio bool + Video bool +} + type User struct { gorm.Model Username string `gorm:"unique"` @@ -91,6 +105,10 @@ func SetOriginalStatus(id uint, status OriginalStatus) error { return db.Model(&Original{}).Where("id = ?", id).Update("status", status).Error } +func SetPlaylistStatus(id uint, status OriginalStatus) error { + return db.Model(&Playlist{}).Where("id = ?", id).Update("status", status).Error +} + func NewDownloadManager() *DownloadManager { return &DownloadManager{ downloads: make(map[uint]*DownloadStatus), diff --git a/templates/videos.html b/templates/videos.html index 21b7394..b95a834 100644 --- a/templates/videos.html +++ b/templates/videos.html @@ -53,6 +53,37 @@ {{end}} + +

Playlists

+
+ {{range .playlists}} +
+
+ {{if eq .Status "completed"}} + {{.Title}} + {{else}} + {{.Title}} + {{end}} +
+
+ {{if eq .Status "completed"}} +
+ +
+ {{else if eq .Status "failed"}} +
+ +
+ {{else if eq .Status "downloading"}} + {{end}} +
+ +
+
+
+ {{end}} +
+

Download New Video

Logout