mvp
This commit is contained in:
parent
97046c28ea
commit
bc5fa6c666
114
db/db.go
Normal file
114
db/db.go
Normal 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
162
main.go
Normal 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
43
templates/index.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user