Add audio-only downloads
This commit is contained in:
@@ -2,3 +2,4 @@ data
|
|||||||
config
|
config
|
||||||
go.sum
|
go.sum
|
||||||
*.mp4
|
*.mp4
|
||||||
|
*.m4a
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
config
|
config
|
||||||
go.sum
|
go.sum
|
||||||
*.mp4
|
*.mp4
|
||||||
|
*.m4a
|
||||||
data
|
data
|
@@ -18,7 +18,8 @@ go run *.go
|
|||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t server .
|
docker build --build-arg GIT_SHA=$(git rev-parse HEAD) \
|
||||||
|
-t server .
|
||||||
|
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-p 3000:8080 \
|
-p 3000:8080 \
|
||||||
|
362
handlers.go
362
handlers.go
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -14,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerHandler(c echo.Context) error {
|
func registerHandler(c echo.Context) error {
|
||||||
@@ -89,22 +89,38 @@ func downloadHandler(c echo.Context) error {
|
|||||||
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)
|
||||||
|
vaStr := c.FormValue("color")
|
||||||
|
|
||||||
original := Original{URL: url, UserID: userID, Status: "pending"}
|
audioOnly := false
|
||||||
|
if vaStr == "audio" {
|
||||||
|
audioOnly = true
|
||||||
|
} else if vaStr == "audio-video" {
|
||||||
|
audioOnly = false
|
||||||
|
} else {
|
||||||
|
return c.Redirect(http.StatusSeeOther, "/download")
|
||||||
|
}
|
||||||
|
|
||||||
|
original := Original{
|
||||||
|
URL: url,
|
||||||
|
UserID: userID,
|
||||||
|
Status: "pending",
|
||||||
|
Audio: audioOnly,
|
||||||
|
Video: !audioOnly,
|
||||||
|
}
|
||||||
db.Create(&original)
|
db.Create(&original)
|
||||||
go startDownload(original.ID, url)
|
go startDownload(original.ID, url, audioOnly)
|
||||||
|
|
||||||
return c.Redirect(http.StatusSeeOther, "/videos")
|
return c.Redirect(http.StatusSeeOther, "/videos")
|
||||||
}
|
}
|
||||||
|
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
title string
|
title string
|
||||||
ext string
|
artist string
|
||||||
|
ext string
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMeta(url string) (Meta, error) {
|
func getYtdlpTitle(url string, args []string) (string, error) {
|
||||||
ytdlp := "yt-dlp"
|
ytdlp := "yt-dlp"
|
||||||
args := []string{"--simulate", "--print", "%(title)s.%(ext)s", url}
|
args = append(args, "--simulate", "--print", "%(title)s", url)
|
||||||
fmt.Println(ytdlp, strings.Join(args, " "))
|
fmt.Println(ytdlp, strings.Join(args, " "))
|
||||||
cmd := exec.Command(ytdlp, args...)
|
cmd := exec.Command(ytdlp, args...)
|
||||||
|
|
||||||
@@ -112,22 +128,73 @@ func getMeta(url string) (Meta, error) {
|
|||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("getTitle error:", err, stdout.String())
|
fmt.Println("getYtdlpTitle error:", err, stdout.String())
|
||||||
return Meta{}, err
|
return "", err
|
||||||
} else {
|
|
||||||
isDot := func(r rune) bool {
|
|
||||||
return r == '.'
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := strings.FieldsFunc(strings.TrimSpace(stdout.String()), isDot)
|
|
||||||
if len(fields) < 2 {
|
|
||||||
return Meta{}, errors.New("couldn't parse ytdlp output")
|
|
||||||
}
|
|
||||||
return Meta{
|
|
||||||
title: strings.Join(fields[:len(fields)-1], "."),
|
|
||||||
ext: fields[len(fields)-1],
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
return strings.TrimSpace(stdout.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getYtdlpArtist(url string, args []string) (string, error) {
|
||||||
|
ytdlp := "yt-dlp"
|
||||||
|
args = append(args, "--simulate", "--print", "%(uploader)s", url)
|
||||||
|
fmt.Println(ytdlp, strings.Join(args, " "))
|
||||||
|
cmd := exec.Command(ytdlp, args...)
|
||||||
|
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("getYtdlpArtist error:", err, stdout.String())
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(stdout.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getYtdlpExt(url string, args []string) (string, error) {
|
||||||
|
ytdlp := "yt-dlp"
|
||||||
|
args = append(args, "--simulate", "--print", "%(ext)s", url)
|
||||||
|
fmt.Println(ytdlp, strings.Join(args, " "))
|
||||||
|
cmd := exec.Command(ytdlp, args...)
|
||||||
|
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("getYtdlpExt error:", err, stdout.String())
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(stdout.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getYtdlpMeta(url string, args []string) (Meta, error) {
|
||||||
|
|
||||||
|
meta := Meta{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
meta.title, err = getYtdlpTitle(url, args)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
meta.artist, err = getYtdlpArtist(url, args)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
meta.ext, err = getYtdlpExt(url, args)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getYtdlpAudioMeta(url string) (Meta, error) {
|
||||||
|
args := []string{"-f", "bestaudio"}
|
||||||
|
return getYtdlpMeta(url, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getYtdlpVideoMeta(url string) (Meta, error) {
|
||||||
|
args := []string{"-f", "bestvideo+bestaudio/best"}
|
||||||
|
return getYtdlpMeta(url, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the length in seconds of a video file at `path`
|
// return the length in seconds of a video file at `path`
|
||||||
@@ -270,6 +337,10 @@ type VideoMeta struct {
|
|||||||
fps float64
|
fps float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AudioMeta struct {
|
||||||
|
rate uint
|
||||||
|
}
|
||||||
|
|
||||||
func getVideoMeta(path string) (VideoMeta, error) {
|
func getVideoMeta(path string) (VideoMeta, error) {
|
||||||
w, err := getVideoWidth(path)
|
w, err := getVideoWidth(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -290,108 +361,231 @@ func getVideoMeta(path string) (VideoMeta, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processOriginal(originalID uint, videoFilename string, origMeta Meta) {
|
func getAudioBitrate(path string) (uint, error) {
|
||||||
|
|
||||||
videoFilepath := filepath.Join(getDataDir(), videoFilename)
|
ffprobe := "ffprobe"
|
||||||
_, err := os.Stat(videoFilepath)
|
ffprobeArgs := []string{
|
||||||
if os.IsNotExist(err) {
|
"-v", "quiet",
|
||||||
fmt.Println("Skipping non-existant file for processOriginal")
|
"-select_streams", "a:0",
|
||||||
return
|
"-show_entries", "stream=bit_rate",
|
||||||
}
|
"-of", "default=noprint_wrappers=1:nokey=1",
|
||||||
|
path}
|
||||||
|
|
||||||
// create video entry for original
|
fmt.Println(ffprobe, strings.Join(ffprobeArgs, " "))
|
||||||
video := Video{
|
cmd := exec.Command(ffprobe, ffprobeArgs...)
|
||||||
OriginalID: originalID,
|
var stdout bytes.Buffer
|
||||||
Filename: videoFilename,
|
cmd.Stdout = &stdout
|
||||||
Source: "original",
|
err := cmd.Run()
|
||||||
Type: origMeta.ext,
|
|
||||||
}
|
|
||||||
fmt.Println("create Video", video)
|
|
||||||
if err := db.Create(&video).Error; err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
videoMeta, err := getVideoMeta(videoFilepath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println("getAudioBitrate error:", err, stdout.String())
|
||||||
} else {
|
return 0, err
|
||||||
fmt.Println(videoMeta)
|
}
|
||||||
db.Model(&Video{}).Where("id = ?", video.ID).Update("fps", videoMeta.fps)
|
bitrateStr := strings.TrimSpace(stdout.String())
|
||||||
db.Model(&Video{}).Where("id = ?", video.ID).Update("width", videoMeta.width)
|
bitrate, err := strconv.ParseUint(bitrateStr, 10, 32)
|
||||||
db.Model(&Video{}).Where("id = ?", video.ID).Update("height", videoMeta.height)
|
if err != nil {
|
||||||
|
fmt.Println("getAudioBitrate error:", err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return uint(bitrate), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAudioMeta(path string) (AudioMeta, error) {
|
||||||
|
rate, err := getAudioBitrate(path)
|
||||||
|
if err != nil {
|
||||||
|
return AudioMeta{}, err
|
||||||
|
}
|
||||||
|
return AudioMeta{
|
||||||
|
rate: rate,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processOriginal(originalID uint) {
|
||||||
|
|
||||||
|
// check if there is an original video
|
||||||
|
hasOriginalVideo := true
|
||||||
|
hasOriginalAudio := true
|
||||||
|
var video Video
|
||||||
|
var audio Audio
|
||||||
|
err := db.Where("source = ?", "original").Where("original_id = ?", originalID).First(&video).Error
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
hasOriginalVideo = false
|
||||||
|
}
|
||||||
|
err = db.Where("source = ?", "original").Where("original_id = ?", originalID).First(&audio).Error
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
hasOriginalAudio = false
|
||||||
}
|
}
|
||||||
|
|
||||||
videoSize, err := getSize(videoFilepath)
|
if hasOriginalVideo {
|
||||||
if err == nil {
|
|
||||||
db.Model(&Video{}).Where("id = ?", video.ID).Update("size", humanSize(videoSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
// create audio transcodes
|
videoFilepath := filepath.Join(getDataDir(), video.Filename)
|
||||||
for _, bitrate := range []uint{64, 96, 128, 160, 192} {
|
_, err := os.Stat(videoFilepath)
|
||||||
t := Transcode{
|
if os.IsNotExist(err) {
|
||||||
SrcID: video.ID,
|
fmt.Println("Skipping non-existant file for processOriginal")
|
||||||
OriginalID: originalID,
|
return
|
||||||
SrcKind: "video",
|
}
|
||||||
DstKind: "audio",
|
videoMeta, err := getVideoMeta(videoFilepath)
|
||||||
Rate: bitrate,
|
if err != nil {
|
||||||
TimeSubmit: time.Now(),
|
fmt.Println(err)
|
||||||
Status: "pending",
|
} else {
|
||||||
|
fmt.Println(videoMeta)
|
||||||
|
db.Model(&Video{}).Where("id = ?", video.ID).Update("fps", videoMeta.fps)
|
||||||
|
db.Model(&Video{}).Where("id = ?", video.ID).Update("width", videoMeta.width)
|
||||||
|
db.Model(&Video{}).Where("id = ?", video.ID).Update("height", videoMeta.height)
|
||||||
}
|
}
|
||||||
db.Create(&t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create video transcodes
|
videoSize, err := getSize(videoFilepath)
|
||||||
for _, targetHeight := range []uint{144, 240, 360, 480, 720, 1080} {
|
if err == nil {
|
||||||
if targetHeight <= videoMeta.height {
|
db.Model(&Video{}).Where("id = ?", video.ID).Update("size", humanSize(videoSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
// create audio transcodes
|
||||||
|
for _, bitrate := range []uint{64, 96, 128, 160, 192} {
|
||||||
t := Transcode{
|
t := Transcode{
|
||||||
SrcID: video.ID,
|
SrcID: video.ID,
|
||||||
OriginalID: originalID,
|
OriginalID: originalID,
|
||||||
SrcKind: "video",
|
SrcKind: "video",
|
||||||
DstKind: "video",
|
DstKind: "audio",
|
||||||
Height: targetHeight,
|
Rate: bitrate,
|
||||||
TimeSubmit: time.Now(),
|
TimeSubmit: time.Now(),
|
||||||
Status: "pending",
|
Status: "pending",
|
||||||
}
|
}
|
||||||
db.Create(&t)
|
db.Create(&t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create video transcodes
|
||||||
|
for _, targetHeight := range []uint{144, 240, 360, 480, 720, 1080} {
|
||||||
|
if targetHeight <= videoMeta.height {
|
||||||
|
t := Transcode{
|
||||||
|
SrcID: video.ID,
|
||||||
|
OriginalID: originalID,
|
||||||
|
SrcKind: "video",
|
||||||
|
DstKind: "video",
|
||||||
|
Height: targetHeight,
|
||||||
|
TimeSubmit: time.Now(),
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
db.Create(&t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if hasOriginalAudio {
|
||||||
|
|
||||||
|
audioFilepath := filepath.Join(getDataDir(), audio.Filename)
|
||||||
|
_, err := os.Stat(audioFilepath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Println("Skipping non-existant audio file for processOriginal")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioMeta, err := getAudioMeta(audioFilepath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(audioMeta)
|
||||||
|
db.Model(&Audio{}).Where("id = ?", audio.ID).Update("rate", fmt.Sprintf("%dk", audioMeta.rate/1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := getSize(audioFilepath)
|
||||||
|
if err == nil {
|
||||||
|
db.Model(&Audio{}).Where("id = ?", audio.ID).Update("size", humanSize(size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// create audio transcodes
|
||||||
|
for _, bitrate := range []uint{64, 96, 128, 160, 192} {
|
||||||
|
t := Transcode{
|
||||||
|
SrcID: audio.ID,
|
||||||
|
OriginalID: originalID,
|
||||||
|
SrcKind: "audio",
|
||||||
|
DstKind: "audio",
|
||||||
|
Rate: bitrate,
|
||||||
|
TimeSubmit: time.Now(),
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
db.Create(&t)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
fmt.Println("No original video or audio found in processOriginal")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startDownload(originalID uint, videoURL string) {
|
func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
||||||
|
fmt.Println("startDownload audioOnly=", audioOnly)
|
||||||
|
|
||||||
// metadata phase
|
// metadata phase
|
||||||
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "metadata")
|
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "metadata")
|
||||||
origMeta, err := getMeta(videoURL)
|
var origMeta Meta
|
||||||
|
var err error
|
||||||
|
if audioOnly {
|
||||||
|
origMeta, err = getYtdlpAudioMeta(videoURL)
|
||||||
|
} else {
|
||||||
|
origMeta, err = getYtdlpVideoMeta(videoURL)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "failed")
|
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("original metadata %v\n", origMeta)
|
fmt.Printf("original metadata %v\n", origMeta)
|
||||||
db.Model(&Original{}).Where("id = ?", originalID).Update("title", origMeta.title)
|
db.Model(&Original{}).Where("id = ?", originalID).Update("title", origMeta.title)
|
||||||
|
db.Model(&Original{}).Where("id = ?", originalID).Update("artist", origMeta.artist)
|
||||||
|
|
||||||
// download original
|
// download original
|
||||||
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "downloading")
|
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "downloading")
|
||||||
videoFilename := fmt.Sprintf("%d-%s.%s", originalID, origMeta.title, origMeta.ext)
|
dlFilename := fmt.Sprintf("%d-%s.%s", originalID, origMeta.title, origMeta.ext)
|
||||||
videoFilepath := filepath.Join(getDataDir(), videoFilename)
|
dlFilepath := filepath.Join(getDataDir(), dlFilename)
|
||||||
cmd := exec.Command("yt-dlp",
|
|
||||||
"-f", "bestvideo+bestaudio/best",
|
var args []string
|
||||||
"-o", videoFilepath,
|
if audioOnly {
|
||||||
videoURL)
|
args = []string{"-f", "bestaudio"}
|
||||||
|
} else {
|
||||||
|
args = []string{"-f", "bestvideo+bestaudio/best"}
|
||||||
|
}
|
||||||
|
|
||||||
|
ytdlp := "yt-dlp"
|
||||||
|
ytdlpArgs := append(args, "-o", dlFilepath, videoURL)
|
||||||
|
|
||||||
|
fmt.Println(ytdlp, strings.Join(ytdlpArgs, " "))
|
||||||
|
cmd := exec.Command(ytdlp, ytdlpArgs...)
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "failed")
|
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "completed")
|
|
||||||
|
|
||||||
processOriginal(originalID, videoFilename, origMeta)
|
if audioOnly {
|
||||||
|
audio := Audio{
|
||||||
|
OriginalID: originalID,
|
||||||
|
Filename: dlFilename,
|
||||||
|
Source: "original",
|
||||||
|
Type: origMeta.ext,
|
||||||
|
}
|
||||||
|
fmt.Println("create Audio", audio)
|
||||||
|
db.Create(&audio)
|
||||||
|
} else {
|
||||||
|
video := Video{
|
||||||
|
OriginalID: originalID,
|
||||||
|
Filename: dlFilename,
|
||||||
|
Source: "original",
|
||||||
|
Type: origMeta.ext,
|
||||||
|
}
|
||||||
|
fmt.Println("create Video", video)
|
||||||
|
db.Create(&video)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Model(&Original{}).Where("id = ?", originalID).Update("status", "completed")
|
||||||
|
processOriginal(originalID)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
return c.Render(http.StatusOK, "videos.html", map[string]interface{}{"videos": origs})
|
return c.Render(http.StatusOK, "videos.html",
|
||||||
|
map[string]interface{}{
|
||||||
|
"videos": origs,
|
||||||
|
"build_id": getGitSHA(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoTemplate struct {
|
type VideoTemplate struct {
|
||||||
@@ -419,7 +613,7 @@ func videoHandler(c echo.Context) error {
|
|||||||
|
|
||||||
dataDir := getDataDir()
|
dataDir := getDataDir()
|
||||||
|
|
||||||
// create remporary URLs
|
// create temporary URLs
|
||||||
var videoURLs []VideoTemplate
|
var videoURLs []VideoTemplate
|
||||||
var audioURLs []AudioTemplate
|
var audioURLs []AudioTemplate
|
||||||
for _, video := range videos {
|
for _, video := range videos {
|
||||||
@@ -443,6 +637,7 @@ func videoHandler(c echo.Context) error {
|
|||||||
"videos": videoURLs,
|
"videos": videoURLs,
|
||||||
"audios": audioURLs,
|
"audios": audioURLs,
|
||||||
"dataDir": dataDir,
|
"dataDir": dataDir,
|
||||||
|
"build_id": getGitSHA(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +662,8 @@ func videoRestartHandler(c echo.Context) error {
|
|||||||
|
|
||||||
orig.Status = "pending"
|
orig.Status = "pending"
|
||||||
db.Save(&orig)
|
db.Save(&orig)
|
||||||
go startDownload(uint(id), orig.URL)
|
|
||||||
|
go startDownload(uint(id), orig.URL, orig.Audio)
|
||||||
|
|
||||||
return c.Redirect(http.StatusSeeOther, "/videos")
|
return c.Redirect(http.StatusSeeOther, "/videos")
|
||||||
}
|
}
|
||||||
|
11
models.go
11
models.go
@@ -16,8 +16,10 @@ type Original struct {
|
|||||||
UserID uint
|
UserID uint
|
||||||
URL string
|
URL string
|
||||||
Title string
|
Title string
|
||||||
Author string
|
Artist string
|
||||||
Status string // "pending", "metadata", "downloading", "completed", "failed", "cancelled"
|
Status string // "pending", "metadata", "downloading", "completed", "failed", "cancelled"
|
||||||
|
Audio bool // video download requested
|
||||||
|
Video bool // audio download requested
|
||||||
}
|
}
|
||||||
|
|
||||||
type Video struct {
|
type Video struct {
|
||||||
@@ -37,7 +39,7 @@ type Video struct {
|
|||||||
type Transcode struct {
|
type Transcode struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Status string // "pending", "running", "failed"
|
Status string // "pending", "running", "failed"
|
||||||
SrcID uint // Video.ID of the source file
|
SrcID uint // Video.ID or Audio.ID of the source file
|
||||||
OriginalID uint // Original.ID
|
OriginalID uint // Original.ID
|
||||||
SrcKind string // "video", "audio"
|
SrcKind string // "video", "audio"
|
||||||
DstKind string // "video", "audio"
|
DstKind string // "video", "audio"
|
||||||
@@ -55,8 +57,9 @@ type Transcode struct {
|
|||||||
|
|
||||||
type Audio struct {
|
type Audio struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
OriginalID uint // Original.ID
|
OriginalID uint // Original.ID
|
||||||
Rate string
|
Source string // "original", "transcode"
|
||||||
|
Rate string // in kbps
|
||||||
Length string
|
Length string
|
||||||
Size string
|
Size string
|
||||||
Type string
|
Type string
|
||||||
|
@@ -11,6 +11,11 @@
|
|||||||
<h1>Download Video</h1>
|
<h1>Download Video</h1>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="url" name="url" placeholder="Video URL" required>
|
<input type="url" name="url" placeholder="Video URL" required>
|
||||||
|
<input type="radio" id="audio-video" name="color" value="audio-video" checked>
|
||||||
|
<label for="audio-video">Audio/Video</label>
|
||||||
|
<input type="radio" id="audio-video" name="color" value="audio">
|
||||||
|
<label for="audio">Audio Only</label>
|
||||||
|
|
||||||
<button type="submit">Download</button>
|
<button type="submit">Download</button>
|
||||||
</form>
|
</form>
|
||||||
<a href="/videos">View Downloaded Videos</a>
|
<a href="/videos">View Downloaded Videos</a>
|
||||||
|
@@ -71,6 +71,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<div class="build-id">Build ID: {{.build_id}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@@ -31,6 +31,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>URL</th>
|
<th>URL</th>
|
||||||
|
<th>Type</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -44,6 +45,14 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td><a href="{{.URL}}">{{.URL}}</a></td>
|
<td><a href="{{.URL}}">{{.URL}}</a></td>
|
||||||
|
<td>
|
||||||
|
{{if .Audio}}
|
||||||
|
Audio
|
||||||
|
{{end}}
|
||||||
|
{{if .Video}}
|
||||||
|
Video
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
<td>{{.Status}}</td>
|
<td>{{.Status}}</td>
|
||||||
<td>
|
<td>
|
||||||
{{if eq .Status "completed"}}
|
{{if eq .Status "completed"}}
|
||||||
@@ -68,6 +77,11 @@
|
|||||||
</table>
|
</table>
|
||||||
<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>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<div class="build-id">Build ID: {{.build_id}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
61
workers.go
61
workers.go
@@ -141,6 +141,56 @@ func videoToAudio(transID uint, bitrate uint, videoFilepath string) {
|
|||||||
db.Delete(&trans)
|
db.Delete(&trans)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func audioToAudio(transID uint, bitrate uint, srcFilepath string) {
|
||||||
|
|
||||||
|
// determine destination path
|
||||||
|
dstFilename := uuid.Must(uuid.NewV7()).String()
|
||||||
|
dstFilename = fmt.Sprintf("%s.mp3", dstFilename)
|
||||||
|
dstFilepath := filepath.Join(getDataDir(), dstFilename)
|
||||||
|
|
||||||
|
// ensure destination directory
|
||||||
|
err := ensureDirFor(dstFilepath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error: couldn't create dir for ", dstFilepath, err)
|
||||||
|
db.Model(&Transcode{}).Where("id = ?", transID).Update("status", "failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ffmpeg := "ffmpeg"
|
||||||
|
ffmpegArgs := []string{"-i", srcFilepath, "-vn", "-acodec",
|
||||||
|
"mp3", "-b:a",
|
||||||
|
fmt.Sprintf("%dk", bitrate),
|
||||||
|
dstFilepath}
|
||||||
|
fmt.Println(ffmpeg, strings.Join(ffmpegArgs, " "))
|
||||||
|
cmd := exec.Command(ffmpeg, ffmpegArgs...)
|
||||||
|
db.Model(&Transcode{}).Where("id = ?", transID).Update("status", "running")
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error: convert to audio file", srcFilepath, "->", dstFilepath)
|
||||||
|
db.Model(&Transcode{}).Where("id = ?", transID).Update("status", "failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// look up original
|
||||||
|
var trans Transcode
|
||||||
|
db.First(&trans, "id = ?", transID)
|
||||||
|
var orig Original
|
||||||
|
db.First(&orig, "id = ?", trans.OriginalID)
|
||||||
|
|
||||||
|
// create audio record
|
||||||
|
audio := Audio{OriginalID: orig.ID, Filename: dstFilename, Rate: fmt.Sprintf("%dk", bitrate)}
|
||||||
|
|
||||||
|
fileSize, err := getSize(dstFilepath)
|
||||||
|
if err == nil {
|
||||||
|
audio.Size = humanSize(fileSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Create(&audio)
|
||||||
|
|
||||||
|
// complete transcode
|
||||||
|
db.Delete(&trans)
|
||||||
|
}
|
||||||
|
|
||||||
func transcodePending() {
|
func transcodePending() {
|
||||||
fmt.Println("transcodePending...")
|
fmt.Println("transcodePending...")
|
||||||
|
|
||||||
@@ -175,6 +225,17 @@ func transcodePending() {
|
|||||||
fmt.Println("unexpected src/dst kinds for Transcode", trans)
|
fmt.Println("unexpected src/dst kinds for Transcode", trans)
|
||||||
db.Delete(&trans)
|
db.Delete(&trans)
|
||||||
}
|
}
|
||||||
|
} else if trans.SrcKind == "audio" {
|
||||||
|
|
||||||
|
var srcAudio Audio
|
||||||
|
err = db.First(&srcAudio, "id = ?", trans.SrcID).Error
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("no such source audio for audio Transcode", trans)
|
||||||
|
db.Delete(&trans)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
srcFilepath := filepath.Join(getDataDir(), srcAudio.Filename)
|
||||||
|
audioToAudio(trans.ID, trans.Rate, srcFilepath)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("unexpected src kind for Transcode", trans)
|
fmt.Println("unexpected src kind for Transcode", trans)
|
||||||
db.Delete(&trans)
|
db.Delete(&trans)
|
||||||
|
Reference in New Issue
Block a user