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