Compare commits

..

8 Commits

Author SHA1 Message Date
d4305bb2f8 re-enabled armv6 builds
code refactoring
2024-03-18 10:27:40 +01:00
3f836d0fa6 added some comments on the server side 2024-03-18 10:19:39 +01:00
b45107c94b Fixed observable logger, added build stage for frontend
dependencies update

closes #131
2024-03-14 11:59:33 +01:00
0x6d61726b
9cf1a3bc7e removed duplicate/unused file (used in 'frontend/src/assets/', relates to #139) (#140) 2024-03-04 14:40:06 +01:00
df3522fcb3 fixed favicon not showing 2024-03-03 22:32:44 +01:00
e2c27c3857 Added favicon
Closes #139
2024-03-03 20:33:35 +01:00
0x6d61726b
51bcd82ea7 Fixed human-readable file size representation (#137)
(as it follows units of IEC 60027-2 A.2 )
2024-03-03 15:48:56 +01:00
0x6d61726b
f763b9657f Extended config.yml example (#136) 2024-03-03 15:47:52 +01:00
71 changed files with 243 additions and 6746 deletions

2
.gitignore vendored
View File

@@ -14,4 +14,4 @@ session.dat
config.yml
cookies.txt
__debug*
app/
ui/

View File

@@ -1,18 +1,30 @@
FROM golang:alpine AS build
RUN apk update && \
apk add nodejs npm
# Node (pnpm) ------------------------------------------------------------------
FROM node:20-slim AS ui
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . /usr/src/yt-dlp-webui
WORKDIR /usr/src/yt-dlp-webui/frontend
RUN npm install
RUN npm run build
RUN rm -rf node_modules
RUN pnpm install
RUN pnpm run build
# -----------------------------------------------------------------------------
# Go --------------------------------------------------------------------------
FROM golang AS build
WORKDIR /usr/src/yt-dlp-webui
RUN CGO_ENABLED=0 GOOS=linux go build -o yt-dlp-webui
COPY . .
COPY --from=ui /usr/src/yt-dlp-webui/frontend /usr/src/yt-dlp-webui/frontend
RUN CGO_ENABLED=0 GOOS=linux go build -o yt-dlp-webui
# -----------------------------------------------------------------------------
# dependencies ----------------------------------------------------------------
FROM alpine:edge
VOLUME /downloads /config

View File

@@ -7,9 +7,10 @@ all:
multiarch:
mkdir -p build
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o build/yt-dlp-webui_linux-arm main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o build/yt-dlp-webui_linux-arm64 main.go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/yt-dlp-webui_linux-amd64 main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o build/yt-dlp-webui_linux-arm64 main.go
CGO_ENABLED=0 GOOS=linux GOARM=6 GOARCH=arm go build -o build/yt-dlp-webui_linux-armv7 main.go
CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -o build/yt-dlp-webui_linux-armv6 main.go
clean:
rm -rf build

View File

@@ -1,3 +1,8 @@
> [!IMPORTANT]
> I'm looking for a co-mantainer.
> Lately I'm not feeling well both physically and mentally.
---
> [!IMPORTANT]
> Major frontend refactoring in progress.
> I won't add features or fix minor issues until completition.
@@ -180,16 +185,31 @@ The config file **will overwrite what have been passed as cli argument**.
# Simple configuration file for yt-dlp webui
---
port: 8989
downloadPath: /home/ren/archive
downloaderPath: /usr/local/bin/yt-dlp
# Host where server will listen at (default: "0.0.0.0")
#host: 0.0.0.0
# Optional settings
# Port where server will listen at (default: 3033)
port: 8989
# Directory where downloaded files will be stored (default: ".")
downloadPath: /home/ren/archive
# [optional] Enable RPC authentication (requires username and password)
require_auth: true
username: my_username
password: my_random_secret
# [optional] The download queue size (default: 8)
queue_size: 4
# [optional] Full path to the yt-dlp (default: "yt-dlp")
downloaderPath: /usr/local/bin/yt-dlp
# [optional] Directory where the log file will be stored (default: ".")
#log_path: .
# [optional] Directory where the session database file will be stored (default: ".")
#session_file_path: .
```
### Systemd integration

View File

@@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/x-icon" href="./src/assets/favicon.ico">
<title>yt-dlp Web UI</title>
</head>

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,6 @@
"@mui/icons-material": "^5.15.4",
"@mui/material": "^5.15.4",
"fp-ts": "^2.16.2",
"million": "^2.6.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.2",
@@ -25,7 +24,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@modyfi/vite-plugin-yaml": "^1.1.0",
"@types/node": "^20.11.4",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
@@ -33,6 +32,7 @@
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.5.0",
"typescript": "^5.3.3",
"vite": "^5.0.11"
"vite": "^5.1.6",
"million": "^3.0.6"
}
}

248
frontend/pnpm-lock.yaml generated
View File

@@ -26,9 +26,6 @@ dependencies:
fp-ts:
specifier: ^2.16.2
version: 2.16.2
million:
specifier: ^2.6.4
version: 2.6.4
react:
specifier: ^18.2.0
version: 18.2.0
@@ -47,8 +44,8 @@ dependencies:
devDependencies:
'@modyfi/vite-plugin-yaml':
specifier: ^1.0.4
version: 1.0.4(vite@5.0.11)
specifier: ^1.1.0
version: 1.1.0(vite@5.1.6)
'@types/node':
specifier: ^20.11.4
version: 20.11.4
@@ -66,13 +63,16 @@ devDependencies:
version: 5.3.3
'@vitejs/plugin-react-swc':
specifier: ^3.5.0
version: 3.5.0(vite@5.0.11)
version: 3.5.0(vite@5.1.6)
million:
specifier: ^3.0.6
version: 3.0.6
typescript:
specifier: ^5.3.3
version: 5.3.3
vite:
specifier: ^5.0.11
version: 5.0.11(@types/node@20.11.4)
specifier: ^5.1.6
version: 5.1.6(@types/node@20.11.4)
packages:
@@ -82,7 +82,7 @@ packages:
dependencies:
'@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.21
dev: false
dev: true
/@babel/code-frame@7.21.4:
resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==}
@@ -97,12 +97,12 @@ packages:
dependencies:
'@babel/highlight': 7.23.4
chalk: 2.4.2
dev: false
dev: true
/@babel/compat-data@7.23.5:
resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==}
engines: {node: '>=6.9.0'}
dev: false
dev: true
/@babel/core@7.23.7:
resolution: {integrity: sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==}
@@ -125,7 +125,7 @@ packages:
semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: false
dev: true
/@babel/generator@7.23.6:
resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==}
@@ -135,7 +135,7 @@ packages:
'@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.21
jsesc: 2.5.2
dev: false
dev: true
/@babel/helper-compilation-targets@7.23.6:
resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==}
@@ -146,12 +146,12 @@ packages:
browserslist: 4.22.2
lru-cache: 5.1.1
semver: 6.3.1
dev: false
dev: true
/@babel/helper-environment-visitor@7.22.20:
resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
engines: {node: '>=6.9.0'}
dev: false
dev: true
/@babel/helper-function-name@7.23.0:
resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
@@ -159,14 +159,14 @@ packages:
dependencies:
'@babel/template': 7.22.15
'@babel/types': 7.23.6
dev: false
dev: true
/@babel/helper-hoist-variables@7.22.5:
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.6
dev: false
dev: true
/@babel/helper-module-imports@7.21.4:
resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==}
@@ -180,7 +180,7 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.6
dev: false
dev: true
/@babel/helper-module-transforms@7.23.3(@babel/core@7.23.7):
resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
@@ -194,26 +194,21 @@ packages:
'@babel/helper-simple-access': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
'@babel/helper-validator-identifier': 7.22.20
dev: false
/@babel/helper-plugin-utils@7.22.5:
resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==}
engines: {node: '>=6.9.0'}
dev: false
dev: true
/@babel/helper-simple-access@7.22.5:
resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.6
dev: false
dev: true
/@babel/helper-split-export-declaration@7.22.6:
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.6
dev: false
dev: true
/@babel/helper-string-parser@7.21.5:
resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==}
@@ -223,7 +218,7 @@ packages:
/@babel/helper-string-parser@7.23.4:
resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==}
engines: {node: '>=6.9.0'}
dev: false
dev: true
/@babel/helper-validator-identifier@7.19.1:
resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
@@ -233,12 +228,12 @@ packages:
/@babel/helper-validator-identifier@7.22.20:
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
engines: {node: '>=6.9.0'}
dev: false
dev: true
/@babel/helper-validator-option@7.23.5:
resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
engines: {node: '>=6.9.0'}
dev: false
dev: true
/@babel/helpers@7.23.8:
resolution: {integrity: sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==}
@@ -249,7 +244,7 @@ packages:
'@babel/types': 7.23.6
transitivePeerDependencies:
- supports-color
dev: false
dev: true
/@babel/highlight@7.18.6:
resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==}
@@ -267,7 +262,7 @@ packages:
'@babel/helper-validator-identifier': 7.22.20
chalk: 2.4.2
js-tokens: 4.0.0
dev: false
dev: true
/@babel/parser@7.23.6:
resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==}
@@ -275,27 +270,7 @@ packages:
hasBin: true
dependencies:
'@babel/types': 7.23.6
dev: false
/@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.7):
resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.23.7
'@babel/helper-plugin-utils': 7.22.5
dev: false
/@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.7):
resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.23.7
'@babel/helper-plugin-utils': 7.22.5
dev: false
dev: true
/@babel/runtime@7.21.5:
resolution: {integrity: sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==}
@@ -318,7 +293,7 @@ packages:
'@babel/code-frame': 7.23.5
'@babel/parser': 7.23.6
'@babel/types': 7.23.6
dev: false
dev: true
/@babel/traverse@7.23.7:
resolution: {integrity: sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==}
@@ -336,7 +311,7 @@ packages:
globals: 11.12.0
transitivePeerDependencies:
- supports-color
dev: false
dev: true
/@babel/types@7.21.5:
resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==}
@@ -354,7 +329,7 @@ packages:
'@babel/helper-string-parser': 7.23.4
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
dev: false
dev: true
/@emotion/babel-plugin@11.11.0:
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
@@ -732,38 +707,38 @@ packages:
'@jridgewell/set-array': 1.1.2
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.21
dev: false
dev: true
/@jridgewell/resolve-uri@3.1.1:
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
engines: {node: '>=6.0.0'}
dev: false
dev: true
/@jridgewell/set-array@1.1.2:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
dev: false
dev: true
/@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
dev: false
dev: true
/@jridgewell/trace-mapping@0.3.21:
resolution: {integrity: sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==}
dependencies:
'@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15
dev: false
dev: true
/@modyfi/vite-plugin-yaml@1.0.4(vite@5.0.11):
resolution: {integrity: sha512-qkT0KiR3AQQRfUvDzLv4+1rYAzXj+QmGhAbyUd0Ordf9xynK76i758lk5GiEfxuQxbvdqDaJ9oXkH/KacbSjQQ==}
/@modyfi/vite-plugin-yaml@1.1.0(vite@5.1.6):
resolution: {integrity: sha512-L26xfzkSo1yamODCAtk/ipVlL6OEw2bcJ92zunyHu8zxi7+meV0zefA9xscRMDCsMY8xL3C3wi3DhMiPxcbxbw==}
peerDependencies:
vite: ^2.6.0 || ^3.0.0 || ^4.0.0
vite: ^3.2.7 || ^4.0.5 || ^5.0.5
dependencies:
'@rollup/pluginutils': 5.0.2
'@rollup/pluginutils': 5.1.0
js-yaml: 4.1.0
tosource: 2.0.0-alpha.3
vite: 5.0.11(@types/node@20.11.4)
vite: 5.1.6(@types/node@20.11.4)
transitivePeerDependencies:
- rollup
dev: true
@@ -955,16 +930,16 @@ packages:
engines: {node: '>=14.0.0'}
dev: false
/@rollup/pluginutils@5.0.2:
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
/@rollup/pluginutils@5.1.0:
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.1
'@types/estree': 1.0.5
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
@@ -1196,10 +1171,6 @@ packages:
resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==}
dev: true
/@types/estree@1.0.1:
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
dev: true
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
dev: true
@@ -1268,13 +1239,13 @@ packages:
/@types/scheduler@0.16.3:
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
/@vitejs/plugin-react-swc@3.5.0(vite@5.0.11):
/@vitejs/plugin-react-swc@3.5.0(vite@5.1.6):
resolution: {integrity: sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==}
peerDependencies:
vite: ^4 || ^5
dependencies:
'@swc/core': 1.3.103
vite: 5.0.11(@types/node@20.11.4)
vite: 5.1.6(@types/node@20.11.4)
transitivePeerDependencies:
- '@swc/helpers'
dev: true
@@ -1283,14 +1254,13 @@ packages:
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: false
dev: true
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
dependencies:
color-convert: 1.9.3
dev: false
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
@@ -1298,7 +1268,7 @@ packages:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
dev: false
dev: true
/argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -1316,14 +1286,14 @@ packages:
/binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
dev: false
dev: true
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
dependencies:
fill-range: 7.0.1
dev: false
dev: true
/browserslist@4.22.2:
resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==}
@@ -1334,7 +1304,7 @@ packages:
electron-to-chromium: 1.4.632
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.22.2)
dev: false
dev: true
/callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
@@ -1343,7 +1313,7 @@ packages:
/caniuse-lite@1.0.30001577:
resolution: {integrity: sha512-rs2ZygrG1PNXMfmncM0B5H1hndY5ZCC9b5TkFaVNfZ+AUlyqcMyVIQtc3fsezi0NUCk5XZfDf9WS6WxMxnfdrg==}
dev: false
dev: true
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
@@ -1352,7 +1322,6 @@ packages:
ansi-styles: 3.2.1
escape-string-regexp: 1.0.5
supports-color: 5.5.0
dev: false
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
@@ -1366,8 +1335,8 @@ packages:
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.2
dev: false
fsevents: 2.3.3
dev: true
/clsx@2.1.0:
resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==}
@@ -1378,11 +1347,9 @@ packages:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
color-name: 1.1.3
dev: false
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
dev: false
/convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
@@ -1390,7 +1357,7 @@ packages:
/convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
dev: false
dev: true
/cosmiconfig@7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
@@ -1416,7 +1383,7 @@ packages:
optional: true
dependencies:
ms: 2.1.2
dev: false
dev: true
/dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
@@ -1427,7 +1394,7 @@ packages:
/electron-to-chromium@1.4.632:
resolution: {integrity: sha512-JGmudTwg7yxMYvR/gWbalqqQiyu7WTFv2Xu3vw4cJHXPFxNgAk0oy8UHaer8nLF4lZJa+rNoj6GsrKIVJTV6Tw==}
dev: false
dev: true
/error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@@ -1469,12 +1436,11 @@ packages:
/escalade@3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'}
dev: false
dev: true
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
dev: false
/escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
@@ -1490,7 +1456,7 @@ packages:
engines: {node: '>=8'}
dependencies:
to-regex-range: 5.0.1
dev: false
dev: true
/find-root@1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
@@ -1500,14 +1466,6 @@ packages:
resolution: {integrity: sha512-CkqAjnIKFqvo3sCyoBTqgJvF+bHrSik584S9nhTjtBESLx26cbtVMR/T9a6ApChOcSDAaM3JydDmWDUn4EEXng==}
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: false
optional: true
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1523,19 +1481,19 @@ packages:
/gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
dev: false
dev: true
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
dependencies:
is-glob: 4.0.3
dev: false
dev: true
/globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
dev: false
dev: true
/hamt_plus@1.0.2:
resolution: {integrity: sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==}
@@ -1544,7 +1502,6 @@ packages:
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
dev: false
/has@1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
@@ -1576,7 +1533,7 @@ packages:
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.2.0
dev: false
dev: true
/is-core-module@2.12.1:
resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==}
@@ -1587,23 +1544,22 @@ packages:
/is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
dev: false
dev: true
/is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
dependencies:
is-extglob: 2.1.1
dev: false
dev: true
/is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
dev: false
dev: true
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: false
/js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
@@ -1616,7 +1572,7 @@ packages:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
engines: {node: '>=4'}
hasBin: true
dev: false
dev: true
/json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
@@ -1626,12 +1582,12 @@ packages:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
dev: false
dev: true
/kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
dev: false
dev: true
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -1648,27 +1604,26 @@ packages:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
dependencies:
yallist: 3.1.1
dev: false
dev: true
/million@2.6.4:
resolution: {integrity: sha512-voUkdd/jHWrG+7NS+mX49Pat+POKdgGW78V7pYMSrTaOjUitR6ySEcAci8hn17Rsx1IMI3+5w41dkADM1J1ZEg==}
/million@3.0.6:
resolution: {integrity: sha512-OLjRVASGOZdyZw2ctBSSOu5kb9PaxafqkueqVvw0iQtUUnTLVRk1EmtqcNAtJWCIm8wn+WGRpDbnp+5Hi8//Kg==}
hasBin: true
dependencies:
'@babel/core': 7.23.7
'@babel/generator': 7.23.6
'@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.7)
'@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.7)
'@babel/types': 7.21.5
'@babel/types': 7.23.6
'@rollup/pluginutils': 5.1.0
kleur: 4.1.5
rollup: 3.29.4
undici: 6.8.0
unplugin: 1.6.0
transitivePeerDependencies:
- rollup
- supports-color
dev: false
dev: true
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: false
dev: true
/nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
@@ -1678,12 +1633,12 @@ packages:
/node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
dev: false
dev: true
/normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
dev: false
dev: true
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
@@ -1718,13 +1673,15 @@ packages:
/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.33:
resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==}
/postcss@8.4.35:
resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.7
@@ -1807,7 +1764,7 @@ packages:
engines: {node: '>=8.10.0'}
dependencies:
picomatch: 2.3.1
dev: false
dev: true
/recoil@0.7.7(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==}
@@ -1848,14 +1805,6 @@ packages:
supports-preserve-symlinks-flag: 1.0.0
dev: false
/rollup@3.29.4:
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.2
dev: false
/rollup@4.9.5:
resolution: {integrity: sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -1894,7 +1843,7 @@ packages:
/semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
dev: false
dev: true
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
@@ -1915,7 +1864,6 @@ packages:
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
dev: false
/supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
@@ -1925,14 +1873,13 @@ packages:
/to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
dev: false
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
dev: false
dev: true
/tosource@2.0.0-alpha.3:
resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==}
@@ -1953,6 +1900,11 @@ packages:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true
/undici@6.8.0:
resolution: {integrity: sha512-22FP0QRSJDQO2PC+bMBVqvsZ3cNQwQnxCNq910N3eIIU4xgMVVpLbEEX7fCg7AalvijPwjlyk5ezenw9FqZfHQ==}
engines: {node: '>=18.0'}
dev: true
/unplugin@1.6.0:
resolution: {integrity: sha512-BfJEpWBu3aE/AyHx8VaNE/WgouoQxgH9baAiH82JjX8cqVyi3uJQstqwD5J+SZxIK326SZIhsSZlALXVBCknTQ==}
dependencies:
@@ -1960,7 +1912,7 @@ packages:
chokidar: 3.5.3
webpack-sources: 3.2.3
webpack-virtual-modules: 0.6.1
dev: false
dev: true
/update-browserslist-db@1.0.13(browserslist@4.22.2):
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
@@ -1971,10 +1923,10 @@ packages:
browserslist: 4.22.2
escalade: 3.1.1
picocolors: 1.0.0
dev: false
dev: true
/vite@5.0.11(@types/node@20.11.4):
resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==}
/vite@5.1.6(@types/node@20.11.4):
resolution: {integrity: sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@@ -2003,7 +1955,7 @@ packages:
dependencies:
'@types/node': 20.11.4
esbuild: 0.19.11
postcss: 8.4.33
postcss: 8.4.35
rollup: 4.9.5
optionalDependencies:
fsevents: 2.3.3
@@ -2012,15 +1964,15 @@ packages:
/webpack-sources@3.2.3:
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
engines: {node: '>=10.13.0'}
dev: false
dev: true
/webpack-virtual-modules@0.6.1:
resolution: {integrity: sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==}
dev: false
dev: true
/yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
dev: false
dev: true
/yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,13 +1,6 @@
import { atom, selector } from 'recoil'
import { rpcClientState } from './rpc'
type StatusState = {
connected: boolean,
updated: boolean,
downloading: boolean,
}
export const connectedState = atom({
key: 'connectedState',
default: false

View File

@@ -17,7 +17,7 @@ import {
} from '@mui/material'
import { useCallback } from 'react'
import { RPCResult } from '../types'
import { ellipsis, formatSpeedMiB, mapProcessStatus, roundMiB } from '../utils'
import { ellipsis, formatSpeedMiB, mapProcessStatus, formatSize } from '../utils'
type Props = {
download: RPCResult
@@ -86,7 +86,7 @@ const DownloadCard: React.FC<Props> = ({ download, onStop, onCopy }) => {
{!isCompleted() ? formatSpeedMiB(download.progress.speed) : ''}
</Typography>
<Typography>
{roundMiB(download.info.filesize_approx ?? 0)}
{formatSize(download.info.filesize_approx ?? 0)}
</Typography>
<Resolution resolution={download.info.resolution} />
</Stack>

View File

@@ -14,7 +14,7 @@ import {
import { useRecoilValue } from 'recoil'
import { activeDownloadsState } from '../atoms/downloads'
import { useRPC } from '../hooks/useRPC'
import { ellipsis, formatSpeedMiB, roundMiB } from "../utils"
import { ellipsis, formatSpeedMiB, formatSize } from "../utils"
const DownloadsListView: React.FC = () => {
@@ -74,7 +74,7 @@ const DownloadsListView: React.FC = () => {
/>
</TableCell>
<TableCell>{formatSpeedMiB(download.progress.speed)}</TableCell>
<TableCell>{roundMiB(download.info.filesize_approx ?? 0)}</TableCell>
<TableCell>{formatSize(download.info.filesize_approx ?? 0)}</TableCell>
<TableCell>
<Button
variant="contained"

View File

@@ -1,7 +1,7 @@
import StorageIcon from '@mui/icons-material/Storage'
import { useRecoilValue } from 'recoil'
import { freeSpaceBytesState } from '../atoms/status'
import { formatGiB } from '../utils'
import { formatSize } from '../utils'
const FreeSpaceIndicator = () => {
const freeSpace = useRecoilValue(freeSpaceBytesState)
@@ -15,7 +15,7 @@ const FreeSpaceIndicator = () => {
}}>
<StorageIcon />
<span>
{formatGiB(freeSpace)}
{formatSize(freeSpace)}
</span>
</div>
)

View File

@@ -1,4 +1,4 @@
import { Box, CircularProgress, Container, Paper, Typography } from '@mui/material'
import { Box, Container, Paper, Typography } from '@mui/material'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useRecoilValue } from 'recoil'
import { serverURL } from '../atoms/settings'
@@ -33,13 +33,16 @@ const LogTerminal: React.FC = () => {
}, [eventSource])
const logEntryStyle = (data: string) => {
const sx = {}
if (data.includes("level=ERROR")) {
return { color: 'red' }
return { ...sx, color: 'red' }
}
if (data.includes("level=WARN")) {
return { color: 'orange' }
return { ...sx, color: 'orange' }
}
return {}
return sx
}
return (
@@ -54,19 +57,6 @@ const LogTerminal: React.FC = () => {
<Typography py={1} variant="h5" color="primary">
{i18n.t('logsTitle')}
</Typography>
{(logBuffer.length === 0) && <Box sx={{
display: 'flex',
flexDirection: 'column',
justifyItems: 'center',
alignItems: 'center',
gap: 1
}}>
<CircularProgress color="primary" size={32} />
<Typography py={1} variant="subtitle2" >
{i18n.t('awaitingLogs')}
</Typography>
</Box>
}
<Box
ref={boxRef}
sx={{
@@ -74,9 +64,15 @@ const LogTerminal: React.FC = () => {
height: '75.5vh',
overflowY: 'auto',
overflowX: 'auto',
fontSize: '15px'
fontSize: '13.5px',
fontWeight: '600',
backgroundColor: 'black',
color: 'white',
padding: '0.5rem',
borderRadius: '0.25rem'
}}
>
{logBuffer.length === 0 && <Box >{i18n.t('awaitingLogs')}</Box>}
{logBuffer.map((log, idx) => (
<Box key={idx} sx={logEntryStyle(log)}>
{log}

View File

@@ -42,14 +42,21 @@ export function toFormatArgs(codes: string[]): string {
return ''
}
export const formatGiB = (bytes: number) =>
`${(bytes / 1_000_000_000).toFixed(0)}GiB`
export function formatSize(bytes: number): string {
const threshold = 1024
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
export const roundMiB = (bytes: number) =>
`${(bytes / 1_000_000).toFixed(2)} MiB`
let i = 0
while (bytes >= threshold) {
bytes /= threshold
i = i + 1
}
return `${bytes.toFixed(i == 0 ? 0 : 2)} ${units.at(i)}`
}
export const formatSpeedMiB = (val: number) =>
`${roundMiB(val)}/s`
`${(val / 1_048_576).toFixed(2)} MiB/s`
export const datetimeCompareFunc = (a: string, b: string) =>
new Date(a).getTime() - new Date(b).getTime()

View File

@@ -41,7 +41,7 @@ import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n'
import { ffetch } from '../lib/httpClient'
import { DirectoryEntry } from '../types'
import { base64URLEncode, roundMiB } from '../utils'
import { base64URLEncode, formatSize } from '../utils'
export default function Downloaded() {
const [menuPos, setMenuPos] = useState({ x: 0, y: 0 })
@@ -237,7 +237,7 @@ export default function Downloaded() {
variant="caption"
component="span"
>
{roundMiB(file.size)}
{formatSize(file.size)}
</Typography>
}
{!file.isDirectory && <>

View File

@@ -35,6 +35,7 @@ func Instance() *Config {
return instance
}
// Initialises the Config struct given its config file
func (c *Config) LoadFile(filename string) error {
fd, err := os.Open(filename)
if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"database/sql"
)
// Run the table migration
func AutoMigrate(ctx context.Context, db *sql.DB) error {
conn, err := db.Conn(ctx)
if err != nil {

View File

@@ -17,6 +17,11 @@ import (
"github.com/marcopeocchi/yt-dlp-web-ui/server/utils"
)
/*
File based operation handlers (should be moved to rest/handlers.go) or in
a entirely self-contained package
*/
type DirectoryEntry struct {
Name string `json:"name"`
Path string `json:"path"`

View File

@@ -236,10 +236,6 @@ func (p *Process) GetFormatsSync() (DownloadFormats, error) {
wg.Add(2)
if err != nil {
return DownloadFormats{}, err
}
log.Println(
cli.BgRed, "Metadata", cli.Reset,
cli.BgBlue, "Formats", cli.Reset,

View File

@@ -1,33 +1,37 @@
package internal
type Node[T any] struct {
Value T
}
type Stack[T any] struct {
Elements []*T
count int
Nodes []*Node[T]
count int
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{
Elements: make([]*T, 10),
Nodes: make([]*Node[T], 10),
}
}
func (s *Stack[T]) Push(val T) {
if s.count >= len(s.Elements) {
Elements := make([]*T, len(s.Elements)*2)
copy(Elements, s.Elements)
s.Elements = Elements
if s.count >= len(s.Nodes) {
Nodes := make([]*Node[T], len(s.Nodes)*2)
copy(Nodes, s.Nodes)
s.Nodes = Nodes
}
s.Elements[s.count] = &val
s.Nodes[s.count] = &Node[T]{Value: val}
s.count++
}
func (s *Stack[T]) Pop() *T {
func (s *Stack[T]) Pop() *Node[T] {
if s.count == 0 {
return nil
}
Element := s.Elements[s.count-1]
node := s.Nodes[s.count-1]
s.count--
return Element
return node
}
func (s *Stack[T]) IsEmpty() bool {

View File

@@ -20,7 +20,9 @@ func NewObservableLogger() *ObservableLogger {
}
func (o *ObservableLogger) Write(p []byte) (n int, err error) {
logsChan <- rxgo.Of(string(p))
go func() {
logsChan <- rxgo.Of(string(p))
}()
n = len(p)
err = nil

View File

@@ -39,6 +39,9 @@ func validateToken(tokenValue string) error {
return nil
}
// Authentication does NOT use http-Only cookies since there's not risk for XSS
// By exposing the server through https it's completely safe to use httpheaders
func Authenticated(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Authentication")

View File

@@ -1,93 +0,0 @@
package middlewares
import (
"fmt"
"io"
"io/fs"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
)
type SpaHandler struct {
Entrypoint string
Filesystem fs.FS
routes []string
}
func NewSpaHandler(index string, fs fs.FS) *SpaHandler {
return &SpaHandler{
Entrypoint: index,
Filesystem: fs,
}
}
func (s *SpaHandler) AddClientRoute(route string) *SpaHandler {
s.routes = append(s.routes, route)
return s
}
// Handler for serving a compiled react frontend
// each client-side routes must be provided
func (s *SpaHandler) Handler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(
w,
http.StatusText(http.StatusMethodNotAllowed),
http.StatusMethodNotAllowed,
)
return
}
path := filepath.Clean(r.URL.Path)
// basically all frontend routes are needed :/
hasRoute := false
for _, route := range s.routes {
hasRoute = strings.HasPrefix(path, route)
if hasRoute {
break
}
}
if path == "/" || hasRoute {
path = s.Entrypoint
}
path = strings.TrimPrefix(path, "/")
file, err := s.Filesystem.Open(path)
if err != nil {
if os.IsNotExist(err) {
http.NotFound(w, r)
return
}
http.Error(
w,
http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError,
)
return
}
contentType := mime.TypeByExtension(filepath.Ext(path))
w.Header().Set("Content-Type", contentType)
if strings.HasPrefix(path, "assets/") {
w.Header().Set("Cache-Control", "public, max-age=2592000")
}
stat, err := file.Stat()
if err == nil && stat.Size() > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
}
w.WriteHeader(http.StatusOK)
io.Copy(w, file)
})
}

View File

@@ -30,8 +30,5 @@ func ApplyRouter(db *sql.DB, mdb *internal.MemoryDB, mq *internal.MessageQueue)
r.Post("/template", h.AddTemplate())
r.Get("/template/all", h.GetTemplates())
r.Delete("/template/{id}", h.DeleteTemplate())
r.Get("/tree", h.DirectoryTree())
r.Get("/d/{id}", h.DownloadFile())
}
}

View File

@@ -2,10 +2,7 @@ package rest
import (
"encoding/json"
"io"
"net/http"
"os"
"path/filepath"
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
@@ -15,6 +12,10 @@ type Handler struct {
service *Service
}
/*
REST version of the JSON-RPC interface
*/
func (h *Handler) Exec() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
@@ -157,55 +158,3 @@ func (h *Handler) DeleteTemplate() http.HandlerFunc {
}
}
}
func (h *Handler) DirectoryTree() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
tree, err := h.service.DirectoryTree(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode(tree)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
func (h *Handler) DownloadFile() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
id := chi.URLParam(r, "id")
path, err := h.service.DownloadFile(r.Context(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add(
"Content-Disposition",
"inline; filename="+filepath.Base(*path),
)
w.Header().Set(
"Content-Type",
"application/octet-stream",
)
fd, err := os.Open(*path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.Copy(w, fd)
}
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/google/uuid"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/server/sys"
)
type Service struct {
@@ -119,16 +118,3 @@ func (s *Service) DeleteTemplate(ctx context.Context, id string) error {
return err
}
func (s *Service) DirectoryTree(ctx context.Context) (*internal.Stack[sys.FSNode], error) {
return sys.DirectoryTree()
}
func (s *Service) DownloadFile(ctx context.Context, id string) (*string, error) {
p, err := s.mdb.Get(id)
if err != nil {
return nil, err
}
return &p.Output.Path, nil
}

View File

@@ -14,6 +14,7 @@ var upgrader = websocket.Upgrader{
},
}
// WebSockets JSON-RPC handler
func WebSocket(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
@@ -47,6 +48,7 @@ func WebSocket(w http.ResponseWriter, r *http.Request) {
}
}
// HTTP-POST JSON-RPC handler
func Post(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()

View File

@@ -140,7 +140,7 @@ func (s *Service) FreeSpace(args NoArgs, free *uint64) error {
}
// Return a flattned tree of the download directory
func (s *Service) DirectoryTree(args NoArgs, tree *internal.Stack[sys.FSNode]) error {
func (s *Service) DirectoryTree(args NoArgs, tree *[]string) error {
dfsTree, err := sys.DirectoryTree()
if dfsTree != nil {
*tree = *dfsTree

View File

@@ -6,13 +6,16 @@ import (
"net/rpc/jsonrpc"
)
// Wrapper for HTTP RPC request that implements io.Reader interface
// Wrapper for jsonrpc.ServeConn that simplifies its usage
type rpcRequest struct {
r io.Reader
rw io.ReadWriter
done chan bool
}
// Takes a reader that can be an *http.Request or anthing that implements
// io.ReadWriter interface.
// Call() will perform the jsonRPC call and write or read from the ReadWriter
func newRequest(r io.Reader) *rpcRequest {
var buf bytes.Buffer
done := make(chan bool)

View File

@@ -6,6 +6,9 @@ import "time"
//
// Debounce emits the most recently emitted value from the source
// withing the timespan set by the span time.Duration
//
// Soon it will be deprecated since it doesn't add anything useful.
// (It lowers the CPU usage by a negligible margin)
func Sample(span time.Duration, source chan []byte, done chan struct{}, fn func(e []byte)) {
var (
item []byte

View File

@@ -18,35 +18,39 @@ func FreeSpace() (uint64, error) {
return (stat.Bavail * uint64(stat.Bsize)), nil
}
type FSNode struct {
path string
children []FSNode
}
// Build a directory tree started from the specified path using DFS.
// Then return the flattened tree represented as a list.
func DirectoryTree() (*internal.Stack[FSNode], error) {
func DirectoryTree() (*[]string, error) {
type Node struct {
path string
children []Node
}
rootPath := config.Instance().DownloadPath
stack := internal.NewStack[FSNode]()
stack := internal.NewStack[Node]()
flattened := make([]string, 0)
stack.Push(FSNode{path: rootPath})
stack.Push(Node{path: rootPath})
flattened = append(flattened, rootPath)
for stack.IsNotEmpty() {
current := stack.Pop()
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 := FSNode{path: childPath}
childNode := Node{path: childPath}
if entry.IsDir() {
current.children = append(current.children, childNode)
stack.Push(childNode)
flattened = append(flattened, childNode.path)
}
}
}
return stack, nil
return &flattened, nil
}

24
ui/.gitignore vendored
View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

View File

@@ -1,47 +0,0 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

View File

@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/svelte.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>yt-dlp WebUI</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,31 +0,0 @@
{
"name": "ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tsconfig/svelte": "^5.0.2",
"@zerodevx/svelte-toast": "^0.9.5",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.34",
"svelte": "^4.2.8",
"svelte-check": "^3.6.2",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^5.0.8"
},
"dependencies": {
"@fontsource/roboto": "^5.0.8",
"fp-ts": "^2.16.2",
"lucide-svelte": "^0.323.0",
"svelte-spa-router": "^4.0.1"
}
}

1693
ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,25 +0,0 @@
<script lang="ts">
import { SvelteToast } from '@zerodevx/svelte-toast';
import Router from 'svelte-spa-router';
import { wrap } from 'svelte-spa-router/wrap';
import Footer from './lib/Footer.svelte';
import Home from './views/Home.svelte';
import Navbar from './lib/Navbar.svelte';
const routes = {
'/': Home,
'/settings': wrap({
asyncComponent: () => import('./views/SettingsView.svelte'),
}),
};
</script>
<main
class="bg-neutral-50 dark:bg-neutral-900 h-screen text-neutral-950 dark:text-neutral-50"
>
<Navbar />
<Router {routes} />
<Footer />
<SvelteToast />
<!-- <FloatingAction /> -->
</main>

View File

@@ -1,7 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* body {
font-family: "Roboto";
} */

View File

@@ -1,15 +0,0 @@
<script lang="ts">
let clazz: string = '';
export let disabled: boolean = false;
export { clazz as class };
</script>
<button
class={`px-2.5 py-2 rounded-lg bg-blue-300 hover:bg-blue-400 hover:duration-150 text-sm font-semibold ${
disabled && 'bg-neutral-300 hover:bg-neutral-300'
} ${clazz}`}
{disabled}
on:click
>
<slot />
</button>

View File

@@ -1,10 +0,0 @@
<script lang="ts">
export let text: string;
</script>
<div
class="flex items-center gap-1.5 p-1 bg-blue-200 rounded-lg text-neutral-900"
>
<slot />
{text}
</div>

View File

@@ -1,137 +0,0 @@
<script lang="ts">
import { toast } from '@zerodevx/svelte-toast';
import * as A from 'fp-ts/Array';
import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/lib/function';
import { get } from 'svelte/store';
import { ffetch } from './ffetch';
import { cookiesTemplate, serverApiEndpoint } from './store';
import { debounce } from './utils';
const flag = '--cookies=cookies.txt';
let cookies = localStorage.getItem('cookies') ?? '';
const validateCookie = (cookie: string) =>
pipe(
cookie,
(cookie) => cookie.replace(/\s\s+/g, ' '),
(cookie) => cookie.replaceAll('\t', ' '),
(cookie) => cookie.split(' '),
E.of,
E.flatMap(
E.fromPredicate(
(f) => f.length === 7,
() => `missing parts`,
),
),
E.flatMap(
E.fromPredicate(
(f) => f[0].length > 0,
() => 'missing domain',
),
),
E.flatMap(
E.fromPredicate(
(f) => f[1] === 'TRUE' || f[1] === 'FALSE',
() => `invalid include subdomains`,
),
),
E.flatMap(
E.fromPredicate(
(f) => f[2].length > 0,
() => 'invalid path',
),
),
E.flatMap(
E.fromPredicate(
(f) => f[3] === 'TRUE' || f[3] === 'FALSE',
() => 'invalid secure flag',
),
),
E.flatMap(
E.fromPredicate(
(f) => isFinite(Number(f[4])),
() => 'invalid expiration',
),
),
E.flatMap(
E.fromPredicate(
(f) => f[5].length > 0,
() => 'invalid name',
),
),
E.flatMap(
E.fromPredicate(
(f) => f[6].length > 0,
() => 'invalid value',
),
),
);
const validateNetscapeCookies = (cookies: string) =>
pipe(
cookies,
(cookies) => cookies.split('\n'),
(cookies) => cookies.filter((f) => !f.startsWith('\n')), // empty lines
(cookies) => cookies.filter((f) => !f.startsWith('# ')), // comments
(cookies) => cookies.filter(Boolean), // empty lines
A.map(validateCookie),
A.mapWithIndex((i, either) =>
pipe(
either,
E.matchW(
(l) => toast.push(`Error in line ${i + 1}: ${l}`),
() => E.isRight(either),
),
),
),
A.filter(Boolean),
A.match(
() => false,
(c) => {
toast.push(`Valid ${c.length} Netscape cookies`);
return true;
},
),
);
const submitCookies = (cookies: string) =>
ffetch(`${get(serverApiEndpoint)}/api/v1/cookies`, {
method: 'POST',
body: JSON.stringify({
cookies,
}),
})();
const execute = (cookies: KeyboardEvent) =>
pipe(
cookies.target as HTMLTextAreaElement,
(cookies) => cookies.value,
O.fromPredicate(validateNetscapeCookies),
O.match(
() => cookiesTemplate.set(''),
async (cookies) => {
pipe(
await submitCookies(cookies),
E.match(
(l) => toast.push(l),
() => {
toast.push(`Saved Netscape cookies`);
cookiesTemplate.set(flag);
localStorage.setItem('cookies', cookies);
},
),
);
},
),
);
</script>
<textarea
cols="80"
rows="8"
value={cookies}
on:keyup={debounce(execute, 500)}
/>

View File

@@ -1,83 +0,0 @@
<script lang="ts">
import { get } from 'svelte/store';
import Button from './Button.svelte';
import Chip from './Chip.svelte';
import { rpcClient, serverApiEndpoint } from './store';
import type { RPCResult } from './types';
import { formatSpeedMiB, roundMiB } from './utils';
export let download: RPCResult;
const remove = (id: string) => get(rpcClient).kill(id);
</script>
<div
class="flex gap-4
bg-neutral-100 dark:bg-neutral-800
p-2 md:p-4
rounded-lg shadow-lg
border dark:border-neutral-700"
>
<div
class="h-full hidden sm:block w-96 bg-cover bg-center rounded"
style="background-image: url({download.info.thumbnail})"
/>
<div class="flex flex-col justify-between gap-2 w-full">
<div>
<h2 class="font-bold text-lg">{download.info.title}</h2>
<p
class="font-mono text-sm mt-2 p-1 break-all bg-neutral-200 dark:bg-neutral-700 rounded"
>
{download.info.url}
</p>
</div>
<div class="flex flex-col justify-end gap-2 select-none flex-wrap">
<div class="hidden sm:flex items-center gap-2 text-sm">
{#if download.info.vcodec}
<Chip text={download.info.vcodec} />
{/if}
{#if download.info.acodec}
<Chip text={download.info.acodec} />
{/if}
{#if download.info.ext}
<Chip text={download.info.ext} />
{/if}
{#if download.info.resolution}
<Chip text={download.info.resolution} />
{/if}
{#if download.info.filesize_approx}
<Chip text={roundMiB(download.info.filesize_approx)} />
{/if}
<!-- {#if download.progress.process_status}
<Chip text={mapProcessStatus(download.progress.process_status)} />
{/if} -->
{#if download.progress.speed}
<Chip text={formatSpeedMiB(download.progress.speed)} />
{/if}
</div>
<div class="flex gap-2">
<Button class="w-14" on:click={() => remove(download.id)}>Stop</Button>
{#if download.progress.process_status === 2}
<Button class="w-18">Download</Button>
<!-- <a href={`${$serverApiEndpoint}/api/v1/d/${download.id}`}>d</a> -->
{/if}
</div>
<div
class="w-full mt-4 h-2 rounded-full bg-neutral-200 dark:bg-neutral-700"
>
<div
class={`h-2 rounded-full ${
download.progress.process_status === 2
? 'bg-green-600'
: 'bg-blue-500'
}`}
style="width: {download.progress.percentage}"
/>
</div>
</div>
</div>
</div>

View File

@@ -1,23 +0,0 @@
<script lang="ts">
import { Plus } from 'lucide-svelte';
</script>
<div class="absolute bottom-10 right-10">
<!-- <div class="relative mb-4 flex flex-col justify-center items-center gap-2">
<button
class="relative flex items-center justify-center bg-blue-500 h-8 w-8 z-10 rounded-2xl shadow-xl text-neutral-100"
>
<Plus size={18} />
</button>
<button
class="relative flex items-center justify-center bg-blue-500 h-8 w-8 z-10 rounded-2xl shadow-xl text-neutral-100"
>
<Plus size={18} />
</button>
</div> -->
<button
class="relative bg-blue-500 p-5 z-10 rounded-2xl shadow-xl text-neutral-100"
>
<Plus />
</button>
</div>

View File

@@ -1,48 +0,0 @@
<script lang="ts">
import { ChevronDown, ChevronUp } from 'lucide-svelte';
import { cubicOut } from 'svelte/easing';
import { tweened } from 'svelte/motion';
import NewDownload from './NewDownload.svelte';
const height = tweened(52, {
duration: 300,
easing: cubicOut,
});
const minHeight = 52;
const maxHeight = window.innerHeight / 1.5;
let open = false;
$: open = $height > minHeight;
</script>
<footer
class="
fixed bottom-0 z-10
w-full
p-2
bg-neutral-100 dark:bg-neutral-800
border-t dark:border-t-neutral-700
shadow-lg
rounded-t-xl"
style="min-height: {$height}px;"
>
<button
class="p-1 bg-neutral-200 dark:bg-neutral-700 rounded-lg border dark:border-neutral-700"
on:click={() => (open ? height.set(minHeight) : height.set(maxHeight))}
>
{#if open}
<ChevronDown />
{:else}
<ChevronUp />
{/if}
</button>
<div />
{#if $height > 100}
<div class="mt-2">
<NewDownload />
</div>
{/if}
</footer>

View File

@@ -1,70 +0,0 @@
<script lang="ts">
import type { DLFormat } from './types';
let group = '';
export let formats: DLFormat[];
$: console.log(group);
</script>
<div class="w-full mt-4">
<div class="mx-auto w-full">
<fieldset class="grid grid-cols-7 gap-2">
{#each formats as format}
<div class="relative">
<input
id="formats"
class="absolute opacity-0 w-0 h-0 peer"
type="radio"
bind:group
name="type"
value="formats"
/>
<label
for="formats"
class="
[&_p]:text-gray-900 [&_span]:text-gray-500
peer-checked:[&_p]:text-white peer-checked:[&_span]:text-blue-100
peer-focus:ring-2 peer-focus:ring-white
peer-focus:ring-opacity-60 peer-focus:ring-offset-2 peer-focus:ring-offset-blue-300
bg-white
relative flex
cursor-pointer
rounded-lg px-5 py-4
shadow-md
focus:outline-none
peer-checked:bg-blue-700/75
peer-checked:text-white"
>
<div class="flex w-full items-center justify-between">
<div class="flex items-center">
<div class="text-sm">
<p class="font-medium" id={format.format_id}>
{format.resolution}
</p>
<span class="inline" id={format.format_id}>
<span>{format.vcodec}</span>
<span aria-hidden="true">·</span>
<span>{format.acodec}</span>
</span>
</div>
</div>
<div class="shrink-0 text-white">
<svg viewBox="0 0 24 24" fill="none" class="h-6 w-6">
<circle cx="12" cy="12" r="12" fill="#fff" opacity="0.2" />
<path
d="M7 13l3 3 7-7"
stroke="#fff"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
</div>
</label>
</div>
{/each}
</fieldset>
</div>
</div>

View File

@@ -1,9 +0,0 @@
<script lang="ts">
import Spinner from './Spinner.svelte';
</script>
<div
class="top-0 left-0 absolute w-full h-full bg-neutral-950/20 flex items-center justify-center z-50"
>
<Spinner />
</div>

View File

@@ -1,97 +0,0 @@
<script lang="ts">
import {
ArrowDownUp,
Github,
HardDrive,
Network,
Settings,
} from 'lucide-svelte';
import { downloads, rpcClient, serverApiEndpoint } from './store';
import { formatGiB, formatSpeedMiB } from './utils';
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/lib/function';
import { onDestroy } from 'svelte';
import { link } from 'svelte-spa-router';
let downloadSpeed = 0;
const unsubscribe = downloads.subscribe((downloads) =>
pipe(
downloads,
O.matchW(
() => (downloadSpeed = 0),
(d) =>
(downloadSpeed = d
.map((d) => d.progress.speed)
.reduce((a, b) => a + b)),
),
),
);
onDestroy(unsubscribe);
</script>
<nav
class="
p-4
flex justify-between items-center
bg-neutral-100 dark:bg-neutral-800
rounded-b-xl
border-b dark:border-b-neutral-700
shadow-lg
select-none"
>
<a use:link={'/'} href="/" class="font-semibold text-lg">yt-dlp WebUI</a>
<div />
<div class="flex items-center gap-2 text-sm">
<div
class="hidden sm:flex items-center gap-1.5 p-1 text-neutral-900 bg-blue-200 rounded-lg"
>
<ArrowDownUp size={18} />
<div>
{formatSpeedMiB(downloadSpeed)}
</div>
</div>
<div class="flex items-center gap-2 text-sm">
<div
class="flex items-center gap-1.5 p-1 text-neutral-900 bg-blue-200 rounded-lg"
>
<HardDrive size={18} />
<div>
{#await $rpcClient.freeSpace()}
Loading...
{:then freeSpace}
{formatGiB(freeSpace.result)}
{/await}
</div>
</div>
<div
class="flex items-center gap-1.5 p-1 text-neutral-900 bg-blue-200 rounded-lg"
>
<Network size={18} />
<div>
{$serverApiEndpoint.split('//')[1]}
</div>
</div>
<a
href="https://github.com/marcopeocchi/yt-dlp-web-ui"
class="flex items-center gap-1.5 p-1 text-neutral-900 bg-blue-200 rounded-lg"
>
<Github size={18} />
</a>
<a
use:link={'/settings'}
href="/settings"
class="flex items-center gap-1.5 p-1 text-neutral-900 bg-blue-200 rounded-lg"
>
<Settings size={18} />
</a>
</div>
</div>
</nav>

View File

@@ -1,49 +0,0 @@
<script lang="ts">
import { get } from 'svelte/store';
import Button from './Button.svelte';
import TextField from './TextField.svelte';
import { downloadTemplates, rpcClient } from './store';
import Select from './Select.svelte';
import type { DLMetadata } from './types';
import FormatsList from './FormatsList.svelte';
let url: string = '';
let args: string = '';
let metadata: DLMetadata;
const download = () =>
get(rpcClient).download({
url,
args,
});
const getFormats = () =>
get(rpcClient)
.formats(url)
?.then((f) => (metadata = f.result));
</script>
<div class="w-full px-8">
<div class="my-4 font-semibold text-xl">New download</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 w-full mb-2">
<TextField placeholder="https://..." label="URL" bind:value={url} />
<TextField
placeholder="arguments separated by space"
label="yt-dlp arguments"
bind:value={args}
/>
<Select bind:value={args}>
<option selected disabled value=""> Select download template </option>
{#each $downloadTemplates as template}
<option id={template.id} value={template.content}>
{template.name}
</option>
{/each}
</Select>
</div>
<Button class="mt-2" on:click={download}>Download</Button>
<Button class="mt-2" on:click={getFormats}>Select format</Button>
{#if metadata}
<FormatsList formats={metadata.formats} />
{/if}
</div>

View File

@@ -1,195 +0,0 @@
import type { DLMetadata, RPCRequest, RPCResponse, RPCResult } from './types'
type DownloadRequestArgs = {
url: string,
args: string,
pathOverride?: string,
renameTo?: string,
playlist?: boolean
}
export class RPCClient {
private seq: number
private httpEndpoint: string
private readonly _socket$: WebSocket
private readonly token?: string
constructor(httpEndpoint: string, webSocketEndpoint: string, token?: string) {
this.seq = 0
this.httpEndpoint = httpEndpoint
this.token = token
this._socket$ = new WebSocket(
token ? `${webSocketEndpoint}?token=${token}` : webSocketEndpoint
)
}
/**
* Websocket connection
*/
public get socket() {
return this._socket$
}
private incrementSeq() {
return String(this.seq++)
}
private send(req: RPCRequest) {
this._socket$.send(JSON.stringify({
...req,
id: this.incrementSeq(),
}))
}
private argsSanitizer(args: string) {
return args
.split(' ')
.map(a => a.trim().replaceAll("'", '').replaceAll('"', ''))
.filter(Boolean)
}
private async sendHTTP<T>(req: RPCRequest) {
const res = await fetch(this.httpEndpoint, {
method: 'POST',
headers: {
'X-Authentication': this.token ?? ''
},
body: JSON.stringify({
...req,
id: this.incrementSeq(),
})
})
const data: RPCResponse<T> = await res.json()
return data
}
/**
* Request a new download. Handles arguments sanitization.
* @param req payload
* @returns
*/
public download(req: DownloadRequestArgs) {
if (!req.url) {
return
}
const rename = req.args.includes('-o')
? req.args
.substring(req.args.indexOf('-o'))
.replaceAll("'", '')
.replaceAll('"', '')
.split('-o')
.map(s => s.trim())
.join('')
.split(' ')
.at(0) ?? ''
: ''
const sanitizedArgs = this.argsSanitizer(
req.args.replace('-o', '').replace(rename, '')
)
if (req.playlist) {
return this.sendHTTP({
method: 'Service.ExecPlaylist',
params: [{
URL: req.url,
Params: sanitizedArgs,
Path: req.pathOverride,
Rename: req.renameTo || rename,
}]
})
}
this.sendHTTP({
method: 'Service.Exec',
params: [{
URL: req.url.split('?list').at(0)!,
Params: sanitizedArgs,
Path: req.pathOverride,
Rename: req.renameTo || rename,
}]
})
}
/**
* Requests the available formats for a given url (-f arg)
* @param url requested url
* @returns
*/
public formats(url: string) {
if (url) {
return this.sendHTTP<DLMetadata>({
method: 'Service.Formats',
params: [{
URL: url.split('?list').at(0)!,
}]
})
}
}
/**
* Requests all downloads
*/
public running() {
this.send({
method: 'Service.Running',
params: [],
})
}
/**
* Stops and removes a download asynchronously
* @param id download id
*/
public kill(id: string) {
this.sendHTTP({
method: 'Service.Kill',
params: [id],
})
}
/**
* Stops and removes all downloads
*/
public killAll() {
this.sendHTTP({
method: 'Service.KillAll',
params: [],
})
}
/**
* Get asynchronously the avaliable space on downloads directory
* @returns free space in bytes
*/
public freeSpace() {
return this.sendHTTP<number>({
method: 'Service.FreeSpace',
params: [],
})
}
/**
* Get asynchronously the tree view of the download directory
* @returns free space in bytes
*/
public directoryTree() {
return this.sendHTTP<string[]>({
method: 'Service.DirectoryTree',
params: [],
})
}
/**
* Updates synchronously yt-dlp executable
* @returns free space in bytes
*/
public updateExecutable() {
return this.sendHTTP({
method: 'Service.UpdateExecutable',
params: []
})
}
}

View File

@@ -1,23 +0,0 @@
<script lang="ts">
export let value: any;
export let disabled: boolean = false;
export let placeholder: string = '';
export { clazz as class };
</script>
<select
class="
p-2
bg-neutral-50
border rounded-lg
appearance-none
text-sm font-semibold
focus:outline-blue-300
"
bind:value
{disabled}
{placeholder}
>
<slot />
</select>

View File

@@ -1,34 +0,0 @@
<script lang="ts">
import { get } from 'svelte/store';
import Button from './Button.svelte';
import TextField from './TextField.svelte';
import { rpcClient, rpcHost, rpcPort } from './store';
import FullscreenSpinner from './FullscreenSpinner.svelte';
let loading: Promise<any>;
const update = () => (loading = get(rpcClient).updateExecutable());
</script>
<div class="w-full">
<div class="font-semibold text-lg mb-4">Settings</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<TextField
label="Server address"
bind:value={$rpcHost}
placeholder="localhost"
/>
<TextField label="Server port" bind:value={$rpcPort} placeholder="3033" />
</div>
<Button class="mt-4" on:click={update}>Update yt-dlp</Button>
{#if loading}
{#await loading}
<FullscreenSpinner />
{/await}
{/if}
<!-- <CookiesTextField /> -->
</div>

View File

@@ -1,19 +0,0 @@
<div role="status">
<svg
aria-hidden="true"
class="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-400"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>

View File

@@ -1,27 +0,0 @@
<script lang="ts">
let clazz: string = '';
export let label: string;
export let value: any;
export let disabled: boolean = false;
export let placeholder: string = '';
export { clazz as class };
</script>
<div class="flex flex-col gap-0.5 text-sm font-semibold">
<label for=""> {label} </label>
<input
type="text"
class={`p-2
bg-neutral-50 border
rounded-lg
focus:outline-blue-300
dark:bg-neutral-700 dark:border-neutral-900
${clazz}
`}
on:keyup
bind:value
{placeholder}
{disabled}
/>
</div>

View File

@@ -1,32 +0,0 @@
import { tryCatch } from 'fp-ts/TaskEither'
/**
* functional fetch(): composable as TaskEither
*/
export const ffetch = <T>(url: string, opt?: RequestInit) => tryCatch(
() => fetcher<T>(url, opt),
(e) => `error while fetching: ${e}`
)
const fetcher = async <T>(url: string, opt?: RequestInit) => {
const jwt = localStorage.getItem('token')
if (opt && !opt.headers) {
opt.headers = {
'Content-Type': 'application/json',
}
}
const res = await fetch(url, {
...opt,
headers: {
...opt?.headers,
'X-Authentication': jwt ?? ''
}
})
if (!res.ok) {
throw await res.text()
}
return res.json() as T
}

View File

@@ -1,57 +0,0 @@
import * as O from 'fp-ts/lib/Option'
import { derived, readable, writable } from 'svelte/store'
import { RPCClient } from './RPCClient'
import { type CustomTemplate, type RPCResult } from './types'
export const rpcHost = writable<string>(localStorage.getItem('rpcHost') ?? 'localhost')
export const rpcPort = writable<number>(Number(localStorage.getItem('rpcPort')) || 3033)
// if authentication is enabled...
export const rpcWebToken = writable<string>(localStorage.getItem('rpcWebToken') ?? '')
// will be used to access the api and archive endpoints
export const serverApiEndpoint = derived(
[rpcHost, rpcPort],
([$host, $port]) => window.location.port == ''
? `${window.location.protocol}//${$host}`
: `${window.location.protocol}//${$host}:${$port}`
)
// access the websocket JSON-RPC 1.0 to gather downloads state
export const websocketRpcEndpoint = derived(
[rpcHost, rpcPort],
([$host, $port]) => window.location.port == ''
? `${window.location.protocol.startsWith('https') ? 'wss:' : 'ws:'}//${$host}/rpc/ws`
: `${window.location.protocol.startsWith('https') ? 'wss:' : 'ws:'}//${$host}:${$port}/rpc/ws`
)
// same as websocket one but using HTTP-POST mainly used to send commands (download, stop, ...)
export const httpPostRpcEndpoint = derived(
serverApiEndpoint,
$ep => window.location.port == '' ? `${$ep}/rpc/http` : `${$ep}/rpc/http`
)
/**
* Will handle Websocket and HTTP-POST communications based on the requested method
*/
export const rpcClient = derived(
[httpPostRpcEndpoint, websocketRpcEndpoint, rpcWebToken],
([$http, $ws, $token]) => new RPCClient($http, $ws, $token)
)
/**
* Stores all the downloads returned by the rpc
*/
export const downloads = writable<O.Option<RPCResult[]>>(O.none)
export const cookiesTemplate = writable<string>('')
/**
* fetches download templates, needs manual update
*/
export const downloadTemplates = readable<CustomTemplate[]>([], (set) => {
serverApiEndpoint
.subscribe(ep => fetch(`${ep}/api/v1/template/all`)
.then(res => res.json())
.then(data => set(data)))
})

View File

@@ -1,90 +0,0 @@
export type RPCMethods =
| "Service.Exec"
| "Service.Kill"
| "Service.Clear"
| "Service.Running"
| "Service.KillAll"
| "Service.FreeSpace"
| "Service.Formats"
| "Service.ExecPlaylist"
| "Service.DirectoryTree"
| "Service.UpdateExecutable"
export type RPCRequest = {
method: RPCMethods
params?: any[]
id?: string
}
export type RPCResponse<T> = Readonly<{
result: T
error: number | null
id?: string
}>
type DownloadInfo = {
url: string
filesize_approx?: number
resolution?: string
thumbnail: string
title: string
vcodec?: string
acodec?: string
ext?: string
created_at: string
}
type DownloadProgress = {
speed: number
eta: number
percentage: string
process_status: number
}
export type RPCResult = Readonly<{
id: string
progress: DownloadProgress
info: DownloadInfo
}>
export type RPCParams = {
URL: string
Params?: string
}
export type DLMetadata = {
formats: Array<DLFormat>
best: DLFormat
thumbnail: string
title: string
}
export type DLFormat = {
format_id: string
format_note: string
fps: number
resolution: string
vcodec: string
acodec: string
filesize_approx: number
}
export type DirectoryEntry = {
name: string
path: string
size: number
shaSum: string
modTime: string
isVideo: boolean
isDirectory: boolean
}
export type DeleteRequest = Pick<DirectoryEntry, 'path' | 'shaSum'>
export type PlayRequest = Pick<DirectoryEntry, 'path'>
export type CustomTemplate = {
id: string
name: string
content: string
}

View File

@@ -1,93 +0,0 @@
import { pipe } from 'fp-ts/lib/function'
import type { RPCResponse } from "./types"
/**
* Validate an ip v4 via regex
* @param {string} ipAddr
* @returns ip validity test
*/
export function validateIP(ipAddr: string): boolean {
let ipRegex = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/gm
return ipRegex.test(ipAddr)
}
export function validateDomain(url: string): boolean {
const urlRegex = /(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
const [name, slug] = url.split('/')
return urlRegex.test(url) || name === 'localhost' && slugRegex.test(slug)
}
export function isValidURL(url: string): boolean {
let urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
return urlRegex.test(url)
}
export function ellipsis(str: string, lim: number): string {
if (str) {
return str.length > lim ? `${str.substring(0, lim)}...` : str
}
return ''
}
export function toFormatArgs(codes: string[]): string {
if (codes.length > 1) {
return codes.reduce((v, a) => ` -f ${v}+${a}`)
}
if (codes.length === 1) {
return ` -f ${codes[0]}`
}
return ''
}
export const formatGiB = (bytes: number) =>
`${(bytes / 1_000_000_000).toFixed(0)}GiB`
export const roundMiB = (bytes: number) =>
`${(bytes / 1_000_000).toFixed(2)} MiB`
export const formatSpeedMiB = (val: number) =>
`${roundMiB(val)}/s`
export const datetimeCompareFunc = (a: string, b: string) =>
new Date(a).getTime() - new Date(b).getTime()
export function isRPCResponse(object: any): object is RPCResponse<any> {
return 'result' in object && 'id' in object
}
export function mapProcessStatus(status: number) {
switch (status) {
case 0:
return 'Pending'
case 1:
return 'Downloading'
case 2:
return 'Completed'
case 3:
return 'Error'
default:
return 'Pending'
}
}
export const prefersDarkMode = () =>
window.matchMedia('(prefers-color-scheme: dark)').matches
export const base64URLEncode = (s: string) => pipe(
s,
s => String.fromCodePoint(...new TextEncoder().encode(s)),
btoa,
encodeURIComponent
)
export const debounce = (callback: Function, wait = 300) => {
let timeout: ReturnType<typeof setTimeout>
return (...args: any[]) => {
clearTimeout(timeout)
timeout = setTimeout(() => callback(...args), wait)
}
}

View File

@@ -1,11 +0,0 @@
import './app.css'
import '@fontsource/roboto'
import '@fontsource/roboto/400-italic.css'
import '@fontsource/roboto/400.css'
import App from './App.svelte'
const app = new App({
target: document.getElementById('app'),
})
export default app

View File

@@ -1,52 +0,0 @@
<script lang="ts">
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/lib/function';
import { onDestroy } from 'svelte';
import DownloadCard from '../lib/DownloadCard.svelte';
import Spinner from '../lib/Spinner.svelte';
import { downloads, rpcClient } from '../lib/store';
import { datetimeCompareFunc, isRPCResponse } from '../lib/utils';
const unsubscribe = rpcClient.subscribe(($client) => {
setInterval(() => $client.running(), 750);
$client.socket.onmessage = (ev: any) => {
const event = JSON.parse(ev.data);
// guards
if (!isRPCResponse(event)) {
return;
}
if (!Array.isArray(event.result)) {
return;
}
if (event.result) {
return downloads.set(
O.of(
event.result
.filter((f) => !!f.info.url)
.sort((a, b) =>
datetimeCompareFunc(b.info.created_at, a.info.created_at),
),
),
);
}
downloads.set(O.none);
};
});
onDestroy(unsubscribe);
</script>
{#if O.isNone($downloads)}
<div class="h-[90vh] w-full flex justify-center items-center">
<Spinner />
</div>
{:else}
<div class="grid grid-cols-1 xl:grid-cols-2 gap-2 p-8">
{#each pipe( $downloads, O.getOrElseW(() => []), ) as download}
<DownloadCard {download} />
{/each}
</div>
{/if}

View File

@@ -1,13 +0,0 @@
<script lang="ts">
import Settings from '../lib/Settings.svelte';
</script>
<main
class="bg-neutral-100 dark:bg-neutral-800
rounded-xl
border dark:border-neutral-700
shadow-lg
m-8 p-4"
>
<Settings />
</main>

View File

@@ -1,2 +0,0 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -1,7 +0,0 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

View File

@@ -1,12 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{svelte,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -1,20 +0,0 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,9 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
})