Add Downloader interface
This commit is contained in:
parent
814e47ef40
commit
2e1c34f0c4
14 changed files with 701 additions and 131 deletions
|
@ -6,4 +6,5 @@ require (
|
|||
github.com/gin-contrib/cors v1.3.1
|
||||
github.com/gin-gonic/gin v1.7.4
|
||||
github.com/kkdai/youtube/v2 v2.7.4
|
||||
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 // indirect
|
||||
)
|
||||
|
|
|
@ -227,8 +227,10 @@ github.com/kkdai/youtube/v2 v2.7.4/go.mod h1:XMmc0kbC9q/gPc6i881DjFYK53CuAAFrg1c
|
|||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
|
@ -282,6 +284,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
|||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 h1:nRDwTcxV9B3elxMt+1xINX0bwaPdpouqp5fbynexY8U=
|
||||
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8/go.mod h1:jOEnp79oIHy5cvQSHeLcgVJk1GHOOHJHQWps/d1N5Yo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
@ -675,6 +679,7 @@ google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/l
|
|||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
|
|
104
backend/main.go
104
backend/main.go
|
@ -6,43 +6,53 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.andreafazzi.eu/andrea/youtube-dl-service/task"
|
||||
"git.andreafazzi.eu/andrea/youtube-dl-service/youtube"
|
||||
youtube_dl "git.andreafazzi.eu/andrea/youtube-dl-service/youtube/youtube-dl"
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/kkdai/youtube/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
StatusDownloading = iota
|
||||
StatusCompleted
|
||||
StatusError
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
Video *youtube.Video
|
||||
Status int
|
||||
}
|
||||
|
||||
var taskCh chan *Task
|
||||
var tasks task.Tasks
|
||||
|
||||
func init() {
|
||||
log.SetPrefix("[youtube-dl-service] ")
|
||||
taskCh = make(chan *Task)
|
||||
tasks = make(task.Tasks, 0)
|
||||
}
|
||||
|
||||
func download(c *gin.Context, downloader youtube.Downloader) error {
|
||||
video, err := downloader.GetVideo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"title": video.Title,
|
||||
"duration": float64(video.Duration) / float64(time.Minute),
|
||||
"thumbnails": video.Thumbnails[0],
|
||||
})
|
||||
|
||||
go downloader.StartDownload(video, tasks)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func status(c *gin.Context, downloader youtube.Downloader) error {
|
||||
id, err := downloader.ExtractVideoID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task, ok := tasks[id]
|
||||
if ok {
|
||||
c.JSON(200, gin.H{
|
||||
"status": task.Status,
|
||||
"download_path": task.DownloadPath,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
go func() {
|
||||
log.Println("Start task scheduler...")
|
||||
for {
|
||||
select {
|
||||
case task := <-taskCh:
|
||||
log.Println(task.Status)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Stop task scheduler...")
|
||||
}()
|
||||
|
||||
r := gin.Default()
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"http://localhost:8080", "http://localhost:5000"},
|
||||
|
@ -57,44 +67,18 @@ func main() {
|
|||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}))
|
||||
|
||||
r.GET("/get_video", func(c *gin.Context) {
|
||||
id, err := youtube.ExtractVideoID(c.Query("url"))
|
||||
if err != nil {
|
||||
r.Static("/data", "./data")
|
||||
|
||||
r.GET("/download", func(c *gin.Context) {
|
||||
if err := download(c, youtube_dl.NewYoutubeDlDownloader(c.Query("url"), "yt-dlp")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
log.Printf("Extracted video ID: %s", id)
|
||||
|
||||
client := youtube.Client{}
|
||||
video, err := client.GetVideo(id)
|
||||
if err != nil {
|
||||
r.GET("/status", func(c *gin.Context) {
|
||||
if err := status(c, youtube_dl.NewYoutubeDlDownloader(c.Query("url"), "yt-dlp")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"title": video.Title,
|
||||
"duration": float64(video.Duration) / float64(time.Minute),
|
||||
"thumbnails": video.Thumbnails[0],
|
||||
})
|
||||
|
||||
go func() {
|
||||
log.Println("Download started.")
|
||||
taskCh <- &Task{
|
||||
video,
|
||||
StatusDownloading,
|
||||
}
|
||||
|
||||
// simulate a long task with time.Sleep(). 5 seconds
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// note that you are using the copied context "cCp", IMPORTANT
|
||||
log.Printf("Video with id %s downloaded.", video.ID)
|
||||
taskCh <- &Task{
|
||||
video,
|
||||
StatusCompleted,
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
})
|
||||
|
||||
r.Run()
|
||||
|
|
14
backend/task/task.go
Normal file
14
backend/task/task.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package task
|
||||
|
||||
const (
|
||||
StatusDownloading = iota + 1
|
||||
StatusCompleted
|
||||
StatusError
|
||||
)
|
||||
|
||||
type Tasks map[string]*Task
|
||||
|
||||
type Task struct {
|
||||
Status int
|
||||
DownloadPath string
|
||||
}
|
Binary file not shown.
29
backend/youtube/downloader.go
Normal file
29
backend/youtube/downloader.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package youtube
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.andreafazzi.eu/andrea/youtube-dl-service/task"
|
||||
)
|
||||
|
||||
type Thumbnails []Thumbnail
|
||||
|
||||
type Thumbnail struct {
|
||||
URL string
|
||||
Width uint
|
||||
Height uint
|
||||
}
|
||||
|
||||
type Video struct {
|
||||
ID string
|
||||
Title string
|
||||
Duration time.Duration
|
||||
Thumbnails Thumbnails
|
||||
DownloadPath string
|
||||
}
|
||||
|
||||
type Downloader interface {
|
||||
GetVideo() (*Video, error)
|
||||
StartDownload(video *Video, tasks task.Tasks)
|
||||
ExtractVideoID() (string, error)
|
||||
}
|
80
backend/youtube/kkdai_youtube/kkdai_youtube.go
Normal file
80
backend/youtube/kkdai_youtube/kkdai_youtube.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package kkdai_youtube
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.andreafazzi.eu/andrea/youtube-dl-service/task"
|
||||
"git.andreafazzi.eu/andrea/youtube-dl-service/youtube"
|
||||
lib "github.com/kkdai/youtube/v2"
|
||||
)
|
||||
|
||||
type LibDownloader struct {
|
||||
Url string
|
||||
}
|
||||
|
||||
func NewLibDownloader(url string) *LibDownloader {
|
||||
return &LibDownloader{url}
|
||||
}
|
||||
|
||||
func (d *LibDownloader) GetVideo() (*youtube.Video, error) {
|
||||
id, err := lib.ExtractVideoID(d.Url)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Printf("Extracted video ID: %s", id)
|
||||
client := lib.Client{}
|
||||
video, err := client.GetVideo(id)
|
||||
|
||||
thumbnails := make(youtube.Thumbnails, 0)
|
||||
|
||||
for _, t := range video.Thumbnails {
|
||||
thumbnails = append(thumbnails, youtube.Thumbnail{URL: t.URL, Width: t.Width, Height: t.Height})
|
||||
}
|
||||
|
||||
return &youtube.Video{
|
||||
ID: id,
|
||||
Title: video.Title,
|
||||
Duration: video.Duration,
|
||||
Thumbnails: thumbnails,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (d *LibDownloader) StartDownload(video *youtube.Video, tasks task.Tasks) {
|
||||
client := lib.Client{}
|
||||
v, err := client.GetVideo(video.ID)
|
||||
tasks[video.ID] = &task.Task{
|
||||
Status: task.StatusDownloading,
|
||||
}
|
||||
|
||||
log.Printf("Download of video ID %s STARTED.", video.ID)
|
||||
formats := v.Formats.WithAudioChannels() // only get videos with audio
|
||||
stream, _, err := client.GetStream(v, &formats[0])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
file, err := os.Create(filepath.Join("data", video.ID+".mp4"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, stream)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Printf("Download of video ID %s COMPLETED.", video.ID)
|
||||
tasks[video.ID] = &task.Task{
|
||||
Status: task.StatusCompleted,
|
||||
DownloadPath: filepath.Join("data", video.ID+".mp4"),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *LibDownloader) ExtractVideoID() (string, error) {
|
||||
return lib.ExtractVideoID(d.Url)
|
||||
}
|
144
backend/youtube/youtube-dl/#youtube-dl.go#
Normal file
144
backend/youtube/youtube-dl/#youtube-dl.go#
Normal file
|
@ -0,0 +1,144 @@
|
|||
package youtube_dl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.andreafazzi.eu/andrea/youtube-dl-service/task"
|
||||
"git.andreafazzi.eu/andrea/youtube-dl-service/youtube"
|
||||
lib "github.com/kkdai/youtube/v2"
|
||||
)
|
||||
|
||||
type YoutubeDlDownloader struct {
|
||||
Url string
|
||||
CommandName string
|
||||
}
|
||||
|
||||
func NewYoutubeDlDownloader(url string, commandName string) *YoutubeDlDownloader {
|
||||
return &YoutubeDlDownloader{url, commandName}
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) GetVideo() (*youtube.Video, error) {
|
||||
videoJson, err := d.getVideoJson(d.Url)
|
||||
|
||||
thumbnails := make([]youtube.Thumbnail, 0)
|
||||
thumbnailsJson := videoJson["thumbnails"].([]interface{})
|
||||
|
||||
for _, i := range thumbnailsJson {
|
||||
t := i.(map[string]interface{})
|
||||
var err error
|
||||
wString, wOk := t["width"].(string)
|
||||
hString, hOk := t["height"].(string)
|
||||
w := 0
|
||||
h := 0
|
||||
if wString != "" && hString != "" {
|
||||
w, err = strconv.Atoi(wString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err = strconv.Atoi(hString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
thumbnails = append(thumbnails, youtube.Thumbnail{
|
||||
URL: t["url"].(string),
|
||||
Width: uint(w),
|
||||
Height: uint(h),
|
||||
})
|
||||
}
|
||||
|
||||
return &youtube.Video{
|
||||
ID: videoJson["id"].(string),
|
||||
Title: videoJson["title"].(string),
|
||||
Duration: time.Duration(videoJson["duration"].(int64) * time.Hour.Nanoseconds()),
|
||||
Thumbnails: thumbnails,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) StartDownload(video *youtube.Video, tasks task.Tasks) {
|
||||
tasks[video.ID] = &task.Task{
|
||||
Status: task.StatusDownloading,
|
||||
}
|
||||
|
||||
log.Printf("Download of video ID %s STARTED.", video.ID)
|
||||
cmd := exec.CommandContext(context.Background(), d.CommandName, "--newline", d.Url)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = cmd.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for {
|
||||
tmp := make([]byte, 1024)
|
||||
_, err := stdout.Read(tmp)
|
||||
fmt.Print(string(tmp))
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Download of video ID %s COMPLETED.", video.ID)
|
||||
tasks[video.ID] = &task.Task{
|
||||
Status: task.StatusCompleted,
|
||||
DownloadPath: filepath.Join("data", video.ID+".mp4"),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) ExtractVideoID() (string, error) {
|
||||
return lib.ExtractVideoID(d.Url)
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) getVideoJson(url string) (map[string]interface{}, error) {
|
||||
cmd := exec.CommandContext(context.Background(), d.CommandName, "-j", url)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]interface{}, 0)
|
||||
err = json.Unmarshal(output, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) getThumbnails() ([]youtube.Thumbnail, error) {
|
||||
thumbnails := make([]youtube.Thumbnail, 0)
|
||||
cmd := exec.CommandContext(context.Background(), d.CommandName, "--list-thumbnails", d.Url)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(string(output), "\n")[3:]
|
||||
for _, line := range lines {
|
||||
splits := strings.Split(line, " ")
|
||||
if len(splits) == 4 {
|
||||
wString := strings.TrimSpace(splits[1])
|
||||
hString := strings.TrimSpace(splits[2])
|
||||
w, err := strconv.Atoi(wString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err := strconv.Atoi(hString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thumbnails = append(thumbnails, youtube.Thumbnail{
|
||||
URL: strings.TrimSpace(splits[3]),
|
||||
Width: uint(w),
|
||||
Height: uint(h),
|
||||
})
|
||||
}
|
||||
}
|
||||
return thumbnails, nil
|
||||
}
|
1
backend/youtube/youtube-dl/.#youtube-dl.go
Symbolic link
1
backend/youtube/youtube-dl/.#youtube-dl.go
Symbolic link
|
@ -0,0 +1 @@
|
|||
andrea@lv5.88143:1634702979
|
144
backend/youtube/youtube-dl/youtube-dl.go
Normal file
144
backend/youtube/youtube-dl/youtube-dl.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package youtube_dl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.andreafazzi.eu/andrea/youtube-dl-service/task"
|
||||
"git.andreafazzi.eu/andrea/youtube-dl-service/youtube"
|
||||
lib "github.com/kkdai/youtube/v2"
|
||||
)
|
||||
|
||||
type YoutubeDlDownloader struct {
|
||||
Url string
|
||||
CommandName string
|
||||
}
|
||||
|
||||
func NewYoutubeDlDownloader(url string, commandName string) *YoutubeDlDownloader {
|
||||
return &YoutubeDlDownloader{url, commandName}
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) GetVideo() (*youtube.Video, error) {
|
||||
videoJson, err := d.getVideoJson(d.Url)
|
||||
|
||||
thumbnails := make([]youtube.Thumbnail, 0)
|
||||
thumbnailsJson := videoJson["thumbnails"].([]interface{})
|
||||
|
||||
for _, i := range thumbnailsJson {
|
||||
t := i.(map[string]interface{})
|
||||
var err error
|
||||
wString := t["width"].(string)
|
||||
hString := t["height"].(string)
|
||||
w := 0
|
||||
h := 0
|
||||
if wString != "" && hString != "" {
|
||||
w, err = strconv.Atoi(wString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err = strconv.Atoi(hString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
thumbnails = append(thumbnails, youtube.Thumbnail{
|
||||
URL: t["url"].(string),
|
||||
Width: uint(w),
|
||||
Height: uint(h),
|
||||
})
|
||||
}
|
||||
|
||||
return &youtube.Video{
|
||||
ID: videoJson["id"].(string),
|
||||
Title: videoJson["title"].(string),
|
||||
Duration: time.Duration(videoJson["duration"].(int64) * time.Hour.Nanoseconds()),
|
||||
Thumbnails: thumbnails,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) StartDownload(video *youtube.Video, tasks task.Tasks) {
|
||||
tasks[video.ID] = &task.Task{
|
||||
Status: task.StatusDownloading,
|
||||
}
|
||||
|
||||
log.Printf("Download of video ID %s STARTED.", video.ID)
|
||||
cmd := exec.CommandContext(context.Background(), d.CommandName, "--newline", d.Url)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = cmd.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for {
|
||||
tmp := make([]byte, 1024)
|
||||
_, err := stdout.Read(tmp)
|
||||
fmt.Print(string(tmp))
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Download of video ID %s COMPLETED.", video.ID)
|
||||
tasks[video.ID] = &task.Task{
|
||||
Status: task.StatusCompleted,
|
||||
DownloadPath: filepath.Join("data", video.ID+".mp4"),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) ExtractVideoID() (string, error) {
|
||||
return lib.ExtractVideoID(d.Url)
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) getVideoJson(url string) (map[string]interface{}, error) {
|
||||
cmd := exec.CommandContext(context.Background(), d.CommandName, "-j", url)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]interface{}, 0)
|
||||
err = json.Unmarshal(output, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *YoutubeDlDownloader) getThumbnails() ([]youtube.Thumbnail, error) {
|
||||
thumbnails := make([]youtube.Thumbnail, 0)
|
||||
cmd := exec.CommandContext(context.Background(), d.CommandName, "--list-thumbnails", d.Url)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(string(output), "\n")[3:]
|
||||
for _, line := range lines {
|
||||
splits := strings.Split(line, " ")
|
||||
if len(splits) == 4 {
|
||||
wString := strings.TrimSpace(splits[1])
|
||||
hString := strings.TrimSpace(splits[2])
|
||||
w, err := strconv.Atoi(wString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err := strconv.Atoi(hString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thumbnails = append(thumbnails, youtube.Thumbnail{
|
||||
URL: strings.TrimSpace(splits[3]),
|
||||
Width: uint(w),
|
||||
Height: uint(h),
|
||||
})
|
||||
}
|
||||
}
|
||||
return thumbnails, nil
|
||||
}
|
29
backend/youtube/youtube-dl/youtube-dl_test.go
Normal file
29
backend/youtube/youtube-dl/youtube-dl_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package youtube_dl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/remogatto/prettytest"
|
||||
)
|
||||
|
||||
// Start of setup
|
||||
type testSuite struct {
|
||||
prettytest.Suite
|
||||
}
|
||||
|
||||
func TestRunner(t *testing.T) {
|
||||
prettytest.Run(
|
||||
t,
|
||||
new(testSuite),
|
||||
)
|
||||
}
|
||||
|
||||
// End of setup
|
||||
|
||||
// Tests start here
|
||||
func (t *testSuite) TestGetThumbnails() {
|
||||
d := NewYoutubeDlDownloader("https://www.youtube.com/watch?v=mWZ6b_I-Djg", "yt-dlp")
|
||||
video, err := d.GetVideo()
|
||||
t.Nil(err)
|
||||
t.True(video.Thumbnails[0].URL != "")
|
||||
}
|
|
@ -701,7 +701,7 @@ var app = (function () {
|
|||
}
|
||||
|
||||
// (36:4) {#if url_is_ok}
|
||||
function create_if_block$1(ctx) {
|
||||
function create_if_block$2(ctx) {
|
||||
let button;
|
||||
let mounted;
|
||||
let dispose;
|
||||
|
@ -732,7 +732,7 @@ var app = (function () {
|
|||
|
||||
dispatch_dev("SvelteRegisterBlock", {
|
||||
block,
|
||||
id: create_if_block$1.name,
|
||||
id: create_if_block$2.name,
|
||||
type: "if",
|
||||
source: "(36:4) {#if url_is_ok}",
|
||||
ctx
|
||||
|
@ -750,7 +750,7 @@ var app = (function () {
|
|||
let dispose;
|
||||
|
||||
function select_block_type(ctx, dirty) {
|
||||
if (/*url_is_ok*/ ctx[1]) return create_if_block$1;
|
||||
if (/*url_is_ok*/ ctx[1]) return create_if_block$2;
|
||||
return create_else_block;
|
||||
}
|
||||
|
||||
|
@ -915,10 +915,10 @@ var app = (function () {
|
|||
const { Error: Error_1 } = globals;
|
||||
const file$2 = "src/Task.svelte";
|
||||
|
||||
// (31:0) {:catch error}
|
||||
// (58:0) {:catch error}
|
||||
function create_catch_block(ctx) {
|
||||
let p;
|
||||
let t_value = /*error*/ ctx[3].message + "";
|
||||
let t_value = /*error*/ ctx[8].message + "";
|
||||
let t;
|
||||
|
||||
const block = {
|
||||
|
@ -926,7 +926,7 @@ var app = (function () {
|
|||
p = element("p");
|
||||
t = text(t_value);
|
||||
set_style(p, "color", "red");
|
||||
add_location(p, file$2, 31, 2, 847);
|
||||
add_location(p, file$2, 58, 2, 1594);
|
||||
},
|
||||
m: function mount(target, anchor) {
|
||||
insert_dev(target, p, anchor);
|
||||
|
@ -942,38 +942,38 @@ var app = (function () {
|
|||
block,
|
||||
id: create_catch_block.name,
|
||||
type: "catch",
|
||||
source: "(31:0) {:catch error}",
|
||||
source: "(58:0) {:catch error}",
|
||||
ctx
|
||||
});
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
// (21:0) {:then video_info}
|
||||
// (46:0) {:then video_info}
|
||||
function create_then_block(ctx) {
|
||||
let a;
|
||||
let div3;
|
||||
let div2;
|
||||
let div0;
|
||||
let h5;
|
||||
let t0_value = /*video_info*/ ctx[1].title + "";
|
||||
let t0_value = /*video_info*/ ctx[3].title + "";
|
||||
let t0;
|
||||
let t1;
|
||||
let div1;
|
||||
let small0;
|
||||
let t3;
|
||||
let div2;
|
||||
let span;
|
||||
let t4;
|
||||
let p;
|
||||
let small1;
|
||||
let t5;
|
||||
let t6;
|
||||
let img;
|
||||
let img_src_value;
|
||||
let t6;
|
||||
let small1;
|
||||
let t7;
|
||||
let if_block = /*status*/ ctx[1].status && create_if_block$1(ctx);
|
||||
|
||||
const block = {
|
||||
c: function create() {
|
||||
a = element("a");
|
||||
div3 = element("div");
|
||||
div2 = element("div");
|
||||
div0 = element("div");
|
||||
h5 = element("h5");
|
||||
t0 = text(t0_value);
|
||||
|
@ -982,57 +982,71 @@ var app = (function () {
|
|||
small0 = element("small");
|
||||
small0.textContent = "2 minutes ago";
|
||||
t3 = space();
|
||||
div2 = element("div");
|
||||
span = element("span");
|
||||
span.textContent = "Status";
|
||||
t5 = space();
|
||||
img = element("img");
|
||||
t6 = space();
|
||||
if (if_block) if_block.c();
|
||||
t4 = space();
|
||||
p = element("p");
|
||||
small1 = element("small");
|
||||
t7 = text(/*url*/ ctx[0]);
|
||||
add_location(h5, file$2, 23, 49, 542);
|
||||
t5 = text(/*url*/ ctx[0]);
|
||||
t6 = space();
|
||||
img = element("img");
|
||||
add_location(h5, file$2, 48, 49, 1206);
|
||||
attr_dev(div0, "class", "pb-2 flex-grow-1 bd-highlight");
|
||||
add_location(div0, file$2, 23, 6, 499);
|
||||
add_location(small0, file$2, 24, 42, 618);
|
||||
add_location(div0, file$2, 48, 6, 1163);
|
||||
add_location(small0, file$2, 49, 42, 1282);
|
||||
attr_dev(div1, "class", "pb-2 px-2 bd-highlight");
|
||||
add_location(div1, file$2, 24, 6, 582);
|
||||
attr_dev(span, "class", "badge bg-primary");
|
||||
add_location(span, file$2, 25, 42, 695);
|
||||
attr_dev(div2, "class", "pb-2 px-2 bd-highlight");
|
||||
add_location(div2, file$2, 25, 6, 659);
|
||||
attr_dev(div3, "class", "d-flex bd-highlight");
|
||||
add_location(div3, file$2, 22, 4, 459);
|
||||
if (!src_url_equal(img.src, img_src_value = /*video_info*/ ctx[1].thumbnails.URL)) attr_dev(img, "src", img_src_value);
|
||||
add_location(img, file$2, 27, 4, 761);
|
||||
add_location(small1, file$2, 28, 4, 804);
|
||||
attr_dev(a, "href", "#");
|
||||
add_location(div1, file$2, 49, 6, 1246);
|
||||
attr_dev(div2, "class", "d-flex bd-highlight");
|
||||
add_location(div2, file$2, 47, 4, 1123);
|
||||
add_location(small1, file$2, 54, 7, 1504);
|
||||
add_location(p, file$2, 54, 4, 1501);
|
||||
if (!src_url_equal(img.src, img_src_value = /*video_info*/ ctx[3].thumbnails.URL)) attr_dev(img, "src", img_src_value);
|
||||
add_location(img, file$2, 55, 4, 1533);
|
||||
attr_dev(a, "href", /*download_path*/ ctx[2]);
|
||||
attr_dev(a, "class", "list-group-item list-group-item-action");
|
||||
attr_dev(a, "aria-current", "true");
|
||||
add_location(a, file$2, 21, 2, 375);
|
||||
add_location(a, file$2, 46, 2, 1027);
|
||||
},
|
||||
m: function mount(target, anchor) {
|
||||
insert_dev(target, a, anchor);
|
||||
append_dev(a, div3);
|
||||
append_dev(div3, div0);
|
||||
append_dev(a, div2);
|
||||
append_dev(div2, div0);
|
||||
append_dev(div0, h5);
|
||||
append_dev(h5, t0);
|
||||
append_dev(div3, t1);
|
||||
append_dev(div3, div1);
|
||||
append_dev(div2, t1);
|
||||
append_dev(div2, div1);
|
||||
append_dev(div1, small0);
|
||||
append_dev(div3, t3);
|
||||
append_dev(div3, div2);
|
||||
append_dev(div2, span);
|
||||
append_dev(a, t5);
|
||||
append_dev(a, img);
|
||||
append_dev(div2, t3);
|
||||
if (if_block) if_block.m(div2, null);
|
||||
append_dev(a, t4);
|
||||
append_dev(a, p);
|
||||
append_dev(p, small1);
|
||||
append_dev(small1, t5);
|
||||
append_dev(a, t6);
|
||||
append_dev(a, small1);
|
||||
append_dev(small1, t7);
|
||||
append_dev(a, img);
|
||||
},
|
||||
p: function update(ctx, dirty) {
|
||||
if (dirty & /*url*/ 1) set_data_dev(t7, /*url*/ ctx[0]);
|
||||
if (/*status*/ ctx[1].status) {
|
||||
if (if_block) {
|
||||
if_block.p(ctx, dirty);
|
||||
} else {
|
||||
if_block = create_if_block$1(ctx);
|
||||
if_block.c();
|
||||
if_block.m(div2, null);
|
||||
}
|
||||
} else if (if_block) {
|
||||
if_block.d(1);
|
||||
if_block = null;
|
||||
}
|
||||
|
||||
if (dirty & /*url*/ 1) set_data_dev(t5, /*url*/ ctx[0]);
|
||||
|
||||
if (dirty & /*download_path*/ 4) {
|
||||
attr_dev(a, "href", /*download_path*/ ctx[2]);
|
||||
}
|
||||
},
|
||||
d: function destroy(detaching) {
|
||||
if (detaching) detach_dev(a);
|
||||
if (if_block) if_block.d();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1040,14 +1054,60 @@ var app = (function () {
|
|||
block,
|
||||
id: create_then_block.name,
|
||||
type: "then",
|
||||
source: "(21:0) {:then video_info}",
|
||||
source: "(46:0) {:then video_info}",
|
||||
ctx
|
||||
});
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
// (19:23) <p>waiting...</p> {:then video_info}
|
||||
// (51:6) {#if status.status}
|
||||
function create_if_block$1(ctx) {
|
||||
let div;
|
||||
let span;
|
||||
let t_value = /*statusBadge*/ ctx[4][/*status*/ ctx[1].status].text + "";
|
||||
let t;
|
||||
let span_class_value;
|
||||
|
||||
const block = {
|
||||
c: function create() {
|
||||
div = element("div");
|
||||
span = element("span");
|
||||
t = text(t_value);
|
||||
attr_dev(span, "class", span_class_value = /*statusBadge*/ ctx[4][/*status*/ ctx[1].status].class);
|
||||
add_location(span, file$2, 51, 37, 1380);
|
||||
attr_dev(div, "class", "pb-2 px-2 bd-highlight");
|
||||
add_location(div, file$2, 51, 1, 1344);
|
||||
},
|
||||
m: function mount(target, anchor) {
|
||||
insert_dev(target, div, anchor);
|
||||
append_dev(div, span);
|
||||
append_dev(span, t);
|
||||
},
|
||||
p: function update(ctx, dirty) {
|
||||
if (dirty & /*status*/ 2 && t_value !== (t_value = /*statusBadge*/ ctx[4][/*status*/ ctx[1].status].text + "")) set_data_dev(t, t_value);
|
||||
|
||||
if (dirty & /*status*/ 2 && span_class_value !== (span_class_value = /*statusBadge*/ ctx[4][/*status*/ ctx[1].status].class)) {
|
||||
attr_dev(span, "class", span_class_value);
|
||||
}
|
||||
},
|
||||
d: function destroy(detaching) {
|
||||
if (detaching) detach_dev(div);
|
||||
}
|
||||
};
|
||||
|
||||
dispatch_dev("SvelteRegisterBlock", {
|
||||
block,
|
||||
id: create_if_block$1.name,
|
||||
type: "if",
|
||||
source: "(51:6) {#if status.status}",
|
||||
ctx
|
||||
});
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
// (44:24) <p>waiting...</p> {:then video_info}
|
||||
function create_pending_block(ctx) {
|
||||
let p;
|
||||
|
||||
|
@ -1055,7 +1115,7 @@ var app = (function () {
|
|||
c: function create() {
|
||||
p = element("p");
|
||||
p.textContent = "waiting...";
|
||||
add_location(p, file$2, 19, 2, 336);
|
||||
add_location(p, file$2, 44, 2, 988);
|
||||
},
|
||||
m: function mount(target, anchor) {
|
||||
insert_dev(target, p, anchor);
|
||||
|
@ -1070,7 +1130,7 @@ var app = (function () {
|
|||
block,
|
||||
id: create_pending_block.name,
|
||||
type: "pending",
|
||||
source: "(19:23) <p>waiting...</p> {:then video_info}",
|
||||
source: "(44:24) <p>waiting...</p> {:then video_info}",
|
||||
ctx
|
||||
});
|
||||
|
||||
|
@ -1088,11 +1148,11 @@ var app = (function () {
|
|||
pending: create_pending_block,
|
||||
then: create_then_block,
|
||||
catch: create_catch_block,
|
||||
value: 1,
|
||||
error: 3
|
||||
value: 3,
|
||||
error: 8
|
||||
};
|
||||
|
||||
handle_promise(/*getVideoInfo*/ ctx[2](), info);
|
||||
handle_promise(/*startDownload*/ ctx[5](), info);
|
||||
|
||||
const block = {
|
||||
c: function create() {
|
||||
|
@ -1138,10 +1198,28 @@ var app = (function () {
|
|||
validate_slots('Task', slots, []);
|
||||
let { url } = $$props;
|
||||
let video_info = {};
|
||||
let status = {};
|
||||
|
||||
async function getVideoInfo() {
|
||||
const res = await fetch(`http://localhost:8080/get_video?url=${url}`);
|
||||
$$invalidate(1, video_info = await res.json());
|
||||
let statusBadge = {
|
||||
1: {
|
||||
"class": "badge bg-secondary",
|
||||
"text": "Downloading"
|
||||
},
|
||||
2: {
|
||||
"class": "badge bg-success",
|
||||
"text": "Completed"
|
||||
},
|
||||
3: {
|
||||
"class": "badge bg-error",
|
||||
"text": "Error"
|
||||
}
|
||||
};
|
||||
|
||||
let download_path = "#";
|
||||
|
||||
async function startDownload() {
|
||||
const res = await fetch(`http://localhost:8080/download?url=${url}`);
|
||||
$$invalidate(3, video_info = await res.json());
|
||||
|
||||
if (res.ok) {
|
||||
return video_info;
|
||||
|
@ -1150,6 +1228,28 @@ var app = (function () {
|
|||
}
|
||||
}
|
||||
|
||||
async function getStatus() {
|
||||
const res = await fetch(`http://localhost:8080/status?url=${url}`);
|
||||
$$invalidate(1, status = await res.json());
|
||||
|
||||
if (res.ok) {
|
||||
if (status.download_path) {
|
||||
$$invalidate(2, download_path = `http://localhost:8080/${status.download_path}`);
|
||||
}
|
||||
|
||||
return status;
|
||||
} else {
|
||||
throw new Error(status);
|
||||
}
|
||||
}
|
||||
|
||||
const interval = setInterval(
|
||||
() => {
|
||||
getStatus();
|
||||
},
|
||||
1000
|
||||
);
|
||||
|
||||
const writable_props = ['url'];
|
||||
|
||||
Object.keys($$props).forEach(key => {
|
||||
|
@ -1160,18 +1260,30 @@ var app = (function () {
|
|||
if ('url' in $$props) $$invalidate(0, url = $$props.url);
|
||||
};
|
||||
|
||||
$$self.$capture_state = () => ({ url, video_info, getVideoInfo });
|
||||
$$self.$capture_state = () => ({
|
||||
url,
|
||||
video_info,
|
||||
status,
|
||||
statusBadge,
|
||||
download_path,
|
||||
startDownload,
|
||||
getStatus,
|
||||
interval
|
||||
});
|
||||
|
||||
$$self.$inject_state = $$props => {
|
||||
if ('url' in $$props) $$invalidate(0, url = $$props.url);
|
||||
if ('video_info' in $$props) $$invalidate(1, video_info = $$props.video_info);
|
||||
if ('video_info' in $$props) $$invalidate(3, video_info = $$props.video_info);
|
||||
if ('status' in $$props) $$invalidate(1, status = $$props.status);
|
||||
if ('statusBadge' in $$props) $$invalidate(4, statusBadge = $$props.statusBadge);
|
||||
if ('download_path' in $$props) $$invalidate(2, download_path = $$props.download_path);
|
||||
};
|
||||
|
||||
if ($$props && "$$inject" in $$props) {
|
||||
$$self.$inject_state($$props.$$inject);
|
||||
}
|
||||
|
||||
return [url, video_info, getVideoInfo];
|
||||
return [url, status, download_path, video_info, statusBadge, startDownload];
|
||||
}
|
||||
|
||||
class Task extends SvelteComponentDev {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,9 +2,16 @@
|
|||
export let url;
|
||||
|
||||
let video_info = {};
|
||||
let status = {};
|
||||
let statusBadge = {
|
||||
1: {"class": "badge bg-secondary", "text": "Downloading"},
|
||||
2: {"class": "badge bg-success", "text": "Completed"},
|
||||
3: {"class": "badge bg-error", "text": "Error"}
|
||||
};
|
||||
let download_path = "#";
|
||||
|
||||
async function getVideoInfo() {
|
||||
const res = await fetch(`http://localhost:8080/get_video?url=${url}`);
|
||||
async function startDownload() {
|
||||
const res = await fetch(`http://localhost:8080/download?url=${url}`);
|
||||
video_info = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
|
@ -14,19 +21,39 @@
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
async function getStatus() {
|
||||
const res = await fetch(`http://localhost:8080/status?url=${url}`);
|
||||
status = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
if (status.download_path) {
|
||||
download_path = `http://localhost:8080/${status.download_path}`;
|
||||
}
|
||||
return status;
|
||||
} else {
|
||||
throw new Error(status);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const interval = setInterval(() => { getStatus(); }, 1000);
|
||||
|
||||
</script>
|
||||
|
||||
{#await getVideoInfo()}
|
||||
{#await startDownload()}
|
||||
<p>waiting...</p>
|
||||
{:then video_info}
|
||||
<a href="#" class="list-group-item list-group-item-action" aria-current="true">
|
||||
<a href={download_path} class="list-group-item list-group-item-action" aria-current="true">
|
||||
<div class="d-flex bd-highlight">
|
||||
<div class="pb-2 flex-grow-1 bd-highlight"><h5>{video_info.title}</h5></div>
|
||||
<div class="pb-2 px-2 bd-highlight"><small>2 minutes ago</small></div>
|
||||
<div class="pb-2 px-2 bd-highlight"><span class="badge bg-primary">Status</span></div>
|
||||
{#if status.status}
|
||||
<div class="pb-2 px-2 bd-highlight"><span class={statusBadge[status.status].class}>{statusBadge[status.status].text}</span></div>
|
||||
{/if}
|
||||
</div>
|
||||
<p><small>{url}</small></p>
|
||||
<img src={video_info.thumbnails.URL}/>
|
||||
<small>{url}</small>
|
||||
</a>
|
||||
{:catch error}
|
||||
<p style="color: red">{error.message}</p>
|
||||
|
|
Loading…
Reference in a new issue