This commit is contained in:
Balakrishnan Balasubramanian 2022-06-14 15:33:34 -04:00
parent 97046c28ea
commit bc5fa6c666
3 changed files with 319 additions and 0 deletions

114
db/db.go Normal file
View File

@ -0,0 +1,114 @@
package db
import (
"encoding/json"
"errors"
"fmt"
"os"
"sync"
)
type DownloadStatus string
var (
NotStarted DownloadStatus = "NotStarted"
InProgress DownloadStatus = "InProgress"
Done DownloadStatus = "Done"
Error DownloadStatus = "Error"
)
type Item struct {
Id int `json:"id"`
Date string `json:"date"`
URL string `json:"url"`
Title string `json:"title"`
Approved bool `json:"approved"`
Status DownloadStatus `json:"status"`
FileName string `json:"file_name"`
Progress string `json:"-"`
}
type Jdb struct {
Items []Item `json:"items"`
}
type Db struct {
items []Item
mutex sync.Mutex
lastId int
path string
}
func (d *Db) Add(i Item) int {
d.mutex.Lock()
defer d.mutex.Unlock()
i.Id = d.lastId
d.lastId++
d.items = append(d.items, i)
d.save()
return i.Id
}
func (d *Db) Update(id int, persist bool, f func(*Item)) error {
d.mutex.Lock()
defer d.mutex.Unlock()
for i, _ := range d.items {
if d.items[i].Id == id {
f(&d.items[i])
if persist {
d.save()
}
return nil
}
}
return fmt.Errorf("Invalid id: %d", id)
}
func (d *Db) save() error {
data, err := json.Marshal(Jdb{d.items})
if err != nil {
return err
}
return os.WriteFile(d.path, data, 0644)
}
func (d *Db) Run(f func(*Jdb)) {
d.mutex.Lock()
defer d.mutex.Unlock()
f(&Jdb{d.items})
}
func (d *Db) Save() error {
d.mutex.Lock()
defer d.mutex.Unlock()
return d.save()
}
func Load(path string) (*Db, error) {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return &Db{
path: path,
lastId: 0,
}, nil
}
return nil, err
}
var jd Jdb
err = json.Unmarshal(data, &jd)
if err != nil {
return nil, err
}
m := 0
for _, item := range jd.Items {
if item.Id > m {
m = item.Id
}
}
return &Db{
items: jd.Items,
path: path,
lastId: m + 1,
}, nil
}

162
main.go Normal file
View File

@ -0,0 +1,162 @@
package main
import (
"bufio"
_ "embed"
"flag"
"fmt"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"os/exec"
"path"
"strings"
"time"
"gitlab.com/balki/ytui/db"
)
//go:embed templates/index.html
var page string
const port = 8080
var (
ytdlCmd = []string{"youtube-dl"}
videosPath = "./vids"
cachePath = "./cache"
dbPath = "./db.json"
approval bool = false
)
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")
flag.StringVar(&cachePath, "cachepath", cachePath, "Path where temporary download files are saved")
flag.StringVar(&dbPath, "datapath", dbPath, "Path where downloaded info is saved")
flag.BoolVar(&approval, "approval", approval, "Is approval required before allowing to watch")
flag.Parse()
if ytcmd != ytdlCmd[0] {
ytdlCmd = strings.Fields(ytcmd)
}
}
func main() {
fmt.Println("vim-go")
parse()
tmpl, err := template.New("page").Parse(page)
if err != nil {
panic(err)
}
d, err = db.Load(dbPath)
if err != nil {
panic(err)
}
defer d.Save()
seen := map[string]struct{}{}
d.Run(func(d *db.Jdb) {
for _, i := range d.Items {
seen[i.URL] = struct{}{}
}
})
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 {
panic(err)
}
})
return
}
if err := r.ParseForm(); err != nil {
panic(err)
}
yturl := r.PostFormValue("youtube_url")
if yturl == "" {
panic("yturl empty")
}
if _, ok := seen[yturl]; !ok {
seen[yturl] = struct{}{}
id := d.Add(db.Item{
Date: time.Now().String(),
URL: yturl,
Title: "Loading",
Approved: false,
Status: db.NotStarted,
})
go getTitle(id, yturl)
go download(id, yturl)
}
http.Redirect(w, r, "/", http.StatusSeeOther)
})
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil))
}
func getTitle(id int, yturl string) {
args := append(ytdlCmd, "--get-title", yturl)
cmd := exec.Command(args[0], args[1:]...)
op, err := cmd.Output()
if err != nil {
panic(err)
}
d.Update(id, true, func(i *db.Item) {
i.Title = string(op)
})
}
func download(id int, yturl string) {
d.Update(id, true, func(i *db.Item) {
i.Status = db.InProgress
})
pathTmpl := fmt.Sprintf("%s/video_%d.%%(ext)s", cachePath, id)
args := append(ytdlCmd, "--newline", "--output", pathTmpl, yturl)
cmd := exec.Command(args[0], args[1:]...)
rc, err := cmd.StdoutPipe()
if err != nil {
log.Panic(err)
}
if err := cmd.Start(); err != nil {
log.Panic(err)
}
br := bufio.NewReader(rc)
for {
line, _, err := br.ReadLine()
if err != nil {
break
}
d.Update(id, false, func(i *db.Item) {
i.Progress = string(line)
})
}
rc.Close()
err = cmd.Wait()
var status db.DownloadStatus
var fname string
if err != nil {
status = db.Error
} else {
status = db.Done
matches, err := fs.Glob(os.DirFS(cachePath), fmt.Sprintf("video_%d.*", id))
if err != nil {
panic(err)
}
if len(matches) != 1 {
panic(len(matches))
}
fname = matches[0]
err = os.Rename(path.Join(cachePath, fname), path.Join(videosPath, fname))
if err != nil {
panic(err)
}
}
d.Update(id, true, func(i *db.Item) {
i.Status = status
i.FileName = fname
})
}

43
templates/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>Srinidhi Youtube videos</title>
<!-- Diable favicon requests: https://stackoverflow.com/a/13416784 -->
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
</head>
<body>
<form method="POST" action="/">
<input type="text" name="youtube_url">
<input type="submit" value="Ask">
</form>
<table>
<caption>Vidoes</caption>
<thead>
<tr>
<th>Date</th>
<th>URL</th>
<th>Title</th>
<th>Link</th>
</tr>
</thead>
<tbody>
{{ range .Items }}
<tr>
<td>{{ .Date }}</td>
<td>{{ .URL }}</td>
<td>{{ .Title }}</td>
<td>
{{ if eq .Status "Done" }}
<a href="./vids/{{ .FileName }}">Watch</a>
{{ else if eq .Status "InProgress" }}
{{ .Progress }}
{{ else }}
{{ .Status }}
{{ end }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</body>
</html>