Some playlist support

This commit is contained in:
Carl Pearson
2024-10-10 06:18:55 -06:00
parent 5ee0154b2b
commit 93eb5ca130
4 changed files with 172 additions and 18 deletions

View File

@@ -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)
}

View File

@@ -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())

View File

@@ -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),

View File

@@ -53,6 +53,37 @@
</div>
{{end}}
</div>
<h1>Playlists</h1>
<div class="video-list">
{{range .playlists}}
<div class="video-card">
<div class="video-title">
{{if eq .Status "completed"}}
<a href="/p/{{.ID}}">{{.Title}}</a>
{{else}}
{{.Title}}
{{end}}
</div>
<div class="video-options">
{{if eq .Status "completed"}}
<form action="/p/{{.ID}}/process" method="post" style="display:inline;">
<button type="submit">Reprocess</button>
</form>
{{else if eq .Status "failed"}}
<form action="/p/{{.ID}}/restart" method="post" style="display:inline;">
<button type="submit">Restart</button>
</form>
{{else if eq .Status "downloading"}}
{{end}}
<form action="/p/{{.ID}}/delete" method="post" style="display:inline;">
<button type="submit">Delete</button>
</form>
</div>
</div>
{{end}}
</div>
<p><a href="/download">Download New Video</a></p>
<p><a href="/logout">Logout</a></p>