This commit is contained in:
2023-01-10 19:10:20 +01:00
parent e590714391
commit 9aef8fc47b
34 changed files with 780 additions and 3 deletions

View File

@@ -16,8 +16,6 @@ import {
Typography
} from "@mui/material";
import { grey } from "@mui/material/colors";
import ListItemButton from '@mui/material/ListItemButton';
import { useEffect, useMemo, useState } from "react";
import { Provider, useSelector } from "react-redux";

25
go.mod Normal file
View File

@@ -0,0 +1,25 @@
module github.com/marcopeocchi/yt-dlp-web-ui
go 1.19
require (
github.com/gofiber/fiber/v2 v2.41.0
github.com/gofiber/websocket/v2 v2.1.2
)
require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/fasthttp/websocket v1.5.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/klauspost/compress v1.15.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.43.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.4.0 // indirect
)

59
go.sum Normal file
View File

@@ -0,0 +1,59 @@
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/fasthttp/websocket v1.5.0 h1:B4zbe3xXyvIdnqjOZrafVFklCUq5ZLo/TqCt5JA1wLE=
github.com/fasthttp/websocket v1.5.0/go.mod h1:n0BlOQvJdPbTuBkZT0O5+jk/sp/1/VCzquR1BehI2F4=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofiber/fiber/v2 v2.40.1/go.mod h1:Gko04sLksnHbzLSRBFWPFdzM9Ws9pRxvvIaohJK1dsk=
github.com/gofiber/fiber/v2 v2.41.0 h1:YhNoUS/OTjEz+/WLYuQ01xI7RXgKEFnGBKMagAu5f0M=
github.com/gofiber/fiber/v2 v2.41.0/go.mod h1:RdebcCuCRFp4W6hr3968/XxwJVg0K+jr9/Ae0PFzZ0Q=
github.com/gofiber/websocket/v2 v2.1.2 h1:EulKyLB/fJgui5+6c8irwEnYQ9FRsrLZfkrq9OfTDGc=
github.com/gofiber/websocket/v2 v2.1.2/go.mod h1:S+sKWo0xeC7Wnz5h4/8f6D/NxsrLFIdWDYB3SyVO9pE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.14.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc=
github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d h1:Q+gqLBOPkFGHyCJxXMRqtUgUbTjI8/Ze8vu8GGyNFwo=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.33.0/go.mod h1:KJRK/MXx0J+yd0c5hlR+s1tIHD72sniU8ZJjl97LIw4=
github.com/valyala/fasthttp v1.41.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g=
github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

39
main.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"context"
"embed"
"io/fs"
"log"
"os"
"github.com/marcopeocchi/yt-dlp-web-ui/server"
)
type ContextKey interface{}
var (
port = os.Getenv("PORT")
//go:embed dist/frontend
frontend embed.FS
)
func init() {
if port == "" {
port = "3033"
}
}
func main() {
frontend, err := fs.Sub(frontend, "dist/frontend")
if err != nil {
log.Fatalln(err)
}
ctx := context.Background()
ctx = context.WithValue(ctx, ContextKey("port"), port)
ctx = context.WithValue(ctx, ContextKey("frontend"), frontend)
server.RunBlocking(ctx)
}

23
server/cli/ascii.go Normal file
View File

@@ -0,0 +1,23 @@
package cli
import "fmt"
const (
// FG
Red = "\033[31m"
Green = "\033[32m"
Yellow = "\033[33m"
Blue = "\033[34m"
Magenta = "\033[35m"
Cyan = "\033[36m"
Reset = "\033[0m"
// BG
BgRed = "\033[1;41m"
BgBlue = "\033[1;44m"
BgGreen = "\033[1;42m"
)
// Formats a message with the specified ascii escape code, then reset.
func Format(message string, code string) string {
return fmt.Sprintf("%s%s%s", code, message, Reset)
}

68
server/handlers.go Normal file
View File

@@ -0,0 +1,68 @@
package server
import (
"log"
"github.com/goccy/go-json"
"github.com/gofiber/websocket/v2"
)
// Websocket handlers
func download(c *websocket.Conn) {
req := DownloadRequest{}
c.ReadJSON(&req)
p := Process{mem: &db, url: req.Url, params: req.Params}
p.Start()
c.WriteJSON(req)
}
func getFormats(c *websocket.Conn) {
log.Println("Requesting formats")
mtype, msg, _ := c.ReadMessage()
req := DownloadRequest{}
json.Unmarshal(msg, &req)
p := Process{mem: &db, url: req.Url}
p.GetFormatsSync()
c.WriteMessage(mtype, msg)
}
func status(c *websocket.Conn) {
mtype, _, _ := c.ReadMessage()
all := db.All()
msg, _ := json.Marshal(all)
c.WriteMessage(mtype, msg)
}
func abort(c *websocket.Conn) {
mtype, msg, _ := c.ReadMessage()
req := AbortRequest{}
json.Unmarshal(msg, &req)
p := db.Get(req.Id)
p.Kill()
c.WriteMessage(mtype, msg)
}
func abortAll(c *websocket.Conn) {
keys := db.Keys()
for _, key := range keys {
proc := db.Get(key)
if proc != nil {
proc.Kill()
}
}
}
func hotUpdate(c *websocket.Conn) {
}

121
server/memory_db.go Normal file
View File

@@ -0,0 +1,121 @@
package server
import (
"log"
"os"
"sync"
"github.com/goccy/go-json"
"github.com/google/uuid"
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
)
// In-Memory volatile Thread-Safe Key-Value Storage
type MemoryDB struct {
table map[string]*Process
mu sync.Mutex
}
// Inits the db with an empty map of string->Process pointer
func (m *MemoryDB) New() {
m.table = make(map[string]*Process)
}
// Get a process pointer given its id
func (m *MemoryDB) Get(id string) *Process {
m.mu.Lock()
res := m.table[id]
m.mu.Unlock()
return res
}
// Store a pointer of a process and return its id
func (m *MemoryDB) Set(process *Process) string {
id := uuid.Must(uuid.NewRandom()).String()
m.mu.Lock()
m.table[id] = process
m.mu.Unlock()
return id
}
// Update a process info/metadata, given the process id
func (m *MemoryDB) Update(id string, info DownloadInfo) {
m.mu.Lock()
if m.table[id] != nil {
m.table[id].Info = info
}
m.mu.Unlock()
}
// Update a process progress data, given the process id
// Used for updating completition percentage or ETA
func (m *MemoryDB) UpdateProgress(id string, progress DownloadProgress) {
m.mu.Lock()
if m.table[id] != nil {
m.table[id].Progress = progress
}
m.mu.Unlock()
}
// Removes a process progress, given the process id
func (m *MemoryDB) Delete(id string) {
m.mu.Lock()
delete(m.table, id)
m.mu.Unlock()
}
// Returns a slice of all currently stored processes id
func (m *MemoryDB) Keys() []string {
m.mu.Lock()
keys := make([]string, len(m.table))
i := 0
for k := range m.table {
keys[i] = k
i++
}
m.mu.Unlock()
return keys
}
// Returns a slice of all currently stored processes progess
func (m *MemoryDB) All() []ProcessResponse {
running := make([]ProcessResponse, len(m.table))
i := 0
for k, v := range m.table {
if v != nil {
running[i] = ProcessResponse{
Id: k,
Info: v.Info,
Progress: v.Progress,
}
i++
}
}
return running
}
// WIP: Persist the database in a single file named "session.dat"
func (m *MemoryDB) Persist() {
running := m.All()
session, err := json.Marshal(Session{
Processes: running,
})
if err != nil {
log.Println(cli.Red, "Failed to persist database", cli.Reset)
return
}
err = os.WriteFile("session.dat", session, 0700)
if err != nil {
log.Println(cli.Red, "Failed to persist database", cli.Reset)
}
}
// WIP: Restore a persisted state
func (m *MemoryDB) Restore() {
feed, _ := os.ReadFile("session.dat")
session := Session{}
json.Unmarshal(feed, &session)
}

154
server/process.go Normal file
View File

@@ -0,0 +1,154 @@
package server
import (
"bufio"
"github.com/goccy/go-json"
"log"
"os"
"os/exec"
"strings"
"time"
"github.com/marcopeocchi/yt-dlp-web-ui/server/rx"
)
const template = `download:
{
"eta":%(progress.eta)s,
"percentage":"%(progress._percent_str)s",
"speed":%(progress.speed)s
}`
const driver = "yt-dlp"
type ProgressTemplate struct {
Percentage string `json:"percentage"`
Speed float32 `json:"speed"`
Size string `json:"size"`
Eta int `json:"eta"`
}
// Process descriptor
type Process struct {
id string
url string
params []string
Info DownloadInfo
Progress DownloadProgress
mem *MemoryDB
proc *os.Process
}
// Starts spawns/forks a new yt-dlp process and parse its stdout.
// The process is spawned to outputting a custom progress text that
// Resembles a JSON Object in order to Unmarshal it later.
// This approach is anyhow not perfect: quotes are not escaped properly.
// Each process is not identified by its PID but by a UUIDv2
func (p *Process) Start() {
params := append([]string{
strings.Split(p.url, "?list")[0], //no playlist
"--newline",
"--no-colors",
"--no-playlist",
"--progress-template", strings.ReplaceAll(template, "\n", ""),
"-o",
"./downloads/%(title)s.%(ext)s",
}, p.params...)
// ----------------- main block ----------------- //
cmd := exec.Command(driver, params...)
r, err := cmd.StdoutPipe()
if err != nil {
log.Panicln(err)
}
scan := bufio.NewScanner(r)
err = cmd.Start()
if err != nil {
log.Panicln(err)
}
p.id = p.mem.Set(p)
p.proc = cmd.Process
// ----------------- info block ----------------- //
// spawn a goroutine that retrieves the info for the download
go func() {
cmd := exec.Command(driver, p.url, "-J")
stdout, err := cmd.Output()
if err != nil {
log.Println("Cannot retrieve info for", p.url)
}
info := DownloadInfo{URL: p.url}
json.Unmarshal(stdout, &info)
p.mem.Update(p.id, info)
}()
// --------------- progress block --------------- //
// unbuffered channe connected to stdout
eventChan := make(chan string)
// spawn a goroutine that does the dirty job of parsing the stdout
// fill the channel with as many stdout line as yt-dlp produces (producer)
go func() {
defer cmd.Wait()
defer r.Close()
defer p.Complete()
for scan.Scan() {
eventChan <- scan.Text()
}
}()
// do the unmarshal operation every 500ms (consumer)
go rx.Sample(time.Millisecond*500, eventChan, func(text string) {
stdout := ProgressTemplate{}
err := json.Unmarshal([]byte(text), &stdout)
if err == nil {
p.mem.UpdateProgress(p.id, DownloadProgress{
Percentage: stdout.Percentage,
Speed: stdout.Speed,
ETA: stdout.Eta,
})
shortId := strings.Split(p.id, "-")[0]
log.Printf("[%s] %s %s\n", shortId, p.url, p.Progress.Percentage)
}
})
// ------------- end progress block ------------- //
}
// Keep process in the memoryDB but marks it as complete
// Convention: All completed processes has progress -1
// and speed 0 bps.
func (p *Process) Complete() {
p.mem.UpdateProgress(p.id, DownloadProgress{
Percentage: "-1",
Speed: 0,
ETA: 0,
})
}
// Kill a process and remove it from the memory
func (p *Process) Kill() error {
err := p.proc.Kill()
p.mem.Delete(p.id)
log.Printf("Killed process %s\n", p.id)
return err
}
func (p *Process) GetFormatsSync() (DownloadInfo, error) {
cmd := exec.Command(driver, p.url, "-J")
stdout, err := cmd.Output()
if err != nil {
return DownloadInfo{}, err
}
cmd.Wait()
info := DownloadInfo{URL: p.url}
json.Unmarshal(stdout, &info)
return info, nil
}

39
server/rpc.go Normal file
View File

@@ -0,0 +1,39 @@
package server
import (
"bytes"
"io"
"net/rpc/jsonrpc"
)
// Wrapper for HTTP RPC request that implements io.Reader interface
type rpcRequest struct {
r io.Reader
rw io.ReadWriter
done chan bool
}
func NewRPCRequest(r io.Reader) *rpcRequest {
var buf bytes.Buffer
done := make(chan bool)
return &rpcRequest{r, &buf, done}
}
func (r *rpcRequest) Read(p []byte) (n int, err error) {
return r.r.Read(p)
}
func (r *rpcRequest) Write(p []byte) (n int, err error) {
return r.rw.Write(p)
}
func (r *rpcRequest) Close() error {
r.done <- true
return nil
}
func (r *rpcRequest) Call() io.Reader {
go jsonrpc.ServeConn(r)
<-r.done
return r.rw
}

47
server/rx/extensions.go Normal file
View File

@@ -0,0 +1,47 @@
package rx
import "time"
/*
Package rx contains:
- Definitions for common reactive programming functions/patterns
*/
// ReactiveX inspired debounce function.
//
// Debounce emits a string from the source channel only after a particular
// time span determined a Go Interval
// --A--B--CD--EFG-------|>
//
// -t-> |>
// -t-> |> t is a timer tick
// -t-> |>
//
// --A-----C-----G-------|>
func Debounce(interval time.Duration, source chan string, cb func(emit string)) {
var item string
timer := time.NewTimer(interval)
for {
select {
case item = <-source:
timer.Reset(interval)
case <-timer.C:
if item != "" {
cb(item)
}
}
}
}
// ReactiveX inspired sample function.
//
// Debounce emits the most recently emitted value from the source
// withing the timespan set by the span time.Duration
func Sample[T any](span time.Duration, source chan T, cb func(emit T)) {
timer := time.NewTimer(span)
for {
<-timer.C
cb(<-source)
timer.Reset(span)
}
}

63
server/server.go Normal file
View File

@@ -0,0 +1,63 @@
package server
import (
"context"
"fmt"
"io/fs"
"log"
"net/http"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/websocket/v2"
)
var db MemoryDB
func init() {
db.New()
}
func RunBlocking(ctx context.Context) {
fe := ctx.Value("frontend").(fs.SubFS)
port := ctx.Value("port")
app := fiber.New()
app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(fe),
}))
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
for {
mtype, msg, err := c.ReadMessage()
if err != nil {
break
}
switch string(msg) {
case "send-url-format-selection":
getFormats(c)
case "send-url":
download(c)
case "abort":
abort(c)
case "abort-all":
abortAll(c)
case "status":
status(c)
case "update-bin":
hotUpdate(c)
}
log.Printf("Read: %s", msg)
err = c.WriteMessage(mtype, msg)
if err != nil {
break
}
}
}))
log.Fatal(app.Listen(fmt.Sprintf(":%s", port)))
}

78
server/service.go Normal file
View File

@@ -0,0 +1,78 @@
package server
import (
"log"
"github.com/marcopeocchi/yt-dlp-web-ui/server/sys"
)
type Service int
type Running []ProcessResponse
type Pending []string
type NoArgs struct{}
type Args struct {
Id string
URL string
Params []string
}
// Exec spawns a Process.
// The result of the execution is the newly spawned process Id.
func (t *Service) Exec(args Args, result *string) error {
log.Printf("Spawning new process for %s\n", args.URL)
p := Process{mem: &db, url: args.URL, params: args.Params}
p.Start()
*result = p.id
return nil
}
// Progess retrieves the Progress of a specific Process given its Id
func (t *Service) Progess(args Args, progress *DownloadProgress) error {
*progress = db.Get(args.Id).Progress
return nil
}
// Pending retrieves a slice of all Pending/Running processes ids
func (t *Service) Pending(args NoArgs, pending *Pending) error {
*pending = Pending(db.Keys())
return nil
}
// Running retrieves a slice of all Processes progress
func (t *Service) Running(args NoArgs, running *Running) error {
*running = db.All()
return nil
}
// Kill kills a process given its id and remove it from the memoryDB
func (t *Service) Kill(args string, killed *string) error {
proc := db.Get(args)
var err error
if proc != nil {
err = proc.Kill()
}
return err
}
// KillAll kills all process unconditionally and removes them from
// the memory db
func (t *Service) KillAll(args NoArgs, killed *string) error {
keys := db.Keys()
var err error
for _, key := range keys {
proc := db.Get(key)
if proc != nil {
proc.Kill()
}
}
return err
}
// FreeSpace gets the available from package sys util
func (t *Service) FreeSpace(args NoArgs, free *uint64) error {
freeSpace, err := sys.FreeSpace()
*free = freeSpace
return err
}

20
server/sys/fs.go Normal file
View File

@@ -0,0 +1,20 @@
package sys
import (
"os"
"golang.org/x/sys/unix"
)
// package containing fs related operation (unix only)
// FreeSpace gets the available Bytes writable to download directory
func FreeSpace() (uint64, error) {
var stat unix.Statfs_t
wd, err := os.Getwd()
if err != nil {
return 0, err
}
unix.Statfs(wd+"/downloads", &stat)
return (stat.Bavail * uint64(stat.Bsize)), nil
}

43
server/types.go Normal file
View File

@@ -0,0 +1,43 @@
package server
type DownloadProgress struct {
Percentage string `json:"percentage"`
Speed float32 `json:"speed"`
ETA int `json:"eta"`
}
type DownloadInfo struct {
URL string `json:"url"`
Title string `json:"title"`
Thumbnail string `json:"thumbnail"`
Resolution string `json:"resolution"`
Size int32 `json:"filesize_approx"`
VCodec string `json:"vcodec"`
ACodec string `json:"acodec"`
Extension string `json:"ext"`
}
// struct representing the response sent to the client
// as JSON-RPC result field
type ProcessResponse struct {
Id string `json:"id"`
Progress DownloadProgress `json:"progress"`
Info DownloadInfo `json:"info"`
}
// struct representing the current status of the memoryDB
// used for serializaton/persistence reasons
type Session struct {
Processes []ProcessResponse `json:"processes"`
}
// struct representing the intent to stop a specific process
type AbortRequest struct {
Id string `json:"id"`
}
// struct representing the intent to start a download
type DownloadRequest struct {
Url string `json:"url"`
Params []string `json:"params"`
}

View File

@@ -9,6 +9,6 @@
"skipLibCheck": true,
},
"include": [
"./server/src/**/*"
"server-node/src/**/*"
]
}