mvp
This commit is contained in:
		
							
								
								
									
										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>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user