Improved playlist handling
This commit is contained in:
@@ -7,7 +7,11 @@ RUN wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux
|
||||
&& chmod +x /usr/local/bin/yt-dlp
|
||||
|
||||
ADD *.go /src/.
|
||||
ADD database /src/database
|
||||
ADD handlers /src/handlers
|
||||
ADD media /src/media
|
||||
ADD originals /src/originals
|
||||
Add playlists /src/playlists
|
||||
ADD go.mod /src/.
|
||||
|
||||
RUN cd /src && go mod tidy
|
||||
|
26
database/database.go
Normal file
26
database/database.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
var log *logrus.Logger
|
||||
|
||||
func Init(d *gorm.DB, logger *logrus.Logger) error {
|
||||
db = d
|
||||
log = logger.WithFields(logrus.Fields{
|
||||
"component": "database",
|
||||
}).Logger
|
||||
return nil
|
||||
}
|
||||
|
||||
func Fini() {}
|
||||
|
||||
func Get() *gorm.DB {
|
||||
if db == nil {
|
||||
panic("didn't call database.Init(...)")
|
||||
}
|
||||
return db
|
||||
}
|
110
handlers.go
110
handlers.go
@@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
"ytdlp-site/media"
|
||||
"ytdlp-site/originals"
|
||||
"ytdlp-site/playlists"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -141,20 +143,21 @@ func downloadPostHandler(c echo.Context) error {
|
||||
}
|
||||
|
||||
if isPlaylistUrl(url) {
|
||||
playlist := Playlist{
|
||||
playlist := playlists.Playlist{
|
||||
URL: url,
|
||||
UserID: userID,
|
||||
Audio: audioOnly,
|
||||
Video: !audioOnly,
|
||||
Status: playlists.StatusNotStarted,
|
||||
}
|
||||
db.Create(&playlist)
|
||||
go startPlaylist(playlist.ID, url, audioOnly)
|
||||
|
||||
} else {
|
||||
original := Original{
|
||||
original := originals.Original{
|
||||
URL: url,
|
||||
UserID: userID,
|
||||
Status: Pending,
|
||||
Status: originals.StatusNotStarted,
|
||||
Audio: audioOnly,
|
||||
Video: !audioOnly,
|
||||
}
|
||||
@@ -548,7 +551,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||
log.Debugf("startDownload audioOnly=%t", audioOnly)
|
||||
|
||||
// metadata phase
|
||||
SetOriginalStatus(originalID, Metadata)
|
||||
originals.SetStatus(db, originalID, originals.StatusMetadata)
|
||||
var origMeta Meta
|
||||
var err error
|
||||
if audioOnly {
|
||||
@@ -558,29 +561,29 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorln("couldn't retrieve metadata:", err)
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
log.Debugf("original metadata %v", origMeta)
|
||||
err = db.Model(&Original{}).Where("id = ?", originalID).Updates(map[string]interface{}{
|
||||
err = db.Model(&originals.Original{}).Where("id = ?", originalID).Updates(map[string]interface{}{
|
||||
"title": origMeta.title,
|
||||
"artist": origMeta.artist,
|
||||
}).Error
|
||||
if err != nil {
|
||||
log.Errorln("couldn't store metadata:", err)
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// download original
|
||||
SetOriginalStatus(originalID, Downloading)
|
||||
originals.SetStatus(db, originalID, originals.StatusDownloading)
|
||||
|
||||
// create temporary directory
|
||||
// do this in the data directory since /tmp is sometimes a different filesystem
|
||||
tempDir, err := os.MkdirTemp(getDataDir(), "dl")
|
||||
if err != nil {
|
||||
log.Errorln("Error creating temporary directory:", err)
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
@@ -601,7 +604,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
log.Errorln("yt-dlp failed")
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -609,7 +612,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||
dirEnts, err := os.ReadDir(tempDir)
|
||||
if err != nil {
|
||||
log.Errorln("Error reading directory:", err)
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
dlFilename := ""
|
||||
@@ -622,7 +625,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||
}
|
||||
if dlFilename == "" {
|
||||
log.Errorln("couldn't find a downloaded file")
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
}
|
||||
|
||||
// move to data directory
|
||||
@@ -632,7 +635,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||
err = os.Rename(srcPath, dlFilepath)
|
||||
if err != nil {
|
||||
log.Errorln("rename downloaded media error", srcPath, "->", dlFilepath, ":", err)
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -640,7 +643,7 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||
mediaMeta, err := getAudioMeta(dlFilepath)
|
||||
if err != nil {
|
||||
log.Errorln("couldn't get audio file metadata", err)
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -654,14 +657,14 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||
fmt.Println("create Audio", audio)
|
||||
if db.Create(&audio).Error != nil {
|
||||
fmt.Println("Couldn't create audio entry", err)
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
mediaMeta, err := getVideoMeta(dlFilepath)
|
||||
if err != nil {
|
||||
log.Errorln("couldn't get video file metadata", err)
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -678,12 +681,12 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||
log.Debugln("create Video", video)
|
||||
if db.Create(&video).Error != nil {
|
||||
log.Errorln("Couldn't create video entry", err)
|
||||
SetOriginalStatus(originalID, Failed)
|
||||
originals.SetStatus(db, originalID, originals.StatusFailed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
SetOriginalStatus(originalID, DownloadCompleted)
|
||||
originals.SetStatus(db, originalID, originals.StatusDownloadCompleted)
|
||||
processOriginal(originalID)
|
||||
}
|
||||
|
||||
@@ -691,22 +694,22 @@ func startPlaylist(id uint, url string, audioOnly bool) {
|
||||
// retrieve playlist metadata
|
||||
pl, err := getYtdlpPlaylist(url)
|
||||
if err != nil {
|
||||
SetPlaylistStatus(id, Failed)
|
||||
playlists.SetStatus(db, id, playlists.StatusFailed)
|
||||
return
|
||||
}
|
||||
err = db.Model(&Playlist{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
err = db.Model(&playlists.Playlist{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"title": pl.Title,
|
||||
}).Error
|
||||
if err != nil {
|
||||
SetPlaylistStatus(id, Failed)
|
||||
playlists.SetStatus(db, id, playlists.StatusFailed)
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range pl.Entries {
|
||||
original := Original{
|
||||
original := originals.Original{
|
||||
Title: entry.Title,
|
||||
URL: entry.URL,
|
||||
Status: StatusNotStarted,
|
||||
Status: originals.StatusNotStarted,
|
||||
Video: !audioOnly,
|
||||
Audio: audioOnly,
|
||||
Playlist: true,
|
||||
@@ -714,19 +717,19 @@ func startPlaylist(id uint, url string, audioOnly bool) {
|
||||
}
|
||||
err = db.Create(&original).Error
|
||||
if err != nil {
|
||||
SetPlaylistStatus(id, Failed)
|
||||
playlists.SetStatus(db, id, playlists.StatusFailed)
|
||||
return
|
||||
}
|
||||
}
|
||||
SetPlaylistStatus(id, Completed)
|
||||
playlists.SetStatus(db, id, playlists.StatusCompleted)
|
||||
}
|
||||
|
||||
func videosHandler(c echo.Context) error {
|
||||
userID := c.Get("user_id").(uint)
|
||||
var origs []Original
|
||||
var origs []originals.Original
|
||||
db.Where("user_id = ?", userID).Find(&origs)
|
||||
|
||||
var playlists []Playlist
|
||||
var playlists []playlists.Playlist
|
||||
db.Where("user_id = ?", userID).Find(&playlists)
|
||||
|
||||
return c.Render(http.StatusOK, "videos.html",
|
||||
@@ -798,7 +801,7 @@ func makeNiceFilename(input string) string {
|
||||
|
||||
func videoHandler(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var orig Original
|
||||
var orig originals.Original
|
||||
if err := db.First(&orig, id).Error; err != nil {
|
||||
return c.Redirect(http.StatusSeeOther, "/videos")
|
||||
}
|
||||
@@ -874,16 +877,20 @@ func videoRestartHandler(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
// FIXME: rewrite this as an update
|
||||
var orig Original
|
||||
var orig originals.Original
|
||||
if err := db.First(&orig, id).Error; err != nil {
|
||||
return c.Redirect(http.StatusSeeOther, "/videos")
|
||||
}
|
||||
orig.Status = Pending
|
||||
orig.Status = originals.StatusNotStarted
|
||||
db.Save(&orig)
|
||||
|
||||
go startDownload(uint(id), orig.URL, orig.Audio)
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, "/videos")
|
||||
referrer := c.Request().Referer()
|
||||
if referrer == "" {
|
||||
referrer = "/videos"
|
||||
}
|
||||
return c.Redirect(http.StatusSeeOther, referrer)
|
||||
}
|
||||
|
||||
func deleteTranscodes(originalID uint) {
|
||||
@@ -934,7 +941,7 @@ func deleteAudiosWithSource(originalID uint, source string) {
|
||||
}
|
||||
|
||||
func deleteOriginal(id uint) error {
|
||||
var orig Original
|
||||
var orig originals.Original
|
||||
if err := db.First(&orig, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1091,7 +1098,7 @@ func processHandler(c echo.Context) error {
|
||||
deleteAudiosWithSource(uint(id), "transcode")
|
||||
deleteTranscodedVideos(uint(id))
|
||||
|
||||
err := SetOriginalStatus(uint(id), DownloadCompleted)
|
||||
err := originals.SetStatus(db, uint(id), originals.StatusDownloadCompleted)
|
||||
if err != nil {
|
||||
log.Errorf("error while setting original %d status: %v", id, err)
|
||||
}
|
||||
@@ -1102,21 +1109,38 @@ func processHandler(c echo.Context) error {
|
||||
}
|
||||
|
||||
func playlistHandler(c echo.Context) error {
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var originals []Original
|
||||
var playlist playlists.Playlist
|
||||
err := db.Where(id).First(&playlist).Error
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
|
||||
}
|
||||
|
||||
err := db.Where("playlist = ?", true).
|
||||
var origs []originals.Original
|
||||
var watchedOrigs []originals.Original
|
||||
|
||||
err = db.Where("playlist = ?", true).
|
||||
Where("playlist_id = ?", id).
|
||||
Find(&originals).Error
|
||||
Where("watched = ?", false).
|
||||
Find(&origs).Error
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
|
||||
}
|
||||
|
||||
err = db.Where("playlist = ?", true).
|
||||
Where("playlist_id = ?", id).
|
||||
Where("watched = ?", true).
|
||||
Find(&watchedOrigs).Error
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "playlist.html",
|
||||
map[string]interface{}{
|
||||
"originals": originals,
|
||||
"playlist": playlist,
|
||||
"unwatched": origs,
|
||||
"watched": watchedOrigs,
|
||||
"Footer": makeFooter(),
|
||||
})
|
||||
}
|
||||
@@ -1125,16 +1149,16 @@ func deletePlaylistHandler(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
// delete all originals
|
||||
var originals []Original
|
||||
err := db.Model(&Original{}).
|
||||
var origs []originals.Original
|
||||
err := db.Model(&originals.Original{}).
|
||||
Where("playlist = ?", true).
|
||||
Where("playlist_id = ?", id).
|
||||
Find(&originals).Error
|
||||
Find(&origs).Error
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
|
||||
for _, original := range originals {
|
||||
for _, original := range origs {
|
||||
err := deleteOriginal(original.ID)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
@@ -1142,7 +1166,7 @@ func deletePlaylistHandler(c echo.Context) error {
|
||||
}
|
||||
|
||||
// delete playlist entry
|
||||
err = db.Delete(&Playlist{}, id).Error
|
||||
err = db.Delete(&playlists.Playlist{}, id).Error
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
|
12
handlers/init.go
Normal file
12
handlers/init.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package handlers
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
var log *logrus.Logger
|
||||
|
||||
func Init(logger *logrus.Logger) error {
|
||||
log = logger.WithFields(logrus.Fields{
|
||||
"component": "handlers",
|
||||
}).Logger
|
||||
return nil
|
||||
}
|
36
handlers/toggle_watched.go
Normal file
36
handlers/toggle_watched.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ytdlp-site/database"
|
||||
"ytdlp-site/originals"
|
||||
)
|
||||
|
||||
func ToggleWatched(c echo.Context) error {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
|
||||
db := database.Get()
|
||||
|
||||
result := db.Model(&originals.Original{}).
|
||||
Where("id = ?", id).
|
||||
Update("watched", gorm.Expr("NOT watched"))
|
||||
|
||||
if result.Error != nil {
|
||||
log.Errorln(result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
log.Errorln(gorm.ErrRecordNotFound)
|
||||
}
|
||||
|
||||
referrer := c.Request().Referer()
|
||||
if referrer == "" {
|
||||
referrer = "/videos"
|
||||
}
|
||||
return c.Redirect(http.StatusSeeOther, referrer)
|
||||
}
|
@@ -12,7 +12,6 @@ import (
|
||||
var log *logrus.Logger
|
||||
|
||||
func initLogger() {
|
||||
|
||||
log = logrus.New()
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
|
14
main.go
14
main.go
@@ -8,7 +8,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"ytdlp-site/database"
|
||||
"ytdlp-site/handlers"
|
||||
"ytdlp-site/media"
|
||||
"ytdlp-site/originals"
|
||||
"ytdlp-site/playlists"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
@@ -46,6 +50,8 @@ func main() {
|
||||
log.Infof("GitSHA: %s", getGitSHA())
|
||||
log.Infof("BuildDate: %s", getBuildDate())
|
||||
|
||||
handlers.Init(log)
|
||||
|
||||
gormLogger := logger.New(
|
||||
golog.New(os.Stdout, "\r\n", golog.LstdFlags), // io writer
|
||||
logger.Config{
|
||||
@@ -80,7 +86,12 @@ func main() {
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
|
||||
// Migrate the schema
|
||||
db.AutoMigrate(&Original{}, &Playlist{}, &media.Video{}, &media.Audio{}, &User{}, &TempURL{}, &Transcode{})
|
||||
db.AutoMigrate(&originals.Original{}, &playlists.Playlist{}, &media.Video{},
|
||||
&media.Audio{}, &User{}, &TempURL{}, &Transcode{})
|
||||
|
||||
database.Init(db, log)
|
||||
defer database.Fini()
|
||||
|
||||
go PeriodicCleanup()
|
||||
|
||||
// create a user
|
||||
@@ -124,6 +135,7 @@ func main() {
|
||||
e.POST("/video/:id/delete", deleteOriginalHandler, authMiddleware)
|
||||
e.GET("/temp/:token", tempHandler)
|
||||
e.POST("/video/:id/process", processHandler, authMiddleware)
|
||||
e.POST("/video/:id/toggle_watched", handlers.ToggleWatched, authMiddleware)
|
||||
e.POST("/delete_video/:id", deleteVideoHandler, authMiddleware)
|
||||
e.POST("/delete_audio/:id", deleteAudioHandler, authMiddleware)
|
||||
e.POST("/transcode_to_video/:id", transcodeToVideoHandler, authMiddleware)
|
||||
|
69
models.go
69
models.go
@@ -5,40 +5,13 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"ytdlp-site/originals"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OriginalStatus string
|
||||
|
||||
const (
|
||||
StatusNotStarted OriginalStatus = "not started"
|
||||
Pending OriginalStatus = "pending"
|
||||
Metadata OriginalStatus = "metadata"
|
||||
Downloading OriginalStatus = "downloading"
|
||||
DownloadCompleted OriginalStatus = "download completed"
|
||||
Transcoding OriginalStatus = "transcoding"
|
||||
Completed OriginalStatus = "completed"
|
||||
Failed OriginalStatus = "failed"
|
||||
)
|
||||
|
||||
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
|
||||
Watched bool
|
||||
|
||||
Playlist bool // part of a playlist
|
||||
PlaylistID uint // Playlist.ID (if part of a playlist)
|
||||
}
|
||||
|
||||
type Transcode struct {
|
||||
gorm.Model
|
||||
Status string // "pending", "running", "failed"
|
||||
@@ -58,16 +31,6 @@ 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"`
|
||||
@@ -102,18 +65,8 @@ func CreateUser(db *gorm.DB, username, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
func SetOriginalStatus(id uint, status originals.Status) error {
|
||||
return db.Model(&originals.Original{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
||||
|
||||
func (dm *DownloadManager) UpdateStatus(id uint, progress float64, status string, err string) {
|
||||
@@ -127,22 +80,6 @@ func (dm *DownloadManager) UpdateStatus(id uint, progress float64, status string
|
||||
dm.downloads[id].Error = err
|
||||
}
|
||||
|
||||
func (dm *DownloadManager) GetStatus(id uint) (DownloadStatus, bool) {
|
||||
dm.mutex.RLock()
|
||||
defer dm.mutex.RUnlock()
|
||||
status, exists := dm.downloads[id]
|
||||
if !exists {
|
||||
return DownloadStatus{}, false
|
||||
}
|
||||
return *status, true
|
||||
}
|
||||
|
||||
func (dm *DownloadManager) RemoveStatus(id uint) {
|
||||
dm.mutex.Lock()
|
||||
defer dm.mutex.Unlock()
|
||||
delete(dm.downloads, id)
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
uuidObj := uuid.Must(uuid.NewV7())
|
||||
return uuidObj.String()
|
||||
|
34
originals/originals.go
Normal file
34
originals/originals.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package originals
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusNotStarted Status = "not started"
|
||||
StatusMetadata Status = "metadata"
|
||||
StatusDownloading Status = "downloading"
|
||||
StatusDownloadCompleted Status = "download completed"
|
||||
StatusTranscoding Status = "transcoding"
|
||||
StatusCompleted Status = "completed"
|
||||
StatusFailed Status = "failed"
|
||||
)
|
||||
|
||||
type Original struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
URL string
|
||||
Title string
|
||||
Artist string
|
||||
Status Status
|
||||
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)
|
||||
}
|
||||
|
||||
func SetStatus(db *gorm.DB, id uint, status Status) error {
|
||||
return db.Model(&Original{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
26
playlists/model.go
Normal file
26
playlists/model.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package playlists
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Status string
|
||||
|
||||
type Playlist struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
URL string
|
||||
Title string
|
||||
Status Status
|
||||
Audio bool
|
||||
Video bool
|
||||
}
|
||||
|
||||
const (
|
||||
StatusNotStarted Status = "not started"
|
||||
StatusDownloading Status = "downloading"
|
||||
StatusCompleted Status = "completed"
|
||||
StatusFailed Status = "failed"
|
||||
)
|
||||
|
||||
func SetStatus(db *gorm.DB, id uint, status Status) error {
|
||||
return db.Model(&Playlist{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
1
playlists/playlists.go
Normal file
1
playlists/playlists.go
Normal file
@@ -0,0 +1 @@
|
||||
package playlists
|
@@ -12,45 +12,19 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Playlist</h1>
|
||||
<h1>{{.playlist.Title}}</h1>
|
||||
<h2>Playlist</h2>
|
||||
|
||||
<div class="video-list">
|
||||
{{range .originals}}
|
||||
<div class="video-card">
|
||||
<div class="video-title">
|
||||
{{if or (eq .Status "download completed") (eq .Status "transcoding") (eq .Status "completed")}}
|
||||
<a href="/video/{{.ID}}">{{.Title}}</a>
|
||||
{{else}}
|
||||
{{.Title}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="video-info">{{.Artist}}</div>
|
||||
<div class="video-info"><a href="{{.URL}}">{{.URL}}</a></div>
|
||||
<div class="video-info">{{.Status}}</div>
|
||||
<div class="video-info">
|
||||
{{if .Audio}}
|
||||
Audio
|
||||
{{end}}
|
||||
{{if .Video}}
|
||||
Video
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="video-options">
|
||||
{{if eq .Status "completed"}}
|
||||
<form action="/video/{{.ID}}/process" method="post" style="display:inline;">
|
||||
<button type="submit">Reprocess</button>
|
||||
</form>
|
||||
{{else if eq .Status "failed"}}
|
||||
<form action="/video/{{.ID}}/restart" method="post" style="display:inline;">
|
||||
<button type="submit">Restart</button>
|
||||
</form>
|
||||
{{else if eq .Status "downloading"}}
|
||||
{{end}}
|
||||
<form action="/video/{{.ID}}/delete" method="post" style="display:inline;">
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{range .unwatched}}
|
||||
{{template "playlist-video-card-html" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<h2>Watched</h2>
|
||||
<div class="video-list">
|
||||
{{range .watched}}
|
||||
{{template "playlist-video-card-html" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
|
56
templates/video_card.html
Normal file
56
templates/video_card.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{{define "playlist-video-card-html"}}
|
||||
<div class="video-card">
|
||||
<div class="video-title">
|
||||
{{if or (eq .Status "download completed") (eq .Status "transcoding") (eq .Status "completed")}}
|
||||
<a href="/video/{{.ID}}">{{.Title}}</a>
|
||||
{{else}}
|
||||
{{.Title}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="video-info">{{.Artist}}</div>
|
||||
<div class="video-info"><a href="{{.URL}}">{{.URL}}</a></div>
|
||||
<div class="video-info">{{.Status}}</div>
|
||||
<div class="video-info">
|
||||
{{if .Audio}}
|
||||
Audio
|
||||
{{end}}
|
||||
{{if .Video}}
|
||||
Video
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="video-options">
|
||||
{{if or (eq .Status "completed") (eq .Status "not started")}}
|
||||
<form action="/video/{{.ID}}/toggle_watched" method="post" style="display:inline;">
|
||||
<button type="submit">
|
||||
{{ if .Watched }}
|
||||
Not Watched
|
||||
{{ else }}
|
||||
Watched
|
||||
{{ end }}
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if eq .Status "completed"}}
|
||||
<form action="/video/{{.ID}}/process" method="post" style="display:inline;">
|
||||
<button type="submit">Reprocess</button>
|
||||
</form>
|
||||
{{else if eq .Status "failed"}}
|
||||
<form action="/video/{{.ID}}/restart" method="post" style="display:inline;">
|
||||
<button type="submit">Restart</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if eq .Status "not started"}}
|
||||
<form action="/video/{{.ID}}/restart" method="post" style="display:inline;">
|
||||
<button type="submit">Start</button>
|
||||
</form>
|
||||
{{end}}
|
||||
<form action="/video/{{.ID}}/delete" method="post" style="display:inline;">
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "playlist-video-card-css"}}
|
||||
<!-- <link rel="stylesheet" href="/static/style/footer.css"> -->
|
||||
{{end}}
|
@@ -65,17 +65,8 @@
|
||||
{{.Title}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="video-info"><a href="{{.URL}}">{{.URL}}</a></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>
|
||||
|
23
workers.go
23
workers.go
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
"ytdlp-site/media"
|
||||
"ytdlp-site/originals"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
@@ -67,7 +68,7 @@ func videoToVideo(transID uint, height uint, srcFilepath string) {
|
||||
// look up original
|
||||
var trans Transcode
|
||||
db.First(&trans, transID)
|
||||
var orig Original
|
||||
var orig originals.Original
|
||||
db.First(&orig, "id = ?", trans.OriginalID)
|
||||
|
||||
// create video record
|
||||
@@ -129,7 +130,7 @@ func videoToAudio(transID uint, kbps uint, videoFilepath string) {
|
||||
// look up original
|
||||
var trans Transcode
|
||||
db.First(&trans, "id = ?", transID)
|
||||
var orig Original
|
||||
var orig originals.Original
|
||||
db.First(&orig, "id = ?", trans.OriginalID)
|
||||
|
||||
// create audio record
|
||||
@@ -187,7 +188,7 @@ func audioToAudio(transID uint, kbps uint, srcFilepath string) {
|
||||
// look up original
|
||||
var trans Transcode
|
||||
db.First(&trans, "id = ?", transID)
|
||||
var orig Original
|
||||
var orig originals.Original
|
||||
db.First(&orig, "id = ?", trans.OriginalID)
|
||||
|
||||
// create audio record
|
||||
@@ -225,29 +226,29 @@ func transcodePending() {
|
||||
var originalsToUpdate []uint
|
||||
|
||||
// find any originals with a transcode job and mark them as transcoding
|
||||
db.Model(&Original{}).
|
||||
db.Model(&originals.Original{}).
|
||||
Select("id").
|
||||
Where("id IN (?)",
|
||||
db.Model(&Transcode{}).
|
||||
Select("original_id"),
|
||||
).
|
||||
Find(&originalsToUpdate)
|
||||
db.Model(&Original{}).
|
||||
db.Model(&originals.Original{}).
|
||||
Where("id IN ?", originalsToUpdate).
|
||||
Update("status", Transcoding)
|
||||
Update("status", originals.StatusTranscoding)
|
||||
|
||||
// originals marked transcoding that don't have a transcode job -> complete
|
||||
db.Model(&Original{}).
|
||||
db.Model(&originals.Original{}).
|
||||
Select("id").
|
||||
Where("status = ? AND id NOT IN (?)",
|
||||
Transcoding,
|
||||
originals.StatusTranscoding,
|
||||
db.Model(&Transcode{}).
|
||||
Select("original_id"),
|
||||
).
|
||||
Find(&originalsToUpdate)
|
||||
db.Model(&Original{}).
|
||||
Where("id IN ? AND status = ?", originalsToUpdate, Transcoding).
|
||||
Update("status", Completed)
|
||||
db.Model(&originals.Original{}).
|
||||
Where("id IN ? AND status = ?", originalsToUpdate, originals.StatusTranscoding).
|
||||
Update("status", originals.StatusCompleted)
|
||||
|
||||
var trans Transcode
|
||||
err := db.Where("status = ?", "pending").
|
||||
|
Reference in New Issue
Block a user