2022-06-14 15:33:34 -04:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2022-07-14 15:47:19 -04:00
|
|
|
"embed"
|
2022-06-14 15:33:34 -04:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"io/fs"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path"
|
2022-06-28 23:02:53 -04:00
|
|
|
"strconv"
|
2022-06-14 15:33:34 -04:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"gitlab.com/balki/ytui/db"
|
2022-06-28 15:26:16 -04:00
|
|
|
"gitlab.com/balki/ytui/pubsub"
|
2022-06-28 23:02:53 -04:00
|
|
|
"golang.org/x/net/websocket"
|
2022-06-14 15:33:34 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
//go:embed templates/index.html
|
|
|
|
var page string
|
|
|
|
|
2022-07-14 15:47:19 -04:00
|
|
|
//go:embed assets
|
|
|
|
var assetsFS embed.FS
|
|
|
|
|
2022-06-14 15:33:34 -04:00
|
|
|
var (
|
2022-06-28 15:26:16 -04:00
|
|
|
ytdlCmd = []string{"youtube-dl"}
|
|
|
|
videosPath = "./vids"
|
|
|
|
videosUrl = "/vids"
|
|
|
|
cachePath = "./cache"
|
2022-07-14 15:47:19 -04:00
|
|
|
assetsPath = "./assets"
|
|
|
|
saveAssets = false
|
2022-06-28 15:26:16 -04:00
|
|
|
dbPath = "./db.json"
|
2022-06-30 11:40:08 -04:00
|
|
|
port = 8080
|
2022-07-14 16:13:27 -04:00
|
|
|
title = "Youtube Downloader"
|
2022-06-14 15:33:34 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
var d *db.Db
|
|
|
|
|
|
|
|
func parse() {
|
|
|
|
ytcmd := ytdlCmd[0]
|
|
|
|
flag.StringVar(&ytcmd, "ytdlcmd", ytcmd, "youtube-dl command seperated by spaces")
|
2022-07-14 16:13:27 -04:00
|
|
|
flag.StringVar(&title, "title", title, "Title of the page")
|
2022-06-14 15:33:34 -04:00
|
|
|
flag.StringVar(&videosPath, "videospath", videosPath, "Path where videos are saved")
|
2022-06-14 21:09:30 -04:00
|
|
|
flag.StringVar(&videosUrl, "videosurl", videosUrl, "Prefix of the url, i.e. https://domain.com/<this var>/<video filename>")
|
2022-06-14 15:33:34 -04:00
|
|
|
flag.StringVar(&cachePath, "cachepath", cachePath, "Path where temporary download files are saved")
|
2022-06-14 21:09:30 -04:00
|
|
|
flag.StringVar(&dbPath, "dbpath", dbPath, "Path where downloaded info is saved")
|
2022-07-14 15:47:19 -04:00
|
|
|
flag.StringVar(&assetsPath, "assetspath", assetsPath, "Path where css files are saved and served")
|
|
|
|
flag.BoolVar(&saveAssets, "saveassets", saveAssets, "Should the assets be saved in dir, so can be served by web server")
|
2022-06-30 11:40:08 -04:00
|
|
|
flag.IntVar(&port, "port", port, "Port to listen on")
|
|
|
|
|
2022-06-14 15:33:34 -04:00
|
|
|
flag.Parse()
|
|
|
|
if ytcmd != ytdlCmd[0] {
|
|
|
|
ytdlCmd = strings.Fields(ytcmd)
|
|
|
|
}
|
2022-06-14 21:09:30 -04:00
|
|
|
os.MkdirAll(cachePath, 0755)
|
|
|
|
os.MkdirAll(videosPath, 0755)
|
|
|
|
os.MkdirAll(path.Dir(dbPath), 0755)
|
2022-07-14 15:47:19 -04:00
|
|
|
if saveAssets {
|
|
|
|
os.MkdirAll(assetsPath, 0755)
|
|
|
|
}
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
2022-06-24 23:43:38 -04:00
|
|
|
log.Print("Youtube UI")
|
|
|
|
log.SetFlags(log.Flags() | log.Lshortfile)
|
2022-06-14 15:33:34 -04:00
|
|
|
parse()
|
2022-06-14 21:09:30 -04:00
|
|
|
tmpl := template.New("page")
|
|
|
|
tmpl, err := tmpl.Parse(page)
|
2022-06-14 15:33:34 -04:00
|
|
|
if err != nil {
|
2022-06-24 23:43:38 -04:00
|
|
|
log.Panic(err)
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
|
|
|
d, err = db.Load(dbPath)
|
|
|
|
if err != nil {
|
2022-06-24 23:43:38 -04:00
|
|
|
log.Panic(err)
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
|
|
|
defer d.Save()
|
2022-07-14 15:47:19 -04:00
|
|
|
|
2022-07-21 16:23:42 -04:00
|
|
|
assets := []string{
|
|
|
|
"assets/bootstrap.min.css",
|
|
|
|
"assets/bootstrap.min.js",
|
|
|
|
}
|
2022-07-14 15:47:19 -04:00
|
|
|
|
|
|
|
if saveAssets {
|
|
|
|
for _, assetFile := range assets {
|
|
|
|
fname := strings.TrimPrefix(assetFile, "assets/")
|
|
|
|
contents, err := assetsFS.ReadFile(assetFile)
|
|
|
|
if err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
|
|
|
destination := path.Join(assetsPath, fname)
|
|
|
|
err = os.WriteFile(destination, contents, 0644)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("failed writing asset, assetFile: %s, destination: %s\n", assetFile, destination)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, assetFile := range assets {
|
|
|
|
http.Handle("/"+assetFile, http.FileServer(http.FS(assetsFS)))
|
|
|
|
}
|
|
|
|
|
2022-06-14 15:33:34 -04:00
|
|
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
2022-07-18 19:21:06 -04:00
|
|
|
if r.URL.Path != "/" {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
switch r.Method {
|
|
|
|
case http.MethodGet:
|
|
|
|
td := struct {
|
|
|
|
Title string
|
|
|
|
VidoesUrl string
|
|
|
|
Items []db.Item
|
|
|
|
}{title, videosUrl, nil}
|
2022-07-14 16:13:27 -04:00
|
|
|
d.Run(func(jd *db.Jdb) {
|
2022-07-14 20:54:00 -04:00
|
|
|
//reverse order
|
|
|
|
for _, item := range jd.Items {
|
|
|
|
td.Items = append([]db.Item{item}, td.Items...)
|
|
|
|
}
|
2022-06-14 15:33:34 -04:00
|
|
|
})
|
2022-07-18 19:21:06 -04:00
|
|
|
if err := tmpl.Execute(w, td); err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
|
|
|
case http.MethodPost:
|
|
|
|
yturl := r.PostFormValue("youtube_url")
|
|
|
|
if yturl == "" {
|
|
|
|
log.Printf("yturl empty, postform:%v\n", r.PostForm)
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
id, isNew := d.Add(db.Item{
|
|
|
|
Date: time.Now().Format("2006-01-02 15:04"),
|
|
|
|
URL: yturl,
|
|
|
|
Title: "Loading",
|
|
|
|
Status: db.NotStarted,
|
|
|
|
})
|
|
|
|
if isNew {
|
|
|
|
go getTitle(id, yturl)
|
|
|
|
go download(id, yturl)
|
|
|
|
}
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
default:
|
|
|
|
w.WriteHeader(http.StatusForbidden)
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
|
|
|
})
|
2022-07-18 19:21:06 -04:00
|
|
|
|
2022-06-28 23:02:53 -04:00
|
|
|
http.HandleFunc("/ws/", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
p := r.URL.Path
|
|
|
|
idStr := strings.TrimPrefix(p, "/ws/")
|
|
|
|
id, err := strconv.Atoi(idStr)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Invalid id, %q, err:%v\n", idStr, err)
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2022-06-29 10:35:43 -04:00
|
|
|
var sc <-chan string
|
2022-06-30 12:49:15 -04:00
|
|
|
d.Transact(id, false, func(i *db.Item) {
|
2022-06-29 17:50:31 -04:00
|
|
|
pt := i.Pt
|
2022-06-29 10:35:43 -04:00
|
|
|
if pt != nil {
|
|
|
|
sc = pt.Subscribe()
|
|
|
|
}
|
|
|
|
})
|
2022-06-28 23:02:53 -04:00
|
|
|
var wh websocket.Handler = func(c *websocket.Conn) {
|
|
|
|
defer c.Close()
|
|
|
|
if sc != nil {
|
|
|
|
for update := range sc {
|
|
|
|
_, err := c.Write([]byte(update))
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("err: %v\n", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
wh.ServeHTTP(w, r)
|
|
|
|
})
|
2022-07-18 19:21:06 -04:00
|
|
|
|
2022-06-29 17:50:31 -04:00
|
|
|
http.HandleFunc("/title/", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
p := r.URL.Path
|
|
|
|
idStr := strings.TrimPrefix(p, "/title/")
|
|
|
|
id, err := strconv.Atoi(idStr)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Invalid id, %q, err:%v\n", idStr, err)
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var tc <-chan struct{}
|
2022-06-30 12:49:15 -04:00
|
|
|
d.Transact(id, false, func(i *db.Item) {
|
2022-06-29 17:50:31 -04:00
|
|
|
tc = i.TitleChan
|
|
|
|
})
|
|
|
|
<-tc
|
|
|
|
var title string
|
2022-06-30 12:49:15 -04:00
|
|
|
d.Transact(id, false, func(i *db.Item) {
|
2022-06-29 17:50:31 -04:00
|
|
|
title = i.Title
|
|
|
|
})
|
|
|
|
w.Write([]byte(title))
|
|
|
|
})
|
|
|
|
|
2022-07-18 19:21:06 -04:00
|
|
|
log.Panic(http.ListenAndServe(fmt.Sprintf(":%v", port), nil))
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func getTitle(id int, yturl string) {
|
2022-06-29 17:50:31 -04:00
|
|
|
tc := make(chan struct{})
|
|
|
|
defer close(tc)
|
2022-06-30 12:49:15 -04:00
|
|
|
d.Transact(id, false, func(i *db.Item) {
|
2022-06-29 17:50:31 -04:00
|
|
|
i.TitleChan = tc
|
|
|
|
})
|
2022-06-14 15:33:34 -04:00
|
|
|
args := append(ytdlCmd, "--get-title", yturl)
|
|
|
|
cmd := exec.Command(args[0], args[1:]...)
|
2022-06-24 23:43:38 -04:00
|
|
|
var title string
|
|
|
|
if op, err := cmd.Output(); err != nil {
|
|
|
|
log.Printf("command failed, cmd: %v, err: %v", cmd, err)
|
|
|
|
title = "ERROR"
|
|
|
|
} else {
|
|
|
|
title = string(op)
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
2022-06-30 12:49:15 -04:00
|
|
|
d.Transact(id, true, func(i *db.Item) {
|
2022-06-24 23:43:38 -04:00
|
|
|
i.Title = title
|
2022-06-14 15:33:34 -04:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func download(id int, yturl string) {
|
2022-06-28 15:26:16 -04:00
|
|
|
pt := pubsub.NewProgressTracker()
|
2022-06-30 12:49:15 -04:00
|
|
|
d.Transact(id, true, func(i *db.Item) {
|
2022-06-14 15:33:34 -04:00
|
|
|
i.Status = db.InProgress
|
2022-06-28 15:26:16 -04:00
|
|
|
i.Pt = pt
|
|
|
|
})
|
|
|
|
pc, err := pt.Publish()
|
|
|
|
defer close(pc)
|
|
|
|
if err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
2022-06-28 23:02:53 -04:00
|
|
|
|
2022-06-29 10:35:43 -04:00
|
|
|
//Log progress
|
2022-06-28 23:02:53 -04:00
|
|
|
go func() {
|
2022-06-29 17:50:31 -04:00
|
|
|
sc := pt.Subscribe()
|
2022-06-29 10:35:43 -04:00
|
|
|
log.Println("Watching download progress for id: ", id)
|
2022-06-28 23:02:53 -04:00
|
|
|
if sc != nil {
|
|
|
|
for update := range sc {
|
|
|
|
log.Println(update)
|
|
|
|
}
|
|
|
|
}
|
2022-06-29 10:35:43 -04:00
|
|
|
log.Println("Done watching download progress for id: ", id)
|
2022-06-28 23:02:53 -04:00
|
|
|
}()
|
|
|
|
|
2022-06-28 15:26:16 -04:00
|
|
|
var status db.DownloadStatus
|
|
|
|
var fname string
|
|
|
|
if fname, err = downloadYt(id, yturl, pc); err != nil {
|
2022-06-28 23:02:53 -04:00
|
|
|
log.Printf("err: %v\n", err)
|
2022-06-28 15:26:16 -04:00
|
|
|
status = db.Error
|
|
|
|
} else {
|
|
|
|
status = db.Done
|
|
|
|
}
|
2022-06-30 12:49:15 -04:00
|
|
|
d.Transact(id, true, func(i *db.Item) {
|
2022-06-28 15:26:16 -04:00
|
|
|
i.Status = status
|
|
|
|
i.FileName = fname
|
2022-06-14 15:33:34 -04:00
|
|
|
})
|
2022-06-28 15:26:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func downloadYt(id int, yturl string, pc chan<- string) (string, error) {
|
2022-06-29 10:35:43 -04:00
|
|
|
pathTmpl := fmt.Sprintf("%s/video_%d.%%(ext)s", cachePath, id)
|
|
|
|
// pathTmpl := fmt.Sprintf("%s/video_%d.mp4", cachePath, id)
|
2022-06-14 15:33:34 -04:00
|
|
|
args := append(ytdlCmd, "--newline", "--output", pathTmpl, yturl)
|
|
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
|
|
rc, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
2022-06-28 15:26:16 -04:00
|
|
|
pc <- "Pre starting error"
|
|
|
|
return "", err
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
2022-06-28 15:26:16 -04:00
|
|
|
defer rc.Close()
|
|
|
|
pc <- "Starting download"
|
2022-06-14 15:33:34 -04:00
|
|
|
if err := cmd.Start(); err != nil {
|
2022-06-28 15:26:16 -04:00
|
|
|
pc <- "Start error"
|
|
|
|
return "", err
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
|
|
|
br := bufio.NewReader(rc)
|
|
|
|
for {
|
|
|
|
line, _, err := br.ReadLine()
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
2022-06-28 15:26:16 -04:00
|
|
|
pc <- string(line)
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
2022-06-28 15:26:16 -04:00
|
|
|
pc <- "Waiting to complete..."
|
2022-06-14 15:33:34 -04:00
|
|
|
err = cmd.Wait()
|
|
|
|
var fname string
|
|
|
|
if err != nil {
|
2022-06-28 15:26:16 -04:00
|
|
|
pc <- "Download Error"
|
2022-07-21 16:23:42 -04:00
|
|
|
return "", fmt.Errorf("download Error, err: %w", err)
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|
2022-06-28 15:26:16 -04:00
|
|
|
pc <- "Download Done, renaming"
|
|
|
|
matches, err := fs.Glob(os.DirFS(cachePath), fmt.Sprintf("video_%d.*", id))
|
|
|
|
if err != nil {
|
|
|
|
pc <- "Match Error"
|
2022-07-21 16:23:42 -04:00
|
|
|
return "", fmt.Errorf("glob match error, err: %w", err)
|
2022-06-28 15:26:16 -04:00
|
|
|
}
|
|
|
|
if len(matches) != 1 {
|
|
|
|
pc <- "Multiple Match Error"
|
2022-07-21 16:23:42 -04:00
|
|
|
return "", fmt.Errorf("got multiple matches, count: %v", len(matches))
|
2022-06-28 15:26:16 -04:00
|
|
|
}
|
|
|
|
fname = matches[0]
|
|
|
|
source := path.Join(cachePath, fname)
|
|
|
|
destination := path.Join(videosPath, fname)
|
|
|
|
if err := os.Rename(source, destination); err != nil {
|
|
|
|
pc <- "Rename error"
|
2022-07-21 16:23:42 -04:00
|
|
|
return "", fmt.Errorf("rename error, fname: %q, source: %q, destination: %q, err: %w", fname, source, destination, err)
|
2022-06-28 15:26:16 -04:00
|
|
|
}
|
|
|
|
return fname, nil
|
2022-06-14 15:33:34 -04:00
|
|
|
}
|