diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..ae66276 --- /dev/null +++ b/db/db.go @@ -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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..cf27f40 --- /dev/null +++ b/main.go @@ -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 + }) +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..27f5b3d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,43 @@ + + + + Srinidhi Youtube videos + + + + +
+ + +
+ + + + + + + + + + + + {{ range .Items }} + + + + + + + {{ end }} + +
Vidoes
DateURLTitleLink
{{ .Date }}{{ .URL }}{{ .Title }} + {{ if eq .Status "Done" }} + Watch + {{ else if eq .Status "InProgress" }} + {{ .Progress }} + {{ else }} + {{ .Status }} + {{ end }} +
+ +