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