Some playlist support
This commit is contained in:
122
handlers.go
122
handlers.go
@@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"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 {
|
func downloadPostHandler(c echo.Context) error {
|
||||||
url := c.FormValue("url")
|
url := c.FormValue("url")
|
||||||
userID := c.Get("user_id").(uint)
|
userID := c.Get("user_id").(uint)
|
||||||
@@ -135,15 +141,28 @@ func downloadPostHandler(c echo.Context) error {
|
|||||||
return c.Redirect(http.StatusSeeOther, "/download")
|
return c.Redirect(http.StatusSeeOther, "/download")
|
||||||
}
|
}
|
||||||
|
|
||||||
original := Original{
|
if isPlaylistUrl(url) {
|
||||||
URL: url,
|
playlist := Playlist{
|
||||||
UserID: userID,
|
URL: url,
|
||||||
Status: Pending,
|
UserID: userID,
|
||||||
Audio: audioOnly,
|
Audio: audioOnly,
|
||||||
Video: !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")
|
return c.Redirect(http.StatusSeeOther, "/videos")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +181,26 @@ func getYtdlpTitle(url string, args []string) (string, error) {
|
|||||||
return strings.TrimSpace(string(stdout)), nil
|
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) {
|
func getYtdlpArtist(url string, args []string) (string, error) {
|
||||||
args = append(args, "--simulate", "--print", "%(uploader)s", url)
|
args = append(args, "--simulate", "--print", "%(uploader)s", url)
|
||||||
stdout, _, err := runYtdlp(args...)
|
stdout, _, err := runYtdlp(args...)
|
||||||
@@ -643,14 +682,61 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
|||||||
processOriginal(originalID)
|
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 {
|
func videosHandler(c echo.Context) error {
|
||||||
userID := c.Get("user_id").(uint)
|
userID := c.Get("user_id").(uint)
|
||||||
var origs []Original
|
var origs []Original
|
||||||
db.Where("user_id = ?", userID).Find(&origs)
|
db.Where("user_id = ?", userID).Find(&origs)
|
||||||
|
|
||||||
|
var playlists []Playlist
|
||||||
|
db.Where("user_id = ?", userID).Find(&playlists)
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "videos.html",
|
return c.Render(http.StatusOK, "videos.html",
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"videos": origs,
|
"videos": origs,
|
||||||
"Footer": makeFooter(),
|
"playlists": playlists,
|
||||||
|
"Footer": makeFooter(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1000,3 +1086,19 @@ func processHandler(c echo.Context) error {
|
|||||||
|
|
||||||
return c.Redirect(http.StatusSeeOther, "/videos")
|
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)
|
||||||
|
}
|
||||||
|
5
main.go
5
main.go
@@ -80,7 +80,7 @@ func main() {
|
|||||||
sqlDB.SetMaxOpenConns(1)
|
sqlDB.SetMaxOpenConns(1)
|
||||||
|
|
||||||
// Migrate the schema
|
// 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()
|
go PeriodicCleanup()
|
||||||
|
|
||||||
// create a user
|
// create a user
|
||||||
@@ -129,6 +129,9 @@ func main() {
|
|||||||
e.POST("/transcode_to_video/:id", transcodeToVideoHandler, authMiddleware)
|
e.POST("/transcode_to_video/:id", transcodeToVideoHandler, authMiddleware)
|
||||||
e.POST("/transcode_to_audio/:id", transcodeToAudioHandler, 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 := e.Group("/data")
|
||||||
dataGroup.Use(authMiddleware)
|
dataGroup.Use(authMiddleware)
|
||||||
dataGroup.Static("/", getDataDir())
|
dataGroup.Static("/", getDataDir())
|
||||||
|
32
models.go
32
models.go
@@ -25,13 +25,17 @@ const (
|
|||||||
|
|
||||||
type Original struct {
|
type Original struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
UserID uint
|
UserID uint
|
||||||
URL string
|
URL string
|
||||||
Title string
|
Title string
|
||||||
Artist string
|
Artist string
|
||||||
Status OriginalStatus
|
Status OriginalStatus
|
||||||
Audio bool // video download requested
|
Audio bool // video download requested
|
||||||
Video bool // audio 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 {
|
type Transcode struct {
|
||||||
@@ -53,6 +57,16 @@ type Transcode struct {
|
|||||||
Rate uint
|
Rate uint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Playlist struct {
|
||||||
|
gorm.Model
|
||||||
|
UserID uint
|
||||||
|
URL string
|
||||||
|
Title string
|
||||||
|
Status OriginalStatus
|
||||||
|
Audio bool
|
||||||
|
Video bool
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Username string `gorm:"unique"`
|
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
|
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 {
|
func NewDownloadManager() *DownloadManager {
|
||||||
return &DownloadManager{
|
return &DownloadManager{
|
||||||
downloads: make(map[uint]*DownloadStatus),
|
downloads: make(map[uint]*DownloadStatus),
|
||||||
|
@@ -53,6 +53,37 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</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="/download">Download New Video</a></p>
|
||||||
<p><a href="/logout">Logout</a></p>
|
<p><a href="/logout">Logout</a></p>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user