ytui/main.go

278 lines
6.5 KiB
Go
Raw Normal View History

2022-06-14 15:33:34 -04:00
package main
import (
"bufio"
_ "embed"
"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
const port = 8080
var (
2022-06-28 15:26:16 -04:00
ytdlCmd = []string{"youtube-dl"}
videosPath = "./vids"
videosUrl = "/vids"
cachePath = "./cache"
dbPath = "./db.json"
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")
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-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-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 = tmpl.Funcs(map[string]any{
"vids_prefix": func() string { return videosUrl },
})
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()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
d.Run(func(d *db.Jdb) {
if err := tmpl.Execute(w, d); err != nil {
2022-06-24 23:43:38 -04:00
log.Panic(err)
2022-06-14 15:33:34 -04:00
}
})
return
}
yturl := r.PostFormValue("youtube_url")
if yturl == "" {
2022-06-24 23:43:38 -04:00
log.Printf("yturl empty, postform:%v\n", r.PostForm)
w.WriteHeader(http.StatusInternalServerError)
return
2022-06-14 15:33:34 -04:00
}
2022-06-28 15:26:16 -04:00
id, isNew := d.Add(db.Item{
Date: time.Now().Format("2006-01-02 15:04"),
URL: yturl,
Title: "Loading",
Status: db.NotStarted,
})
if isNew {
2022-06-14 15:33:34 -04:00
go getTitle(id, yturl)
go download(id, yturl)
}
http.Redirect(w, r, "/", http.StatusSeeOther)
})
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
}
fmt.Println("path is", p, "id is", id)
2022-06-29 10:35:43 -04:00
var sc <-chan string
2022-06-29 17:50:31 -04:00
d.Update(id, false, func(i *db.Item) {
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
}
}
}
var status db.DownloadStatus
var fname string
2022-06-29 17:50:31 -04:00
d.Update(id, true, func(i *db.Item) {
2022-06-28 23:02:53 -04:00
status = i.Status
fname = i.FileName
})
if status != db.Done {
_, err := c.Write([]byte(status))
if err != nil {
log.Printf("err: %v\n", err)
}
return
}
2022-06-29 10:35:43 -04:00
alink := fmt.Sprintf(`<a target="_blank" href="%s/%s">Watch</a>`, videosUrl, fname)
c.Write([]byte(alink))
2022-06-28 23:02:53 -04:00
}
wh.ServeHTTP(w, r)
})
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{}
d.Update(id, false, func(i *db.Item) {
tc = i.TitleChan
})
<-tc
var title string
d.Update(id, false, func(i *db.Item) {
title = i.Title
})
w.Write([]byte(title))
})
2022-06-24 23:43:38 -04:00
log.Panic(http.ListenAndServe(fmt.Sprintf(":%v", port), nil))
2022-06-29 17:50:31 -04:00
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)
d.Update(id, false, func(i *db.Item) {
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
}
d.Update(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-14 15:33:34 -04:00
d.Update(id, true, func(i *db.Item) {
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
}
d.Update(id, true, func(i *db.Item) {
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"
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"
return "", fmt.Errorf("Glob match error, err: %w\n", err)
}
if len(matches) != 1 {
pc <- "Multiple Match Error"
return "", fmt.Errorf("Got multiple matches, count: %v", len(matches))
}
fname = matches[0]
source := path.Join(cachePath, fname)
destination := path.Join(videosPath, fname)
if err := os.Rename(source, destination); err != nil {
pc <- "Rename error"
return "", fmt.Errorf("Rename error, fname: %q, source: %q, destination: %q, err: %w\n", fname, source, destination, err)
}
return fname, nil
2022-06-14 15:33:34 -04:00
}