From 9aef8fc47b27d35d43fc0f23cddd521780abc8de Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Tue, 10 Jan 2023 19:10:20 +0100 Subject: [PATCH 01/11] ?? --- frontend/src/App.tsx | 2 - go.mod | 25 +++ go.sum | 59 +++++++ main.go | 39 +++++ .../src/core/HTTPServer.ts | 0 {server => server-node}/src/core/Process.ts | 0 .../src/core/downloadArchive.ts | 0 .../src/core/downloader.ts | 0 {server => server-node}/src/core/states.ts | 0 {server => server-node}/src/core/streamer.ts | 0 {server => server-node}/src/db/memoryDB.ts | 0 .../src/interfaces/IDownloadMetadata.d.ts | 0 .../src/interfaces/IPayload.d.ts | 0 .../src/interfaces/IRecord.d.ts | 0 .../src/interfaces/ISettings.d.ts | 0 {server => server-node}/src/main.ts | 0 {server => server-node}/src/types/index.d.ts | 0 .../src/utils/BetterLogger.ts | 0 .../src/utils/directoryUtils.ts | 0 {server => server-node}/src/utils/logger.ts | 0 {server => server-node}/src/utils/params.ts | 0 .../src/utils/procUtils.ts | 0 {server => server-node}/src/utils/updater.ts | 0 server/cli/ascii.go | 23 +++ server/handlers.go | 68 ++++++++ server/memory_db.go | 121 ++++++++++++++ server/process.go | 154 ++++++++++++++++++ server/rpc.go | 39 +++++ server/rx/extensions.go | 47 ++++++ server/server.go | 63 +++++++ server/service.go | 78 +++++++++ server/sys/fs.go | 20 +++ server/types.go | 43 +++++ tsconfig.json | 2 +- 34 files changed, 780 insertions(+), 3 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go rename {server => server-node}/src/core/HTTPServer.ts (100%) rename {server => server-node}/src/core/Process.ts (100%) rename {server => server-node}/src/core/downloadArchive.ts (100%) rename {server => server-node}/src/core/downloader.ts (100%) rename {server => server-node}/src/core/states.ts (100%) rename {server => server-node}/src/core/streamer.ts (100%) rename {server => server-node}/src/db/memoryDB.ts (100%) rename {server => server-node}/src/interfaces/IDownloadMetadata.d.ts (100%) rename {server => server-node}/src/interfaces/IPayload.d.ts (100%) rename {server => server-node}/src/interfaces/IRecord.d.ts (100%) rename {server => server-node}/src/interfaces/ISettings.d.ts (100%) rename {server => server-node}/src/main.ts (100%) rename {server => server-node}/src/types/index.d.ts (100%) rename {server => server-node}/src/utils/BetterLogger.ts (100%) rename {server => server-node}/src/utils/directoryUtils.ts (100%) rename {server => server-node}/src/utils/logger.ts (100%) rename {server => server-node}/src/utils/params.ts (100%) rename {server => server-node}/src/utils/procUtils.ts (100%) rename {server => server-node}/src/utils/updater.ts (100%) create mode 100644 server/cli/ascii.go create mode 100644 server/handlers.go create mode 100644 server/memory_db.go create mode 100644 server/process.go create mode 100644 server/rpc.go create mode 100644 server/rx/extensions.go create mode 100644 server/server.go create mode 100644 server/service.go create mode 100644 server/sys/fs.go create mode 100644 server/types.go diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c8e3bbb..5290988 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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"; diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..838113d --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..83c2916 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6de05a8 --- /dev/null +++ b/main.go @@ -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) +} diff --git a/server/src/core/HTTPServer.ts b/server-node/src/core/HTTPServer.ts similarity index 100% rename from server/src/core/HTTPServer.ts rename to server-node/src/core/HTTPServer.ts diff --git a/server/src/core/Process.ts b/server-node/src/core/Process.ts similarity index 100% rename from server/src/core/Process.ts rename to server-node/src/core/Process.ts diff --git a/server/src/core/downloadArchive.ts b/server-node/src/core/downloadArchive.ts similarity index 100% rename from server/src/core/downloadArchive.ts rename to server-node/src/core/downloadArchive.ts diff --git a/server/src/core/downloader.ts b/server-node/src/core/downloader.ts similarity index 100% rename from server/src/core/downloader.ts rename to server-node/src/core/downloader.ts diff --git a/server/src/core/states.ts b/server-node/src/core/states.ts similarity index 100% rename from server/src/core/states.ts rename to server-node/src/core/states.ts diff --git a/server/src/core/streamer.ts b/server-node/src/core/streamer.ts similarity index 100% rename from server/src/core/streamer.ts rename to server-node/src/core/streamer.ts diff --git a/server/src/db/memoryDB.ts b/server-node/src/db/memoryDB.ts similarity index 100% rename from server/src/db/memoryDB.ts rename to server-node/src/db/memoryDB.ts diff --git a/server/src/interfaces/IDownloadMetadata.d.ts b/server-node/src/interfaces/IDownloadMetadata.d.ts similarity index 100% rename from server/src/interfaces/IDownloadMetadata.d.ts rename to server-node/src/interfaces/IDownloadMetadata.d.ts diff --git a/server/src/interfaces/IPayload.d.ts b/server-node/src/interfaces/IPayload.d.ts similarity index 100% rename from server/src/interfaces/IPayload.d.ts rename to server-node/src/interfaces/IPayload.d.ts diff --git a/server/src/interfaces/IRecord.d.ts b/server-node/src/interfaces/IRecord.d.ts similarity index 100% rename from server/src/interfaces/IRecord.d.ts rename to server-node/src/interfaces/IRecord.d.ts diff --git a/server/src/interfaces/ISettings.d.ts b/server-node/src/interfaces/ISettings.d.ts similarity index 100% rename from server/src/interfaces/ISettings.d.ts rename to server-node/src/interfaces/ISettings.d.ts diff --git a/server/src/main.ts b/server-node/src/main.ts similarity index 100% rename from server/src/main.ts rename to server-node/src/main.ts diff --git a/server/src/types/index.d.ts b/server-node/src/types/index.d.ts similarity index 100% rename from server/src/types/index.d.ts rename to server-node/src/types/index.d.ts diff --git a/server/src/utils/BetterLogger.ts b/server-node/src/utils/BetterLogger.ts similarity index 100% rename from server/src/utils/BetterLogger.ts rename to server-node/src/utils/BetterLogger.ts diff --git a/server/src/utils/directoryUtils.ts b/server-node/src/utils/directoryUtils.ts similarity index 100% rename from server/src/utils/directoryUtils.ts rename to server-node/src/utils/directoryUtils.ts diff --git a/server/src/utils/logger.ts b/server-node/src/utils/logger.ts similarity index 100% rename from server/src/utils/logger.ts rename to server-node/src/utils/logger.ts diff --git a/server/src/utils/params.ts b/server-node/src/utils/params.ts similarity index 100% rename from server/src/utils/params.ts rename to server-node/src/utils/params.ts diff --git a/server/src/utils/procUtils.ts b/server-node/src/utils/procUtils.ts similarity index 100% rename from server/src/utils/procUtils.ts rename to server-node/src/utils/procUtils.ts diff --git a/server/src/utils/updater.ts b/server-node/src/utils/updater.ts similarity index 100% rename from server/src/utils/updater.ts rename to server-node/src/utils/updater.ts diff --git a/server/cli/ascii.go b/server/cli/ascii.go new file mode 100644 index 0000000..509f1f4 --- /dev/null +++ b/server/cli/ascii.go @@ -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) +} diff --git a/server/handlers.go b/server/handlers.go new file mode 100644 index 0000000..acf5091 --- /dev/null +++ b/server/handlers.go @@ -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) { + +} diff --git a/server/memory_db.go b/server/memory_db.go new file mode 100644 index 0000000..4dd914a --- /dev/null +++ b/server/memory_db.go @@ -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) +} diff --git a/server/process.go b/server/process.go new file mode 100644 index 0000000..10ba5d3 --- /dev/null +++ b/server/process.go @@ -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 +} diff --git a/server/rpc.go b/server/rpc.go new file mode 100644 index 0000000..ec48f1f --- /dev/null +++ b/server/rpc.go @@ -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 +} diff --git a/server/rx/extensions.go b/server/rx/extensions.go new file mode 100644 index 0000000..9886712 --- /dev/null +++ b/server/rx/extensions.go @@ -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) + } +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..ae139e4 --- /dev/null +++ b/server/server.go @@ -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))) +} diff --git a/server/service.go b/server/service.go new file mode 100644 index 0000000..0e5c8c6 --- /dev/null +++ b/server/service.go @@ -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 +} diff --git a/server/sys/fs.go b/server/sys/fs.go new file mode 100644 index 0000000..00ef630 --- /dev/null +++ b/server/sys/fs.go @@ -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 +} diff --git a/server/types.go b/server/types.go new file mode 100644 index 0000000..4138249 --- /dev/null +++ b/server/types.go @@ -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"` +} diff --git a/tsconfig.json b/tsconfig.json index 67b0c3f..f27b390 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,6 @@ "skipLibCheck": true, }, "include": [ - "./server/src/**/*" + "server-node/src/**/*" ] } \ No newline at end of file From 299b195b52786075bf04b51bca057e72b3c4d4e3 Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Tue, 10 Jan 2023 21:33:21 +0100 Subject: [PATCH 02/11] prototyping --- server/process.go | 6 +++--- server/server.go | 29 +++++++++-------------------- server/service.go | 8 ++++++++ server/types.go | 17 +++++++++++++++++ 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/server/process.go b/server/process.go index 10ba5d3..0f92180 100644 --- a/server/process.go +++ b/server/process.go @@ -137,17 +137,17 @@ func (p *Process) Kill() error { return err } -func (p *Process) GetFormatsSync() (DownloadInfo, error) { +func (p *Process) GetFormatsSync() (DownloadFormats, error) { cmd := exec.Command(driver, p.url, "-J") stdout, err := cmd.Output() if err != nil { - return DownloadInfo{}, err + return DownloadFormats{}, err } cmd.Wait() - info := DownloadInfo{URL: p.url} + info := DownloadFormats{URL: p.url} json.Unmarshal(stdout, &info) return info, nil diff --git a/server/server.go b/server/server.go index ae139e4..947eed7 100644 --- a/server/server.go +++ b/server/server.go @@ -3,9 +3,11 @@ package server import ( "context" "fmt" + "io" "io/fs" "log" "net/http" + "net/rpc" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/filesystem" @@ -22,6 +24,9 @@ func RunBlocking(ctx context.Context) { fe := ctx.Value("frontend").(fs.SubFS) port := ctx.Value("port") + service := new(Service) + rpc.Register(service) + app := fiber.New() app.Use("/", filesystem.New(filesystem.Config{ @@ -30,32 +35,16 @@ func RunBlocking(ctx context.Context) { app.Get("/ws", websocket.New(func(c *websocket.Conn) { for { - mtype, msg, err := c.ReadMessage() + mtype, reader, err := c.NextReader() 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) + writer, err := c.NextWriter(mtype) if err != nil { break } + res := NewRPCRequest(reader).Call() + io.Copy(writer, res) } })) diff --git a/server/service.go b/server/service.go index 0e5c8c6..1889572 100644 --- a/server/service.go +++ b/server/service.go @@ -34,6 +34,14 @@ func (t *Service) Progess(args Args, progress *DownloadProgress) error { return nil } +// Progess retrieves the Progress of a specific Process given its Id +func (t *Service) Formats(args Args, progress *DownloadFormats) error { + var err error + p := Process{url: args.URL} + *progress, err = p.GetFormatsSync() + return err +} + // Pending retrieves a slice of all Pending/Running processes ids func (t *Service) Pending(args NoArgs, pending *Pending) error { *pending = Pending(db.Keys()) diff --git a/server/types.go b/server/types.go index 4138249..26fcede 100644 --- a/server/types.go +++ b/server/types.go @@ -17,6 +17,23 @@ type DownloadInfo struct { Extension string `json:"ext"` } +type DownloadFormats struct { + Formats []Format `json:"formats"` + Best Format `json:"best"` + Thumbnail string `json:"thumbnail"` + Title string `json:"title"` + URL string `json:"url"` +} + +type Format struct { + Format_id string `json:"format_id"` + Format_note string `json:"format_note"` + FPS float32 `json:"fps"` + Resolution string `json:"resolution"` + VCodec string `json:"vcodec"` + ACodec string `json:"acodec"` +} + // struct representing the response sent to the client // as JSON-RPC result field type ProcessResponse struct { From b29cdf802d596b752ddd2c14ce40fdd0dd3f391c Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Wed, 11 Jan 2023 12:41:20 +0100 Subject: [PATCH 03/11] pre frontend integration --- go.mod | 7 +++-- go.sum | 5 ++-- server/handlers.go | 68 ---------------------------------------------- server/process.go | 11 +++++++- 4 files changed, 16 insertions(+), 75 deletions(-) delete mode 100644 server/handlers.go diff --git a/go.mod b/go.mod index 838113d..e5d5f39 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,17 @@ module github.com/marcopeocchi/yt-dlp-web-ui go 1.19 require ( + github.com/goccy/go-json v0.10.0 github.com/gofiber/fiber/v2 v2.41.0 github.com/gofiber/websocket/v2 v2.1.2 + github.com/google/uuid v1.3.0 + github.com/marcopeocchi/fazzoletti v0.0.0-20221114144444-1e802380a7db + golang.org/x/sys v0.4.0 ) 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 @@ -21,5 +23,4 @@ require ( 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 ) diff --git a/go.sum b/go.sum index 83c2916..5a88ba0 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,11 @@ github.com/gofiber/websocket/v2 v2.1.2/go.mod h1:S+sKWo0xeC7Wnz5h4/8f6D/NxsrLFId 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/marcopeocchi/fazzoletti v0.0.0-20221114144444-1e802380a7db h1:SmKRgCLsImPxBTIzmUpbQyv+7FembiZaq/QTwtDqar4= +github.com/marcopeocchi/fazzoletti v0.0.0-20221114144444-1e802380a7db/go.mod h1:RvfVo/6Sbnfra9kkvIxDW8NYOOaYsHjF0DdtMCs9cdo= 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= @@ -23,7 +24,6 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn 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= @@ -48,7 +48,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w 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= diff --git a/server/handlers.go b/server/handlers.go deleted file mode 100644 index acf5091..0000000 --- a/server/handlers.go +++ /dev/null @@ -1,68 +0,0 @@ -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) { - -} diff --git a/server/process.go b/server/process.go index 0f92180..faab93b 100644 --- a/server/process.go +++ b/server/process.go @@ -2,6 +2,7 @@ package server import ( "bufio" + "regexp" "github.com/goccy/go-json" @@ -11,6 +12,7 @@ import ( "strings" "time" + "github.com/marcopeocchi/fazzoletti/slices" "github.com/marcopeocchi/yt-dlp-web-ui/server/rx" ) @@ -47,6 +49,13 @@ type Process struct { // 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() { + // escape bash variable escaping and command piping, you'll never know + // what they might come with... + p.params = slices.Filter(p.params, func(e string) bool { + match, _ := regexp.MatchString(`(\$\{)|(\&\&)`, e) + return !match + }) + params := append([]string{ strings.Split(p.url, "?list")[0], //no playlist "--newline", @@ -87,7 +96,7 @@ func (p *Process) Start() { }() // --------------- progress block --------------- // - // unbuffered channe connected to stdout + // unbuffered channel connected to stdout eventChan := make(chan string) // spawn a goroutine that does the dirty job of parsing the stdout From 4d4582b3f77e2e5cabf57511718a8751e49b45f2 Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Wed, 11 Jan 2023 20:49:25 +0100 Subject: [PATCH 04/11] download working --- frontend/src/App.tsx | 6 +- frontend/src/Home.tsx | 873 ++++++++++---------- frontend/src/Settings.tsx | 5 +- frontend/src/components/StackableResult.tsx | 29 +- frontend/src/rpcClient.ts | 81 ++ frontend/src/types.d.ts | 43 + frontend/src/utils.ts | 6 +- server/server.go | 15 +- 8 files changed, 584 insertions(+), 474 deletions(-) create mode 100644 frontend/src/rpcClient.ts create mode 100644 frontend/src/types.d.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5290988..7894f46 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -39,7 +39,7 @@ function AppContent() { const settings = useSelector((state: RootState) => state.settings) const status = useSelector((state: RootState) => state.status) - const socket = useMemo(() => io(getWebSocketEndpoint()), []) + const socket = useMemo(() => new WebSocket(getWebSocketEndpoint()), []) const mode = settings.theme @@ -60,9 +60,7 @@ function AppContent() { /* Get disk free space */ useEffect(() => { - socket.on('free-space', (res: string) => { - setFreeDiskSpace(res) - }) + }, []) return ( diff --git a/frontend/src/Home.tsx b/frontend/src/Home.tsx index d87e077..b0b3c68 100644 --- a/frontend/src/Home.tsx +++ b/frontend/src/Home.tsx @@ -1,492 +1,455 @@ import { FileUpload } from "@mui/icons-material"; import { - Backdrop, - Button, - ButtonGroup, - CircularProgress, - Container, - FormControl, - Grid, - IconButton, - InputAdornment, - InputLabel, - MenuItem, - Paper, - Select, - Snackbar, - styled, - TextField, - Typography + Backdrop, + Button, + ButtonGroup, + CircularProgress, + Container, + FormControl, + Grid, + IconButton, + InputAdornment, + InputLabel, + MenuItem, + Paper, + Select, + Snackbar, + styled, + TextField, + Typography } from "@mui/material"; import { Buffer } from 'buffer'; import { Fragment, useEffect, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { Socket } from "socket.io-client"; import { CliArguments } from "./classes"; import { StackableResult } from "./components/StackableResult"; import { serverStates } from "./events"; -import { connected, downloading, finished } from "./features/status/statusSlice"; +import { connected } from "./features/status/statusSlice"; import { I18nBuilder } from "./i18n"; -import { IDLMetadata, IDLMetadataAndPID, IMessage } from "./interfaces"; +import { IDLMetadata, IMessage } from "./interfaces"; +import { RPCClient } from "./rpcClient"; import { RootState } from "./stores/store"; +import { RPCResult } from "./types"; import { isValidURL, toFormatArgs, updateInStateMap } from "./utils"; type Props = { - socket: Socket + socket: WebSocket } export default function Home({ socket }: Props) { - // redux state - const settings = useSelector((state: RootState) => state.settings) - const status = useSelector((state: RootState) => state.status) - const dispatch = useDispatch() + // redux state + const settings = useSelector((state: RootState) => state.settings) + const status = useSelector((state: RootState) => state.status) + const dispatch = useDispatch() - // ephemeral state - const [progressMap, setProgressMap] = useState(new Map()); - const [messageMap, setMessageMap] = useState(new Map()); - const [downloadInfoMap, setDownloadInfoMap] = useState(new Map()); - const [downloadFormats, setDownloadFormats] = useState(); - const [pickedVideoFormat, setPickedVideoFormat] = useState(''); - const [pickedAudioFormat, setPickedAudioFormat] = useState(''); - const [pickedBestFormat, setPickedBestFormat] = useState(''); + // ephemeral state + const [progressMap, setProgressMap] = useState(new Map()); + const [messageMap, setMessageMap] = useState(new Map()); - const [downloadPath, setDownloadPath] = useState(0); - const [availableDownloadPaths, setAvailableDownloadPaths] = useState([]); + const [activeDownloads, setActiveDownloads] = useState(new Array()); + const [downloadInfoMap, setDownloadInfoMap] = useState(new Map()); + const [downloadFormats, setDownloadFormats] = useState(); + const [pickedVideoFormat, setPickedVideoFormat] = useState(''); + const [pickedAudioFormat, setPickedAudioFormat] = useState(''); + const [pickedBestFormat, setPickedBestFormat] = useState(''); - const [fileNameOverride, setFilenameOverride] = useState(''); + const [downloadPath, setDownloadPath] = useState(0); + const [availableDownloadPaths, setAvailableDownloadPaths] = useState([]); - const [url, setUrl] = useState(''); - const [workingUrl, setWorkingUrl] = useState(''); - const [showBackdrop, setShowBackdrop] = useState(false); - const [showToast, setShowToast] = useState(true); + const [fileNameOverride, setFilenameOverride] = useState(''); - // memos - const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language]) - const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs]) + const [url, setUrl] = useState(''); + const [workingUrl, setWorkingUrl] = useState(''); + const [showBackdrop, setShowBackdrop] = useState(false); + const [showToast, setShowToast] = useState(true); - /* -------------------- Effects -------------------- */ - /* WebSocket connect event handler*/ - useEffect(() => { - socket.on('connect', () => { - dispatch(connected()) - socket.emit('fetch-jobs') - socket.emit('disk-space') - socket.emit('retrieve-jobs') - }); - }, []) + // memos + const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language]) + const client = useMemo(() => new RPCClient(socket), [settings.serverAddr, settings.serverPort]) + const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs]) - /* Ask server for pending jobs / background jobs */ - useEffect(() => { - socket.on('pending-jobs', (count: number) => { - count === 0 ? setShowBackdrop(false) : setShowBackdrop(true) - }) - }, []) - - /* Handle download information sent by server */ - useEffect(() => { - socket.on('available-formats', (data: IDLMetadata) => { - setShowBackdrop(false) - setDownloadFormats(data); - }) - }, []) - - /* Handle download information sent by server */ - useEffect(() => { - socket.on('metadata', (data: IDLMetadataAndPID) => { - setShowBackdrop(false) - dispatch(downloading()) - updateInStateMap(data.pid, data.metadata, downloadInfoMap, setDownloadInfoMap); - }) - }, []) - - /* Handle per-download progress */ - useEffect(() => { - socket.on('progress', (data: IMessage) => { - if (data.status === serverStates.PROG_DONE || data.status === serverStates.PROC_ABORT) { - setShowBackdrop(false) - updateInStateMap(data.pid, serverStates.PROG_DONE, messageMap, setMessageMap); - updateInStateMap(data.pid, 0, progressMap, setProgressMap); - socket.emit('disk-space') - dispatch(finished()) - return; - } - updateInStateMap(data.pid, data, messageMap, setMessageMap); - if (data.progress) { - updateInStateMap(data.pid, - Math.ceil(Number(data.progress.replace('%', ''))), - progressMap, - setProgressMap - ); - } - }) - }, []) - - useEffect(() => { - fetch(`${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}/tree`) - .then(res => res.json()) - .then(data => { - setAvailableDownloadPaths(data.flat) - }) - }, []) - - /* -------------------- component functions -------------------- */ - - /** - * Retrive url from input, cli-arguments from checkboxes and emits via WebSocket - */ - const sendUrl = (immediate?: string) => { - const codes = new Array(); - if (pickedVideoFormat !== '') codes.push(pickedVideoFormat); - if (pickedAudioFormat !== '') codes.push(pickedAudioFormat); - if (pickedBestFormat !== '') codes.push(pickedBestFormat); - - socket.emit('send-url', { - url: immediate || url || workingUrl, - path: availableDownloadPaths[downloadPath], - params: cliArgs.toString() + toFormatArgs(codes), - renameTo: fileNameOverride, - }) - - setUrl('') - setWorkingUrl('') - setFilenameOverride('') - - setTimeout(() => { - resetInput() - setShowBackdrop(true) - setDownloadFormats(undefined) - }, 250); + /* -------------------- Effects -------------------- */ + /* WebSocket connect event handler*/ + useEffect(() => { + socket.onopen = () => { + dispatch(connected()) + console.log('oke') + socket.send('fetch-jobs') + socket.send('disk-space') + socket.send('retrieve-jobs') } + }, []) - /** - * Retrive url from input and display the formats selection view - */ - const sendUrlFormatSelection = () => { - socket.emit('send-url-format-selection', { - url: url, - }) + useEffect(() => { + const interval = setInterval(() => client.running(), 1000) + return () => clearInterval(interval) + }, []) - setWorkingUrl(url) - setUrl('') - setPickedAudioFormat('') - setPickedVideoFormat('') - setPickedBestFormat('') - - setTimeout(() => { - resetInput() - setShowBackdrop(true) - }, 250) + useEffect(() => { + socket.onmessage = (event) => { + const res = client.decode(event.data) + if (showBackdrop) { + setShowBackdrop(false) + } + switch (typeof res.result) { + case 'object': + setActiveDownloads( + res.result + .filter((r: RPCResult) => !!r.info.url) + .sort((a: RPCResult, b: RPCResult) => a.info.title.localeCompare(b.info.title)) + ) + break + default: + break + } } + }, []) - /** - * Update the url state whenever the input value changes - * @param e Input change event - */ - const handleUrlChange = (e: React.ChangeEvent) => { - setUrl(e.target.value) + useEffect(() => { + fetch(`${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}/tree`) + .then(res => res.json()) + .then(data => { + setAvailableDownloadPaths(data.flat) + }) + }, []) + + /* -------------------- component functions -------------------- */ + + /** + * Retrive url from input, cli-arguments from checkboxes and emits via WebSocket + */ + const sendUrl = (immediate?: string) => { + const codes = new Array(); + if (pickedVideoFormat !== '') codes.push(pickedVideoFormat); + if (pickedAudioFormat !== '') codes.push(pickedAudioFormat); + if (pickedBestFormat !== '') codes.push(pickedBestFormat); + + client.download( + immediate || url || workingUrl, + cliArgs.toString() + toFormatArgs(codes), + ) + + setUrl('') + setWorkingUrl('') + setFilenameOverride('') + + setTimeout(() => { + resetInput() + setShowBackdrop(true) + setDownloadFormats(undefined) + }, 250); + } + + /** + * Retrive url from input and display the formats selection view + */ + const sendUrlFormatSelection = () => { + setWorkingUrl(url) + setUrl('') + setPickedAudioFormat('') + setPickedVideoFormat('') + setPickedBestFormat('') + + setTimeout(() => { + resetInput() + setShowBackdrop(true) + }, 250) + } + + /** + * Update the url state whenever the input value changes + * @param e Input change event + */ + const handleUrlChange = (e: React.ChangeEvent) => { + setUrl(e.target.value) + } + + /** + * Update the filename override state whenever the input value changes + * @param e Input change event + */ + const handleFilenameOverrideChange = (e: React.ChangeEvent) => { + setFilenameOverride(e.target.value) + } + + /** + * Abort a specific download if id's provided, other wise abort all running ones. + * @param id The download id / pid + * @returns void + */ + const abort = (id?: string) => { + if (id) { + client.kill(id) + return } + client.killAll() + } - /** - * Update the filename override state whenever the input value changes - * @param e Input change event - */ - const handleFilenameOverrideChange = (e: React.ChangeEvent) => { - setFilenameOverride(e.target.value) + const parseUrlListFile = (event: any) => { + const urlList = event.target.files + const reader = new FileReader() + reader.addEventListener('load', $event => { + const base64 = $event.target?.result!.toString().split(',')[1] + Buffer.from(base64!, 'base64') + .toString() + .trimEnd() + .split('\n') + .filter(_url => isValidURL(_url)) + .forEach(_url => sendUrl(_url)) + }) + reader.readAsDataURL(urlList[0]) + } + + const resetInput = () => { + const input = document.getElementById('urlInput') as HTMLInputElement; + input.value = ''; + + const filename = document.getElementById('customFilenameInput') as HTMLInputElement; + if (filename) { + filename.value = ''; } + } - /** - * Abort a specific download if id's provided, other wise abort all running ones. - * @param id The download id / pid - * @returns void - */ - const abort = (id?: number) => { - if (id) { - updateInStateMap(id, null, downloadInfoMap, setDownloadInfoMap, true) - socket.emit('abort', { pid: id }) - return - } - setDownloadFormats(undefined) - socket.emit('abort-all') - } + /* -------------------- styled components -------------------- */ - const parseUrlListFile = (event: any) => { - const urlList = event.target.files - const reader = new FileReader() - reader.addEventListener('load', $event => { - const base64 = $event.target?.result!.toString().split(',')[1] - Buffer.from(base64!, 'base64') - .toString() - .trimEnd() - .split('\n') - .filter(_url => isValidURL(_url)) - .forEach(_url => sendUrl(_url)) - }) - reader.readAsDataURL(urlList[0]) - } + const Input = styled('input')({ + display: 'none', + }); - const resetInput = () => { - const input = document.getElementById('urlInput') as HTMLInputElement; - input.value = ''; - - const filename = document.getElementById('customFilenameInput') as HTMLInputElement; - if (filename) { - filename.value = ''; - } - } - - /* -------------------- styled components -------------------- */ - - const Input = styled('input')({ - display: 'none', - }); - - return ( - - theme.zIndex.drawer + 1 }} - open={showBackdrop} - > - - - - - - - - - - ), - }} - /> - - - { - settings.fileRenaming ? - - - : - null - } - { - settings.pathOverriding ? - - - {i18n.t('customPath')} - - - : - null - } - - - - - - - - - - - - - {/* Format Selection grid */} - { - downloadFormats ? - - - - - - {downloadFormats.title} - - {/* */} - - - - - {/* video only */} - - - Best quality - - - - - - {/* video only */} - {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? - - - Video data {downloadFormats.formats[1].acodec} - - - : null - } - {downloadFormats.formats - .filter(format => format.acodec === 'none' && format.vcodec !== 'none') - .map((format, idx) => ( - - - - )) - } - {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? - - - Audio data - - - : null - } - {downloadFormats.formats - .filter(format => format.acodec !== 'none' && format.vcodec === 'none') - .map((format, idx) => ( - - - - )) - } - - - - - - - - - - : null - } - - { - Array - .from(messageMap) - .filter(flattened => [...flattened][0]) - .filter(flattened => [...flattened][1].toString() !== serverStates.PROG_DONE) - .flatMap(message => ( - - { - /* - Message[0] => key, the pid which is shared with the progress and download Maps - Message[1] => value, the actual formatted message sent from server - */ - } - - abort(message[0])} - resolution={ - settings.formatSelection - ? '' - : downloadInfoMap.get(message[0])?.best.resolution ?? '' - } - /> - - - )) - } + return ( + + theme.zIndex.drawer + 1 }} + open={showBackdrop} + > + + + + + + + + + + ), + }} + /> - setShowToast(false)} - /> - - ); + + { + settings.fileRenaming ? + + + : + null + } + { + settings.pathOverriding ? + + + {i18n.t('customPath')} + + + : + null + } + + + + + + + + + + + + + {/* Format Selection grid */} + { + downloadFormats ? + + + + + + {downloadFormats.title} + + {/* */} + + + + + {/* video only */} + + + Best quality + + + + + + {/* video only */} + {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? + + + Video data {downloadFormats.formats[1].acodec} + + + : null + } + {downloadFormats.formats + .filter(format => format.acodec === 'none' && format.vcodec !== 'none') + .map((format, idx) => ( + + + + )) + } + {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? + + + Audio data + + + : null + } + {downloadFormats.formats + .filter(format => format.acodec !== 'none' && format.vcodec === 'none') + .map((format, idx) => ( + + + + )) + } + + + + + + + + + + : null + } + + { + activeDownloads.map(download => ( + + + abort(download.id)} + resolution={download.info.resolution ?? ''} + speed={download.progress.speed} + size={download.info.filesize_approx ?? 0} + /> + + + )) + } + + setShowToast(false)} + /> + + ); } \ No newline at end of file diff --git a/frontend/src/Settings.tsx b/frontend/src/Settings.tsx index 7ea39fe..f26590b 100644 --- a/frontend/src/Settings.tsx +++ b/frontend/src/Settings.tsx @@ -40,7 +40,7 @@ import { RootState } from "./stores/store"; import { validateDomain, validateIP } from "./utils"; type Props = { - socket: Socket + socket: WebSocket } export default function Settings({ socket }: Props) { @@ -112,8 +112,7 @@ export default function Settings({ socket }: Props) { * Send via WebSocket a message in order to update the yt-dlp binary from server */ const updateBinary = () => { - socket.emit('update-bin') - dispatch(alreadyUpdated()) + } return ( diff --git a/frontend/src/components/StackableResult.tsx b/frontend/src/components/StackableResult.tsx index bcee75b..3dfa0c9 100644 --- a/frontend/src/components/StackableResult.tsx +++ b/frontend/src/components/StackableResult.tsx @@ -4,15 +4,24 @@ import { IMessage } from "../interfaces"; import { ellipsis } from "../utils"; type Props = { - formattedLog: IMessage, title: string, thumbnail: string, resolution: string - progress: number, + percentage: string, + size: number, + speed: number, stopCallback: VoidFunction, } -export function StackableResult({ formattedLog, title, thumbnail, resolution, progress, stopCallback }: Props) { +export function StackableResult({ + title, + thumbnail, + resolution, + percentage, + speed, + size, + stopCallback +}: Props) { const guessResolution = (xByY: string): any => { if (!xByY) return null; if (xByY.includes('4320')) return (); @@ -22,6 +31,8 @@ export function StackableResult({ formattedLog, title, thumbnail, resolution, pr return null; } + const percentageToNumber = () => Number(percentage.replace('%', '')) + const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB` return ( @@ -43,14 +54,14 @@ export function StackableResult({ formattedLog, title, thumbnail, resolution, pr } - - {formattedLog.progress} - {formattedLog.dlSpeed} - {roundMB(formattedLog.size ?? 0)} + + {percentage} + {speed} + {roundMB(size ?? 0)} {guessResolution(resolution)} - {progress ? - : + {percentage ? + : null } diff --git a/frontend/src/rpcClient.ts b/frontend/src/rpcClient.ts new file mode 100644 index 0000000..94d5b83 --- /dev/null +++ b/frontend/src/rpcClient.ts @@ -0,0 +1,81 @@ +import type { RPCRequest, RPCResponse } from "./types" +import type { IDLMetadata } from './interfaces' + +export class RPCClient { + private socket: WebSocket + private seq: number + + constructor(socket: WebSocket) { + this.socket = socket + this.seq = 0 + } + + private incrementSeq() { + return String(this.seq++) + } + + private send(req: RPCRequest) { + this.socket.send(JSON.stringify(req)) + } + + private sendHTTP(req: RPCRequest) { + return new Promise>((resolve, reject) => { + fetch('/rpc-http', { + method: 'POST', + body: JSON.stringify(req) + }) + .then(res => res.json()) + .then(data => resolve(data)) + }) + } + + public download(url: string, args: string) { + if (url) { + this.send({ + id: this.incrementSeq(), + method: 'Service.Exec', + params: [{ + URL: url.split("?list").at(0)!, + Params: args.split(" ").map(a => a.trim()), + }] + }) + } + } + + public formats(url: string) { + if (url) { + return this.sendHTTP({ + id: this.incrementSeq(), + method: 'Service.Formats', + params: [{ + URL: url.split("?list").at(0)!, + }] + }) + } + } + + public running() { + this.send({ + method: 'Service.Running', + params: [], + }) + } + + public kill(id: string) { + this.send({ + method: 'Service.Kill', + params: [id], + }) + } + + public killAll() { + this.send({ + method: 'Service.KillAll', + params: [], + }) + } + + public decode(data: any): RPCResponse { + return JSON.parse(data) + } +} \ No newline at end of file diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts new file mode 100644 index 0000000..e8559ee --- /dev/null +++ b/frontend/src/types.d.ts @@ -0,0 +1,43 @@ +export type RPCMethods = + | "Service.Exec" + | "Service.Kill" + | "Service.Running" + | "Service.KillAll" + | "Service.FreeSpace" + | "Service.Formats" + +export type RPCRequest = { + method: RPCMethods, + params?: any[], + id?: string +} + +export type RPCResponse = { + result: T, + error: number | null + id?: string +} + +export type RPCResult = { + id: string + progress: { + speed: number + eta: number + percentage: string + } + info: { + url: string + filesize_approx?: number + resolution?: string + thumbnail: string + title: string + vcodec?: string + acodec?: string + ext?: string + } +} + +export type RPCParams = { + URL: string + Params?: string +} \ No newline at end of file diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 4206bf4..f0db68b 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -106,5 +106,9 @@ export function toFormatArgs(codes: string[]): string { } export function getWebSocketEndpoint() { - return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}` + return `ws://${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/ws-rpc` +} + +export function getHttpRPCEndpoint() { + return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/http-rpc` } \ No newline at end of file diff --git a/server/server.go b/server/server.go index 947eed7..2cb6b3a 100644 --- a/server/server.go +++ b/server/server.go @@ -33,20 +33,31 @@ func RunBlocking(ctx context.Context) { Root: http.FS(fe), })) - app.Get("/ws", websocket.New(func(c *websocket.Conn) { + app.Get("/ws-rpc", websocket.New(func(c *websocket.Conn) { for { mtype, reader, err := c.NextReader() if err != nil { break } + res := NewRPCRequest(reader).Call() + writer, err := c.NextWriter(mtype) if err != nil { break } - res := NewRPCRequest(reader).Call() io.Copy(writer, res) } })) + app.Post("/http-rpc", func(c *fiber.Ctx) error { + reader := c.Context().RequestBodyStream() + writer := c.Response().BodyWriter() + res := NewRPCRequest(reader).Call() + io.Copy(writer, res) + return nil + }) + + app.Server().StreamRequestBody = true + log.Fatal(app.Listen(fmt.Sprintf(":%s", port))) } From 4c7faa1b46865b6f02c35b6a737fd627cc2b3caa Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Wed, 11 Jan 2023 23:19:37 +0100 Subject: [PATCH 05/11] converted dir tree --- frontend/src/App.tsx | 27 +---- frontend/src/Home.tsx | 29 +++-- frontend/src/components/StackableResult.tsx | 124 ++++++++++---------- frontend/src/features/status/statusSlice.ts | 33 ++++-- frontend/src/rpcClient.ts | 18 ++- frontend/src/types.d.ts | 1 + frontend/src/utils.ts | 4 + package.json | 1 + pnpm-lock.yaml | 7 ++ server/internal/stack.go | 37 ++++++ server/process.go | 5 + server/server.go | 3 + server/service.go | 6 + server/sys/fs.go | 41 +++++++ 14 files changed, 229 insertions(+), 107 deletions(-) create mode 100644 server/internal/stack.go diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7894f46..870f5f2 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,18 +23,16 @@ import { BrowserRouter as Router, Link, Route, Routes } from 'react-router-dom'; -import { io } from "socket.io-client"; import ArchivedDownloads from "./Archived"; import { AppBar } from "./components/AppBar"; import { Drawer } from "./components/Drawer"; import Home from "./Home"; import Settings from "./Settings"; import { RootState, store } from './stores/store'; -import { getWebSocketEndpoint } from "./utils"; +import { formatGiB, getWebSocketEndpoint } from "./utils"; function AppContent() { const [open, setOpen] = useState(false); - const [freeDiskSpace, setFreeDiskSpace] = useState(''); const settings = useSelector((state: RootState) => state.settings) const status = useSelector((state: RootState) => state.status) @@ -58,11 +56,6 @@ function AppContent() { setOpen(!open); }; - /* Get disk free space */ - useEffect(() => { - - }, []) - return ( @@ -96,14 +89,14 @@ function AppContent() { yt-dlp WebUI { - freeDiskSpace ? + status.freeSpace ?
-  {freeDiskSpace}  +  {formatGiB(status.freeSpace)} 
: null } @@ -145,20 +138,6 @@ function AppContent() { - {/* Next release: list downloaded files */} - {/* - - - - - - - */} { socket.onopen = () => { dispatch(connected()) - console.log('oke') - socket.send('fetch-jobs') - socket.send('disk-space') - socket.send('retrieve-jobs') } }, []) @@ -85,6 +81,11 @@ export default function Home({ socket }: Props) { return () => clearInterval(interval) }, []) + useEffect(() => { + client.freeSpace() + .then(bytes => dispatch(setFreeSpace(bytes.result))) + }, []) + useEffect(() => { socket.onmessage = (event) => { const res = client.decode(event.data) @@ -106,10 +107,9 @@ export default function Home({ socket }: Props) { }, []) useEffect(() => { - fetch(`${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}/tree`) - .then(res => res.json()) + client.directoryTree() .then(data => { - setAvailableDownloadPaths(data.flat) + setAvailableDownloadPaths(data.result) }) }, []) @@ -150,10 +150,15 @@ export default function Home({ socket }: Props) { setPickedVideoFormat('') setPickedBestFormat('') - setTimeout(() => { - resetInput() - setShowBackdrop(true) - }, 250) + setShowBackdrop(true) + + client.formats(url) + ?.then(formats => { + console.log(formats) + setDownloadFormats(formats.result) + setShowBackdrop(false) + resetInput() + }) } /** diff --git a/frontend/src/components/StackableResult.tsx b/frontend/src/components/StackableResult.tsx index 3dfa0c9..29ee858 100644 --- a/frontend/src/components/StackableResult.tsx +++ b/frontend/src/components/StackableResult.tsx @@ -4,73 +4,73 @@ import { IMessage } from "../interfaces"; import { ellipsis } from "../utils"; type Props = { - title: string, - thumbnail: string, - resolution: string - percentage: string, - size: number, - speed: number, - stopCallback: VoidFunction, + title: string, + thumbnail: string, + resolution: string + percentage: string, + size: number, + speed: number, + stopCallback: VoidFunction, } export function StackableResult({ - title, - thumbnail, - resolution, - percentage, - speed, - size, - stopCallback + title, + thumbnail, + resolution, + percentage, + speed, + size, + stopCallback }: Props) { - const guessResolution = (xByY: string): any => { - if (!xByY) return null; - if (xByY.includes('4320')) return (); - if (xByY.includes('2160')) return (); - if (xByY.includes('1080')) return (); - if (xByY.includes('720')) return (); - return null; - } + const guessResolution = (xByY: string): any => { + if (!xByY) return null; + if (xByY.includes('4320')) return (); + if (xByY.includes('2160')) return (); + if (xByY.includes('1080')) return (); + if (xByY.includes('720')) return (); + return null; + } - const percentageToNumber = () => Number(percentage.replace('%', '')) + const percentageToNumber = () => Number(percentage.replace('%', '')) - const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB` + const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB` - return ( - - - {thumbnail !== '' ? - : - - } - - {title !== '' ? - - {ellipsis(title, 54)} - : - - } - - - {percentage} - {speed} - {roundMB(size ?? 0)} - {guessResolution(resolution)} - - {percentage ? - : - null - } - - - - - - - ) + return ( + + + {thumbnail !== '' ? + : + + } + + {title !== '' ? + + {ellipsis(title, 54)} + : + + } + + + {percentage} + {speed} + {roundMB(size ?? 0)} + {guessResolution(resolution)} + + {percentage ? + : + null + } + + + + + + + ) } \ No newline at end of file diff --git a/frontend/src/features/status/statusSlice.ts b/frontend/src/features/status/statusSlice.ts index 9486cc7..a0d55af 100644 --- a/frontend/src/features/status/statusSlice.ts +++ b/frontend/src/features/status/statusSlice.ts @@ -1,30 +1,47 @@ -import { createSlice } from "@reduxjs/toolkit" +import { createSlice, PayloadAction } from "@reduxjs/toolkit" export interface StatusState { connected: boolean, updated: boolean, downloading: boolean, + freeSpace: number, } const initialState: StatusState = { connected: false, updated: false, downloading: false, + freeSpace: 0, } export const statusSlice = createSlice({ name: 'status', initialState, reducers: { - connected: (state) => { state.connected = true }, - disconnected: (state) => { state.connected = false }, - updated: (state) => { state.updated = true }, - alreadyUpdated: (state) => { state.updated = false }, - downloading: (state) => { state.downloading = true }, - finished: (state) => { state.downloading = false }, + connected: (state) => { + state.connected = true + }, + disconnected: (state) => { + state.connected = false + }, + updated: (state) => { + state.updated = true + }, + alreadyUpdated: (state) => { + state.updated = false + }, + downloading: (state) => { + state.downloading = true + }, + finished: (state) => { + state.downloading = false + }, + setFreeSpace: (state, action: PayloadAction) => { + state.freeSpace = action.payload + } } }) -export const { connected, disconnected, updated, alreadyUpdated, downloading, finished } = statusSlice.actions +export const { connected, disconnected, updated, alreadyUpdated, downloading, finished, setFreeSpace } = statusSlice.actions export default statusSlice.reducer \ No newline at end of file diff --git a/frontend/src/rpcClient.ts b/frontend/src/rpcClient.ts index 94d5b83..c75b337 100644 --- a/frontend/src/rpcClient.ts +++ b/frontend/src/rpcClient.ts @@ -1,6 +1,8 @@ import type { RPCRequest, RPCResponse } from "./types" import type { IDLMetadata } from './interfaces' +import { getHttpRPCEndpoint } from './utils' + export class RPCClient { private socket: WebSocket private seq: number @@ -20,7 +22,7 @@ export class RPCClient { private sendHTTP(req: RPCRequest) { return new Promise>((resolve, reject) => { - fetch('/rpc-http', { + fetch(getHttpRPCEndpoint(), { method: 'POST', body: JSON.stringify(req) }) @@ -75,6 +77,20 @@ export class RPCClient { }) } + public freeSpace() { + return this.sendHTTP({ + method: 'Service.FreeSpace', + params: [], + }) + } + + public directoryTree() { + return this.sendHTTP({ + method: 'Service.DirectoryTree', + params: [], + }) + } + public decode(data: any): RPCResponse { return JSON.parse(data) } diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index e8559ee..be65a37 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -5,6 +5,7 @@ export type RPCMethods = | "Service.KillAll" | "Service.FreeSpace" | "Service.Formats" + | "Service.DirectoryTree" export type RPCRequest = { method: RPCMethods, diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index f0db68b..454441b 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -111,4 +111,8 @@ export function getWebSocketEndpoint() { export function getHttpRPCEndpoint() { return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/http-rpc` +} + +export function formatGiB(bytes: number) { + return `${(bytes / 1_000_000_000).toFixed(0)}GiB` } \ No newline at end of file diff --git a/package.json b/package.json index b5b8901..195ad3f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "koa-router": "^10.1.1", "koa-static": "^5.0.0", "mime-types": "^2.1.35", + "radash": "^10.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2321e9f..0b0e568 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,7 @@ specifiers: mime-types: ^2.1.35 path-browserify: ^1.0.1 process: ^0.11.10 + radash: ^10.6.0 react: ^18.2.0 react-dom: ^18.2.0 react-redux: ^8.0.1 @@ -46,6 +47,7 @@ dependencies: koa-router: 10.1.1 koa-static: 5.0.0 mime-types: 2.1.35 + radash: 10.6.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-redux: 8.0.4_5uumaiclxbdbzaqafclbf6maf4 @@ -1830,6 +1832,11 @@ packages: react-is: 16.13.1 dev: false + /radash/10.6.0: + resolution: {integrity: sha512-L0PD+kBVaPGxn0UO9yVLJUKUkuu7bLqroZbieecPUGuSEtByCtMedDSyw+arA8pnLtZduYTgHnMjRfN90gozpQ==} + engines: {node: '>=14.18.0'} + dev: false + /react-dom/18.2.0_react@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: diff --git a/server/internal/stack.go b/server/internal/stack.go new file mode 100644 index 0000000..c7738c1 --- /dev/null +++ b/server/internal/stack.go @@ -0,0 +1,37 @@ +package internal + +type Node[T any] struct { + Value T +} + +type Stack[T any] struct { + Nodes []*Node[T] + count int +} + +func (s *Stack[T]) Push(n *Node[T]) { + if s.count >= len(s.Nodes) { + Nodes := make([]*Node[T], len(s.Nodes)*2) + copy(Nodes, s.Nodes) + s.Nodes = Nodes + } + s.Nodes[s.count] = n + s.count++ +} + +func (s *Stack[T]) Pop() *Node[T] { + if s.count == 0 { + return nil + } + node := s.Nodes[s.count-1] + s.count-- + return node +} + +func (s *Stack[T]) IsEmpty() bool { + return s.count == 0 +} + +func (s *Stack[T]) IsNotEmpty() bool { + return s.count != 0 +} diff --git a/server/process.go b/server/process.go index faab93b..c209a9d 100644 --- a/server/process.go +++ b/server/process.go @@ -157,7 +157,12 @@ func (p *Process) GetFormatsSync() (DownloadFormats, error) { cmd.Wait() info := DownloadFormats{URL: p.url} + best := Format{} + json.Unmarshal(stdout, &info) + json.Unmarshal(stdout, &best) + + info.Best = best return info, nil } diff --git a/server/server.go b/server/server.go index 2cb6b3a..ac275f8 100644 --- a/server/server.go +++ b/server/server.go @@ -10,6 +10,7 @@ import ( "net/rpc" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/filesystem" "github.com/gofiber/websocket/v2" ) @@ -29,6 +30,8 @@ func RunBlocking(ctx context.Context) { app := fiber.New() + app.Use(cors.New()) + app.Use("/", filesystem.New(filesystem.Config{ Root: http.FS(fe), })) diff --git a/server/service.go b/server/service.go index 1889572..2803ae9 100644 --- a/server/service.go +++ b/server/service.go @@ -84,3 +84,9 @@ func (t *Service) FreeSpace(args NoArgs, free *uint64) error { *free = freeSpace return err } + +func (t *Service) DirectoryTree(args NoArgs, tree *[]string) error { + dfsTree, err := sys.DirectoryTree("downloads") + *tree = *dfsTree + return err +} diff --git a/server/sys/fs.go b/server/sys/fs.go index 00ef630..c086316 100644 --- a/server/sys/fs.go +++ b/server/sys/fs.go @@ -2,7 +2,9 @@ package sys import ( "os" + "path/filepath" + "github.com/marcopeocchi/yt-dlp-web-ui/server/internal" "golang.org/x/sys/unix" ) @@ -18,3 +20,42 @@ func FreeSpace() (uint64, error) { unix.Statfs(wd+"/downloads", &stat) return (stat.Bavail * uint64(stat.Bsize)), nil } + +func DirectoryTree(rootPath string) (*[]string, error) { + type Node struct { + path string + children []Node + } + + stack := internal.Stack[Node]{ + Nodes: make([]*internal.Node[Node], 5), + } + flattened := make([]string, 0) + + root := Node{path: rootPath} + stack.Push(&internal.Node[Node]{ + Value: root, + }) + flattened = append(flattened, rootPath) + + for stack.IsNotEmpty() { + current := stack.Pop().Value + children, err := os.ReadDir(current.path) + if err != nil { + return nil, err + } + for _, entry := range children { + childPath := filepath.Join(current.path, entry.Name()) + childNode := Node{path: childPath} + + if entry.IsDir() { + current.children = append(current.children, childNode) + stack.Push(&internal.Node[Node]{ + Value: childNode, + }) + flattened = append(flattened, childNode.path) + } + } + } + return &flattened, nil +} From 733e2ab006b2fdd3b2882ae16324f63ca9532f93 Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Thu, 12 Jan 2023 12:05:53 +0100 Subject: [PATCH 06/11] it just works --- .dockerignore | 3 +- .gitignore | 4 +- Makefile | 16 + frontend/index.html | 2 +- package.json => frontend/package.json | 24 +- frontend/src/App.tsx | 8 +- frontend/src/Archived.tsx | 46 - frontend/src/Home.tsx | 26 +- frontend/src/Settings.tsx | 13 +- frontend/src/components/StackableResult.tsx | 55 +- frontend/src/components/Statistics.tsx | 52 - .../core/argsParser.ts} | 0 frontend/src/{ => features/core}/events.ts | 0 .../src/{i18n.ts => features/core/intl.ts} | 4 +- frontend/src/{ => features/core}/rpcClient.ts | 9 +- frontend/{ => src}/index.tsx | 2 +- frontend/src/interfaces.ts | 33 - frontend/src/types.d.ts | 17 + frontend/src/utils.ts | 32 - vite.config.ts => frontend/vite.config.ts | 4 +- go.mod | 1 + go.sum | 3 + main.go | 31 +- pnpm-lock.yaml | 2209 ----------------- server-node/src/core/HTTPServer.ts | 86 - server-node/src/core/Process.ts | 166 -- server-node/src/core/downloadArchive.ts | 30 - server-node/src/core/downloader.ts | 251 -- server-node/src/core/states.ts | 9 - server-node/src/core/streamer.ts | 39 - server-node/src/db/memoryDB.ts | 80 - .../src/interfaces/IDownloadMetadata.d.ts | 15 - server-node/src/interfaces/IPayload.d.ts | 13 - server-node/src/interfaces/IRecord.d.ts | 14 - server-node/src/interfaces/ISettings.d.ts | 5 - server-node/src/main.ts | 128 - server-node/src/types/index.d.ts | 6 - server-node/src/utils/BetterLogger.ts | 59 - server-node/src/utils/directoryUtils.ts | 59 - server-node/src/utils/logger.ts | 25 - server-node/src/utils/params.ts | 4 - server-node/src/utils/procUtils.ts | 36 - server-node/src/utils/updater.ts | 90 - server/config/config_singleton.go | 60 + server/process.go | 40 +- server/server.go | 4 +- server/service.go | 26 +- server/sys/fs.go | 13 +- server/types.go | 5 +- server/updater/forced_update.go | 45 + server/updater/types.go | 6 + server/updater/update.go | 17 + settings.json | 5 - tsconfig.json | 14 - 54 files changed, 336 insertions(+), 3608 deletions(-) create mode 100644 Makefile rename package.json => frontend/package.json (56%) delete mode 100644 frontend/src/Archived.tsx delete mode 100644 frontend/src/components/Statistics.tsx rename frontend/src/{classes.ts => features/core/argsParser.ts} (100%) rename frontend/src/{ => features/core}/events.ts (100%) rename frontend/src/{i18n.ts => features/core/intl.ts} (87%) rename frontend/src/{ => features/core}/rpcClient.ts (86%) rename frontend/{ => src}/index.tsx (87%) delete mode 100644 frontend/src/interfaces.ts rename vite.config.ts => frontend/vite.config.ts (76%) delete mode 100644 pnpm-lock.yaml delete mode 100644 server-node/src/core/HTTPServer.ts delete mode 100644 server-node/src/core/Process.ts delete mode 100644 server-node/src/core/downloadArchive.ts delete mode 100644 server-node/src/core/downloader.ts delete mode 100644 server-node/src/core/states.ts delete mode 100644 server-node/src/core/streamer.ts delete mode 100644 server-node/src/db/memoryDB.ts delete mode 100644 server-node/src/interfaces/IDownloadMetadata.d.ts delete mode 100644 server-node/src/interfaces/IPayload.d.ts delete mode 100644 server-node/src/interfaces/IRecord.d.ts delete mode 100644 server-node/src/interfaces/ISettings.d.ts delete mode 100644 server-node/src/main.ts delete mode 100644 server-node/src/types/index.d.ts delete mode 100644 server-node/src/utils/BetterLogger.ts delete mode 100644 server-node/src/utils/directoryUtils.ts delete mode 100644 server-node/src/utils/logger.ts delete mode 100644 server-node/src/utils/params.ts delete mode 100644 server-node/src/utils/procUtils.ts delete mode 100644 server-node/src/utils/updater.ts create mode 100644 server/config/config_singleton.go create mode 100644 server/updater/forced_update.go create mode 100644 server/updater/types.go create mode 100644 server/updater/update.go delete mode 100644 settings.json delete mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore index dc69752..acfeba9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,4 +11,5 @@ src/server/core/yt-dlp .env *.mp4 *.ytdl -*.db \ No newline at end of file +*.db +build/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8a804c8..0f5b409 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ src/server/core/yt-dlp *.part *.db downloads -.DS_Store \ No newline at end of file +.DS_Store +build/ +yt-dlp-webui diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..882764c --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +default: + go build -o yt-dlp-webui main.go + +all: + cd frontend && pnpm build && cd .. + go build -o yt-dlp-webui main.go + +multiarch: + GOOS=linux GOARCH=arm go build -o yt-dlp-webui_linux-arm *.go + GOOS=linux GOARCH=arm64 go build -o yt-dlp-webui_linux-arm64 *.go + GOOS=linux GOARCH=amd64 go build -o yt-dlp-webui_linux-amd64 *.go + mkdir -p build + mv yt-dlp-webui* build + +clean: + rm -rf build \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 5ff2e1e..c657b3f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,7 +10,7 @@
- + \ No newline at end of file diff --git a/package.json b/frontend/package.json similarity index 56% rename from package.json rename to frontend/package.json index 195ad3f..85b0d42 100644 --- a/package.json +++ b/frontend/package.json @@ -3,17 +3,8 @@ "version": "1.1.0", "description": "A terrible webUI for yt-dlp, all-in-one solution.", "scripts": { - "dev": "nodemon dist/main.js", - "start": "node dist/main.js", - "watch": "tsc --build -w", - "build": "vite build", - "build-server": "tsc --build", - "build-all": "tsc --build && npm run build && npm run fetch", - "clean": "tsc --build --clean", - "clean-all": "rm -r dist", - "fe": "vite", - "fetch-dev": "./fetch-yt-dlp.sh && mv yt-dlp ./server/core", - "fetch": "./fetch-yt-dlp.sh && mv yt-dlp ./dist/core" + "dev": "vite", + "build": "vite build" }, "author": "marcobaobao", "license": "ISC", @@ -24,26 +15,17 @@ "@mui/icons-material": "^5.6.2", "@mui/material": "^5.6.4", "@reduxjs/toolkit": "^1.8.1", - "koa": "^2.13.4", - "koa-router": "^10.1.1", - "koa-static": "^5.0.0", - "mime-types": "^2.1.35", "radash": "^10.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.1", "react-router-dom": "^6.3.0", "rxjs": "^7.4.0", - "socket.io": "^4.3.2", - "socket.io-client": "^4.3.2", "uuid": "^8.3.2" }, "devDependencies": { "@modyfi/vite-plugin-yaml": "^1.0.2", - "@types/koa": "^2.13.4", - "@types/koa-router": "^7.4.4", - "@types/mime-types": "^2.1.1", - "@types/node": "^17.0.31", + "@types/node": "^18.11.18", "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", "@types/react-router-dom": "^5.3.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 870f5f2..c218829 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,13 +17,12 @@ import { } from "@mui/material"; import { grey } from "@mui/material/colors"; import ListItemButton from '@mui/material/ListItemButton'; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { Provider, useSelector } from "react-redux"; import { BrowserRouter as Router, Link, Route, Routes } from 'react-router-dom'; -import ArchivedDownloads from "./Archived"; import { AppBar } from "./components/AppBar"; import { Drawer } from "./components/Drawer"; import Home from "./Home"; @@ -163,9 +162,8 @@ function AppContent() { > - }> - }> - }> + } /> + } /> diff --git a/frontend/src/Archived.tsx b/frontend/src/Archived.tsx deleted file mode 100644 index 722188e..0000000 --- a/frontend/src/Archived.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Backdrop, CircularProgress, Container, Grid } from "@mui/material"; -import { ArchiveResult } from "./components/ArchiveResult"; -import { useSelector } from "react-redux"; -import { RootState } from "./stores/store"; - -export default function archivedDownloads() { - const [loading, setLoading] = useState(true); - const [archived, setArchived] = useState([]); - - const settings = useSelector((state: RootState) => state.settings) - - useEffect(() => { - fetch(`http://${settings.serverAddr}:3022/getAllDownloaded`) - .then(res => res.json()) - .then(data => setArchived(data)) - .then(() => setLoading(false)) - }, []); - - return ( - - theme.zIndex.drawer + 1 }} - open={loading} - > - - - {/* - archived.length > 0 ? - - { - archived.map((el, idx) => - - - - ) - } - - : null - */} - - ); -} \ No newline at end of file diff --git a/frontend/src/Home.tsx b/frontend/src/Home.tsx index 3582cb0..c2311ff 100644 --- a/frontend/src/Home.tsx +++ b/frontend/src/Home.tsx @@ -21,16 +21,14 @@ import { import { Buffer } from 'buffer'; import { Fragment, useEffect, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { CliArguments } from "./classes"; import { StackableResult } from "./components/StackableResult"; -import { serverStates } from "./events"; +import { CliArguments } from "./features/core/argsParser"; +import I18nBuilder from "./features/core/intl"; +import { RPCClient } from "./features/core/rpcClient"; import { connected, setFreeSpace } from "./features/status/statusSlice"; -import { I18nBuilder } from "./i18n"; -import { IDLMetadata, IMessage } from "./interfaces"; -import { RPCClient } from "./rpcClient"; import { RootState } from "./stores/store"; -import { RPCResult } from "./types"; -import { isValidURL, toFormatArgs, updateInStateMap } from "./utils"; +import { IDLMetadata, RPCResult } from "./types"; +import { isValidURL, toFormatArgs } from "./utils"; type Props = { socket: WebSocket @@ -43,11 +41,7 @@ export default function Home({ socket }: Props) { const dispatch = useDispatch() // ephemeral state - const [progressMap, setProgressMap] = useState(new Map()); - const [messageMap, setMessageMap] = useState(new Map()); - const [activeDownloads, setActiveDownloads] = useState(new Array()); - const [downloadInfoMap, setDownloadInfoMap] = useState(new Map()); const [downloadFormats, setDownloadFormats] = useState(); const [pickedVideoFormat, setPickedVideoFormat] = useState(''); const [pickedAudioFormat, setPickedAudioFormat] = useState(''); @@ -60,6 +54,7 @@ export default function Home({ socket }: Props) { const [url, setUrl] = useState(''); const [workingUrl, setWorkingUrl] = useState(''); + const [showBackdrop, setShowBackdrop] = useState(false); const [showToast, setShowToast] = useState(true); @@ -89,9 +84,9 @@ export default function Home({ socket }: Props) { useEffect(() => { socket.onmessage = (event) => { const res = client.decode(event.data) - if (showBackdrop) { - setShowBackdrop(false) - } + + setShowBackdrop(false) + switch (typeof res.result) { case 'object': setActiveDownloads( @@ -127,6 +122,8 @@ export default function Home({ socket }: Props) { client.download( immediate || url || workingUrl, cliArgs.toString() + toFormatArgs(codes), + availableDownloadPaths[downloadPath] ?? '', + fileNameOverride ) setUrl('') @@ -154,7 +151,6 @@ export default function Home({ socket }: Props) { client.formats(url) ?.then(formats => { - console.log(formats) setDownloadFormats(formats.result) setShowBackdrop(false) resetInput() diff --git a/frontend/src/Settings.tsx b/frontend/src/Settings.tsx index f26590b..6379329 100644 --- a/frontend/src/Settings.tsx +++ b/frontend/src/Settings.tsx @@ -20,8 +20,8 @@ import { import { useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { debounceTime, distinctUntilChanged, map, of, takeWhile } from "rxjs"; -import { Socket } from "socket.io-client"; -import { CliArguments } from "./classes"; +import { CliArguments } from "./features/core/argsParser"; +import I18nBuilder from "./features/core/intl"; import { LanguageUnion, setCliArgs, @@ -34,16 +34,11 @@ import { setTheme, ThemeUnion } from "./features/settings/settingsSlice"; -import { alreadyUpdated, updated } from "./features/status/statusSlice"; -import { I18nBuilder } from "./i18n"; +import { updated } from "./features/status/statusSlice"; import { RootState } from "./stores/store"; import { validateDomain, validateIP } from "./utils"; -type Props = { - socket: WebSocket -} - -export default function Settings({ socket }: Props) { +export default function Settings() { const settings = useSelector((state: RootState) => state.settings) const status = useSelector((state: RootState) => state.status) const dispatch = useDispatch() diff --git a/frontend/src/components/StackableResult.tsx b/frontend/src/components/StackableResult.tsx index 29ee858..097be39 100644 --- a/frontend/src/components/StackableResult.tsx +++ b/frontend/src/components/StackableResult.tsx @@ -1,6 +1,18 @@ import { EightK, FourK, Hd, Sd } from "@mui/icons-material"; -import { Button, Card, CardActionArea, CardActions, CardContent, CardMedia, Chip, LinearProgress, Skeleton, Stack, Typography } from "@mui/material"; -import { IMessage } from "../interfaces"; +import { + Button, + Card, + CardActionArea, + CardActions, + CardContent, + CardMedia, + Chip, + LinearProgress, + Skeleton, + Stack, + Typography +} from "@mui/material"; +import { useEffect, useState } from "react"; import { ellipsis } from "../utils"; type Props = { @@ -22,6 +34,14 @@ export function StackableResult({ size, stopCallback }: Props) { + const [isCompleted, setIsCompleted] = useState(false) + + useEffect(() => { + if (percentage === '-1') { + setIsCompleted(true) + } + }, [percentage]) + const guessResolution = (xByY: string): any => { if (!xByY) return null; if (xByY.includes('4320')) return (); @@ -31,9 +51,10 @@ export function StackableResult({ return null; } - const percentageToNumber = () => Number(percentage.replace('%', '')) + const percentageToNumber = () => isCompleted ? 100 : Number(percentage.replace('%', '')) - const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB` + const roundMiB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)} MiB` + const formatSpeedMiB = (val: number) => `${roundMiB(val)}/s` return ( @@ -54,21 +75,33 @@ export function StackableResult({ } - - {percentage} - {speed} - {roundMB(size ?? 0)} + + {!isCompleted ? percentage : ''} + {!isCompleted ? formatSpeedMiB(speed) : ''} + {roundMiB(size ?? 0)} {guessResolution(resolution)} {percentage ? - : + : null } - diff --git a/frontend/src/components/Statistics.tsx b/frontend/src/components/Statistics.tsx deleted file mode 100644 index ff6b522..0000000 --- a/frontend/src/components/Statistics.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Line } from "react-chartjs-2"; -import { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, -} from 'chart.js'; -import { on } from "../events"; - -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend -); - -export function Statistics() { - const dataset = new Array(); - const chartRef = useRef(null) - - useEffect(() => { - on('dlSpeed', (data: CustomEvent) => { - dataset.push(data.detail) - chartRef.current.update() - }) - }, []) - - const data = { - labels: dataset.map(() => ''), - datasets: [ - { - data: dataset, - label: 'download speed', - borderColor: 'rgb(53, 162, 235)', - } - ] - } - - return ( -
- -
- ) -} \ No newline at end of file diff --git a/frontend/src/classes.ts b/frontend/src/features/core/argsParser.ts similarity index 100% rename from frontend/src/classes.ts rename to frontend/src/features/core/argsParser.ts diff --git a/frontend/src/events.ts b/frontend/src/features/core/events.ts similarity index 100% rename from frontend/src/events.ts rename to frontend/src/features/core/events.ts diff --git a/frontend/src/i18n.ts b/frontend/src/features/core/intl.ts similarity index 87% rename from frontend/src/i18n.ts rename to frontend/src/features/core/intl.ts index 9152f89..6d6ac9f 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/features/core/intl.ts @@ -1,7 +1,7 @@ // @ts-nocheck -import i18n from "./assets/i18n.yaml"; +import i18n from "../../assets/i18n.yaml"; -export class I18nBuilder { +export default class I18nBuilder { private language: string; private textMap = i18n.languages; diff --git a/frontend/src/rpcClient.ts b/frontend/src/features/core/rpcClient.ts similarity index 86% rename from frontend/src/rpcClient.ts rename to frontend/src/features/core/rpcClient.ts index c75b337..b693ac7 100644 --- a/frontend/src/rpcClient.ts +++ b/frontend/src/features/core/rpcClient.ts @@ -1,7 +1,6 @@ -import type { RPCRequest, RPCResponse } from "./types" -import type { IDLMetadata } from './interfaces' +import type { RPCRequest, RPCResponse, IDLMetadata } from "../../types" -import { getHttpRPCEndpoint } from './utils' +import { getHttpRPCEndpoint } from '../../utils' export class RPCClient { private socket: WebSocket @@ -31,7 +30,7 @@ export class RPCClient { }) } - public download(url: string, args: string) { + public download(url: string, args: string, pathOverride = '', renameTo = '') { if (url) { this.send({ id: this.incrementSeq(), @@ -39,6 +38,8 @@ export class RPCClient { params: [{ URL: url.split("?list").at(0)!, Params: args.split(" ").map(a => a.trim()), + Path: pathOverride, + Rename: renameTo, }] }) } diff --git a/frontend/index.tsx b/frontend/src/index.tsx similarity index 87% rename from frontend/index.tsx rename to frontend/src/index.tsx index 762aa39..be257af 100644 --- a/frontend/index.tsx +++ b/frontend/src/index.tsx @@ -1,6 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import { App } from './src/App' +import { App } from './App' const root = ReactDOM.createRoot(document.getElementById('root')!) root.render( diff --git a/frontend/src/interfaces.ts b/frontend/src/interfaces.ts deleted file mode 100644 index 032d37d..0000000 --- a/frontend/src/interfaces.ts +++ /dev/null @@ -1,33 +0,0 @@ -export interface IMessage { - status: string, - progress?: string, - size?: number, - dlSpeed?: string - pid: number -} - -export interface IDLMetadata { - formats: Array, - best: IDLFormat, - thumbnail: string, - title: string, -} - -export interface IDLFormat { - format_id: string, - format_note: string, - fps: number, - resolution: string, - vcodec: string, - acodec: string, -} - -export interface IDLMetadataAndPID { - pid: number, - metadata: IDLMetadata -} - -export interface IDLSpeed { - effective: number, - unit: string, -} diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index be65a37..ccb8726 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -6,6 +6,7 @@ export type RPCMethods = | "Service.FreeSpace" | "Service.Formats" | "Service.DirectoryTree" + | "Service.UpdateExecutable" export type RPCRequest = { method: RPCMethods, @@ -41,4 +42,20 @@ export type RPCResult = { export type RPCParams = { URL: string Params?: string +} + +export interface IDLMetadata { + formats: Array, + best: IDLFormat, + thumbnail: string, + title: string, +} + +export interface IDLFormat { + format_id: string, + format_note: string, + fps: number, + resolution: string, + vcodec: string, + acodec: string, } \ No newline at end of file diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 454441b..fe8f138 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -1,5 +1,3 @@ -import { IMessage } from "./interfaces" - /** * Validate an ip v4 via regex * @param {string} ipAddr @@ -65,36 +63,6 @@ export function detectSpeed(str: string): number { } } -/** - * Update a map stored in React State, in this specific impl. all maps have integer keys - * @param k Map key - * @param v Map value - * @param target The target map saved in-state - * @param callback calls React's StateAction function with the newly created Map - * @param remove -optional- is it an update or a deletion operation? - */ -export function updateInStateMap(k: K, v: any, target: Map, callback: Function, remove: boolean = false) { - if (remove) { - const _target = target - _target.delete(k) - callback(new Map(_target)) - return; - } - callback(new Map(target.set(k, v))); -} - -export function updateInStateArray(v: T, target: Array, callback: Function) { } - -/** - * Pre like function - * @param data - * @returns formatted server message - */ -export function buildMessage(data: IMessage) { - return `operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`; -} - - export function toFormatArgs(codes: string[]): string { if (codes.length > 1) { return codes.reduce((v, a) => ` -f ${v}+${a}`) diff --git a/vite.config.ts b/frontend/vite.config.ts similarity index 76% rename from vite.config.ts rename to frontend/vite.config.ts index 9a0149f..fb6b74a 100644 --- a/vite.config.ts +++ b/frontend/vite.config.ts @@ -9,10 +9,10 @@ export default defineConfig(() => { react(), ViteYaml(), ], - root: resolve(__dirname, 'frontend'), + root: resolve(__dirname, '.'), build: { emptyOutDir: true, - outDir: resolve(__dirname, 'dist', 'frontend'), + outDir: resolve(__dirname, 'dist'), } } }) diff --git a/go.mod b/go.mod index e5d5f39..cd92fe1 100644 --- a/go.mod +++ b/go.mod @@ -23,4 +23,5 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.43.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5a88ba0..d7f4b9d 100644 --- a/go.sum +++ b/go.sum @@ -56,3 +56,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX 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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 6de05a8..7a1283d 100644 --- a/main.go +++ b/main.go @@ -3,34 +3,51 @@ package main import ( "context" "embed" + "flag" "io/fs" "log" - "os" "github.com/marcopeocchi/yt-dlp-web-ui/server" + "github.com/marcopeocchi/yt-dlp-web-ui/server/config" ) type ContextKey interface{} var ( - port = os.Getenv("PORT") - //go:embed dist/frontend + port int + downloadPath string + downloaderPath string + configFile string + + //go:embed frontend/dist frontend embed.FS ) func init() { - if port == "" { - port = "3033" - } + flag.IntVar(&port, "port", 3033, "Port where server will listen at") + flag.StringVar(&downloadPath, "out", ".", "Directory where files will be saved") + flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path") + flag.StringVar(&configFile, "conf", "", "yt-dlp-WebUI config file path") + flag.Parse() } func main() { - frontend, err := fs.Sub(frontend, "dist/frontend") + frontend, err := fs.Sub(frontend, "frontend/dist") if err != nil { log.Fatalln(err) } + cfg := config.Instance() + + if configFile != "" { + cfg.LoadFromFile(configFile) + } + + cfg.SetPort(port) + cfg.DownloadPath(downloadPath) + cfg.DownloaderPath(downloaderPath) + ctx := context.Background() ctx = context.WithValue(ctx, ContextKey("port"), port) ctx = context.WithValue(ctx, ContextKey("frontend"), frontend) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 0b0e568..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,2209 +0,0 @@ -lockfileVersion: 5.4 - -specifiers: - '@emotion/react': ^11.9.0 - '@emotion/styled': ^11.8.1 - '@koa/cors': ^3.3.0 - '@modyfi/vite-plugin-yaml': ^1.0.2 - '@mui/icons-material': ^5.6.2 - '@mui/material': ^5.6.4 - '@reduxjs/toolkit': ^1.8.1 - '@types/koa': ^2.13.4 - '@types/koa-router': ^7.4.4 - '@types/mime-types': ^2.1.1 - '@types/node': ^17.0.31 - '@types/react': ^18.0.21 - '@types/react-dom': ^18.0.6 - '@types/react-router-dom': ^5.3.3 - '@types/uuid': ^8.3.4 - '@vitejs/plugin-react': ^1.3.2 - buffer: ^6.0.3 - koa: ^2.13.4 - koa-router: ^10.1.1 - koa-static: ^5.0.0 - mime-types: ^2.1.35 - path-browserify: ^1.0.1 - process: ^0.11.10 - radash: ^10.6.0 - react: ^18.2.0 - react-dom: ^18.2.0 - react-redux: ^8.0.1 - react-router-dom: ^6.3.0 - rxjs: ^7.4.0 - socket.io: ^4.3.2 - socket.io-client: ^4.3.2 - typescript: ^4.6.4 - uuid: ^8.3.2 - vite: ^2.9.10 - -dependencies: - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/styled': 11.10.4_g3tud4ene45llglqap74b5kkse - '@koa/cors': 3.4.3 - '@mui/icons-material': 5.10.9_6usjrp3ypnzobhq35dcwvjrt3m - '@mui/material': 5.10.9_ikcgkdnp4bn3rgptamntbhbo7e - '@reduxjs/toolkit': 1.8.6_kuo2ie247izvzll3jejufdtq3q - koa: 2.13.4 - koa-router: 10.1.1 - koa-static: 5.0.0 - mime-types: 2.1.35 - radash: 10.6.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-redux: 8.0.4_5uumaiclxbdbzaqafclbf6maf4 - react-router-dom: 6.4.2_biqbaboplfbrettd7655fr4n2y - rxjs: 7.5.7 - socket.io: 4.5.2 - socket.io-client: 4.5.2 - uuid: 8.3.2 - -devDependencies: - '@modyfi/vite-plugin-yaml': 1.0.3_vite@2.9.15 - '@types/koa': 2.13.5 - '@types/koa-router': 7.4.4 - '@types/mime-types': 2.1.1 - '@types/node': 17.0.45 - '@types/react': 18.0.21 - '@types/react-dom': 18.0.6 - '@types/react-router-dom': 5.3.3 - '@types/uuid': 8.3.4 - '@vitejs/plugin-react': 1.3.2 - buffer: 6.0.3 - path-browserify: 1.0.1 - process: 0.11.10 - typescript: 4.8.4 - vite: 2.9.15 - -packages: - - /@ampproject/remapping/2.2.0: - resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.17 - dev: true - - /@babel/code-frame/7.18.6: - resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.18.6 - - /@babel/compat-data/7.19.4: - resolution: {integrity: sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/core/7.19.3: - resolution: {integrity: sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.19.5 - '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.19.3 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helpers': 7.19.4 - '@babel/parser': 7.19.4 - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.4 - '@babel/types': 7.19.4 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/generator/7.19.5: - resolution: {integrity: sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.4 - '@jridgewell/gen-mapping': 0.3.2 - jsesc: 2.5.2 - dev: true - - /@babel/helper-annotate-as-pure/7.18.6: - resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.4 - dev: true - - /@babel/helper-compilation-targets/7.19.3_@babel+core@7.19.3: - resolution: {integrity: sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.19.4 - '@babel/core': 7.19.3 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.21.4 - semver: 6.3.0 - dev: true - - /@babel/helper-environment-visitor/7.18.9: - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-function-name/7.19.0: - resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.18.10 - '@babel/types': 7.19.4 - dev: true - - /@babel/helper-hoist-variables/7.18.6: - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.4 - dev: true - - /@babel/helper-module-imports/7.18.6: - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.4 - - /@babel/helper-module-transforms/7.19.0: - resolution: {integrity: sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.19.4 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.4 - '@babel/types': 7.19.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-plugin-utils/7.19.0: - resolution: {integrity: sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==} - engines: {node: '>=6.9.0'} - - /@babel/helper-simple-access/7.19.4: - resolution: {integrity: sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.4 - dev: true - - /@babel/helper-split-export-declaration/7.18.6: - resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.4 - dev: true - - /@babel/helper-string-parser/7.19.4: - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} - engines: {node: '>=6.9.0'} - - /@babel/helper-validator-identifier/7.19.1: - resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} - engines: {node: '>=6.9.0'} - - /@babel/helper-validator-option/7.18.6: - resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helpers/7.19.4: - resolution: {integrity: sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.4 - '@babel/types': 7.19.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/highlight/7.18.6: - resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.19.1 - chalk: 2.4.2 - js-tokens: 4.0.0 - - /@babel/parser/7.19.4: - resolution: {integrity: sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.19.4 - dev: true - - /@babel/plugin-syntax-jsx/7.18.6: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/helper-plugin-utils': 7.19.0 - dev: false - - /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.19.3: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.3 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.19.3: - resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.3 - '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.19.3 - dev: true - - /@babel/plugin-transform-react-jsx-self/7.18.6_@babel+core@7.19.3: - resolution: {integrity: sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.3 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-react-jsx-source/7.18.6_@babel+core@7.19.3: - resolution: {integrity: sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.3 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-react-jsx/7.19.0_@babel+core@7.19.3: - resolution: {integrity: sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.3 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.19.3 - '@babel/types': 7.19.4 - dev: true - - /@babel/runtime/7.19.4: - resolution: {integrity: sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.10 - dev: false - - /@babel/template/7.18.10: - resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.19.4 - '@babel/types': 7.19.4 - dev: true - - /@babel/traverse/7.19.4: - resolution: {integrity: sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.19.5 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.19.4 - '@babel/types': 7.19.4 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/types/7.19.4: - resolution: {integrity: sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 - - /@emotion/babel-plugin/11.10.2: - resolution: {integrity: sha512-xNQ57njWTFVfPAc3cjfuaPdsgLp5QOSuRsj9MA6ndEhH/AzuZM86qIQzt6rq+aGBwj3n5/TkLmU5lhAfdRmogA==} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.18.6 - '@babel/runtime': 7.19.4 - '@emotion/hash': 0.9.0 - '@emotion/memoize': 0.8.0 - '@emotion/serialize': 1.1.0 - babel-plugin-macros: 3.1.0 - convert-source-map: 1.9.0 - escape-string-regexp: 4.0.0 - find-root: 1.1.0 - source-map: 0.5.7 - stylis: 4.0.13 - dev: false - - /@emotion/cache/11.10.3: - resolution: {integrity: sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ==} - dependencies: - '@emotion/memoize': 0.8.0 - '@emotion/sheet': 1.2.0 - '@emotion/utils': 1.2.0 - '@emotion/weak-memoize': 0.3.0 - stylis: 4.0.13 - dev: false - - /@emotion/hash/0.9.0: - resolution: {integrity: sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==} - dev: false - - /@emotion/is-prop-valid/1.2.0: - resolution: {integrity: sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==} - dependencies: - '@emotion/memoize': 0.8.0 - dev: false - - /@emotion/memoize/0.8.0: - resolution: {integrity: sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==} - dev: false - - /@emotion/react/11.10.4_iapumuv4e6jcjznwuxpf4tt22e: - resolution: {integrity: sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA==} - peerDependencies: - '@babel/core': ^7.0.0 - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@babel/core': - optional: true - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.19.4 - '@emotion/babel-plugin': 11.10.2 - '@emotion/cache': 11.10.3 - '@emotion/serialize': 1.1.0 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.0_react@18.2.0 - '@emotion/utils': 1.2.0 - '@emotion/weak-memoize': 0.3.0 - '@types/react': 18.0.21 - hoist-non-react-statics: 3.3.2 - react: 18.2.0 - dev: false - - /@emotion/serialize/1.1.0: - resolution: {integrity: sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==} - dependencies: - '@emotion/hash': 0.9.0 - '@emotion/memoize': 0.8.0 - '@emotion/unitless': 0.8.0 - '@emotion/utils': 1.2.0 - csstype: 3.1.1 - dev: false - - /@emotion/sheet/1.2.0: - resolution: {integrity: sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==} - dev: false - - /@emotion/styled/11.10.4_g3tud4ene45llglqap74b5kkse: - resolution: {integrity: sha512-pRl4R8Ez3UXvOPfc2bzIoV8u9P97UedgHS4FPX594ntwEuAMA114wlaHvOK24HB48uqfXiGlYIZYCxVJ1R1ttQ==} - peerDependencies: - '@babel/core': ^7.0.0 - '@emotion/react': ^11.0.0-rc.0 - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@babel/core': - optional: true - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.19.4 - '@emotion/babel-plugin': 11.10.2 - '@emotion/is-prop-valid': 1.2.0 - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/serialize': 1.1.0 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.0_react@18.2.0 - '@emotion/utils': 1.2.0 - '@types/react': 18.0.21 - react: 18.2.0 - dev: false - - /@emotion/unitless/0.8.0: - resolution: {integrity: sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==} - dev: false - - /@emotion/use-insertion-effect-with-fallbacks/1.0.0_react@18.2.0: - resolution: {integrity: sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==} - peerDependencies: - react: '>=16.8.0' - dependencies: - react: 18.2.0 - dev: false - - /@emotion/utils/1.2.0: - resolution: {integrity: sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==} - dev: false - - /@emotion/weak-memoize/0.3.0: - resolution: {integrity: sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==} - dev: false - - /@esbuild/linux-loong64/0.14.54: - resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@jridgewell/gen-mapping/0.1.1: - resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: true - - /@jridgewell/gen-mapping/0.3.2: - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.17 - dev: true - - /@jridgewell/resolve-uri/3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/set-array/1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/sourcemap-codec/1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - dev: true - - /@jridgewell/trace-mapping/0.3.17: - resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: true - - /@koa/cors/3.4.3: - resolution: {integrity: sha512-WPXQUaAeAMVaLTEFpoq3T2O1C+FstkjJnDQqy95Ck1UdILajsRhu6mhJ8H2f4NFPRBoCNN+qywTJfq/gGki5mw==} - engines: {node: '>= 8.0.0'} - dependencies: - vary: 1.1.2 - dev: false - - /@modyfi/vite-plugin-yaml/1.0.3_vite@2.9.15: - resolution: {integrity: sha512-UB9A9b5h9v4fruyZPeLQh4k7ZF7KPrOuMnTfSIJD2WYXx9rHE0U19/RzLjFhDzWnTBPZWu6RoT4wxsaDkxXUzw==} - requiresBuild: true - peerDependencies: - vite: ^2.6.0 || ^3.0.0 - dependencies: - '@rollup/pluginutils': 4.2.1 - js-yaml: 4.1.0 - tosource: 2.0.0-alpha.3 - vite: 2.9.15 - dev: true - - /@mui/base/5.0.0-alpha.101_rj7ozvcq3uehdlnj3cbwzbi5ce: - resolution: {integrity: sha512-a54BcXvArGOKUZ2zyS/7B9GNhAGgfomEQSkfEZ88Nc9jKvXA+Mppenfz5o4JCAnD8c4VlePmz9rKOYvvum1bZw==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@types/react': ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.19.4 - '@emotion/is-prop-valid': 1.2.0 - '@mui/types': 7.2.0_@types+react@18.0.21 - '@mui/utils': 5.10.9_react@18.2.0 - '@popperjs/core': 2.11.6 - '@types/react': 18.0.21 - clsx: 1.2.1 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-is: 18.2.0 - dev: false - - /@mui/core-downloads-tracker/5.10.9: - resolution: {integrity: sha512-rqoFu4qww6KJBbXYhyRd9YXjwBHa3ylnBPSWbGf1bdfG0AYMKmVzg8zxkWvxAWOp97kvx3M2kNPb0xMIDZiogQ==} - dev: false - - /@mui/icons-material/5.10.9_6usjrp3ypnzobhq35dcwvjrt3m: - resolution: {integrity: sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@mui/material': ^5.0.0 - '@types/react': ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.19.4 - '@mui/material': 5.10.9_ikcgkdnp4bn3rgptamntbhbo7e - '@types/react': 18.0.21 - react: 18.2.0 - dev: false - - /@mui/material/5.10.9_ikcgkdnp4bn3rgptamntbhbo7e: - resolution: {integrity: sha512-sdOzlgpCmyw48je+E7o9UGGJpgBaF+60FlTRpVpcd/z+LUhnuzzuis891yPI5dPPXLBDL/bO4SsGg51lgNeLBw==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@emotion/react': ^11.5.0 - '@emotion/styled': ^11.3.0 - '@types/react': ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@emotion/react': - optional: true - '@emotion/styled': - optional: true - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.19.4 - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/styled': 11.10.4_g3tud4ene45llglqap74b5kkse - '@mui/base': 5.0.0-alpha.101_rj7ozvcq3uehdlnj3cbwzbi5ce - '@mui/core-downloads-tracker': 5.10.9 - '@mui/system': 5.10.9_h33l6npc22g7vcra72cibfsrvm - '@mui/types': 7.2.0_@types+react@18.0.21 - '@mui/utils': 5.10.9_react@18.2.0 - '@types/react': 18.0.21 - '@types/react-transition-group': 4.4.5 - clsx: 1.2.1 - csstype: 3.1.1 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-is: 18.2.0 - react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y - dev: false - - /@mui/private-theming/5.10.9_iapumuv4e6jcjznwuxpf4tt22e: - resolution: {integrity: sha512-BN7/CnsVPVyBaQpDTij4uV2xGYHHHhOgpdxeYLlIu+TqnsVM7wUeF+37kXvHovxM6xmL5qoaVUD98gDC0IZnHg==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@types/react': ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.19.4 - '@mui/utils': 5.10.9_react@18.2.0 - '@types/react': 18.0.21 - prop-types: 15.8.1 - react: 18.2.0 - dev: false - - /@mui/styled-engine/5.10.8_hfzxdiydbrbhhfpkwuv3jhvwmq: - resolution: {integrity: sha512-w+y8WI18EJV6zM/q41ug19cE70JTeO6sWFsQ7tgePQFpy6ToCVPh0YLrtqxUZXSoMStW5FMw0t9fHTFAqPbngw==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@emotion/react': ^11.4.1 - '@emotion/styled': ^11.3.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@emotion/react': - optional: true - '@emotion/styled': - optional: true - dependencies: - '@babel/runtime': 7.19.4 - '@emotion/cache': 11.10.3 - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/styled': 11.10.4_g3tud4ene45llglqap74b5kkse - csstype: 3.1.1 - prop-types: 15.8.1 - react: 18.2.0 - dev: false - - /@mui/system/5.10.9_h33l6npc22g7vcra72cibfsrvm: - resolution: {integrity: sha512-B6fFC0sK06hNmqY7fAUfwShQv594+u/DT1YEFHPtK4laouTu7V4vSGQWi1WJT9Bjs9Db5D1bRDJ+Yy+tc3QOYA==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@emotion/react': ^11.5.0 - '@emotion/styled': ^11.3.0 - '@types/react': ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@emotion/react': - optional: true - '@emotion/styled': - optional: true - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.19.4 - '@emotion/react': 11.10.4_iapumuv4e6jcjznwuxpf4tt22e - '@emotion/styled': 11.10.4_g3tud4ene45llglqap74b5kkse - '@mui/private-theming': 5.10.9_iapumuv4e6jcjznwuxpf4tt22e - '@mui/styled-engine': 5.10.8_hfzxdiydbrbhhfpkwuv3jhvwmq - '@mui/types': 7.2.0_@types+react@18.0.21 - '@mui/utils': 5.10.9_react@18.2.0 - '@types/react': 18.0.21 - clsx: 1.2.1 - csstype: 3.1.1 - prop-types: 15.8.1 - react: 18.2.0 - dev: false - - /@mui/types/7.2.0_@types+react@18.0.21: - resolution: {integrity: sha512-lGXtFKe5lp3UxTBGqKI1l7G8sE2xBik8qCfrLHD5olwP/YU0/ReWoWT7Lp1//ri32dK39oPMrJN8TgbkCSbsNA==} - peerDependencies: - '@types/react': '*' - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.0.21 - dev: false - - /@mui/utils/5.10.9_react@18.2.0: - resolution: {integrity: sha512-2tdHWrq3+WCy+G6TIIaFx3cg7PorXZ71P375ExuX61od1NOAJP1mK90VxQ8N4aqnj2vmO3AQDkV4oV2Ktvt4bA==} - engines: {node: '>=12.0.0'} - peerDependencies: - react: ^17.0.0 || ^18.0.0 - dependencies: - '@babel/runtime': 7.19.4 - '@types/prop-types': 15.7.5 - '@types/react-is': 17.0.3 - prop-types: 15.8.1 - react: 18.2.0 - react-is: 18.2.0 - dev: false - - /@popperjs/core/2.11.6: - resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} - dev: false - - /@reduxjs/toolkit/1.8.6_kuo2ie247izvzll3jejufdtq3q: - resolution: {integrity: sha512-4Ia/Loc6WLmdSOzi7k5ff7dLK8CgG2b8aqpLsCAJhazAzGdp//YBUSaj0ceW6a3kDBDNRrq5CRwyCS0wBiL1ig==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.0.2 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - dependencies: - immer: 9.0.15 - react: 18.2.0 - react-redux: 8.0.4_5uumaiclxbdbzaqafclbf6maf4 - redux: 4.2.0 - redux-thunk: 2.4.1_redux@4.2.0 - reselect: 4.1.6 - dev: false - - /@remix-run/router/1.0.2: - resolution: {integrity: sha512-GRSOFhJzjGN+d4sKHTMSvNeUPoZiDHWmRnXfzaxrqe7dE/Nzlc8BiMSJdLDESZlndM7jIUrZ/F4yWqVYlI0rwQ==} - engines: {node: '>=14'} - dev: false - - /@rollup/pluginutils/4.2.1: - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} - dependencies: - estree-walker: 2.0.2 - picomatch: 2.3.1 - dev: true - - /@socket.io/component-emitter/3.1.0: - resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} - dev: false - - /@types/accepts/1.3.5: - resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} - dependencies: - '@types/node': 17.0.45 - dev: true - - /@types/body-parser/1.19.2: - resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} - dependencies: - '@types/connect': 3.4.35 - '@types/node': 17.0.45 - dev: true - - /@types/connect/3.4.35: - resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} - dependencies: - '@types/node': 17.0.45 - dev: true - - /@types/content-disposition/0.5.5: - resolution: {integrity: sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==} - dev: true - - /@types/cookie/0.4.1: - resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} - dev: false - - /@types/cookies/0.7.7: - resolution: {integrity: sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==} - dependencies: - '@types/connect': 3.4.35 - '@types/express': 4.17.14 - '@types/keygrip': 1.0.2 - '@types/node': 17.0.45 - dev: true - - /@types/cors/2.8.12: - resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==} - dev: false - - /@types/express-serve-static-core/4.17.31: - resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==} - dependencies: - '@types/node': 17.0.45 - '@types/qs': 6.9.7 - '@types/range-parser': 1.2.4 - dev: true - - /@types/express/4.17.14: - resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==} - dependencies: - '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.31 - '@types/qs': 6.9.7 - '@types/serve-static': 1.15.0 - dev: true - - /@types/history/4.7.11: - resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} - dev: true - - /@types/hoist-non-react-statics/3.3.1: - resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} - dependencies: - '@types/react': 18.0.21 - hoist-non-react-statics: 3.3.2 - dev: false - - /@types/http-assert/1.5.3: - resolution: {integrity: sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==} - dev: true - - /@types/http-errors/1.8.2: - resolution: {integrity: sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==} - dev: true - - /@types/keygrip/1.0.2: - resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==} - dev: true - - /@types/koa-compose/3.2.5: - resolution: {integrity: sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==} - dependencies: - '@types/koa': 2.13.5 - dev: true - - /@types/koa-router/7.4.4: - resolution: {integrity: sha512-3dHlZ6CkhgcWeF6wafEUvyyqjWYfKmev3vy1PtOmr0mBc3wpXPU5E8fBBd4YQo5bRpHPfmwC5yDaX7s4jhIN6A==} - dependencies: - '@types/koa': 2.13.5 - dev: true - - /@types/koa/2.13.5: - resolution: {integrity: sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA==} - dependencies: - '@types/accepts': 1.3.5 - '@types/content-disposition': 0.5.5 - '@types/cookies': 0.7.7 - '@types/http-assert': 1.5.3 - '@types/http-errors': 1.8.2 - '@types/keygrip': 1.0.2 - '@types/koa-compose': 3.2.5 - '@types/node': 17.0.45 - dev: true - - /@types/mime-types/2.1.1: - resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==} - dev: true - - /@types/mime/3.0.1: - resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} - dev: true - - /@types/node/17.0.45: - resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - - /@types/parse-json/4.0.0: - resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} - dev: false - - /@types/prop-types/15.7.5: - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - - /@types/qs/6.9.7: - resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - dev: true - - /@types/range-parser/1.2.4: - resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - dev: true - - /@types/react-dom/18.0.6: - resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==} - dependencies: - '@types/react': 18.0.21 - - /@types/react-is/17.0.3: - resolution: {integrity: sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==} - dependencies: - '@types/react': 18.0.21 - dev: false - - /@types/react-router-dom/5.3.3: - resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.21 - '@types/react-router': 5.1.19 - dev: true - - /@types/react-router/5.1.19: - resolution: {integrity: sha512-Fv/5kb2STAEMT3wHzdKQK2z8xKq38EDIGVrutYLmQVVLe+4orDFquU52hQrULnEHinMKv9FSA6lf9+uNT1ITtA==} - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.21 - dev: true - - /@types/react-transition-group/4.4.5: - resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} - dependencies: - '@types/react': 18.0.21 - dev: false - - /@types/react/18.0.21: - resolution: {integrity: sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==} - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 - csstype: 3.1.1 - - /@types/scheduler/0.16.2: - resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} - - /@types/serve-static/1.15.0: - resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} - dependencies: - '@types/mime': 3.0.1 - '@types/node': 17.0.45 - dev: true - - /@types/use-sync-external-store/0.0.3: - resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} - dev: false - - /@types/uuid/8.3.4: - resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} - dev: true - - /@vitejs/plugin-react/1.3.2: - resolution: {integrity: sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==} - engines: {node: '>=12.0.0'} - dependencies: - '@babel/core': 7.19.3 - '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.19.3 - '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.19.3 - '@babel/plugin-transform-react-jsx-self': 7.18.6_@babel+core@7.19.3 - '@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.19.3 - '@rollup/pluginutils': 4.2.1 - react-refresh: 0.13.0 - resolve: 1.22.1 - transitivePeerDependencies: - - supports-color - dev: true - - /accepts/1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - dev: false - - /ansi-styles/3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 - - /argparse/2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /babel-plugin-macros/3.1.0: - resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} - engines: {node: '>=10', npm: '>=6'} - dependencies: - '@babel/runtime': 7.19.4 - cosmiconfig: 7.0.1 - resolve: 1.22.1 - dev: false - - /base64-js/1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true - - /base64id/2.0.0: - resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} - engines: {node: ^4.5.0 || >= 5.9} - dev: false - - /browserslist/4.21.4: - resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001419 - electron-to-chromium: 1.4.282 - node-releases: 2.0.6 - update-browserslist-db: 1.0.10_browserslist@4.21.4 - dev: true - - /buffer/6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: true - - /cache-content-type/1.0.1: - resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} - engines: {node: '>= 6.0.0'} - dependencies: - mime-types: 2.1.35 - ylru: 1.3.2 - dev: false - - /callsites/3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: false - - /caniuse-lite/1.0.30001419: - resolution: {integrity: sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw==} - dev: true - - /chalk/2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - /clsx/1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - dev: false - - /co/4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - dev: false - - /color-convert/1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - - /color-name/1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - /content-disposition/0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - dependencies: - safe-buffer: 5.2.1 - dev: false - - /content-type/1.0.4: - resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} - engines: {node: '>= 0.6'} - dev: false - - /convert-source-map/1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - - /cookie/0.4.2: - resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} - engines: {node: '>= 0.6'} - dev: false - - /cookies/0.8.0: - resolution: {integrity: sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==} - engines: {node: '>= 0.8'} - dependencies: - depd: 2.0.0 - keygrip: 1.1.0 - dev: false - - /cors/2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - dev: false - - /cosmiconfig/7.0.1: - resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} - engines: {node: '>=10'} - dependencies: - '@types/parse-json': 4.0.0 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - dev: false - - /csstype/3.1.1: - resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} - - /debug/3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: false - - /debug/4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - - /deep-equal/1.0.1: - resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} - dev: false - - /delegates/1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - dev: false - - /depd/1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - dev: false - - /depd/2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dev: false - - /destroy/1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dev: false - - /dom-helpers/5.2.1: - resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dependencies: - '@babel/runtime': 7.19.4 - csstype: 3.1.1 - dev: false - - /ee-first/1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - dev: false - - /electron-to-chromium/1.4.282: - resolution: {integrity: sha512-Dki0WhHNh/br/Xi1vAkueU5mtIc9XLHcMKB6tNfQKk+kPG0TEUjRh5QEMAUbRp30/rYNMFD1zKKvbVzwq/4wmg==} - dev: true - - /encodeurl/1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - dev: false - - /engine.io-client/6.2.3: - resolution: {integrity: sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==} - dependencies: - '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4 - engine.io-parser: 5.0.4 - ws: 8.2.3 - xmlhttprequest-ssl: 2.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - - /engine.io-parser/5.0.4: - resolution: {integrity: sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==} - engines: {node: '>=10.0.0'} - dev: false - - /engine.io/6.2.0: - resolution: {integrity: sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==} - engines: {node: '>=10.0.0'} - dependencies: - '@types/cookie': 0.4.1 - '@types/cors': 2.8.12 - '@types/node': 17.0.45 - accepts: 1.3.8 - base64id: 2.0.0 - cookie: 0.4.2 - cors: 2.8.5 - debug: 4.3.4 - engine.io-parser: 5.0.4 - ws: 8.2.3 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - - /error-ex/1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - dependencies: - is-arrayish: 0.2.1 - dev: false - - /esbuild-android-64/0.14.54: - resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /esbuild-android-arm64/0.14.54: - resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /esbuild-darwin-64/0.14.54: - resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /esbuild-darwin-arm64/0.14.54: - resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /esbuild-freebsd-64/0.14.54: - resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-freebsd-arm64/0.14.54: - resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-32/0.14.54: - resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-64/0.14.54: - resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-arm/0.14.54: - resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-arm64/0.14.54: - resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-mips64le/0.14.54: - resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-ppc64le/0.14.54: - resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-riscv64/0.14.54: - resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-s390x/0.14.54: - resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-netbsd-64/0.14.54: - resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-openbsd-64/0.14.54: - resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-sunos-64/0.14.54: - resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /esbuild-windows-32/0.14.54: - resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /esbuild-windows-64/0.14.54: - resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /esbuild-windows-arm64/0.14.54: - resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /esbuild/0.14.54: - resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/linux-loong64': 0.14.54 - esbuild-android-64: 0.14.54 - esbuild-android-arm64: 0.14.54 - esbuild-darwin-64: 0.14.54 - esbuild-darwin-arm64: 0.14.54 - esbuild-freebsd-64: 0.14.54 - esbuild-freebsd-arm64: 0.14.54 - esbuild-linux-32: 0.14.54 - esbuild-linux-64: 0.14.54 - esbuild-linux-arm: 0.14.54 - esbuild-linux-arm64: 0.14.54 - esbuild-linux-mips64le: 0.14.54 - esbuild-linux-ppc64le: 0.14.54 - esbuild-linux-riscv64: 0.14.54 - esbuild-linux-s390x: 0.14.54 - esbuild-netbsd-64: 0.14.54 - esbuild-openbsd-64: 0.14.54 - esbuild-sunos-64: 0.14.54 - esbuild-windows-32: 0.14.54 - esbuild-windows-64: 0.14.54 - esbuild-windows-arm64: 0.14.54 - dev: true - - /escalade/3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: true - - /escape-html/1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - dev: false - - /escape-string-regexp/1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - /escape-string-regexp/4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: false - - /estree-walker/2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true - - /find-root/1.1.0: - resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} - dev: false - - /fresh/0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - dev: false - - /fsevents/2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /function-bind/1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - - /gensync/1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: true - - /globals/11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: true - - /has-flag/3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - /has-symbols/1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - dev: false - - /has-tostringtag/1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: false - - /has/1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} - dependencies: - function-bind: 1.1.1 - - /hoist-non-react-statics/3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - dependencies: - react-is: 16.13.1 - dev: false - - /http-assert/1.5.0: - resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} - engines: {node: '>= 0.8'} - dependencies: - deep-equal: 1.0.1 - http-errors: 1.8.1 - dev: false - - /http-errors/1.6.3: - resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} - engines: {node: '>= 0.6'} - dependencies: - depd: 1.1.2 - inherits: 2.0.3 - setprototypeof: 1.1.0 - statuses: 1.5.0 - dev: false - - /http-errors/1.8.1: - resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} - engines: {node: '>= 0.6'} - dependencies: - depd: 1.1.2 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 1.5.0 - toidentifier: 1.0.1 - dev: false - - /ieee754/1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true - - /immer/9.0.15: - resolution: {integrity: sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==} - dev: false - - /import-fresh/3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: false - - /inherits/2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - dev: false - - /inherits/2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false - - /is-arrayish/0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: false - - /is-core-module/2.10.0: - resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} - dependencies: - has: 1.0.3 - - /is-generator-function/1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: false - - /js-tokens/4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - /js-yaml/4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /jsesc/2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - dev: true - - /json-parse-even-better-errors/2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: false - - /json5/2.2.1: - resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==} - engines: {node: '>=6'} - hasBin: true - dev: true - - /keygrip/1.1.0: - resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} - engines: {node: '>= 0.6'} - dependencies: - tsscmp: 1.0.6 - dev: false - - /koa-compose/4.1.0: - resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} - dev: false - - /koa-convert/2.0.0: - resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} - engines: {node: '>= 10'} - dependencies: - co: 4.6.0 - koa-compose: 4.1.0 - dev: false - - /koa-router/10.1.1: - resolution: {integrity: sha512-z/OzxVjf5NyuNO3t9nJpx7e1oR3FSBAauiwXtMQu4ppcnuNZzTaQ4p21P8A6r2Es8uJJM339oc4oVW+qX7SqnQ==} - engines: {node: '>= 8.0.0'} - dependencies: - debug: 4.3.4 - http-errors: 1.8.1 - koa-compose: 4.1.0 - methods: 1.1.2 - path-to-regexp: 6.2.1 - transitivePeerDependencies: - - supports-color - dev: false - - /koa-send/5.0.1: - resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==} - engines: {node: '>= 8'} - dependencies: - debug: 4.3.4 - http-errors: 1.8.1 - resolve-path: 1.4.0 - transitivePeerDependencies: - - supports-color - dev: false - - /koa-static/5.0.0: - resolution: {integrity: sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==} - engines: {node: '>= 7.6.0'} - dependencies: - debug: 3.2.7 - koa-send: 5.0.1 - transitivePeerDependencies: - - supports-color - dev: false - - /koa/2.13.4: - resolution: {integrity: sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==} - engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} - dependencies: - accepts: 1.3.8 - cache-content-type: 1.0.1 - content-disposition: 0.5.4 - content-type: 1.0.4 - cookies: 0.8.0 - debug: 4.3.4 - delegates: 1.0.0 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - fresh: 0.5.2 - http-assert: 1.5.0 - http-errors: 1.8.1 - is-generator-function: 1.0.10 - koa-compose: 4.1.0 - koa-convert: 2.0.0 - on-finished: 2.4.1 - only: 0.0.2 - parseurl: 1.3.3 - statuses: 1.5.0 - type-is: 1.6.18 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: false - - /lines-and-columns/1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: false - - /loose-envify/1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - dependencies: - js-tokens: 4.0.0 - dev: false - - /media-typer/0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - dev: false - - /methods/1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - dev: false - - /mime-db/1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: false - - /mime-types/2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: false - - /ms/2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - /ms/2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: false - - /nanoid/3.3.4: - resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - - /negotiator/0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - dev: false - - /node-releases/2.0.6: - resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} - dev: true - - /object-assign/4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: false - - /on-finished/2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - dependencies: - ee-first: 1.1.1 - dev: false - - /only/0.0.2: - resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} - dev: false - - /parent-module/1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - dev: false - - /parse-json/5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - dependencies: - '@babel/code-frame': 7.18.6 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - dev: false - - /parseurl/1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - dev: false - - /path-browserify/1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - dev: true - - /path-is-absolute/1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: false - - /path-parse/1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - /path-to-regexp/6.2.1: - resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} - dev: false - - /path-type/4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: false - - /picocolors/1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true - - /picomatch/2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /postcss/8.4.18: - resolution: {integrity: sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.4 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: true - - /process/0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - dev: true - - /prop-types/15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - dev: false - - /radash/10.6.0: - resolution: {integrity: sha512-L0PD+kBVaPGxn0UO9yVLJUKUkuu7bLqroZbieecPUGuSEtByCtMedDSyw+arA8pnLtZduYTgHnMjRfN90gozpQ==} - engines: {node: '>=14.18.0'} - dev: false - - /react-dom/18.2.0_react@18.2.0: - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 - dependencies: - loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 - dev: false - - /react-is/16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: false - - /react-is/18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: false - - /react-redux/8.0.4_5uumaiclxbdbzaqafclbf6maf4: - resolution: {integrity: sha512-yMfQ7mX6bWuicz2fids6cR1YT59VTuT8MKyyE310wJQlINKENCeT1UcPdEiX6znI5tF8zXyJ/VYvDgeGuaaNwQ==} - peerDependencies: - '@types/react': ^16.8 || ^17.0 || ^18.0 - '@types/react-dom': ^16.8 || ^17.0 || ^18.0 - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - react-native: '>=0.59' - redux: ^4 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - react-dom: - optional: true - react-native: - optional: true - redux: - optional: true - dependencies: - '@babel/runtime': 7.19.4 - '@types/hoist-non-react-statics': 3.3.1 - '@types/react': 18.0.21 - '@types/react-dom': 18.0.6 - '@types/use-sync-external-store': 0.0.3 - hoist-non-react-statics: 3.3.2 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-is: 18.2.0 - use-sync-external-store: 1.2.0_react@18.2.0 - dev: false - - /react-refresh/0.13.0: - resolution: {integrity: sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==} - engines: {node: '>=0.10.0'} - dev: true - - /react-router-dom/6.4.2_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-yM1kjoTkpfjgczPrcyWrp+OuQMyB1WleICiiGfstnQYo/S8hPEEnVjr/RdmlH6yKK4Tnj1UGXFSa7uwAtmDoLQ==} - engines: {node: '>=14'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' - dependencies: - '@remix-run/router': 1.0.2 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-router: 6.4.2_react@18.2.0 - dev: false - - /react-router/6.4.2_react@18.2.0: - resolution: {integrity: sha512-Rb0BAX9KHhVzT1OKhMvCDMw776aTYM0DtkxqUBP8dNBom3mPXlfNs76JNGK8wKJ1IZEY1+WGj+cvZxHVk/GiKw==} - engines: {node: '>=14'} - peerDependencies: - react: '>=16.8' - dependencies: - '@remix-run/router': 1.0.2 - react: 18.2.0 - dev: false - - /react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} - peerDependencies: - react: '>=16.6.0' - react-dom: '>=16.6.0' - dependencies: - '@babel/runtime': 7.19.4 - dom-helpers: 5.2.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false - - /react/18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} - dependencies: - loose-envify: 1.4.0 - dev: false - - /redux-thunk/2.4.1_redux@4.2.0: - resolution: {integrity: sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==} - peerDependencies: - redux: ^4 - dependencies: - redux: 4.2.0 - dev: false - - /redux/4.2.0: - resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==} - dependencies: - '@babel/runtime': 7.19.4 - dev: false - - /regenerator-runtime/0.13.10: - resolution: {integrity: sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==} - dev: false - - /reselect/4.1.6: - resolution: {integrity: sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==} - dev: false - - /resolve-from/4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: false - - /resolve-path/1.4.0: - resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} - engines: {node: '>= 0.8'} - dependencies: - http-errors: 1.6.3 - path-is-absolute: 1.0.1 - dev: false - - /resolve/1.22.1: - resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} - hasBin: true - dependencies: - is-core-module: 2.10.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - /rollup/2.77.3: - resolution: {integrity: sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==} - engines: {node: '>=10.0.0'} - hasBin: true - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /rxjs/7.5.7: - resolution: {integrity: sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==} - dependencies: - tslib: 2.4.0 - dev: false - - /safe-buffer/5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false - - /scheduler/0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} - dependencies: - loose-envify: 1.4.0 - dev: false - - /semver/6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true - dev: true - - /setprototypeof/1.1.0: - resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} - dev: false - - /setprototypeof/1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - dev: false - - /socket.io-adapter/2.4.0: - resolution: {integrity: sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==} - dev: false - - /socket.io-client/4.5.2: - resolution: {integrity: sha512-naqYfFu7CLDiQ1B7AlLhRXKX3gdeaIMfgigwavDzgJoIUYulc1qHH5+2XflTsXTPY7BlPH5rppJyUjhjrKQKLg==} - engines: {node: '>=10.0.0'} - dependencies: - '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4 - engine.io-client: 6.2.3 - socket.io-parser: 4.2.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - - /socket.io-parser/4.2.1: - resolution: {integrity: sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==} - engines: {node: '>=10.0.0'} - dependencies: - '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: false - - /socket.io/4.5.2: - resolution: {integrity: sha512-6fCnk4ARMPZN448+SQcnn1u8OHUC72puJcNtSgg2xS34Cu7br1gQ09YKkO1PFfDn/wyUE9ZgMAwosJed003+NQ==} - engines: {node: '>=10.0.0'} - dependencies: - accepts: 1.3.8 - base64id: 2.0.0 - debug: 4.3.4 - engine.io: 6.2.0 - socket.io-adapter: 2.4.0 - socket.io-parser: 4.2.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - - /source-map-js/1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - dev: true - - /source-map/0.5.7: - resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} - engines: {node: '>=0.10.0'} - dev: false - - /statuses/1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - dev: false - - /stylis/4.0.13: - resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==} - dev: false - - /supports-color/5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - dependencies: - has-flag: 3.0.0 - - /supports-preserve-symlinks-flag/1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - /to-fast-properties/2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - - /toidentifier/1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - dev: false - - /tosource/2.0.0-alpha.3: - resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} - engines: {node: '>=10'} - dev: true - - /tslib/2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - dev: false - - /tsscmp/1.0.6: - resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} - engines: {node: '>=0.6.x'} - dev: false - - /type-is/1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - dev: false - - /typescript/4.8.4: - resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} - engines: {node: '>=4.2.0'} - hasBin: true - dev: true - - /update-browserslist-db/1.0.10_browserslist@4.21.4: - resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.21.4 - escalade: 3.1.1 - picocolors: 1.0.0 - dev: true - - /use-sync-external-store/1.2.0_react@18.2.0: - resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.2.0 - dev: false - - /uuid/8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - dev: false - - /vary/1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - dev: false - - /vite/2.9.15: - resolution: {integrity: sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==} - engines: {node: '>=12.2.0'} - hasBin: true - peerDependencies: - less: '*' - sass: '*' - stylus: '*' - peerDependenciesMeta: - less: - optional: true - sass: - optional: true - stylus: - optional: true - dependencies: - esbuild: 0.14.54 - postcss: 8.4.18 - resolve: 1.22.1 - rollup: 2.77.3 - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /ws/8.2.3: - resolution: {integrity: sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false - - /xmlhttprequest-ssl/2.0.0: - resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} - engines: {node: '>=0.4.0'} - dev: false - - /yaml/1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - dev: false - - /ylru/1.3.2: - resolution: {integrity: sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==} - engines: {node: '>= 4.0.0'} - dev: false diff --git a/server-node/src/core/HTTPServer.ts b/server-node/src/core/HTTPServer.ts deleted file mode 100644 index cc05960..0000000 --- a/server-node/src/core/HTTPServer.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { createServer, Server } from 'http'; -import { parse as urlParse } from 'url'; -import { open, close, readFile, fstat } from 'fs'; -import { parse, join } from 'path'; - -namespace server { - export const mimes = { - '.html': 'text/html', - '.ico': 'image/x-icon', - '.js': 'text/javascript', - '.json': 'application/json', - '.css': 'text/css', - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.webp': 'image/webp', - }; -} - -class Jean { - private workingDir: string; - - /** - * Jean static file server its only purpose is serving SPA and images - * with the lowest impact possible. - * @param workingDir sets the root directory automatically trying index.html - * If specified the file in addition to the directory it will serve the - * file directly. - * *e.g* new Jean(path.join(__dirname, 'dist')) will try - * index.html from the dist directory; - * @author me :D - */ - - constructor(workingDir: string) { - this.workingDir = workingDir; - } - - /** - * Create a static file server - * @returns an instance of a standard NodeJS http.Server - */ - public createServer(): Server { - return createServer((req, res) => { - // parse the current given url - const parsedUrl = urlParse(req.url, false) - // extract the pathname and guard it with the working dir - let pathname = join(this.workingDir, `.${parsedUrl.pathname}`); - // extract the file extension - const ext = parse(pathname).ext; - - // open the file or directory and fetch its descriptor - open(pathname, 'r', (err, fd) => { - // whoops, not found, send a 404 - if (err) { - res.statusCode = 404; - res.end(`File ${pathname} not found!`); - return; - } - // something's gone wrong it's not a file or a directory - fstat(fd, (err, stat) => { - if (err) { - res.statusCode = 500; - res.end(err); - } - // try file index.html - if (stat.isDirectory()) { - pathname = join(pathname, 'index.html') - } - // read the file - readFile(pathname, (err, data) => { - if (err) { - res.statusCode = 500; - res.end(`Error reading the file: ${err}`); - } else { - // infer it's extension otherwise it's the index.html - res.setHeader('Content-type', server.mimes[ext] || 'text/html'); - res.end(data); - close(fd); - } - }); - }) - }); - }) - } -} - -export default Jean; \ No newline at end of file diff --git a/server-node/src/core/Process.ts b/server-node/src/core/Process.ts deleted file mode 100644 index c14914f..0000000 --- a/server-node/src/core/Process.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { spawn } from 'child_process'; -import { join } from 'path'; -import { Readable } from 'stream'; -import { ISettings } from '../interfaces/ISettings'; -import { availableParams } from '../utils/params'; -import Logger from '../utils/BetterLogger'; -import { IDownloadFormat, IDownloadMetadata } from '../interfaces/IDownloadMetadata'; - -const log = Logger.instance; - -/** - * Represents a download process that spawns yt-dlp. - * @param url - The downlaod url. - * @param params - The cli arguments passed by the frontend. - * @param settings - The download settings passed by the frontend. - */ - -class Process { - public readonly url: string; - public readonly params: Array; - private settings: ISettings; - private stdout: Readable; - private pid: number; - private metadata?: IDownloadMetadata; - private exePath = join(__dirname, 'yt-dlp'); - private customFileName?: string; - - private readonly template = `download: - { - "eta":%(progress.eta)s, - "percentage":"%(progress._percent_str)s", - "speed":"%(progress._speed_str)s", - "size":%(info.filesize_approx)s - }` - .replace(/\s\s+/g, ' ') - .replace('\n', ''); - - constructor(url: string, params: Array, settings: any, customFileName?: string) { - this.url = url; - this.params = params || []; - this.settings = settings - this.stdout = undefined; - this.pid = undefined; - this.metadata = undefined; - this.customFileName = customFileName; - } - - /** - * function that launch the download process, sets the stdout property and the pid - * @param callback not yet implemented - * @returns the process instance - */ - public async start(callback?: Function): Promise { - const sanitizedParams = this.params.filter((param: string) => availableParams.includes(param)); - - if (this.settings?.download_path) { - if (this.settings.download_path.charAt(this.settings.download_path.length - 1) !== '/') { - this.settings.download_path = `${this.settings.download_path}/` - } - } - - const ytldp = spawn(this.exePath, - [ - '-o', `${this.settings?.download_path || 'downloads/'}${this.customFileName || '%(title)s'}.%(ext)s`, - '--progress-template', this.template, - '--no-colors', - ] - .concat(sanitizedParams) - .concat((this.settings?.cliArgs ?? []).map(arg => arg.split(' ')).flat()) - .concat([this.url]) - ); - - this.pid = ytldp.pid; - this.stdout = ytldp.stdout; - - log.info('proc', `Spawned a new process, pid: ${this.pid}`) - - if (callback) { - callback() - } - - return this; - } - - /** - * function used internally by the download process to fetch information, usually thumbnail and title - * @returns Promise to the lock - */ - public getMetadata(): Promise { - if (!this.metadata) { - let stdoutChunks = []; - const ytdlpInfo = spawn(this.exePath, ['-J', this.url]); - - ytdlpInfo.stdout.on('data', (data) => { - stdoutChunks.push(data); - }); - - return new Promise((resolve, reject) => { - ytdlpInfo.on('exit', () => { - try { - const buffer = Buffer.concat(stdoutChunks); - const json = JSON.parse(buffer.toString()); - const info = { - formats: json.formats.map((format: IDownloadFormat) => { - return { - format_id: format.format_id ?? '', - format_note: format.format_note ?? '', - fps: format.fps ?? '', - resolution: format.resolution ?? '', - vcodec: format.vcodec ?? '', - acodec: format.acodec ?? '', - } - }).filter((format: IDownloadFormat) => format.format_note !== 'storyboard'), - best: { - format_id: json.format_id ?? '', - format_note: json.format_note ?? '', - fps: json.fps ?? '', - resolution: json.resolution ?? '', - vcodec: json.vcodec ?? '', - acodec: json.acodec ?? '', - }, - thumbnail: json.thumbnail, - title: json.title, - } - resolve(info); - this.metadata = info; - - } catch (e) { - reject('failed fetching formats, downloading best available'); - } - }); - }) - } - return new Promise((resolve) => { resolve(this.metadata!) }); - } - - /** - * function that kills the current process - */ - async kill() { - spawn('kill', [String(this.pid)]).on('exit', () => { - log.info('proc', `Stopped ${this.pid} because SIGKILL`) - }); - } - - /** - * pid getter function - * @returns {number} pid - */ - getPid(): number { - if (!this.pid) { - throw "Process isn't started" - } - return this.pid; - } - - /** - * stdout getter function - * @returns {Readable} stdout as stream - */ - getStdout(): Readable { - return this.stdout - } -} - -export default Process; \ No newline at end of file diff --git a/server-node/src/core/downloadArchive.ts b/server-node/src/core/downloadArchive.ts deleted file mode 100644 index af5ce1c..0000000 --- a/server-node/src/core/downloadArchive.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { resolve as pathResolve } from "path"; -import { readdir } from "fs"; -import { ISettings } from "../interfaces/ISettings"; -import Logger from "../utils/BetterLogger"; - -let settings: ISettings; -const log = Logger.instance; - -try { - settings = require('../../settings.json'); -} catch (e) { - log.warn('dl', 'settings.json not found'); -} - -export function listDownloaded(ctx: any) { - return new Promise((resolve, reject) => { - readdir(pathResolve(settings.download_path || 'download'), (err, files) => { - if (err) { - reject({ err: true }) - return - } - ctx.body = files.map(file => { - resolve({ - filename: file, - path: pathResolve(file), - }) - }) - }) - }) -} diff --git a/server-node/src/core/downloader.ts b/server-node/src/core/downloader.ts deleted file mode 100644 index dd1d53b..0000000 --- a/server-node/src/core/downloader.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { spawn } from 'child_process'; -import { from, interval } from 'rxjs'; -import { map, throttle } from 'rxjs/operators'; -import { Socket } from 'socket.io'; -import MemoryDB from '../db/memoryDB'; -import { IPayload } from '../interfaces/IPayload'; -import { ISettings } from '../interfaces/ISettings'; -import { CLIProgress } from '../types'; -import Logger from '../utils/BetterLogger'; -import Process from './Process'; -import { states } from './states'; - -// settings read from settings.json -let settings: ISettings; -const log = Logger.instance; - -const mem_db = new MemoryDB(); - -try { - settings = require('../../settings.json'); -} -catch (e) { - new Promise(resolve => setTimeout(resolve, 500)) - .then(() => log.warn('dl', 'settings.json not found, ignore if using Docker')); -} -/** - * Get download info such as thumbnail, title, resolution and list all formats - * @param socket - * @param url - */ -export async function getFormatsAndMetadata(socket: Socket, url: string) { - let p = new Process(url, [], settings); - try { - const formats = await p.getMetadata(); - socket.emit('available-formats', formats) - } catch (e) { - log.warn('dl', e) - socket.emit('progress', { - status: states.PROG_DONE, - pid: -1, - }); - } finally { - p = null; - } -} - -/** - * Invoke a new download. - * Called by the websocket messages listener. - * @param {Socket} socket current connection socket - * @param {object} payload frontend download payload - * @returns - */ -export async function download(socket: Socket, payload: IPayload) { - if (!payload || payload.url === '' || payload.url === null) { - socket.emit('progress', { status: states.PROG_DONE }); - return; - } - - const url = payload.url; - const params = typeof payload.params !== 'object' ? - payload.params.split(' ') : - payload.params; - - const renameTo = payload.renameTo - - const scopedSettings: ISettings = { - ...settings, - download_path: payload.path - } - - let p = new Process(url, params, scopedSettings, renameTo); - - p.start().then(downloader => { - mem_db.add(downloader) - displayDownloadMetadata(downloader, socket); - streamProcess(downloader, socket); - }); - - // GC - p = null; -} - -/** - * Send via websocket download info "chunk" - * @param process - * @param socket - */ -function displayDownloadMetadata(process: Process, socket: Socket) { - process.getMetadata() - .then(metadata => { - socket.emit('metadata', { - pid: process.getPid(), - metadata: metadata, - }); - }) - .catch((e) => { - socket.emit('progress', { - status: states.PROG_DONE, - pid: process.getPid(), - }); - log.warn('dl', e) - }) -} - -/** - * Stream via websocket download stdoud "chunks" - * @param process - * @param socket - */ -function streamProcess(process: Process, socket: Socket) { - const emitAbort = () => { - socket.emit('progress', { - status: states.PROG_DONE, - pid: process.getPid(), - }); - } - - from(process.getStdout().removeAllListeners()) // stdout as observable - .pipe( - throttle(() => interval(500)), // discard events closer than 500ms - map(stdout => formatter(String(stdout), process.getPid())) - ) - .subscribe({ - next: (stdout) => { - socket.emit('progress', stdout) - }, - complete: () => { - process.kill().then(() => { - emitAbort(); - mem_db.remove(process); - }); - }, - error: () => { - emitAbort(); - mem_db.remove(process); - } - }); -} - -/** - * Retrieve all downloads. - * If the server has just been launched retrieve the ones saved to the database. - * If the server is running fetches them from the process pool. - * @param {Socket} socket current connection socket - * @returns - */ -export async function retrieveDownload(socket: Socket) { - // it's a cold restart: the server has just been started with pending - // downloads, so fetch them from the database and resume. - - // if (coldRestart) { - // coldRestart = false; - // let downloads = []; - // // sanitize - // downloads = [...new Set(downloads.filter(el => el !== undefined))]; - // log.info('dl', `Cold restart, retrieving ${downloads.length} jobs`) - // for (const entry of downloads) { - // if (entry) { - // await download(socket, entry); - // } - // } - // return; - // } - - // it's an hot-reload the server it's running and the frontend ask for - // the pending job: retrieve them from the "in-memory database" (ProcessPool) - - const _poolSize = mem_db.size() - log.info('dl', `Retrieving ${_poolSize} jobs from pool`) - socket.emit('pending-jobs', _poolSize) - - const it = mem_db.iterator(); - - // resume the jobs - for (const entry of it) { - const [, process] = entry - displayDownloadMetadata(process, socket); - streamProcess(process, socket); - } -} - -/** - * Abort a specific download if pid is provided, in the other case - * calls the abortAllDownloads function - * @see abortAllDownloads - * @param {Socket} socket currenct connection socket - * @param {*} args args sent by the frontend. MUST contain the PID. - * @returns - */ -export function abortDownload(socket: Socket, args: any) { - if (!args) { - abortAllDownloads(socket); - return; - } - const { pid } = args; - - spawn('kill', [pid]) - .on('exit', () => { - socket.emit('progress', { - status: states.PROC_ABORT, - process: pid, - }); - log.warn('dl', `Aborting download ${pid}`); - }); -} - -/** - * Unconditionally kills all yt-dlp process. - * @param {Socket} socket currenct connection socket - */ -export function abortAllDownloads(socket: Socket) { - spawn('killall', ['yt-dlp']) - .on('exit', () => { - socket.emit('progress', { status: states.PROC_ABORT }); - log.info('dl', 'Aborting downloads'); - }); - mem_db.flush(); -} - -/** - * Get pool current size - */ -export function getQueueSize(): number { - return mem_db.size(); -} - -/** - * @private Formats the yt-dlp stdout to a frontend-readable format - * @param {string} stdout stdout as string - * @param {number} pid current process id relative to stdout - * @returns - */ -const formatter = (stdout: string, pid: number) => { - try { - const p: CLIProgress = JSON.parse(stdout); - if (p) { - return { - status: states.PROC_DOWNLOAD, - progress: p.percentage, - size: p.size, - dlSpeed: p.speed, - pid: pid, - } - } - } catch (e) { - return { - progress: 0, - } - } -} diff --git a/server-node/src/core/states.ts b/server-node/src/core/states.ts deleted file mode 100644 index 788786c..0000000 --- a/server-node/src/core/states.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Possible server states map - */ -export const states = { - PROC_DOWNLOAD: 'download', - PROC_MERGING: 'merging', - PROC_ABORT: 'abort', - PROG_DONE: 'status_done', -} \ No newline at end of file diff --git a/server-node/src/core/streamer.ts b/server-node/src/core/streamer.ts deleted file mode 100644 index 3ee2448..0000000 --- a/server-node/src/core/streamer.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { stat, createReadStream } from 'fs'; -import { lookup } from 'mime-types'; - -export function streamer(ctx: any, next: any) { - const filepath = '' - stat(filepath, (err, stat) => { - if (err) { - ctx.response.status = 404; - ctx.body = { err: 'resource not found' }; - next(); - } - const fileSize = stat.size; - const range = ctx.headers.range; - if (range) { - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; - const chunksize = end - start + 1; - const file = createReadStream(filepath, { start, end }); - const head = { - 'Content-Range': `bytes ${start}-${end}/${fileSize}`, - 'Accept-Ranges': 'bytes', - 'Content-Length': chunksize, - 'Content-Type': lookup(filepath) - }; - ctx.res.writeHead(206, head); - file.pipe(ctx.res); - next(); - } else { - const head = { - 'Content-Length': fileSize, - 'Content-Type': 'video/mp4' - }; - ctx.res.writeHead(200, head); - createReadStream(ctx.params.filepath).pipe(ctx.res); - next(); - } - }); -} \ No newline at end of file diff --git a/server-node/src/db/memoryDB.ts b/server-node/src/db/memoryDB.ts deleted file mode 100644 index 4e775f8..0000000 --- a/server-node/src/db/memoryDB.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Represents a download process that spawns yt-dlp. - */ - -import Process from "../core/Process"; - -class MemoryDB { - private _pool: Map - private _size: number - - constructor() { - this.init() - } - - private init() { - this._pool = new Map() - this._size = 0 - } - - /** - * Pool size getter - * @returns {number} pool's size - */ - size(): number { - return this._size - } - - /** - * Add a process to the pool - * @param {Process} process - */ - add(process: Process) { - this._pool.set(process.getPid(), process) - this._size++ - } - - /** - * Delete a process from the pool - * @param {Process} process - */ - remove(process: Process) { - if (this._size === 0) return - this._pool.delete(process.getPid()) - this._size-- - } - - /** - * Delete a process from the pool by its pid - * @param {number} pid - */ - removeByPid(pid: number) { - this._pool.delete(pid) - } - - /** - * get an iterator for the pool - * @returns {IterableIterator} iterator - */ - iterator(): IterableIterator<[number, Process]> { - return this._pool.entries() - } - - /** - * get a process by its pid - * @param {number} pid - * @returns {Process} - */ - getByPid(pid: number): Process { - return this._pool.get(pid) - } - - /** - * Clear memory db - */ - flush() { - this.init() - } -} - -export default MemoryDB; \ No newline at end of file diff --git a/server-node/src/interfaces/IDownloadMetadata.d.ts b/server-node/src/interfaces/IDownloadMetadata.d.ts deleted file mode 100644 index 0702307..0000000 --- a/server-node/src/interfaces/IDownloadMetadata.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface IDownloadMetadata { - formats: Array, - best: IDownloadFormat, - thumbnail: string, - title: string, -} - -export interface IDownloadFormat { - format_id: string, - format_note: string, - fps: number, - resolution: string, - vcodec: string, - acodec: string, -} \ No newline at end of file diff --git a/server-node/src/interfaces/IPayload.d.ts b/server-node/src/interfaces/IPayload.d.ts deleted file mode 100644 index ab6e953..0000000 --- a/server-node/src/interfaces/IPayload.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Represent a download payload sent by the frontend - */ - -export interface IPayload { - url: string - params: Array | string - path: string - title?: string - thumbnail?: string - size?: string - renameTo?: string -} \ No newline at end of file diff --git a/server-node/src/interfaces/IRecord.d.ts b/server-node/src/interfaces/IRecord.d.ts deleted file mode 100644 index e4e6109..0000000 --- a/server-node/src/interfaces/IRecord.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Represent a download db record - */ - -export interface IRecord { - uid: string, - url: string, - title: string, - thumbnail: string, - created: Date, - size: string, - pid: number, - params: string, -} \ No newline at end of file diff --git a/server-node/src/interfaces/ISettings.d.ts b/server-node/src/interfaces/ISettings.d.ts deleted file mode 100644 index 4e9a385..0000000 --- a/server-node/src/interfaces/ISettings.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ISettings { - download_path: string, - cliArgs?: string[], - port?: number, -} \ No newline at end of file diff --git a/server-node/src/main.ts b/server-node/src/main.ts deleted file mode 100644 index beda902..0000000 --- a/server-node/src/main.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { splash } from './utils/logger'; -import { join } from 'path'; -import { Server } from 'socket.io'; -import { ytdlpUpdater } from './utils/updater'; -import { - download, - abortDownload, - retrieveDownload, - abortAllDownloads, - getFormatsAndMetadata -} from './core/downloader'; -import { getFreeDiskSpace } from './utils/procUtils'; -import { listDownloaded } from './core/downloadArchive'; -import { createServer } from 'http'; -import { streamer } from './core/streamer'; -import * as Koa from 'koa'; -import * as Router from 'koa-router'; -import * as serve from 'koa-static'; -import * as cors from '@koa/cors'; -import Logger from './utils/BetterLogger'; -import { ISettings } from './interfaces/ISettings'; -import { directoryTree } from './utils/directoryUtils'; - -const app = new Koa(); -const server = createServer(app.callback()); -const router = new Router(); -const log = Logger.instance; -const io = new Server(server, { - cors: { - origin: "*", - methods: ["GET", "POST"] - } -}); - -let settings: ISettings; - -try { - settings = require('../settings.json'); -} catch (e) { - log.warn('settings', 'file not found, ignore if using Docker'); -} - -// Koa routing -router.get('/settings', (ctx, next) => { - ctx.redirect('/') - next() -}) -router.get('/downloaded', (ctx, next) => { - ctx.redirect('/') - next() -}) -router.get('/archive', (ctx, next) => { - listDownloaded(ctx) - .then((res: any) => { - ctx.body = res - next() - }) - .catch((err: any) => { - ctx.body = err; - next() - }) -}) -router.get('/stream/:filepath', (ctx, next) => { - streamer(ctx, next) -}) -router.get('/tree', (ctx, next) => { - ctx.body = directoryTree() - next() -}) - -// WebSocket listeners -io.on('connection', socket => { - log.info('ws', `${socket.handshake.address} connected!`) - - socket.on('send-url', (args) => { - log.info('ws', args?.url) - download(socket, args) - }) - socket.on('send-url-format-selection', (args) => { - log.info('ws', `Formats ${args?.url}`) - if (args.url) getFormatsAndMetadata(socket, args?.url) - }) - socket.on('abort', (args) => { - abortDownload(socket, args) - }) - socket.on('abort-all', () => { - abortAllDownloads(socket) - }) - socket.on('update-bin', () => { - ytdlpUpdater(socket) - }) - socket.on('retrieve-jobs', () => { - retrieveDownload(socket) - }) - socket.on('disk-space', () => { - getFreeDiskSpace(socket, settings.download_path || 'downloads/') - }) -}) - -io.on('disconnect', (socket) => { - log.info('ws', `${socket.handshake.address} disconnected`) -}) - -app.use(serve(join(__dirname, 'frontend'))) -app.use(cors()) -app.use(router.routes()) - -server.listen(process.env.PORT || settings.port || 3022) - -splash() -log.info('http', `Server started on port ${process.env.PORT || settings.port || 3022}`) - -/** - * Cleanup handler - */ -const gracefullyStop = () => { - log.warn('proc', 'Shutting down...') - io.disconnectSockets(true) - server.close() - log.info('proc', 'Done!') - process.exit(0) -} - -// Intercepts singnals and perform cleanups before shutting down. -process - .on('SIGTERM', () => gracefullyStop()) - .on('SIGUSR1', () => gracefullyStop()) - .on('SIGUSR2', () => gracefullyStop()) diff --git a/server-node/src/types/index.d.ts b/server-node/src/types/index.d.ts deleted file mode 100644 index 189a7ea..0000000 --- a/server-node/src/types/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type CLIProgress = { - percentage: string - speed: string - size: number - eta: number -} \ No newline at end of file diff --git a/server-node/src/utils/BetterLogger.ts b/server-node/src/utils/BetterLogger.ts deleted file mode 100644 index 0852464..0000000 --- a/server-node/src/utils/BetterLogger.ts +++ /dev/null @@ -1,59 +0,0 @@ -const ansi = { - reset: '\u001b[0m', - red: '\u001b[31m', - cyan: '\u001b[36m', - green: '\u001b[32m', - yellow: '\u001b[93m', - bold: '\u001b[1m', - normal: '\u001b[22m', -} - -class Logger { - private static _instance: Logger; - - constructor() { }; - - static get instance() { - if (this._instance) { - return this._instance - } - this._instance = new Logger() - return this._instance; - } - /** - * Print a standard info message - * @param {string} proto the context/protocol/section outputting the message - * @param {string} args the acutal message - */ - public info(proto: string, args: string) { - process.stdout.write( - this.formatter(proto, args) - ) - } - /** - * Print a warn message - * @param {string} proto the context/protocol/section outputting the message - * @param {string} args the acutal message - */ - public warn(proto: string, args: string) { - process.stdout.write( - `${ansi.yellow}${this.formatter(proto, args)}${ansi.reset}` - ) - } - /** - * Print an error message - * @param {string} proto the context/protocol/section outputting the message - * @param {string} args the acutal message - */ - public err(proto: string, args: string) { - process.stdout.write( - `${ansi.red}${this.formatter(proto, args)}${ansi.reset}` - ) - } - - private formatter(proto: any, args: any) { - return `${ansi.bold}[${proto}]${ansi.normal}\t${args}\n` - } -} - -export default Logger; \ No newline at end of file diff --git a/server-node/src/utils/directoryUtils.ts b/server-node/src/utils/directoryUtils.ts deleted file mode 100644 index 5ebcca9..0000000 --- a/server-node/src/utils/directoryUtils.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { readdirSync, statSync } from "fs"; -import { ISettings } from "../interfaces/ISettings"; - -let settings: ISettings; - -class Node { - public path: string - public children: Node[] - - constructor(path: string) { - this.path = path - this.children = [] - } -} - -function buildTreeDFS(rootPath: string, directoryOnly: boolean) { - const root = new Node(rootPath) - const stack: Node[] = [] - const flattened: string[] = [] - - stack.push(root) - flattened.push(rootPath) - - while (stack.length) { - const current = stack.pop() - if (current) { - const children = readdirSync(current.path) - for (const it of children) { - const childPath = `${current.path}/${it}` - const childNode = new Node(childPath) - - if (directoryOnly) { - if (statSync(childPath).isDirectory()) { - current.children.push(childNode) - stack.push(childNode) - flattened.push(childNode.path) - } - } else { - current.children.push(childNode) - if (statSync(childPath).isDirectory()) { - stack.push(childNode) - flattened.push(childNode.path) - } - } - } - } - } - - return { - tree: root, - flat: flattened - } -} - -try { - settings = require('../../settings.json'); -} catch (e) { } - -export const directoryTree = () => buildTreeDFS(settings.download_path || 'downloads', true) \ No newline at end of file diff --git a/server-node/src/utils/logger.ts b/server-node/src/utils/logger.ts deleted file mode 100644 index cfe322e..0000000 --- a/server-node/src/utils/logger.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Simplest logger function, takes two argument: first one put between - * square brackets (the protocol), the second one it's the effective message - * @param {string} proto protocol - * @param {string} args message - */ -export const logger = (proto: string, args: string) => { - console.log(`[${proto}]\t${args}`) -} - -/** - * CLI splash - */ - -export const splash = () => { - const fg = "\u001b[38;2;50;113;168m" - const reset = "\u001b[0m" - console.log(`${fg} __ ____ __ __ ______`) - console.log(" __ __/ /________/ / /__ _ _____ / / / / / / _/") - console.log(" / // / __/___/ _ / / _ \\ | |/|/ / -_) _ \\/ /_/ // / ") - console.log(" \\_, /\\__/ \\_,_/_/ .__/ |__,__/\\__/_.__/\\____/___/ ") - console.log(`/___/ /_/ \n${reset}`) - console.log(" yt-dlp-webUI - A web-ui for yt-dlp, simply enough") - console.log("---------------------------------------------------\n") -} \ No newline at end of file diff --git a/server-node/src/utils/params.ts b/server-node/src/utils/params.ts deleted file mode 100644 index 79bd8d1..0000000 --- a/server-node/src/utils/params.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const availableParams = [ - '--no-mtime', - '-x', -] \ No newline at end of file diff --git a/server-node/src/utils/procUtils.ts b/server-node/src/utils/procUtils.ts deleted file mode 100644 index 26043c1..0000000 --- a/server-node/src/utils/procUtils.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { exec, spawn } from 'child_process'; -import { statSync } from 'fs'; -import Logger from './BetterLogger'; - -const log = Logger.instance; - -/** - * Browse /proc in order to find the specific pid - * @param {number} pid - * @returns {*} process stats if any - */ -export function existsInProc(pid: number): any { - try { - return statSync(`/proc/${pid}`) - } catch (e) { - log.warn('proc', `pid ${pid} not found in procfs`) - } -} - -/** - * Kills a process with a sys-call - * @param {number} pid the killed process pid - */ -export async function killProcess(pid: number) { - const res = spawn('kill', [String(pid)]) - res.on('exit', () => { - log.info('proc', `Successfully killed yt-dlp process, pid: ${pid}`) - }) -} - -export function getFreeDiskSpace(socket: any, path: string) { - const message: string = 'free-space'; - exec(`df -h ${path} | tail -1 | awk '{print $4}'`, (_, stdout) => { - socket.emit(message, stdout) - }) -} \ No newline at end of file diff --git a/server-node/src/utils/updater.ts b/server-node/src/utils/updater.ts deleted file mode 100644 index f0a5de2..0000000 --- a/server-node/src/utils/updater.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { get } from 'https'; -import { rmSync, createWriteStream, chmod } from 'fs'; -import { join } from 'path'; - -// endpoint to github API -const options = { - hostname: 'api.github.com', - path: '/repos/yt-dlp/yt-dlp/releases/latest', - headers: { - 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0' - }, - method: 'GET', - port: 443, -} - -/** - * Build the binary url based on the release tag - * @param {string} release yt-dlp GitHub release tag - * @returns {*} the fetch options with the correct tag and headers - */ -function buildDonwloadOptions(release) { - return { - hostname: 'github.com', - path: `/yt-dlp/yt-dlp/releases/download/${release}/yt-dlp`, - headers: { - 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0' - }, - method: 'GET', - port: 443, - } -} - -/** - * gets the yt-dlp latest binary URL from GitHub API - */ -async function update() { - // ensure that the binary has been removed - try { - rmSync(join(__dirname, '..', 'core', 'yt-dlp')) - } - catch (e) { - console.log('file not found!') - } - // body buffer - let chunks = [] - get(options, res => { - // push the http packets chunks into the buffer - res.on('data', chunk => { - chunks.push(chunk) - }); - // the connection has ended so build the body from the buffer - // parse it as a JSON and get the tag_name - res.on('end', () => { - const buffer = Buffer.concat(chunks) - const release = JSON.parse(buffer.toString())['tag_name'] - console.log('The latest release is:', release) - // invoke the binary downloader - downloadBinary(buildDonwloadOptions(release)) - }) - }) -} -/** - * Utility that Pipes the latest binary to a file - * @param {string} url yt-dlp GitHub release url - */ -function downloadBinary(url) { - get(url, res => { - // if it is a redirect follow the url - if (res.statusCode === 301 || res.statusCode === 302) { - return downloadBinary(res.headers.location) - } - let bin = createWriteStream(join(__dirname, '..', 'core', 'yt-dlp')) - res.pipe(bin) - // once the connection has ended make the file executable - res.on('end', () => { - chmod(join(__dirname, '..', 'core', 'yt-dlp'), 0o775, err => { - err ? console.error('failed updating!') : console.log('done!') - }) - }) - }) -} -/** - * Invoke the yt-dlp update procedure - * @param {Socket} socket the current connection socket - */ -export function ytdlpUpdater(socket) { - update().then(() => { - socket.emit('updated') - }) -} \ No newline at end of file diff --git a/server/config/config_singleton.go b/server/config/config_singleton.go new file mode 100644 index 0000000..fae1c04 --- /dev/null +++ b/server/config/config_singleton.go @@ -0,0 +1,60 @@ +package config + +import ( + "os" + "sync" + + "gopkg.in/yaml.v3" +) + +var lock = &sync.Mutex{} + +type serverConfig struct { + Port int `yaml:"port"` + DownloadPath string `yaml:"downloadPath"` + DownloaderPath string `yaml:"downloaderPath"` +} + +type config struct { + cfg serverConfig +} + +func (c *config) LoadFromFile(filename string) (serverConfig, error) { + bytes, err := os.ReadFile(filename) + if err != nil { + return serverConfig{}, err + } + + yaml.Unmarshal(bytes, &c.cfg) + + return c.cfg, nil +} + +func (c *config) GetConfig() serverConfig { + return c.cfg +} + +func (c *config) SetPort(port int) { + c.cfg.Port = port +} + +func (c *config) DownloadPath(path string) { + c.cfg.DownloadPath = path +} + +func (c *config) DownloaderPath(path string) { + c.cfg.DownloaderPath = path +} + +var instance *config + +func Instance() *config { + if instance == nil { + lock.Lock() + defer lock.Unlock() + if instance == nil { + instance = &config{serverConfig{}} + } + } + return instance +} diff --git a/server/process.go b/server/process.go index c209a9d..2f73768 100644 --- a/server/process.go +++ b/server/process.go @@ -2,6 +2,7 @@ package server import ( "bufio" + "fmt" "regexp" "github.com/goccy/go-json" @@ -13,6 +14,7 @@ import ( "time" "github.com/marcopeocchi/fazzoletti/slices" + "github.com/marcopeocchi/yt-dlp-web-ui/server/config" "github.com/marcopeocchi/yt-dlp-web-ui/server/rx" ) @@ -23,7 +25,9 @@ const template = `download: "speed":%(progress.speed)s }` -const driver = "yt-dlp" +var ( + cfg = config.Instance() +) type ProgressTemplate struct { Percentage string `json:"percentage"` @@ -43,12 +47,17 @@ type Process struct { proc *os.Process } +type downloadOutput struct { + path string + filaneme string +} + // 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() { +func (p *Process) Start(path, filename string) { // escape bash variable escaping and command piping, you'll never know // what they might come with... p.params = slices.Filter(p.params, func(e string) bool { @@ -56,6 +65,18 @@ func (p *Process) Start() { return !match }) + out := downloadOutput{ + path: cfg.GetConfig().DownloadPath, + filaneme: "%(title)s.%(ext)s", + } + + if path != "" { + out.path = path + } + if filename != "" { + out.filaneme = filename + ".%(ext)s" + } + params := append([]string{ strings.Split(p.url, "?list")[0], //no playlist "--newline", @@ -63,11 +84,11 @@ func (p *Process) Start() { "--no-playlist", "--progress-template", strings.ReplaceAll(template, "\n", ""), "-o", - "./downloads/%(title)s.%(ext)s", + fmt.Sprintf("%s/%s", out.path, out.filaneme), }, p.params...) // ----------------- main block ----------------- // - cmd := exec.Command(driver, params...) + cmd := exec.Command(cfg.GetConfig().DownloaderPath, params...) r, err := cmd.StdoutPipe() if err != nil { log.Panicln(err) @@ -85,7 +106,7 @@ func (p *Process) Start() { // ----------------- info block ----------------- // // spawn a goroutine that retrieves the info for the download go func() { - cmd := exec.Command(driver, p.url, "-J") + cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.url, "-J") stdout, err := cmd.Output() if err != nil { log.Println("Cannot retrieve info for", p.url) @@ -100,18 +121,19 @@ func (p *Process) Start() { 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) + // filling the channel with as many stdout line as yt-dlp produces (producer) go func() { - defer cmd.Wait() defer r.Close() defer p.Complete() + defer close(eventChan) for scan.Scan() { eventChan <- scan.Text() } + cmd.Wait() }() // do the unmarshal operation every 500ms (consumer) - go rx.Sample(time.Millisecond*500, eventChan, func(text string) { + go rx.Debounce(time.Millisecond*500, eventChan, func(text string) { stdout := ProgressTemplate{} err := json.Unmarshal([]byte(text), &stdout) if err == nil { @@ -147,7 +169,7 @@ func (p *Process) Kill() error { } func (p *Process) GetFormatsSync() (DownloadFormats, error) { - cmd := exec.Command(driver, p.url, "-J") + cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.url, "-J") stdout, err := cmd.Output() if err != nil { diff --git a/server/server.go b/server/server.go index ac275f8..ba0bb4c 100644 --- a/server/server.go +++ b/server/server.go @@ -23,7 +23,7 @@ func init() { func RunBlocking(ctx context.Context) { fe := ctx.Value("frontend").(fs.SubFS) - port := ctx.Value("port") + port := ctx.Value("port").(int) service := new(Service) rpc.Register(service) @@ -62,5 +62,5 @@ func RunBlocking(ctx context.Context) { app.Server().StreamRequestBody = true - log.Fatal(app.Listen(fmt.Sprintf(":%s", port))) + log.Fatal(app.Listen(fmt.Sprintf(":%d", port))) } diff --git a/server/service.go b/server/service.go index 2803ae9..bdf87ae 100644 --- a/server/service.go +++ b/server/service.go @@ -4,6 +4,7 @@ import ( "log" "github.com/marcopeocchi/yt-dlp-web-ui/server/sys" + "github.com/marcopeocchi/yt-dlp-web-ui/server/updater" ) type Service int @@ -12,18 +13,27 @@ type Running []ProcessResponse type Pending []string type NoArgs struct{} + type Args struct { Id string URL string Params []string } +type DownloadSpecificArgs struct { + Id string + URL string + Path string + Rename 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 { +func (t *Service) Exec(args DownloadSpecificArgs, 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() + p.Start(args.Path, args.Rename) *result = p.id return nil } @@ -86,7 +96,17 @@ func (t *Service) FreeSpace(args NoArgs, free *uint64) error { } func (t *Service) DirectoryTree(args NoArgs, tree *[]string) error { - dfsTree, err := sys.DirectoryTree("downloads") + dfsTree, err := sys.DirectoryTree() *tree = *dfsTree return err } + +func (t *Service) UpdateExecutable(args NoArgs, updated *bool) error { + err := updater.UpdateExecutable() + if err != nil { + *updated = true + return err + } + *updated = false + return err +} diff --git a/server/sys/fs.go b/server/sys/fs.go index c086316..2473ab3 100644 --- a/server/sys/fs.go +++ b/server/sys/fs.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" + "github.com/marcopeocchi/yt-dlp-web-ui/server/config" "github.com/marcopeocchi/yt-dlp-web-ui/server/internal" "golang.org/x/sys/unix" ) @@ -13,20 +14,20 @@ import ( // 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) + unix.Statfs(config.Instance().GetConfig().DownloadPath, &stat) return (stat.Bavail * uint64(stat.Bsize)), nil } -func DirectoryTree(rootPath string) (*[]string, error) { +// Build a directory tree started from the specified path using DFS. +// Then return the flattened tree represented as a list. +func DirectoryTree() (*[]string, error) { type Node struct { path string children []Node } + rootPath := config.Instance().GetConfig().DownloadPath + stack := internal.Stack[Node]{ Nodes: make([]*internal.Node[Node], 5), } diff --git a/server/types.go b/server/types.go index 26fcede..54e4964 100644 --- a/server/types.go +++ b/server/types.go @@ -55,6 +55,7 @@ type AbortRequest struct { // struct representing the intent to start a download type DownloadRequest struct { - Url string `json:"url"` - Params []string `json:"params"` + Url string `json:"url"` + Params []string `json:"params"` + RenameTo string `json:"renameTo"` } diff --git a/server/updater/forced_update.go b/server/updater/forced_update.go new file mode 100644 index 0000000..1dbb909 --- /dev/null +++ b/server/updater/forced_update.go @@ -0,0 +1,45 @@ +package updater + +import ( + "io" + "log" + "net/http" + + "github.com/goccy/go-json" +) + +const ( + gitHubAPILatest = "https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest" + gitHubAPIDownload = "https://api.github.com/repos/yt-dlp/yt-dlp/releases/download" +) + +var ( + client = &http.Client{ + CheckRedirect: http.DefaultClient.CheckRedirect, + } +) + +func getLatestReleaseTag() (string, error) { + res, err := client.Get(gitHubAPILatest) + if err != nil { + log.Println("Cannot get release tag from GitHub API") + return "", err + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + + if err != nil { + log.Println("Cannot parse response from GitHub API") + return "", err + } + + tag := ReleaseLatestResponse{} + json.Unmarshal(body, &tag) + + return tag.TagName, nil +} + +func ForceUpdate() { + getLatestReleaseTag() +} diff --git a/server/updater/types.go b/server/updater/types.go new file mode 100644 index 0000000..d967425 --- /dev/null +++ b/server/updater/types.go @@ -0,0 +1,6 @@ +package updater + +type ReleaseLatestResponse struct { + Name string `json:"name"` + TagName string `json:"tag_name"` +} diff --git a/server/updater/update.go b/server/updater/update.go new file mode 100644 index 0000000..8bf8b37 --- /dev/null +++ b/server/updater/update.go @@ -0,0 +1,17 @@ +package updater + +import ( + "os/exec" + + "github.com/marcopeocchi/yt-dlp-web-ui/server/config" +) + +var path = config.Instance().GetConfig().DownloaderPath + +func UpdateExecutable() error { + cmd := exec.Command(path, "-U") + cmd.Start() + + err := cmd.Wait() + return err +} diff --git a/settings.json b/settings.json deleted file mode 100644 index 667e474..0000000 --- a/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "port": 0, - "download_path": "", - "cliArgs": [] -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index f27b390..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "outDir": "./dist", - "target": "ES2020", - "strict": false, - "noEmit": false, - "moduleResolution": "node", - "module": "commonjs", - "skipLibCheck": true, - }, - "include": [ - "server-node/src/**/*" - ] -} \ No newline at end of file From d93fc379432207a935c0e3645d294987f025dc6b Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Thu, 12 Jan 2023 19:00:53 +0100 Subject: [PATCH 07/11] It just works --- frontend/src/App.tsx | 2 +- frontend/src/Settings.tsx | 6 ++++-- frontend/src/features/core/rpcClient.ts | 7 +++++++ server/process.go | 15 ++++++++++++--- server/service.go | 5 ++++- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c218829..709a816 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -163,7 +163,7 @@ function AppContent() { } /> - } /> + } /> diff --git a/frontend/src/Settings.tsx b/frontend/src/Settings.tsx index 6379329..bac849a 100644 --- a/frontend/src/Settings.tsx +++ b/frontend/src/Settings.tsx @@ -22,6 +22,7 @@ import { useDispatch, useSelector } from "react-redux"; import { debounceTime, distinctUntilChanged, map, of, takeWhile } from "rxjs"; import { CliArguments } from "./features/core/argsParser"; import I18nBuilder from "./features/core/intl"; +import { RPCClient } from "./features/core/rpcClient"; import { LanguageUnion, setCliArgs, @@ -38,7 +39,7 @@ import { updated } from "./features/status/statusSlice"; import { RootState } from "./stores/store"; import { validateDomain, validateIP } from "./utils"; -export default function Settings() { +export default function Settings({ socket }: { socket: WebSocket }) { const settings = useSelector((state: RootState) => state.settings) const status = useSelector((state: RootState) => state.status) const dispatch = useDispatch() @@ -46,6 +47,7 @@ export default function Settings() { const [invalidIP, setInvalidIP] = useState(false); const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language]) + const client = useMemo(() => new RPCClient(socket), [settings.serverAddr, settings.serverPort]) const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs]) /** * Update the server ip address state and localstorage whenever the input value changes. @@ -107,7 +109,7 @@ export default function Settings() { * Send via WebSocket a message in order to update the yt-dlp binary from server */ const updateBinary = () => { - + client.updateExecutable().then(() => dispatch(updated())) } return ( diff --git a/frontend/src/features/core/rpcClient.ts b/frontend/src/features/core/rpcClient.ts index b693ac7..3ca0363 100644 --- a/frontend/src/features/core/rpcClient.ts +++ b/frontend/src/features/core/rpcClient.ts @@ -92,6 +92,13 @@ export class RPCClient { }) } + public updateExecutable() { + return this.sendHTTP({ + method: 'Service.UpdateExecutable', + params: [] + }) + } + public decode(data: any): RPCResponse { return JSON.parse(data) } diff --git a/server/process.go b/server/process.go index 2f73768..f7cff0b 100644 --- a/server/process.go +++ b/server/process.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "regexp" + "syscall" "github.com/goccy/go-json" @@ -89,6 +90,8 @@ func (p *Process) Start(path, filename string) { // ----------------- main block ----------------- // cmd := exec.Command(cfg.GetConfig().DownloaderPath, params...) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + r, err := cmd.StdoutPipe() if err != nil { log.Panicln(err) @@ -107,6 +110,8 @@ func (p *Process) Start(path, filename string) { // spawn a goroutine that retrieves the info for the download go func() { cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.url, "-J") + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + stdout, err := cmd.Output() if err != nil { log.Println("Cannot retrieve info for", p.url) @@ -132,8 +137,8 @@ func (p *Process) Start(path, filename string) { cmd.Wait() }() - // do the unmarshal operation every 500ms (consumer) - go rx.Debounce(time.Millisecond*500, eventChan, func(text string) { + // do the unmarshal operation every 250ms (consumer) + go rx.Debounce(time.Millisecond*250, eventChan, func(text string) { stdout := ProgressTemplate{} err := json.Unmarshal([]byte(text), &stdout) if err == nil { @@ -162,7 +167,11 @@ func (p *Process) Complete() { // Kill a process and remove it from the memory func (p *Process) Kill() error { - err := p.proc.Kill() + pgid, err := syscall.Getpgid(p.proc.Pid) + if err != nil { + return err + } + err = syscall.Kill(-pgid, syscall.SIGTERM) p.mem.Delete(p.id) log.Printf("Killed process %s\n", p.id) return err diff --git a/server/service.go b/server/service.go index bdf87ae..62bb73c 100644 --- a/server/service.go +++ b/server/service.go @@ -31,7 +31,7 @@ type DownloadSpecificArgs struct { // Exec spawns a Process. // The result of the execution is the newly spawned process Id. func (t *Service) Exec(args DownloadSpecificArgs, result *string) error { - log.Printf("Spawning new process for %s\n", args.URL) + log.Println("Spawning new process for", args.URL) p := Process{mem: &db, url: args.URL, params: args.Params} p.Start(args.Path, args.Rename) *result = p.id @@ -66,6 +66,7 @@ func (t *Service) Running(args NoArgs, running *Running) error { // Kill kills a process given its id and remove it from the memoryDB func (t *Service) Kill(args string, killed *string) error { + log.Println("Trying killing process with id", args) proc := db.Get(args) var err error if proc != nil { @@ -77,6 +78,7 @@ func (t *Service) Kill(args string, killed *string) error { // KillAll kills all process unconditionally and removes them from // the memory db func (t *Service) KillAll(args NoArgs, killed *string) error { + log.Println("Killing all spawned processes", args) keys := db.Keys() var err error for _, key := range keys { @@ -102,6 +104,7 @@ func (t *Service) DirectoryTree(args NoArgs, tree *[]string) error { } func (t *Service) UpdateExecutable(args NoArgs, updated *bool) error { + log.Println("Updating yt-dlp executable to the latest release") err := updater.UpdateExecutable() if err != nil { *updated = true From 658cb64a0280c5cba17a5800ba67227326a06b8b Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Thu, 12 Jan 2023 21:10:23 +0100 Subject: [PATCH 08/11] Refactoring --- frontend/src/App.tsx | 17 ++++++----------- frontend/src/features/status/statusSlice.ts | 10 +++++++++- server/process.go | 5 +++++ server/server.go | 5 +++-- server/service.go | 2 ++ server/types.go | 4 ++++ server/updater/update.go | 1 + 7 files changed, 30 insertions(+), 14 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 709a816..8bb10e8 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,7 +31,7 @@ import { RootState, store } from './stores/store'; import { formatGiB, getWebSocketEndpoint } from "./utils"; function AppContent() { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false) const settings = useSelector((state: RootState) => state.settings) const status = useSelector((state: RootState) => state.status) @@ -39,7 +39,6 @@ function AppContent() { const socket = useMemo(() => new WebSocket(getWebSocketEndpoint()), []) const mode = settings.theme - const theme = useMemo(() => createTheme({ palette: { @@ -49,11 +48,11 @@ function AppContent() { }, }, }), [settings.theme] - ); + ) const toggleDrawer = () => { - setOpen(!open); - }; + setOpen(!open) + } return ( @@ -61,11 +60,7 @@ function AppContent() { - + - + ); } \ No newline at end of file diff --git a/frontend/src/features/status/statusSlice.ts b/frontend/src/features/status/statusSlice.ts index a0d55af..209bc53 100644 --- a/frontend/src/features/status/statusSlice.ts +++ b/frontend/src/features/status/statusSlice.ts @@ -42,6 +42,14 @@ export const statusSlice = createSlice({ } }) -export const { connected, disconnected, updated, alreadyUpdated, downloading, finished, setFreeSpace } = statusSlice.actions +export const { + connected, + disconnected, + updated, + alreadyUpdated, + downloading, + finished, + setFreeSpace +} = statusSlice.actions export default statusSlice.reducer \ No newline at end of file diff --git a/server/process.go b/server/process.go index f7cff0b..4ce0ac3 100644 --- a/server/process.go +++ b/server/process.go @@ -167,6 +167,10 @@ func (p *Process) Complete() { // Kill a process and remove it from the memory func (p *Process) Kill() error { + // yt-dlp uses multiple child process the parent process + // has been spawned with setPgid = true. To properly kill + // all subprocesses a SIGTERM need to be sent to the correct + // process group pgid, err := syscall.Getpgid(p.proc.Pid) if err != nil { return err @@ -177,6 +181,7 @@ func (p *Process) Kill() error { return err } +// Returns the available format for this URL func (p *Process) GetFormatsSync() (DownloadFormats, error) { cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.url, "-J") stdout, err := cmd.Output() diff --git a/server/server.go b/server/server.go index ba0bb4c..7a9f548 100644 --- a/server/server.go +++ b/server/server.go @@ -31,11 +31,12 @@ func RunBlocking(ctx context.Context) { app := fiber.New() app.Use(cors.New()) - app.Use("/", filesystem.New(filesystem.Config{ Root: http.FS(fe), })) + // RPC handlers + // websocket app.Get("/ws-rpc", websocket.New(func(c *websocket.Conn) { for { mtype, reader, err := c.NextReader() @@ -51,7 +52,7 @@ func RunBlocking(ctx context.Context) { io.Copy(writer, res) } })) - + // http-post app.Post("/http-rpc", func(c *fiber.Ctx) error { reader := c.Context().RequestBodyStream() writer := c.Response().BodyWriter() diff --git a/server/service.go b/server/service.go index 62bb73c..4a3cdb8 100644 --- a/server/service.go +++ b/server/service.go @@ -97,12 +97,14 @@ func (t *Service) FreeSpace(args NoArgs, free *uint64) error { return err } +// Return a flattned tree of the download directory func (t *Service) DirectoryTree(args NoArgs, tree *[]string) error { dfsTree, err := sys.DirectoryTree() *tree = *dfsTree return err } +// Updates the yt-dlp binary using its builtin function func (t *Service) UpdateExecutable(args NoArgs, updated *bool) error { log.Println("Updating yt-dlp executable to the latest release") err := updater.UpdateExecutable() diff --git a/server/types.go b/server/types.go index 54e4964..8257aa1 100644 --- a/server/types.go +++ b/server/types.go @@ -1,11 +1,13 @@ package server +// Progress for the Running call type DownloadProgress struct { Percentage string `json:"percentage"` Speed float32 `json:"speed"` ETA int `json:"eta"` } +// Used to deser the yt-dlp -J output type DownloadInfo struct { URL string `json:"url"` Title string `json:"title"` @@ -17,6 +19,7 @@ type DownloadInfo struct { Extension string `json:"ext"` } +// Used to deser the formats in the -J output type DownloadFormats struct { Formats []Format `json:"formats"` Best Format `json:"best"` @@ -25,6 +28,7 @@ type DownloadFormats struct { URL string `json:"url"` } +// A skimmed yt-dlp format node type Format struct { Format_id string `json:"format_id"` Format_note string `json:"format_note"` diff --git a/server/updater/update.go b/server/updater/update.go index 8bf8b37..994e2ac 100644 --- a/server/updater/update.go +++ b/server/updater/update.go @@ -8,6 +8,7 @@ import ( var path = config.Instance().GetConfig().DownloaderPath +// Update using the builtin function of yt-dlp func UpdateExecutable() error { cmd := exec.Command(path, "-U") cmd.Start() From f8091b6d14ff200e38a555a2e11e806d1941e775 Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Thu, 12 Jan 2023 21:37:51 +0100 Subject: [PATCH 09/11] enabled custom args --- frontend/src/Home.tsx | 37 +++++++++++++++++-- frontend/src/Settings.tsx | 12 ++++++ frontend/src/assets/i18n.yaml | 16 +++++++- .../src/features/settings/settingsSlice.ts | 25 ++++++++----- 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/frontend/src/Home.tsx b/frontend/src/Home.tsx index c2311ff..4b0eb4b 100644 --- a/frontend/src/Home.tsx +++ b/frontend/src/Home.tsx @@ -47,6 +47,7 @@ export default function Home({ socket }: Props) { const [pickedAudioFormat, setPickedAudioFormat] = useState(''); const [pickedBestFormat, setPickedBestFormat] = useState(''); + const [customArgs, setCustomArgs] = useState(''); const [downloadPath, setDownloadPath] = useState(0); const [availableDownloadPaths, setAvailableDownloadPaths] = useState([]); @@ -68,6 +69,7 @@ export default function Home({ socket }: Props) { useEffect(() => { socket.onopen = () => { dispatch(connected()) + setCustomArgs(localStorage.getItem('last-input-args') ?? '') } }, []) @@ -84,9 +86,6 @@ export default function Home({ socket }: Props) { useEffect(() => { socket.onmessage = (event) => { const res = client.decode(event.data) - - setShowBackdrop(false) - switch (typeof res.result) { case 'object': setActiveDownloads( @@ -101,6 +100,12 @@ export default function Home({ socket }: Props) { } }, []) + useEffect(() => { + if (activeDownloads.length > 0 && showBackdrop) { + setShowBackdrop(false) + } + }, [activeDownloads, showBackdrop]) + useEffect(() => { client.directoryTree() .then(data => { @@ -121,7 +126,7 @@ export default function Home({ socket }: Props) { client.download( immediate || url || workingUrl, - cliArgs.toString() + toFormatArgs(codes), + `${cliArgs.toString()} ${toFormatArgs(codes)} ${customArgs}`, availableDownloadPaths[downloadPath] ?? '', fileNameOverride ) @@ -173,6 +178,15 @@ export default function Home({ socket }: Props) { setFilenameOverride(e.target.value) } + /** + * Update the custom args state whenever the input value changes + * @param e Input change event + */ + const handleCustomArgsChange = (e: React.ChangeEvent) => { + setCustomArgs(e.target.value) + localStorage.setItem("last-input-args", e.target.value) + } + /** * Abort a specific download if id's provided, other wise abort all running ones. * @param id The download id / pid @@ -257,6 +271,21 @@ export default function Home({ socket }: Props) { /> + { + settings.enableCustomArgs ? + + + : + null + } { settings.fileRenaming ? diff --git a/frontend/src/Settings.tsx b/frontend/src/Settings.tsx index bac849a..5e2faad 100644 --- a/frontend/src/Settings.tsx +++ b/frontend/src/Settings.tsx @@ -26,6 +26,7 @@ import { RPCClient } from "./features/core/rpcClient"; import { LanguageUnion, setCliArgs, + setEnableCustomArgs, setFileRenaming, setFormatSelection, setLanguage, @@ -255,6 +256,17 @@ export default function Settings({ socket }: { socket: WebSocket }) { } label={i18n.t('filenameOverrideOption')} /> + { + dispatch(setEnableCustomArgs(!settings.enableCustomArgs)) + }} + /> + } + label={i18n.t('customArgs')} + /> diff --git a/frontend/src/assets/i18n.yaml b/frontend/src/assets/i18n.yaml index 96fadcb..7bdc5af 100644 --- a/frontend/src/assets/i18n.yaml +++ b/frontend/src/assets/i18n.yaml @@ -25,6 +25,8 @@ languages: filenameOverrideOption: Enable output file name overriding customFilename: Custom filemame (leave blank to use default) customPath: Custom path + customArgs: Enable custom yt-dlp args (great power = great responsabilities) + customArgsInput: Custom yt-dlp arguments italian: urlInput: URL di YouTube o di qualsiasi altro servizio supportato statusTitle: Stato @@ -50,6 +52,8 @@ languages: filenameOverrideOption: Abilita sovrascrittura del nome del file di output customFilename: Custom filemame (leave blank to use default) customPath: Custom path + customArgs: Enable custom yt-dlp args (great power = great responsabilities) + customArgsInput: Custom yt-dlp arguments chinese: urlInput: YouTube 或其他受支持服务的视频网址 statusTitle: 状态 @@ -75,6 +79,8 @@ languages: filenameOverrideOption: Enable output file name overriding customFilename: Custom filemame (leave blank to use default) customPath: Custom path + customArgs: Enable custom yt-dlp args (great power = great responsabilities) + customArgsInput: Custom yt-dlp arguments spanish: urlInput: YouTube or other supported service video url statusTitle: Status @@ -100,6 +106,8 @@ languages: filenameOverrideOption: Enable output file name overriding customFilename: Custom filemame (leave blank to use default) customPath: Custom path + customArgs: Enable custom yt-dlp args (great power = great responsabilities) + customArgsInput: Custom yt-dlp arguments russian: urlInput: YouTube or other supported service video url statusTitle: Status @@ -125,6 +133,8 @@ languages: filenameOverrideOption: Enable output file name overriding customFilename: Custom filemame (leave blank to use default) customPath: Custom path + customArgs: Enable custom yt-dlp args (great power = great responsabilities) + customArgsInput: Custom yt-dlp arguments korean: urlInput: YouTube나 다른 지원되는 사이트의 URL statusTitle: 상태 @@ -150,6 +160,8 @@ languages: filenameOverrideOption: Enable output file name overriding customFilename: Custom filemame (leave blank to use default) customPath: Custom path + customArgs: Enable custom yt-dlp args (great power = great responsabilities) + customArgsInput: Custom yt-dlp arguments japanese: urlInput: YouTubeまたはサポート済み動画のURL statusTitle: 状態 @@ -174,4 +186,6 @@ languages: pathOverrideOption: Enable output path overriding filenameOverrideOption: Enable output file name overriding customFilename: Custom filemame (leave blank to use default) - customPath: Custom path \ No newline at end of file + customPath: Custom path + customArgs: Enable custom yt-dlp args (great power = great responsabilities) + customArgsInput: Custom yt-dlp arguments \ No newline at end of file diff --git a/frontend/src/features/settings/settingsSlice.ts b/frontend/src/features/settings/settingsSlice.ts index e0a7cf8..2dcfdd0 100644 --- a/frontend/src/features/settings/settingsSlice.ts +++ b/frontend/src/features/settings/settingsSlice.ts @@ -4,15 +4,16 @@ export type LanguageUnion = "english" | "chinese" | "russian" | "italian" | "spa export type ThemeUnion = "light" | "dark" export interface SettingsState { - serverAddr: string, - serverPort: string, - language: LanguageUnion, - theme: ThemeUnion, - cliArgs: string, - formatSelection: boolean, - ratelimit: string, - fileRenaming: boolean, - pathOverriding: boolean, + serverAddr: string + serverPort: string + language: LanguageUnion + theme: ThemeUnion + cliArgs: string + formatSelection: boolean + ratelimit: string + fileRenaming: boolean + pathOverriding: boolean + enableCustomArgs: boolean } const initialState: SettingsState = { @@ -25,6 +26,7 @@ const initialState: SettingsState = { ratelimit: localStorage.getItem("rate-limit") ?? "", fileRenaming: localStorage.getItem("file-renaming") === "true", pathOverriding: localStorage.getItem("path-overriding") === "true", + enableCustomArgs: localStorage.getItem("enable-custom-args") === "true", } export const settingsSlice = createSlice({ @@ -67,6 +69,10 @@ export const settingsSlice = createSlice({ state.fileRenaming = action.payload localStorage.setItem("file-renaming", action.payload.toString()) }, + setEnableCustomArgs: (state, action: PayloadAction) => { + state.enableCustomArgs = action.payload + localStorage.setItem("enable-custom-args", action.payload.toString()) + }, } }) @@ -80,6 +86,7 @@ export const { setRateLimit, setFileRenaming, setPathOverriding, + setEnableCustomArgs, } = settingsSlice.actions export default settingsSlice.reducer \ No newline at end of file From 5e3a40baf4f5b60f8e2445b62f0557b285218ff7 Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Thu, 12 Jan 2023 23:18:50 +0100 Subject: [PATCH 10/11] it just works --- frontend/src/Home.tsx | 11 +++++++---- frontend/src/types.d.ts | 1 + server/process.go | 5 +++-- server/service.go | 7 +++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/frontend/src/Home.tsx b/frontend/src/Home.tsx index 4b0eb4b..f3cbf4d 100644 --- a/frontend/src/Home.tsx +++ b/frontend/src/Home.tsx @@ -74,9 +74,12 @@ export default function Home({ socket }: Props) { }, []) useEffect(() => { - const interval = setInterval(() => client.running(), 1000) - return () => clearInterval(interval) - }, []) + if (status.connected) { + client.running() + const interval = setInterval(() => client.running(), 1000) + return () => clearInterval(interval) + } + }, [status.connected]) useEffect(() => { client.freeSpace() @@ -89,7 +92,7 @@ export default function Home({ socket }: Props) { switch (typeof res.result) { case 'object': setActiveDownloads( - res.result + (res.result ?? []) .filter((r: RPCResult) => !!r.info.url) .sort((a: RPCResult, b: RPCResult) => a.info.title.localeCompare(b.info.title)) ) diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index ccb8726..71ed726 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -1,6 +1,7 @@ export type RPCMethods = | "Service.Exec" | "Service.Kill" + | "Service.Clear" | "Service.Running" | "Service.KillAll" | "Service.FreeSpace" diff --git a/server/process.go b/server/process.go index 4ce0ac3..6c152df 100644 --- a/server/process.go +++ b/server/process.go @@ -167,6 +167,7 @@ func (p *Process) Complete() { // Kill a process and remove it from the memory func (p *Process) Kill() error { + p.mem.Delete(p.id) // yt-dlp uses multiple child process the parent process // has been spawned with setPgid = true. To properly kill // all subprocesses a SIGTERM need to be sent to the correct @@ -176,8 +177,8 @@ func (p *Process) Kill() error { return err } err = syscall.Kill(-pgid, syscall.SIGTERM) - p.mem.Delete(p.id) - log.Printf("Killed process %s\n", p.id) + + log.Println("Killed process", p.id) return err } diff --git a/server/service.go b/server/service.go index 4a3cdb8..fcc0809 100644 --- a/server/service.go +++ b/server/service.go @@ -90,6 +90,13 @@ func (t *Service) KillAll(args NoArgs, killed *string) error { return err } +// Remove a process from the db rendering it unusable if active +func (t *Service) Clear(args string, killed *string) error { + log.Println("Clearing process with id", args) + db.Delete(args) + return nil +} + // FreeSpace gets the available from package sys util func (t *Service) FreeSpace(args NoArgs, free *uint64) error { freeSpace, err := sys.FreeSpace() From 6aea4c60607e065984192f7440c63ab5939cbbd7 Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Thu, 12 Jan 2023 23:29:00 +0100 Subject: [PATCH 11/11] updated dockerfile --- Dockerfile | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5ce79e7..b987779 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,20 @@ -FROM node:18-alpine -RUN mkdir -p /usr/src/yt-dlp-webui/download +FROM alpine:3.17 +# folder structure +WORKDIR /usr/src/yt-dlp-webui/downloads VOLUME /usr/src/yt-dlp-webui/downloads WORKDIR /usr/src/yt-dlp-webui -COPY package*.json ./ # install core dependencies RUN apk update -RUN apk add curl wget psmisc python3 ffmpeg +RUN apk add curl wget psmisc python3 ffmpeg nodejs go yt-dlp +# copy srcs COPY . . -RUN chmod +x ./fetch-yt-dlp.sh # install node dependencies +WORKDIR /usr/src/yt-dlp-webui/frontend RUN npm i RUN npm run build -RUN npm run build-server -RUN npm run fetch +# install go dependencies +WORKDIR /usr/src/yt-dlp-webui +RUN npm go build -o yt-dlp-webui # expose and run -EXPOSE 3022 -CMD [ "node" , "./dist/main.js" ] +EXPOSE 3033 +CMD [ "yt-dlp-webui" , "--out", "./downloads" ]