Compare commits
21 Commits
v3.2.1
...
230-extern
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a82e51292 | |||
| 0946d374e3 | |||
|
|
f68c29f838 | ||
|
|
c46e39e736 | ||
|
|
2885d6b5d8 | ||
|
|
ab7932ae92 | ||
| 6c9118f67e | |||
| 34c78c7e2d | |||
|
|
01c6edef74 | ||
| 4a87ea559a | |||
|
|
846fb294d0 | ||
|
|
baa25afa27 | ||
|
|
b0dac0adda | ||
|
|
8c0e7b3cb8 | ||
| 38da65a848 | |||
|
|
64fbdbbbdf | ||
|
|
a00059ca88 | ||
|
|
7abccaec71 | ||
|
|
f075d3d531 | ||
|
|
190d891578 | ||
| 645cc6f9a1 |
28
.dockerignore
Normal file
28
.dockerignore
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
.pre-commit-config.yaml
|
||||||
|
.direnv/
|
||||||
|
result/
|
||||||
|
result
|
||||||
|
dist
|
||||||
|
.pnpm-debug.log
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
*.mp4
|
||||||
|
*.ytdl
|
||||||
|
*.part
|
||||||
|
*.db
|
||||||
|
downloads
|
||||||
|
.DS_Store
|
||||||
|
build/
|
||||||
|
yt-dlp-webui
|
||||||
|
session.dat
|
||||||
|
config.yml
|
||||||
|
cookies.txt
|
||||||
|
__debug*
|
||||||
|
ui/
|
||||||
|
.idea
|
||||||
|
frontend/.pnp.cjs
|
||||||
|
frontend/.pnp.loader.mjs
|
||||||
|
frontend/.yarn/install-state.gz
|
||||||
|
.db.lock
|
||||||
|
livestreams.dat
|
||||||
|
.git
|
||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
|
|||||||
issuehunt: # Replace with a single IssueHunt username
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
otechie: # Replace with a single Otechie username
|
otechie: # Replace with a single Otechie username
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: ['https://paypal.me/marcofw']
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -4,17 +4,15 @@ default:
|
|||||||
go run main.go
|
go run main.go
|
||||||
|
|
||||||
fe:
|
fe:
|
||||||
cd frontend && pnpm build
|
cd frontend && pnpm install && pnpm build
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
cd frontend && pnpm dev
|
cd frontend && pnpm install && pnpm dev
|
||||||
|
|
||||||
all:
|
all: fe
|
||||||
$(MAKE) fe && cd ..
|
|
||||||
CGO_ENABLED=0 go build -o yt-dlp-webui main.go
|
CGO_ENABLED=0 go build -o yt-dlp-webui main.go
|
||||||
|
|
||||||
multiarch:
|
multiarch: fe
|
||||||
$(MAKE) fe
|
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/yt-dlp-webui_linux-amd64 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 GOARCH=arm64 go build -o build/yt-dlp-webui_linux-arm64 main.go
|
||||||
|
|||||||
86
README.md
86
README.md
@@ -1,7 +1,14 @@
|
|||||||
|
> [!NOTE]
|
||||||
|
> A poll is up to decide the future of yt-dlp-web-ui frontend! If you're interested you can take part.
|
||||||
|
> https://github.com/marcopeocchi/yt-dlp-web-ui/discussions/223
|
||||||
|
|
||||||
# yt-dlp Web UI
|
# yt-dlp Web UI
|
||||||
|
|
||||||
A not so terrible web ui for yt-dlp.
|
A not so terrible web ui for yt-dlp.
|
||||||
Created for the only purpose of *fetching* videos from my server/nas.
|
|
||||||
|
High performance extendeable web ui and RPC server for yt-dlp with low impact on resources.
|
||||||
|
|
||||||
|
Created for the only purpose of *fetching* videos from my server/nas and monitor upcoming livestreams.
|
||||||
|
|
||||||
**Docker images are available on [Docker Hub](https://hub.docker.com/r/marcobaobao/yt-dlp-webui) or [ghcr.io](https://github.com/marcopeocchi/yt-dlp-web-ui/pkgs/container/yt-dlp-web-ui)**.
|
**Docker images are available on [Docker Hub](https://hub.docker.com/r/marcobaobao/yt-dlp-webui) or [ghcr.io](https://github.com/marcopeocchi/yt-dlp-web-ui/pkgs/container/yt-dlp-web-ui)**.
|
||||||
|
|
||||||
@@ -13,6 +20,16 @@ docker pull marcobaobao/yt-dlp-webui
|
|||||||
docker pull ghcr.io/marcopeocchi/yt-dlp-web-ui:latest
|
docker pull ghcr.io/marcopeocchi/yt-dlp-web-ui:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Donate to yt-dlp-webui development
|
||||||
|
[PayPal](https://paypal.me/marcofw)
|
||||||
|
|
||||||
|
*Keeps the project alive!* 😃
|
||||||
|
|
||||||
|
## Some screeshots
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## Video showcase
|
## Video showcase
|
||||||
[app.webm](https://github.com/marcopeocchi/yt-dlp-web-ui/assets/35533749/91545bc4-233d-4dde-8504-27422cb26964)
|
[app.webm](https://github.com/marcopeocchi/yt-dlp-web-ui/assets/35533749/91545bc4-233d-4dde-8504-27422cb26964)
|
||||||
|
|
||||||
@@ -115,21 +132,31 @@ Usage yt-dlp-webui:
|
|||||||
-auth
|
-auth
|
||||||
Enable RPC authentication
|
Enable RPC authentication
|
||||||
-conf string
|
-conf string
|
||||||
Config file path
|
Config file path (default "./config.yml")
|
||||||
|
-db string
|
||||||
|
local database path (default "local.db")
|
||||||
-driver string
|
-driver string
|
||||||
yt-dlp executable path (default "yt-dlp")
|
yt-dlp executable path (default "yt-dlp")
|
||||||
-out string
|
-fl
|
||||||
Where files will be saved (default ".")
|
enable file based logging
|
||||||
-host string
|
-host string
|
||||||
Host where server will listen at (default "0.0.0.0")
|
Host where server will listen at (default "0.0.0.0")
|
||||||
|
-lf string
|
||||||
|
set log file location (default "yt-dlp-webui.log")
|
||||||
|
-out string
|
||||||
|
Where files will be saved (default ".")
|
||||||
|
-pass string
|
||||||
|
Password required for auth
|
||||||
-port int
|
-port int
|
||||||
Port where server will listen at (default 3033)
|
Port where server will listen at (default 3033)
|
||||||
-qs int
|
-qs int
|
||||||
Download queue size (defaults to the number of logical CPU. A min of 2 is recomended.)
|
Queue size (concurrent downloads) (default 2)
|
||||||
|
-session string
|
||||||
|
session file path (default ".")
|
||||||
-user string
|
-user string
|
||||||
Username required for auth
|
Username required for auth
|
||||||
-pass string
|
-web string
|
||||||
Password required for auth
|
frontend web resources path
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config file
|
### Config file
|
||||||
@@ -155,17 +182,26 @@ require_auth: true
|
|||||||
username: my_username
|
username: my_username
|
||||||
password: my_random_secret
|
password: my_random_secret
|
||||||
|
|
||||||
# [optional] The download queue size (default: 8)
|
# [optional] The download queue size (default: logical cpu cores)
|
||||||
queue_size: 4
|
queue_size: 4 # min. 2
|
||||||
|
|
||||||
# [optional] Full path to the yt-dlp (default: "yt-dlp")
|
# [optional] Full path to the yt-dlp (default: "yt-dlp")
|
||||||
downloaderPath: /usr/local/bin/yt-dlp
|
#downloaderPath: /usr/local/bin/yt-dlp
|
||||||
|
|
||||||
|
# [optional] Enable file based logging with rotation (default: false)
|
||||||
|
#enable_file_logging: false
|
||||||
|
|
||||||
# [optional] Directory where the log file will be stored (default: ".")
|
# [optional] Directory where the log file will be stored (default: ".")
|
||||||
#log_path: .
|
#log_path: .
|
||||||
|
|
||||||
# [optional] Directory where the session database file will be stored (default: ".")
|
# [optional] Directory where the session database file will be stored (default: ".")
|
||||||
#session_file_path: .
|
#session_file_path: .
|
||||||
|
|
||||||
|
# [optional] Path where the sqlite database will be created/opened (default: "./local.db")
|
||||||
|
#local_database_path
|
||||||
|
|
||||||
|
# [optional] Path where a custom frontend will be loaded (instead of the embedded one)
|
||||||
|
#frontend_path: ./web/solid-frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
### Systemd integration
|
### Systemd integration
|
||||||
@@ -214,13 +250,9 @@ ExecStart=/usr/local/bin/yt-dlp-webui --conf /home/your_user/yt-dlp-webui-workin
|
|||||||
|
|
||||||
## Manual installation
|
## Manual installation
|
||||||
```sh
|
```sh
|
||||||
# the dependencies are: python3, ffmpeg, nodejs, psmisc, go.
|
# the dependencies are: yt-dlp, ffmpeg, nodejs, go, make.
|
||||||
|
|
||||||
cd frontend
|
make all
|
||||||
npm i
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
go build -o yt-dlp-webui main.go
|
|
||||||
```
|
```
|
||||||
## Open-API
|
## Open-API
|
||||||
Navigate to `/openapi` to see the related swagger.
|
Navigate to `/openapi` to see the related swagger.
|
||||||
@@ -235,6 +267,22 @@ It is **planned** to also expose a **gRPC** server.
|
|||||||
|
|
||||||
For more information open an issue on GitHub and I will provide more info ASAP.
|
For more information open an issue on GitHub and I will provide more info ASAP.
|
||||||
|
|
||||||
|
## Custom frontend
|
||||||
|
To load a custom frontend you need to specify its path either in the config file ([see config file](#config-file)) or via flags.
|
||||||
|
|
||||||
|
The frontend needs to follow this structure:
|
||||||
|
```
|
||||||
|
path/to/my/frontend
|
||||||
|
├── assets
|
||||||
|
│ ├── js-chunk-1.js (example)
|
||||||
|
│ ├── js-chunk-2.js (example)
|
||||||
|
│ ├── style.css (example)
|
||||||
|
└── index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
`assets` is where the resources will be loaded.
|
||||||
|
`index.html` is the entrypoint.
|
||||||
|
|
||||||
## Nix
|
## Nix
|
||||||
This repo adds support for Nix(OS) in various ways through a `flake-parts` flake.
|
This repo adds support for Nix(OS) in various ways through a `flake-parts` flake.
|
||||||
For more info, please refer to the [official documentation](https://nixos.org/learn/).
|
For more info, please refer to the [official documentation](https://nixos.org/learn/).
|
||||||
@@ -243,7 +291,7 @@ For more info, please refer to the [official documentation](https://nixos.org/le
|
|||||||
`yt-dlp-webui` isn't your ordinary website where to download stuff from the internet, so don't try asking for links of where this is hosted. It's a self hosted platform for a Linux NAS.
|
`yt-dlp-webui` isn't your ordinary website where to download stuff from the internet, so don't try asking for links of where this is hosted. It's a self hosted platform for a Linux NAS.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
- **It says that it isn't connected/ip in the header is not defined.**
|
- **It says that it isn't connected.**
|
||||||
- You must set the server ip address in the settings section (gear icon).
|
- In some circumstances, you must set the server ip address or hostname in the settings section (gear icon).
|
||||||
- **The download doesn't start.**
|
- **The download doesn't start.**
|
||||||
- As before server address is not specified or simply yt-dlp process takes a lot of time to fire up. (Forking yt-dlp isn't fast especially if you have a lower-end/low-power NAS/server/desktop where the server is running)
|
- Simply, yt-dlp process takes a lot of time to fire up. (yt-dlp isn't fast especially if you have a lower-end/low-power NAS/server/desktop. Furthermore some yt-dlp builds are slower than others)
|
||||||
|
|||||||
27
examples/docker-compose-nginx/app.conf
Normal file
27
examples/docker-compose-nginx/app.conf
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://app:3033;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|
||||||
|
client_max_body_size 20000m;
|
||||||
|
proxy_connect_timeout 5000;
|
||||||
|
proxy_send_timeout 5000;
|
||||||
|
proxy_read_timeout 5000;
|
||||||
|
send_timeout 5000;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
examples/docker-compose-nginx/docker-compose.yml
Normal file
15
examples/docker-compose-nginx/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: marcobaobao/yt-dlp-webui
|
||||||
|
volumes:
|
||||||
|
- ./downloads:/downloads
|
||||||
|
restart: unless-stopped
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./app.conf:/etc/nginx/conf.d/app.conf
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "yt-dlp-webui",
|
"name": "yt-dlp-webui",
|
||||||
"version": "3.2.1",
|
"version": "3.2.2",
|
||||||
"description": "Frontend compontent of yt-dlp-webui",
|
"description": "Frontend compontent of yt-dlp-webui",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build"
|
"build": "vite build"
|
||||||
},
|
},
|
||||||
"author": "marcopeocchi",
|
"author": "marcopeocchi",
|
||||||
"license": "MPL-2.0",
|
"license": "GPL-3.0-only",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
"react-virtuoso": "^4.7.11",
|
"react-virtuoso": "^4.7.11",
|
||||||
"recoil": "^0.7.7",
|
"jotai": "^2.10.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
55
frontend/pnpm-lock.yaml
generated
55
frontend/pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
|||||||
fp-ts:
|
fp-ts:
|
||||||
specifier: ^2.16.5
|
specifier: ^2.16.5
|
||||||
version: 2.16.5
|
version: 2.16.5
|
||||||
|
jotai:
|
||||||
|
specifier: ^2.10.2
|
||||||
|
version: 2.10.2(@types/react@18.3.3)(react@18.3.1)
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
@@ -41,9 +44,6 @@ importers:
|
|||||||
react-virtuoso:
|
react-virtuoso:
|
||||||
specifier: ^4.7.11
|
specifier: ^4.7.11
|
||||||
version: 4.7.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 4.7.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
recoil:
|
|
||||||
specifier: ^0.7.7
|
|
||||||
version: 0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
rxjs:
|
rxjs:
|
||||||
specifier: ^7.8.1
|
specifier: ^7.8.1
|
||||||
version: 7.8.1
|
version: 7.8.1
|
||||||
@@ -59,7 +59,7 @@ importers:
|
|||||||
version: 18.3.3
|
version: 18.3.3
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^18.2.18
|
specifier: ^18.2.18
|
||||||
version: 18.2.18
|
version: 18.3.1
|
||||||
'@types/react-helmet':
|
'@types/react-helmet':
|
||||||
specifier: ^6.1.11
|
specifier: ^6.1.11
|
||||||
version: 6.1.11
|
version: 6.1.11
|
||||||
@@ -689,8 +689,8 @@ packages:
|
|||||||
'@types/prop-types@15.7.11':
|
'@types/prop-types@15.7.11':
|
||||||
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
|
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
|
||||||
|
|
||||||
'@types/react-dom@18.2.18':
|
'@types/react-dom@18.3.1':
|
||||||
resolution: {integrity: sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==}
|
resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==}
|
||||||
|
|
||||||
'@types/react-helmet@6.1.11':
|
'@types/react-helmet@6.1.11':
|
||||||
resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==}
|
resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==}
|
||||||
@@ -854,9 +854,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
hamt_plus@1.0.2:
|
|
||||||
resolution: {integrity: sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==}
|
|
||||||
|
|
||||||
has-flag@3.0.0:
|
has-flag@3.0.0:
|
||||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -894,6 +891,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
|
jotai@2.10.2:
|
||||||
|
resolution: {integrity: sha512-DqsBTlRglIBviuJLfK6JxZzpd6vKfbuJ4IqRCz70RFEDeZf46Fcteb/FXxNr1UnoxR5oUy3oq7IE8BrEq0G5DQ==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=17.0.0'
|
||||||
|
react: '>=17.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -1025,18 +1034,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
engines: {node: '>=8.10.0'}
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
recoil@0.7.7:
|
|
||||||
resolution: {integrity: sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16.13.1'
|
|
||||||
react-dom: '*'
|
|
||||||
react-native: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
react-dom:
|
|
||||||
optional: true
|
|
||||||
react-native:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
regenerator-runtime@0.14.1:
|
regenerator-runtime@0.14.1:
|
||||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||||
|
|
||||||
@@ -1711,7 +1708,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/prop-types@15.7.11': {}
|
'@types/prop-types@15.7.11': {}
|
||||||
|
|
||||||
'@types/react-dom@18.2.18':
|
'@types/react-dom@18.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.3.3
|
'@types/react': 18.3.3
|
||||||
|
|
||||||
@@ -1894,8 +1891,6 @@ snapshots:
|
|||||||
|
|
||||||
globals@11.12.0: {}
|
globals@11.12.0: {}
|
||||||
|
|
||||||
hamt_plus@1.0.2: {}
|
|
||||||
|
|
||||||
has-flag@3.0.0: {}
|
has-flag@3.0.0: {}
|
||||||
|
|
||||||
has@1.0.3:
|
has@1.0.3:
|
||||||
@@ -1929,6 +1924,11 @@ snapshots:
|
|||||||
|
|
||||||
is-number@7.0.0: {}
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
|
jotai@2.10.2(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@4.1.0:
|
js-yaml@4.1.0:
|
||||||
@@ -2050,13 +2050,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
recoil@0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
|
||||||
dependencies:
|
|
||||||
hamt_plus: 1.0.2
|
|
||||||
react: 18.3.1
|
|
||||||
optionalDependencies:
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
|
|
||||||
regenerator-runtime@0.14.1: {}
|
regenerator-runtime@0.14.1: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { RouterProvider } from 'react-router-dom'
|
import { RouterProvider } from 'react-router-dom'
|
||||||
import { RecoilRoot } from 'recoil'
|
import { Provider } from 'jotai'
|
||||||
import { router } from './router'
|
import { router } from './router'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<RecoilRoot>
|
<Provider>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</RecoilRoot>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,6 @@ import Typography from '@mui/material/Typography'
|
|||||||
import { grey } from '@mui/material/colors'
|
import { grey } from '@mui/material/colors'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { Link, Outlet } from 'react-router-dom'
|
import { Link, Outlet } from 'react-router-dom'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { settingsState } from './atoms/settings'
|
import { settingsState } from './atoms/settings'
|
||||||
import AppBar from './components/AppBar'
|
import AppBar from './components/AppBar'
|
||||||
import Drawer from './components/Drawer'
|
import Drawer from './components/Drawer'
|
||||||
@@ -29,11 +28,12 @@ import SocketSubscriber from './components/SocketSubscriber'
|
|||||||
import ThemeToggler from './components/ThemeToggler'
|
import ThemeToggler from './components/ThemeToggler'
|
||||||
import { useI18n } from './hooks/useI18n'
|
import { useI18n } from './hooks/useI18n'
|
||||||
import Toaster from './providers/ToasterProvider'
|
import Toaster from './providers/ToasterProvider'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const settings = useRecoilValue(settingsState)
|
const settings = useAtomValue(settingsState)
|
||||||
|
|
||||||
const mode = settings.theme
|
const mode = settings.theme
|
||||||
const theme = useMemo(() =>
|
const theme = useMemo(() =>
|
||||||
|
|||||||
@@ -515,38 +515,38 @@ languages:
|
|||||||
customPath: 保存先
|
customPath: 保存先
|
||||||
customArgs: yt-dlpのオプションの有効化 (最適設定にする場合)
|
customArgs: yt-dlpのオプションの有効化 (最適設定にする場合)
|
||||||
customArgsInput: yt-dlpのオプション
|
customArgsInput: yt-dlpのオプション
|
||||||
rpcConnErr: Error while conencting to RPC server
|
rpcConnErr: RPCサーバーへの接続中にエラーが発生しました
|
||||||
splashText: No active downloads
|
splashText: アクティブなダウンロードはありません
|
||||||
archiveTitle: Archive
|
archiveTitle: アーカイブ
|
||||||
clipboardAction: Copied URL to clipboard
|
clipboardAction: URLをクリップボードにコピーしました
|
||||||
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
|
playlistCheckbox: プレイリストをダウンロード (これには時間がかかりますが、処理中はウィンドウを閉じることができます)
|
||||||
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
|
servedFromReverseProxyCheckbox: リバースプロキシのサブフォルダにあります
|
||||||
newDownloadButton: New download
|
newDownloadButton: 新しくダウンロード
|
||||||
homeButtonLabel: Home
|
homeButtonLabel: ホーム
|
||||||
archiveButtonLabel: Archive
|
archiveButtonLabel: アーカイブ
|
||||||
settingsButtonLabel: Settings
|
settingsButtonLabel: 設定
|
||||||
rpcAuthenticationLabel: RPC authentication
|
rpcAuthenticationLabel: RPC認証
|
||||||
themeTogglerLabel: Theme toggler
|
themeTogglerLabel: テーマ切り替え
|
||||||
loadingLabel: Loading...
|
loadingLabel: 読み込み中...
|
||||||
appTitle: App title
|
appTitle: アプリタイトル
|
||||||
savedTemplates: Saved templates
|
savedTemplates: 保存したテンプレート
|
||||||
templatesEditor: Templates editor
|
templatesEditor: テンプレートエディター
|
||||||
templatesEditorNameLabel: Template name
|
templatesEditorNameLabel: テンプレート名
|
||||||
templatesEditorContentLabel: Template content
|
templatesEditorContentLabel: テンプレート内容
|
||||||
logsTitle: 'Logs'
|
logsTitle: 'ログ'
|
||||||
awaitingLogs: 'Awaiting logs...'
|
awaitingLogs: 'ログを待機中...'
|
||||||
bulkDownload: 'Download files in a zip archive'
|
bulkDownload: 'ダウンロードしたファイルをZIPで保存'
|
||||||
livestreamURLInput: Livestream URL
|
livestreamURLInput: ライブストリームURL
|
||||||
livestreamStatusWaiting: Waiting/Wait start
|
livestreamStatusWaiting: 開始を待っています
|
||||||
livestreamStatusDownloading: Downloading
|
livestreamStatusDownloading: ダウンロード中
|
||||||
livestreamStatusCompleted: Completed
|
livestreamStatusCompleted: 完了
|
||||||
livestreamStatusErrored: Errored
|
livestreamStatusErrored: エラー
|
||||||
livestreamStatusUnknown: Unknown
|
livestreamStatusUnknown: 不明
|
||||||
livestreamDownloadInfo: |
|
livestreamDownloadInfo: |
|
||||||
This will monitor yet to start livestream. Each process will be executed with --wait-for-video 10.
|
まだ開始されていないライブストリームを監視します。各プロセスは、--wait-for-video 10 で実行されます。
|
||||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
すでに開始されているライブストリームが提供された場合、ダウンロードは継続されますが進行状況は追跡されません。
|
||||||
Once started the livestream will be migrated to the downloads page.
|
ライブストリームが開始されると、ダウンロードページに移動されます。
|
||||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
livestreamExperimentalWarning: この機能は実験的なものです。何かが壊れるかもしれません!
|
||||||
catalan:
|
catalan:
|
||||||
urlInput: URL de YouTube o d'un altre servei compatible
|
urlInput: URL de YouTube o d'un altre servei compatible
|
||||||
statusTitle: Estat
|
statusTitle: Estat
|
||||||
|
|||||||
@@ -1,44 +1,34 @@
|
|||||||
import { getOrElse } from 'fp-ts/lib/Either'
|
import { getOrElse } from 'fp-ts/lib/Either'
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { atom, selector } from 'recoil'
|
|
||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
import { CustomTemplate } from '../types'
|
import { CustomTemplate } from '../types'
|
||||||
import { serverSideCookiesState, serverURL } from './settings'
|
import { serverSideCookiesState, serverURL } from './settings'
|
||||||
|
import { atom } from 'jotai'
|
||||||
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
|
|
||||||
export const cookiesTemplateState = selector({
|
export const cookiesTemplateState = atom<Promise<string>>(async (get) =>
|
||||||
key: 'cookiesTemplateState',
|
await get(serverSideCookiesState)
|
||||||
get: ({ get }) => get(serverSideCookiesState)
|
|
||||||
? '--cookies=cookies.txt'
|
? '--cookies=cookies.txt'
|
||||||
: ''
|
: ''
|
||||||
})
|
)
|
||||||
|
|
||||||
export const customArgsState = atom({
|
export const customArgsState = atomWithStorage(
|
||||||
key: 'customArgsState',
|
'customArgs',
|
||||||
default: localStorage.getItem('customArgs') ?? '',
|
localStorage.getItem('customArgs') ?? ''
|
||||||
effects: [
|
)
|
||||||
({ onSet }) => onSet(e => localStorage.setItem('customArgs', e))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const filenameTemplateState = atom({
|
export const filenameTemplateState = atomWithStorage(
|
||||||
key: 'filenameTemplateState',
|
'lastFilenameTemplate',
|
||||||
default: localStorage.getItem('lastFilenameTemplate') ?? '',
|
localStorage.getItem('lastFilenameTemplate') ?? ''
|
||||||
effects: [
|
)
|
||||||
({ onSet }) => onSet(e => localStorage.setItem('lastFilenameTemplate', e))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const downloadTemplateState = selector({
|
export const downloadTemplateState = atom<string>((get) =>
|
||||||
key: 'downloadTemplateState',
|
|
||||||
get: ({ get }) =>
|
|
||||||
`${get(customArgsState)} ${get(cookiesTemplateState)}`
|
`${get(customArgsState)} ${get(cookiesTemplateState)}`
|
||||||
.replace(/ +/g, ' ')
|
.replace(/ +/g, ' ')
|
||||||
.trim()
|
.trim()
|
||||||
})
|
)
|
||||||
|
|
||||||
export const savedTemplatesState = selector<CustomTemplate[]>({
|
export const savedTemplatesState = atom<Promise<CustomTemplate[]>>(async (get) => {
|
||||||
key: 'savedTemplatesState',
|
|
||||||
get: async ({ get }) => {
|
|
||||||
const task = ffetch<CustomTemplate[]>(`${get(serverURL)}/api/v1/template/all`)
|
const task = ffetch<CustomTemplate[]>(`${get(serverURL)}/api/v1/template/all`)
|
||||||
const either = await task()
|
const either = await task()
|
||||||
|
|
||||||
@@ -46,5 +36,5 @@ export const savedTemplatesState = selector<CustomTemplate[]>({
|
|||||||
either,
|
either,
|
||||||
getOrElse(() => new Array<CustomTemplate>())
|
getOrElse(() => new Array<CustomTemplate>())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
@@ -1,22 +1,13 @@
|
|||||||
import * as O from 'fp-ts/Option'
|
import * as O from 'fp-ts/Option'
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { atom, selector } from 'recoil'
|
|
||||||
import { RPCResult } from '../types'
|
import { RPCResult } from '../types'
|
||||||
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
export const downloadsState = atom<O.Option<RPCResult[]>>({
|
export const downloadsState = atom<O.Option<RPCResult[]>>(O.none)
|
||||||
key: 'downloadsState',
|
|
||||||
default: O.none
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadingDownloadsState = selector<boolean>({
|
export const loadingDownloadsState = atom<boolean>((get) => O.isNone(get(downloadsState)))
|
||||||
key: 'loadingDownloadsState',
|
|
||||||
get: ({ get }) => O.isNone(get(downloadsState))
|
|
||||||
})
|
|
||||||
|
|
||||||
export const activeDownloadsState = selector<RPCResult[]>({
|
export const activeDownloadsState = atom<RPCResult[]>((get) => pipe(
|
||||||
key: 'activeDownloadsState',
|
|
||||||
get: ({ get }) => pipe(
|
|
||||||
get(downloadsState),
|
get(downloadsState),
|
||||||
O.getOrElse(() => new Array<RPCResult>())
|
O.getOrElse(() => new Array<RPCResult>())
|
||||||
)
|
))
|
||||||
})
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import { selector } from 'recoil'
|
import { atom } from 'jotai'
|
||||||
import I18nBuilder from '../lib/intl'
|
import I18nBuilder from '../lib/intl'
|
||||||
import { languageState } from './settings'
|
import { languageState } from './settings'
|
||||||
|
|
||||||
export const i18nBuilderState = selector({
|
export const i18nBuilderState = atom((get) => new I18nBuilder(get(languageState)))
|
||||||
key: 'i18nBuilderState',
|
|
||||||
get: ({ get }) => new I18nBuilder(get(languageState)),
|
|
||||||
dangerouslyAllowMutability: true,
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
import { atom, selector } from 'recoil'
|
import { atom } from 'jotai'
|
||||||
import { RPCClient } from '../lib/rpcClient'
|
import { RPCClient } from '../lib/rpcClient'
|
||||||
import { rpcHTTPEndpoint, rpcWebSocketEndpoint } from './settings'
|
import { rpcHTTPEndpoint, rpcWebSocketEndpoint } from './settings'
|
||||||
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
|
|
||||||
export const rpcClientState = selector({
|
export const rpcClientState = atom((get) =>
|
||||||
key: 'rpcClientState',
|
|
||||||
get: ({ get }) =>
|
|
||||||
new RPCClient(
|
new RPCClient(
|
||||||
get(rpcHTTPEndpoint),
|
get(rpcHTTPEndpoint),
|
||||||
get(rpcWebSocketEndpoint),
|
get(rpcWebSocketEndpoint),
|
||||||
localStorage.getItem('token') ?? ''
|
localStorage.getItem('token') ?? ''
|
||||||
),
|
),
|
||||||
dangerouslyAllowMutability: true,
|
)
|
||||||
})
|
|
||||||
|
|
||||||
export const rpcPollingTimeState = atom({
|
export const rpcPollingTimeState = atomWithStorage(
|
||||||
key: 'rpcPollingTimeState',
|
'rpc-polling-time',
|
||||||
default: Number(localStorage.getItem('rpc-polling-time')) || 1000,
|
Number(localStorage.getItem('rpc-polling-time')) || 1000
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('rpc-polling-time', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
@@ -1,19 +1,22 @@
|
|||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { matchW } from 'fp-ts/lib/TaskEither'
|
import { matchW } from 'fp-ts/lib/TaskEither'
|
||||||
import { atom, selector } from 'recoil'
|
|
||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
import { prefersDarkMode } from '../utils'
|
import { prefersDarkMode } from '../utils'
|
||||||
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
export const languages = [
|
export const languages = [
|
||||||
'english',
|
'english',
|
||||||
'chinese',
|
'chinese',
|
||||||
'russian',
|
'russian',
|
||||||
|
'french',
|
||||||
'italian',
|
'italian',
|
||||||
'spanish',
|
'spanish',
|
||||||
'korean',
|
'korean',
|
||||||
'japanese',
|
'japanese',
|
||||||
'catalan',
|
'catalan',
|
||||||
'ukrainian',
|
'ukrainian',
|
||||||
|
'swedish',
|
||||||
'polish',
|
'polish',
|
||||||
'german'
|
'german'
|
||||||
] as const
|
] as const
|
||||||
@@ -38,126 +41,72 @@ export interface SettingsState {
|
|||||||
appTitle: string
|
appTitle: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const languageState = atom<Language>({
|
export const languageState = atomWithStorage<Language>(
|
||||||
key: 'languageState',
|
'language',
|
||||||
default: localStorage.getItem('language') as Language || 'english',
|
localStorage.getItem('language') as Language || 'english'
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(l => localStorage.setItem('language', l.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const themeState = atom<Theme>({
|
export const themeState = atomWithStorage<Theme>(
|
||||||
key: 'themeStateState',
|
'theme',
|
||||||
default: localStorage.getItem('theme') as Theme || 'system',
|
localStorage.getItem('theme') as Theme || 'system'
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(l => localStorage.setItem('theme', l.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const serverAddressState = atom<string>({
|
export const serverAddressState = atomWithStorage<string>(
|
||||||
key: 'serverAddressState',
|
'server-addr',
|
||||||
default: localStorage.getItem('server-addr') || window.location.hostname,
|
localStorage.getItem('server-addr') || window.location.hostname
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('server-addr', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const serverPortState = atom<number>({
|
export const serverPortState = atomWithStorage<number>(
|
||||||
key: 'serverPortState',
|
'server-port',
|
||||||
default: Number(localStorage.getItem('server-port')) || Number(window.location.port),
|
Number(localStorage.getItem('server-port')) || Number(window.location.port)
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('server-port', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const latestCliArgumentsState = atom<string>({
|
export const latestCliArgumentsState = atomWithStorage<string>(
|
||||||
key: 'latestCliArgumentsState',
|
'cli-args',
|
||||||
default: localStorage.getItem('cli-args') || '--no-mtime',
|
localStorage.getItem('cli-args') || '--no-mtime'
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('cli-args', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const formatSelectionState = atom({
|
export const formatSelectionState = atomWithStorage(
|
||||||
key: 'formatSelectionState',
|
'format-selection',
|
||||||
default: localStorage.getItem('format-selection') === "true",
|
localStorage.getItem('format-selection') === 'true'
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('format-selection', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const fileRenamingState = atom({
|
export const fileRenamingState = atomWithStorage(
|
||||||
key: 'fileRenamingState',
|
'file-renaming',
|
||||||
default: localStorage.getItem('file-renaming') === "true",
|
localStorage.getItem('file-renaming') === 'true'
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('file-renaming', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const pathOverridingState = atom({
|
export const pathOverridingState = atomWithStorage(
|
||||||
key: 'pathOverridingState',
|
'path-overriding',
|
||||||
default: localStorage.getItem('path-overriding') === "true",
|
localStorage.getItem('path-overriding') === 'true'
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('path-overriding', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const enableCustomArgsState = atom({
|
export const enableCustomArgsState = atomWithStorage(
|
||||||
key: 'enableCustomArgsState',
|
'enable-custom-args',
|
||||||
default: localStorage.getItem('enable-custom-args') === "true",
|
localStorage.getItem('enable-custom-args') === 'true'
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('enable-custom-args', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const listViewState = atom({
|
export const listViewState = atomWithStorage(
|
||||||
key: 'listViewState',
|
'listview',
|
||||||
default: localStorage.getItem('listview') === "true",
|
localStorage.getItem('listview') === 'true'
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('listview', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const servedFromReverseProxyState = atom({
|
export const servedFromReverseProxyState = atomWithStorage(
|
||||||
key: 'servedFromReverseProxyState',
|
'reverseProxy',
|
||||||
default: localStorage.getItem('reverseProxy') === "true" || window.location.port == "",
|
localStorage.getItem('reverseProxy') === 'true' || window.location.port == ''
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('reverseProxy', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const servedFromReverseProxySubDirState = atom<string>({
|
export const servedFromReverseProxySubDirState = atomWithStorage<string>(
|
||||||
key: 'servedFromReverseProxySubDirState',
|
'reverseProxySubDir',
|
||||||
default: localStorage.getItem('reverseProxySubDir') ?? '',
|
localStorage.getItem('reverseProxySubDir') ?? ''
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('reverseProxySubDir', a))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const appTitleState = atom({
|
export const appTitleState = atomWithStorage(
|
||||||
key: 'appTitleState',
|
'appTitle',
|
||||||
default: localStorage.getItem('appTitle') ?? 'yt-dlp Web UI',
|
localStorage.getItem('appTitle') ?? 'yt-dlp Web UI'
|
||||||
effects: [
|
)
|
||||||
({ onSet }) =>
|
|
||||||
onSet(a => localStorage.setItem('appTitle', a.toString()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const serverAddressAndPortState = selector({
|
export const serverAddressAndPortState = atom((get) => {
|
||||||
key: 'serverAddressAndPortState',
|
|
||||||
get: ({ get }) => {
|
|
||||||
if (get(servedFromReverseProxySubDirState)) {
|
if (get(servedFromReverseProxySubDirState)) {
|
||||||
return `${get(serverAddressState)}/${get(servedFromReverseProxySubDirState)}/`
|
return `${get(serverAddressState)}/${get(servedFromReverseProxySubDirState)}/`
|
||||||
}
|
}
|
||||||
@@ -165,56 +114,42 @@ export const serverAddressAndPortState = selector({
|
|||||||
return `${get(serverAddressState)}`
|
return `${get(serverAddressState)}`
|
||||||
}
|
}
|
||||||
return `${get(serverAddressState)}:${get(serverPortState)}`
|
return `${get(serverAddressState)}:${get(serverPortState)}`
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const serverURL = selector({
|
export const serverURL = atom((get) =>
|
||||||
key: 'serverURL',
|
|
||||||
get: ({ get }) =>
|
|
||||||
`${window.location.protocol}//${get(serverAddressAndPortState)}`
|
`${window.location.protocol}//${get(serverAddressAndPortState)}`
|
||||||
})
|
)
|
||||||
|
|
||||||
export const rpcWebSocketEndpoint = selector({
|
export const rpcWebSocketEndpoint = atom((get) => {
|
||||||
key: 'rpcWebSocketEndpoint',
|
|
||||||
get: ({ get }) => {
|
|
||||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
return `${proto}//${get(serverAddressAndPortState)}/rpc/ws`
|
return `${proto}//${get(serverAddressAndPortState)}/rpc/ws`
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
export const rpcHTTPEndpoint = selector({
|
export const rpcHTTPEndpoint = atom((get) => {
|
||||||
key: 'rpcHTTPEndpoint',
|
|
||||||
get: ({ get }) => {
|
|
||||||
const proto = window.location.protocol
|
const proto = window.location.protocol
|
||||||
return `${proto}//${get(serverAddressAndPortState)}/rpc/http`
|
return `${proto}//${get(serverAddressAndPortState)}/rpc/http`
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
export const serverSideCookiesState = selector<string>({
|
export const serverSideCookiesState = atom<Promise<string>>(async (get) => await pipe(
|
||||||
key: 'serverSideCookiesState',
|
|
||||||
get: async ({ get }) => await pipe(
|
|
||||||
ffetch<Readonly<{ cookies: string }>>(`${get(serverURL)}/api/v1/cookies`),
|
ffetch<Readonly<{ cookies: string }>>(`${get(serverURL)}/api/v1/cookies`),
|
||||||
matchW(
|
matchW(
|
||||||
() => '',
|
() => '',
|
||||||
(r) => r.cookies
|
(r) => r.cookies
|
||||||
)
|
)
|
||||||
)()
|
)())
|
||||||
})
|
|
||||||
|
|
||||||
const themeSelector = selector<ThemeNarrowed>({
|
const themeSelector = atom<ThemeNarrowed>((get) => {
|
||||||
key: 'themeSelector',
|
|
||||||
get: ({ get }) => {
|
|
||||||
const theme = get(themeState)
|
const theme = get(themeState)
|
||||||
if ((theme === 'system' && prefersDarkMode()) || theme === 'dark') {
|
if ((theme === 'system' && prefersDarkMode()) || theme === 'dark') {
|
||||||
return 'dark'
|
return 'dark'
|
||||||
}
|
}
|
||||||
return 'light'
|
return 'light'
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
export const settingsState = selector<SettingsState>({
|
export const settingsState = atom<SettingsState>((get) => ({
|
||||||
key: 'settingsState',
|
|
||||||
get: ({ get }) => ({
|
|
||||||
serverAddr: get(serverAddressState),
|
serverAddr: get(serverAddressState),
|
||||||
serverPort: get(serverPortState),
|
serverPort: get(serverPortState),
|
||||||
language: get(languageState),
|
language: get(languageState),
|
||||||
@@ -227,5 +162,5 @@ export const settingsState = selector<SettingsState>({
|
|||||||
listView: get(listViewState),
|
listView: get(listViewState),
|
||||||
servedFromReverseProxy: get(servedFromReverseProxyState),
|
servedFromReverseProxy: get(servedFromReverseProxyState),
|
||||||
appTitle: get(appTitleState)
|
appTitle: get(appTitleState)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
@@ -1,38 +1,28 @@
|
|||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { of } from 'fp-ts/lib/Task'
|
import { of } from 'fp-ts/lib/Task'
|
||||||
import { getOrElse } from 'fp-ts/lib/TaskEither'
|
import { getOrElse } from 'fp-ts/lib/TaskEither'
|
||||||
import { atom, selector } from 'recoil'
|
|
||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
import { RPCVersion } from '../types'
|
import { RPCVersion } from '../types'
|
||||||
import { rpcClientState } from './rpc'
|
import { rpcClientState } from './rpc'
|
||||||
import { serverURL } from './settings'
|
import { serverURL } from './settings'
|
||||||
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
export const connectedState = atom({
|
export const connectedState = atom(false)
|
||||||
key: 'connectedState',
|
|
||||||
default: false
|
|
||||||
})
|
|
||||||
|
|
||||||
export const freeSpaceBytesState = selector({
|
export const freeSpaceBytesState = atom(async (get) => {
|
||||||
key: 'freeSpaceBytesState',
|
const res = await get(rpcClientState)
|
||||||
get: async ({ get }) => {
|
.freeSpace()
|
||||||
const res = await get(rpcClientState).freeSpace()
|
|
||||||
.catch(() => ({ result: 0 }))
|
.catch(() => ({ result: 0 }))
|
||||||
return res.result
|
return res.result
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const availableDownloadPathsState = selector({
|
export const availableDownloadPathsState = atom(async (get) => {
|
||||||
key: 'availableDownloadPathsState',
|
|
||||||
get: async ({ get }) => {
|
|
||||||
const res = await get(rpcClientState).directoryTree()
|
const res = await get(rpcClientState).directoryTree()
|
||||||
.catch(() => ({ result: [] }))
|
.catch(() => ({ result: [] }))
|
||||||
return res.result
|
return res.result
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ytdlpRpcVersionState = selector<RPCVersion>({
|
export const ytdlpRpcVersionState = atom<Promise<RPCVersion>>(async (get) => await pipe(
|
||||||
key: 'ytdlpRpcVersionState',
|
|
||||||
get: async ({ get }) => await pipe(
|
|
||||||
ffetch<RPCVersion>(`${get(serverURL)}/api/v1/version`),
|
ffetch<RPCVersion>(`${get(serverURL)}/api/v1/version`),
|
||||||
getOrElse(() => pipe(
|
getOrElse(() => pipe(
|
||||||
{
|
{
|
||||||
@@ -41,5 +31,4 @@ export const ytdlpRpcVersionState = selector<RPCVersion>({
|
|||||||
},
|
},
|
||||||
of
|
of
|
||||||
)),
|
)),
|
||||||
)()
|
)())
|
||||||
})
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AlertColor } from '@mui/material'
|
import { AlertColor } from '@mui/material'
|
||||||
import { atom } from 'recoil'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
export type Toast = {
|
export type Toast = {
|
||||||
open: boolean,
|
open: boolean,
|
||||||
@@ -9,7 +9,4 @@ export type Toast = {
|
|||||||
severity?: AlertColor
|
severity?: AlertColor
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toastListState = atom<Toast[]>({
|
export const toastListState = atom<Toast[]>([])
|
||||||
key: 'toastListState',
|
|
||||||
default: [],
|
|
||||||
})
|
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import { atom, selector } from 'recoil'
|
import { atom } from 'jotai'
|
||||||
import { activeDownloadsState } from './downloads'
|
import { activeDownloadsState } from './downloads'
|
||||||
|
|
||||||
export const loadingAtom = atom({
|
export const loadingAtom = atom(true)
|
||||||
key: 'loadingAtom',
|
|
||||||
default: true
|
|
||||||
})
|
|
||||||
|
|
||||||
export const totalDownloadSpeedState = selector<number>({
|
export const totalDownloadSpeedState = atom<number>((get) =>
|
||||||
key: 'totalDownloadSpeedState',
|
get(activeDownloadsState)
|
||||||
get: ({ get }) => get(activeDownloadsState)
|
|
||||||
.map(d => d.progress.speed)
|
.map(d => d.progress.speed)
|
||||||
.reduce((curr, next) => curr + next, 0)
|
.reduce((curr, next) => curr + next, 0)
|
||||||
})
|
)
|
||||||
@@ -5,12 +5,12 @@ import * as O from 'fp-ts/Option'
|
|||||||
import { matchW } from 'fp-ts/lib/TaskEither'
|
import { matchW } from 'fp-ts/lib/TaskEither'
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { Subject, debounceTime, distinctUntilChanged } from 'rxjs'
|
import { Subject, debounceTime, distinctUntilChanged } from 'rxjs'
|
||||||
import { serverSideCookiesState, serverURL } from '../atoms/settings'
|
import { serverSideCookiesState, serverURL } from '../atoms/settings'
|
||||||
import { useSubscription } from '../hooks/observable'
|
import { useSubscription } from '../hooks/observable'
|
||||||
import { useToast } from '../hooks/toast'
|
import { useToast } from '../hooks/toast'
|
||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const validateCookie = (cookie: string) => pipe(
|
const validateCookie = (cookie: string) => pipe(
|
||||||
cookie,
|
cookie,
|
||||||
@@ -75,8 +75,8 @@ const noopValidator = (s: string): E.Either<string, string[]> => pipe(
|
|||||||
const isCommentOrNewLine = (s: string) => s === '' || s.startsWith('\n') || s.startsWith('#')
|
const isCommentOrNewLine = (s: string) => s === '' || s.startsWith('\n') || s.startsWith('#')
|
||||||
|
|
||||||
const CookiesTextField: React.FC = () => {
|
const CookiesTextField: React.FC = () => {
|
||||||
const serverAddr = useRecoilValue(serverURL)
|
const serverAddr = useAtomValue(serverURL)
|
||||||
const savedCookies = useRecoilValue(serverSideCookiesState)
|
const savedCookies = useAtomValue(serverSideCookiesState)
|
||||||
|
|
||||||
const { pushMessage } = useToast()
|
const { pushMessage } = useToast()
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { serverURL } from '../atoms/settings'
|
import { serverURL } from '../atoms/settings'
|
||||||
import { RPCResult } from '../types'
|
import { RPCResult } from '../types'
|
||||||
import { base64URLEncode, ellipsis, formatSize, formatSpeedMiB, mapProcessStatus } from '../utils'
|
import { base64URLEncode, ellipsis, formatSize, formatSpeedMiB, mapProcessStatus } from '../utils'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
download: RPCResult
|
download: RPCResult
|
||||||
@@ -37,7 +37,7 @@ const Resolution: React.FC<{ resolution?: string }> = ({ resolution }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DownloadCard: React.FC<Props> = ({ download, onStop, onCopy }) => {
|
const DownloadCard: React.FC<Props> = ({ download, onStop, onCopy }) => {
|
||||||
const serverAddr = useRecoilValue(serverURL)
|
const serverAddr = useAtomValue(serverURL)
|
||||||
|
|
||||||
const isCompleted = useCallback(
|
const isCompleted = useCallback(
|
||||||
() => download.progress.percentage === '-1',
|
() => download.progress.percentage === '-1',
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
useTransition
|
useTransition
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
|
||||||
import { customArgsState, downloadTemplateState, filenameTemplateState, savedTemplatesState } from '../atoms/downloadTemplate'
|
import { customArgsState, downloadTemplateState, filenameTemplateState, savedTemplatesState } from '../atoms/downloadTemplate'
|
||||||
import { settingsState } from '../atoms/settings'
|
import { settingsState } from '../atoms/settings'
|
||||||
import { availableDownloadPathsState, connectedState } from '../atoms/status'
|
import { availableDownloadPathsState, connectedState } from '../atoms/status'
|
||||||
@@ -40,6 +39,9 @@ import { useRPC } from '../hooks/useRPC'
|
|||||||
import type { DLMetadata } from '../types'
|
import type { DLMetadata } from '../types'
|
||||||
import { toFormatArgs } from '../utils'
|
import { toFormatArgs } from '../utils'
|
||||||
import ExtraDownloadOptions from './ExtraDownloadOptions'
|
import ExtraDownloadOptions from './ExtraDownloadOptions'
|
||||||
|
import { useToast } from '../hooks/toast'
|
||||||
|
import LoadingBackdrop from './LoadingBackdrop'
|
||||||
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const Transition = forwardRef(function Transition(
|
const Transition = forwardRef(function Transition(
|
||||||
props: TransitionProps & {
|
props: TransitionProps & {
|
||||||
@@ -57,22 +59,23 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
|
const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
|
||||||
const settings = useRecoilValue(settingsState)
|
const settings = useAtomValue(settingsState)
|
||||||
const isConnected = useRecoilValue(connectedState)
|
const isConnected = useAtomValue(connectedState)
|
||||||
const availableDownloadPaths = useRecoilValue(availableDownloadPathsState)
|
const availableDownloadPaths = useAtomValue(availableDownloadPathsState)
|
||||||
const downloadTemplate = useRecoilValue(downloadTemplateState)
|
const downloadTemplate = useAtomValue(downloadTemplateState)
|
||||||
const savedTemplates = useRecoilValue(savedTemplatesState)
|
const savedTemplates = useAtomValue(savedTemplatesState)
|
||||||
|
|
||||||
const [downloadFormats, setDownloadFormats] = useState<DLMetadata>()
|
const [downloadFormats, setDownloadFormats] = useState<DLMetadata>()
|
||||||
const [pickedVideoFormat, setPickedVideoFormat] = useState('')
|
const [pickedVideoFormat, setPickedVideoFormat] = useState('')
|
||||||
const [pickedAudioFormat, setPickedAudioFormat] = useState('')
|
const [pickedAudioFormat, setPickedAudioFormat] = useState('')
|
||||||
const [pickedBestFormat, setPickedBestFormat] = useState('')
|
const [pickedBestFormat, setPickedBestFormat] = useState('')
|
||||||
|
const [isFormatsLoading, setIsFormatsLoading] = useState(false)
|
||||||
|
|
||||||
const [customArgs, setCustomArgs] = useRecoilState(customArgsState)
|
const [customArgs, setCustomArgs] = useAtom(customArgsState)
|
||||||
|
|
||||||
const [downloadPath, setDownloadPath] = useState('')
|
const [downloadPath, setDownloadPath] = useState('')
|
||||||
|
|
||||||
const [filenameTemplate, setFilenameTemplate] = useRecoilState(
|
const [filenameTemplate, setFilenameTemplate] = useAtom(
|
||||||
filenameTemplateState
|
filenameTemplateState
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,6 +85,7 @@ const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
|
|||||||
|
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
const { client } = useRPC()
|
const { client } = useRPC()
|
||||||
|
const { pushMessage } = useToast()
|
||||||
|
|
||||||
const urlInputRef = useRef<HTMLInputElement>(null)
|
const urlInputRef = useRef<HTMLInputElement>(null)
|
||||||
const customFilenameInputRef = useRef<HTMLInputElement>(null)
|
const customFilenameInputRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -129,11 +133,28 @@ const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
|
|||||||
setPickedVideoFormat('')
|
setPickedVideoFormat('')
|
||||||
setPickedBestFormat('')
|
setPickedBestFormat('')
|
||||||
|
|
||||||
|
|
||||||
|
if (isPlaylist) {
|
||||||
|
pushMessage('Format selection on playlist is not supported', 'warning')
|
||||||
|
resetInput()
|
||||||
|
onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFormatsLoading(true)
|
||||||
|
|
||||||
client.formats(url)
|
client.formats(url)
|
||||||
?.then(formats => {
|
?.then(formats => {
|
||||||
|
if (formats.result._type === 'playlist') {
|
||||||
|
pushMessage('Format selection on playlist is not supported. Downloading as playlist.', 'info')
|
||||||
|
resetInput()
|
||||||
|
onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
setDownloadFormats(formats.result)
|
setDownloadFormats(formats.result)
|
||||||
resetInput()
|
resetInput()
|
||||||
})
|
})
|
||||||
|
.then(() => setIsFormatsLoading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -175,10 +196,7 @@ const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
TransitionComponent={Transition}
|
TransitionComponent={Transition}
|
||||||
>
|
>
|
||||||
<Backdrop
|
<LoadingBackdrop isLoading={isPending || isFormatsLoading} />
|
||||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
|
||||||
open={isPending}
|
|
||||||
/>
|
|
||||||
<AppBar sx={{ position: 'relative' }}>
|
<AppBar sx={{ position: 'relative' }}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
|
||||||
import { loadingDownloadsState } from '../atoms/downloads'
|
import { loadingDownloadsState } from '../atoms/downloads'
|
||||||
import { listViewState } from '../atoms/settings'
|
import { listViewState } from '../atoms/settings'
|
||||||
import { loadingAtom } from '../atoms/ui'
|
import { loadingAtom } from '../atoms/ui'
|
||||||
@@ -7,10 +7,10 @@ import DownloadsGridView from './DownloadsGridView'
|
|||||||
import DownloadsTableView from './DownloadsTableView'
|
import DownloadsTableView from './DownloadsTableView'
|
||||||
|
|
||||||
const Downloads: React.FC = () => {
|
const Downloads: React.FC = () => {
|
||||||
const tableView = useRecoilValue(listViewState)
|
const tableView = useAtomValue(listViewState)
|
||||||
const loadingDownloads = useRecoilValue(loadingDownloadsState)
|
const loadingDownloads = useAtomValue(loadingDownloadsState)
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useRecoilState(loadingAtom)
|
const [isLoading, setIsLoading] = useAtom(loadingAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingDownloads) {
|
if (loadingDownloads) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Grid } from '@mui/material'
|
import { Grid } from '@mui/material'
|
||||||
import { useRecoilValue } from 'recoil'
|
import { useAtomValue } from 'jotai'
|
||||||
import { activeDownloadsState } from '../atoms/downloads'
|
import { activeDownloadsState } from '../atoms/downloads'
|
||||||
import { useToast } from '../hooks/toast'
|
import { useToast } from '../hooks/toast'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
@@ -8,7 +8,7 @@ import { ProcessStatus, RPCResult } from '../types'
|
|||||||
import DownloadCard from './DownloadCard'
|
import DownloadCard from './DownloadCard'
|
||||||
|
|
||||||
const DownloadsGridView: React.FC = () => {
|
const DownloadsGridView: React.FC = () => {
|
||||||
const downloads = useRecoilValue(activeDownloadsState)
|
const downloads = useAtomValue(activeDownloadsState)
|
||||||
|
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
const { client } = useRPC()
|
const { client } = useRPC()
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import {
|
|||||||
} from "@mui/material"
|
} from "@mui/material"
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
import { TableComponents, TableVirtuoso } from 'react-virtuoso'
|
import { TableComponents, TableVirtuoso } from 'react-virtuoso'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { activeDownloadsState } from '../atoms/downloads'
|
import { activeDownloadsState } from '../atoms/downloads'
|
||||||
import { serverURL } from '../atoms/settings'
|
import { serverURL } from '../atoms/settings'
|
||||||
import { useRPC } from '../hooks/useRPC'
|
import { useRPC } from '../hooks/useRPC'
|
||||||
import { ProcessStatus, RPCResult } from '../types'
|
import { ProcessStatus, RPCResult } from '../types'
|
||||||
import { base64URLEncode, formatSize, formatSpeedMiB } from "../utils"
|
import { base64URLEncode, formatSize, formatSpeedMiB } from "../utils"
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -119,8 +119,8 @@ function fixedHeaderContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DownloadsTableView: React.FC = () => {
|
const DownloadsTableView: React.FC = () => {
|
||||||
const downloads = useRecoilValue(activeDownloadsState)
|
const downloads = useAtomValue(activeDownloadsState)
|
||||||
const serverAddr = useRecoilValue(serverURL)
|
const serverAddr = useAtomValue(serverURL)
|
||||||
const { client } = useRPC()
|
const { client } = useRPC()
|
||||||
|
|
||||||
const viewFile = (path: string) => {
|
const viewFile = (path: string) => {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Autocomplete, Box, TextField, Typography } from '@mui/material'
|
import { Autocomplete, Box, TextField, Typography } from '@mui/material'
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
|
||||||
import { customArgsState, savedTemplatesState } from '../atoms/downloadTemplate'
|
import { customArgsState, savedTemplatesState } from '../atoms/downloadTemplate'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const ExtraDownloadOptions: React.FC = () => {
|
const ExtraDownloadOptions: React.FC = () => {
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
|
|
||||||
const customTemplates = useRecoilValue(savedTemplatesState)
|
const customTemplates = useAtomValue(savedTemplatesState)
|
||||||
const [, setCustomArgs] = useRecoilState(customArgsState)
|
const [, setCustomArgs] = useAtom(customArgsState)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import DownloadIcon from '@mui/icons-material/Download'
|
|||||||
import SettingsEthernet from '@mui/icons-material/SettingsEthernet'
|
import SettingsEthernet from '@mui/icons-material/SettingsEthernet'
|
||||||
import { AppBar, CircularProgress, Divider, Toolbar } from '@mui/material'
|
import { AppBar, CircularProgress, Divider, Toolbar } from '@mui/material'
|
||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { settingsState } from '../atoms/settings'
|
import { settingsState } from '../atoms/settings'
|
||||||
import { connectedState } from '../atoms/status'
|
import { connectedState } from '../atoms/status'
|
||||||
import { totalDownloadSpeedState } from '../atoms/ui'
|
import { totalDownloadSpeedState } from '../atoms/ui'
|
||||||
@@ -10,11 +9,12 @@ import { useI18n } from '../hooks/useI18n'
|
|||||||
import { formatSpeedMiB } from '../utils'
|
import { formatSpeedMiB } from '../utils'
|
||||||
import FreeSpaceIndicator from './FreeSpaceIndicator'
|
import FreeSpaceIndicator from './FreeSpaceIndicator'
|
||||||
import VersionIndicator from './VersionIndicator'
|
import VersionIndicator from './VersionIndicator'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
const settings = useRecoilValue(settingsState)
|
const settings = useAtomValue(settingsState)
|
||||||
const isConnected = useRecoilValue(connectedState)
|
const isConnected = useAtomValue(connectedState)
|
||||||
const totalDownloadSpeed = useRecoilValue(totalDownloadSpeedState)
|
const totalDownloadSpeed = useAtomValue(totalDownloadSpeedState)
|
||||||
|
|
||||||
const mode = settings.theme
|
const mode = settings.theme
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import StorageIcon from '@mui/icons-material/Storage'
|
import StorageIcon from '@mui/icons-material/Storage'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { freeSpaceBytesState } from '../atoms/status'
|
import { freeSpaceBytesState } from '../atoms/status'
|
||||||
import { formatSize } from '../utils'
|
import { formatSize } from '../utils'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const FreeSpaceIndicator = () => {
|
const FreeSpaceIndicator = () => {
|
||||||
const freeSpace = useRecoilValue(freeSpaceBytesState)
|
const freeSpace = useAtomValue(freeSpaceBytesState)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { useSetAtom } from 'jotai'
|
||||||
import { Suspense, useState } from 'react'
|
import { Suspense, useState } from 'react'
|
||||||
import { useRecoilState } from 'recoil'
|
|
||||||
import { loadingAtom } from '../atoms/ui'
|
import { loadingAtom } from '../atoms/ui'
|
||||||
import { useToast } from '../hooks/toast'
|
import { useToast } from '../hooks/toast'
|
||||||
import DownloadDialog from './DownloadDialog'
|
import DownloadDialog from './DownloadDialog'
|
||||||
@@ -7,7 +7,7 @@ import HomeSpeedDial from './HomeSpeedDial'
|
|||||||
import TemplatesEditor from './TemplatesEditor'
|
import TemplatesEditor from './TemplatesEditor'
|
||||||
|
|
||||||
const HomeActions: React.FC = () => {
|
const HomeActions: React.FC = () => {
|
||||||
const [, setIsLoading] = useRecoilState(loadingAtom)
|
const setIsLoading = useSetAtom(loadingAtom)
|
||||||
|
|
||||||
const [openDownload, setOpenDownload] = useState(false)
|
const [openDownload, setOpenDownload] = useState(false)
|
||||||
const [openEditor, setOpenEditor] = useState(false)
|
const [openEditor, setOpenEditor] = useState(false)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
SpeedDialAction,
|
SpeedDialAction,
|
||||||
SpeedDialIcon
|
SpeedDialIcon
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
import { listViewState, serverURL } from '../atoms/settings'
|
import { listViewState, serverURL } from '../atoms/settings'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
import { useRPC } from '../hooks/useRPC'
|
import { useRPC } from '../hooks/useRPC'
|
||||||
@@ -20,8 +20,8 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HomeSpeedDial: React.FC<Props> = ({ onDownloadOpen, onEditorOpen }) => {
|
const HomeSpeedDial: React.FC<Props> = ({ onDownloadOpen, onEditorOpen }) => {
|
||||||
const serverAddr = useRecoilValue(serverURL)
|
const serverAddr = useAtomValue(serverURL)
|
||||||
const [listView, setListView] = useRecoilState(listViewState)
|
const [listView, setListView] = useAtom(listViewState)
|
||||||
|
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
const { client } = useRPC()
|
const { client } = useRPC()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { serverURL } from '../atoms/settings'
|
import { serverURL } from '../atoms/settings'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ const LogTerminal: React.FC = () => {
|
|||||||
|
|
||||||
const boxRef = useRef<HTMLDivElement>(null)
|
const boxRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const serverAddr = useRecoilValue(serverURL)
|
const serverAddr = useAtomValue(serverURL)
|
||||||
|
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
|
|
||||||
@@ -22,8 +22,8 @@ const LogTerminal: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
eventSource.addEventListener('log', event => {
|
eventSource.addEventListener('log', event => {
|
||||||
const msg: string[] = JSON.parse(event.data)
|
const msg: string = JSON.parse(event.data)
|
||||||
setLogBuffer(buff => [...buff, ...msg].slice(-500))
|
setLogBuffer(buff => [...buff, msg].slice(-500))
|
||||||
|
|
||||||
boxRef.current?.scrollTo(0, boxRef.current.scrollHeight)
|
boxRef.current?.scrollTo(0, boxRef.current.scrollHeight)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import * as O from 'fp-ts/Option'
|
import * as O from 'fp-ts/Option'
|
||||||
import { useEffect, useMemo } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
|
||||||
import { take, timer } from 'rxjs'
|
import { take, timer } from 'rxjs'
|
||||||
import { downloadsState } from '../atoms/downloads'
|
import { downloadsState } from '../atoms/downloads'
|
||||||
import { rpcPollingTimeState } from '../atoms/rpc'
|
import { rpcPollingTimeState } from '../atoms/rpc'
|
||||||
@@ -12,15 +11,16 @@ import { useToast } from '../hooks/toast'
|
|||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
import { useRPC } from '../hooks/useRPC'
|
import { useRPC } from '../hooks/useRPC'
|
||||||
import { datetimeCompareFunc, isRPCResponse } from '../utils'
|
import { datetimeCompareFunc, isRPCResponse } from '../utils'
|
||||||
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<HTMLBaseElement> { }
|
interface Props extends React.HTMLAttributes<HTMLBaseElement> { }
|
||||||
|
|
||||||
const SocketSubscriber: React.FC<Props> = () => {
|
const SocketSubscriber: React.FC<Props> = () => {
|
||||||
const [connected, setIsConnected] = useRecoilState(connectedState)
|
const [connected, setIsConnected] = useAtom(connectedState)
|
||||||
const [, setDownloads] = useRecoilState(downloadsState)
|
const setDownloads = useSetAtom(downloadsState)
|
||||||
|
|
||||||
const serverAddressAndPort = useRecoilValue(serverAddressAndPortState)
|
const serverAddressAndPort = useAtomValue(serverAddressAndPortState)
|
||||||
const rpcPollingTime = useRecoilValue(rpcPollingTimeState)
|
const rpcPollingTime = useAtomValue(rpcPollingTimeState)
|
||||||
|
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
const { client } = useRPC()
|
const { client } = useRPC()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
|
import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
|
||||||
import { Container, SvgIcon, Typography, styled } from '@mui/material'
|
import { Container, SvgIcon, Typography, styled } from '@mui/material'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { activeDownloadsState } from '../atoms/downloads'
|
import { activeDownloadsState } from '../atoms/downloads'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const FlexContainer = styled(Container)({
|
const FlexContainer = styled(Container)({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -23,7 +23,7 @@ const Title = styled(Typography)({
|
|||||||
|
|
||||||
export default function Splash() {
|
export default function Splash() {
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
const activeDownloads = useRecoilValue(activeDownloadsState)
|
const activeDownloads = useAtomValue(activeDownloadsState)
|
||||||
|
|
||||||
if (activeDownloads.length !== 0) {
|
if (activeDownloads.length !== 0) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
67
frontend/src/components/TemplateTextField.tsx
Normal file
67
frontend/src/components/TemplateTextField.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
TextField
|
||||||
|
} from '@mui/material'
|
||||||
|
import { useI18n } from '../hooks/useI18n'
|
||||||
|
import { CustomTemplate } from '../types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
template: CustomTemplate
|
||||||
|
onChange: (template: CustomTemplate) => void
|
||||||
|
onDelete: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const TemplateTextField: FC<Props> = ({ template, onChange, onDelete }) => {
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
|
||||||
|
const [editedTemplate, setEditedTemplate] = useState(template)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={2}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
key={template.id}
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
>
|
||||||
|
<Grid item xs={3}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={i18n.t('templatesEditorNameLabel')}
|
||||||
|
defaultValue={template.name}
|
||||||
|
onChange={(e) => setEditedTemplate({ ...editedTemplate, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={9}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={i18n.t('templatesEditorContentLabel')}
|
||||||
|
defaultValue={template.content}
|
||||||
|
onChange={(e) => setEditedTemplate({ ...editedTemplate, content: e.target.value })}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: <div style={{ display: 'flex', gap: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
onClick={() => onChange(editedTemplate)}>
|
||||||
|
<EditIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
onClick={() => onDelete(editedTemplate.id)}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TemplateTextField
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import AddIcon from '@mui/icons-material/Add'
|
import AddIcon from '@mui/icons-material/Add'
|
||||||
import CloseIcon from '@mui/icons-material/Close'
|
import CloseIcon from '@mui/icons-material/Close'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AppBar,
|
AppBar,
|
||||||
@@ -20,12 +21,13 @@ import { TransitionProps } from '@mui/material/transitions'
|
|||||||
import { matchW } from 'fp-ts/lib/Either'
|
import { matchW } from 'fp-ts/lib/Either'
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { forwardRef, useEffect, useState, useTransition } from 'react'
|
import { forwardRef, useEffect, useState, useTransition } from 'react'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { serverURL } from '../atoms/settings'
|
import { serverURL } from '../atoms/settings'
|
||||||
import { useToast } from '../hooks/toast'
|
import { useToast } from '../hooks/toast'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
import { CustomTemplate } from '../types'
|
import { CustomTemplate } from '../types'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
import TemplateTextField from './TemplateTextField'
|
||||||
|
|
||||||
const Transition = forwardRef(function Transition(
|
const Transition = forwardRef(function Transition(
|
||||||
props: TransitionProps & {
|
props: TransitionProps & {
|
||||||
@@ -45,7 +47,7 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
const [templateName, setTemplateName] = useState('')
|
const [templateName, setTemplateName] = useState('')
|
||||||
const [templateContent, setTemplateContent] = useState('')
|
const [templateContent, setTemplateContent] = useState('')
|
||||||
|
|
||||||
const serverAddr = useRecoilValue(serverURL)
|
const serverAddr = useAtomValue(serverURL)
|
||||||
const [isPending, startTransition] = useTransition()
|
const [isPending, startTransition] = useTransition()
|
||||||
|
|
||||||
const [templates, setTemplates] = useState<CustomTemplate[]>([])
|
const [templates, setTemplates] = useState<CustomTemplate[]>([])
|
||||||
@@ -55,11 +57,11 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
getTemplates()
|
fetchTemplates()
|
||||||
}
|
}
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
const getTemplates = async () => {
|
const fetchTemplates = async () => {
|
||||||
const task = ffetch<CustomTemplate[]>(`${serverAddr}/api/v1/template/all`)
|
const task = ffetch<CustomTemplate[]>(`${serverAddr}/api/v1/template/all`)
|
||||||
const either = await task()
|
const either = await task()
|
||||||
|
|
||||||
@@ -89,7 +91,7 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
(l) => pushMessage(l, 'warning'),
|
(l) => pushMessage(l, 'warning'),
|
||||||
() => {
|
() => {
|
||||||
pushMessage('Added template')
|
pushMessage('Added template')
|
||||||
getTemplates()
|
fetchTemplates()
|
||||||
setTemplateName('')
|
setTemplateName('')
|
||||||
setTemplateContent('')
|
setTemplateContent('')
|
||||||
}
|
}
|
||||||
@@ -97,6 +99,26 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateTemplate = async (template: CustomTemplate) => {
|
||||||
|
const task = ffetch<CustomTemplate>(`${serverAddr}/api/v1/template`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(template)
|
||||||
|
})
|
||||||
|
|
||||||
|
const either = await task()
|
||||||
|
|
||||||
|
pipe(
|
||||||
|
either,
|
||||||
|
matchW(
|
||||||
|
(l) => pushMessage(l, 'warning'),
|
||||||
|
(r) => {
|
||||||
|
pushMessage(`Updated template ${r.name}`)
|
||||||
|
fetchTemplates()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const deleteTemplate = async (id: string) => {
|
const deleteTemplate = async (id: string) => {
|
||||||
const task = ffetch<unknown>(`${serverAddr}/api/v1/template/${id}`, {
|
const task = ffetch<unknown>(`${serverAddr}/api/v1/template/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -110,7 +132,7 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
(l) => pushMessage(l, 'warning'),
|
(l) => pushMessage(l, 'warning'),
|
||||||
() => {
|
() => {
|
||||||
pushMessage('Deleted template')
|
pushMessage('Deleted template')
|
||||||
getTemplates()
|
fetchTemplates()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -188,38 +210,12 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
{templates.map(template => (
|
{templates.map(template => (
|
||||||
<Grid
|
<TemplateTextField
|
||||||
container
|
|
||||||
spacing={2}
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
key={template.id}
|
key={template.id}
|
||||||
sx={{ mt: 1 }}
|
template={template}
|
||||||
>
|
onChange={updateTemplate}
|
||||||
<Grid item xs={3}>
|
onDelete={deleteTemplate}
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label={i18n.t('templatesEditorNameLabel')}
|
|
||||||
value={template.name}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
|
||||||
<Grid item xs={9}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label={i18n.t('templatesEditorContentLabel')}
|
|
||||||
value={template.content}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: <Button
|
|
||||||
variant='contained'
|
|
||||||
onClick={() => {
|
|
||||||
startTransition(() => { deleteTemplate(template.id) })
|
|
||||||
}}>
|
|
||||||
<DeleteIcon />
|
|
||||||
</Button>
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
))}
|
))}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import Brightness4 from '@mui/icons-material/Brightness4'
|
|||||||
import Brightness5 from '@mui/icons-material/Brightness5'
|
import Brightness5 from '@mui/icons-material/Brightness5'
|
||||||
import BrightnessAuto from '@mui/icons-material/BrightnessAuto'
|
import BrightnessAuto from '@mui/icons-material/BrightnessAuto'
|
||||||
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
|
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
|
||||||
import { useRecoilState } from 'recoil'
|
|
||||||
import { Theme, themeState } from '../atoms/settings'
|
import { Theme, themeState } from '../atoms/settings'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
|
||||||
const ThemeToggler: React.FC = () => {
|
const ThemeToggler: React.FC = () => {
|
||||||
const [theme, setTheme] = useRecoilState(themeState)
|
const [theme, setTheme] = useAtom(themeState)
|
||||||
|
|
||||||
const actions: Record<Theme, React.ReactNode> = {
|
const actions: Record<Theme, React.ReactNode> = {
|
||||||
system: <BrightnessAuto />,
|
system: <BrightnessAuto />,
|
||||||
|
|||||||
33
frontend/src/components/UpdateBinaryButton.tsx
Normal file
33
frontend/src/components/UpdateBinaryButton.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Button, CircularProgress } from '@mui/material'
|
||||||
|
import { useI18n } from '../hooks/useI18n'
|
||||||
|
import { useRPC } from '../hooks/useRPC'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useToast } from '../hooks/toast'
|
||||||
|
|
||||||
|
const UpdateBinaryButton: React.FC = () => {
|
||||||
|
const { i18n } = useI18n()
|
||||||
|
const { client } = useRPC()
|
||||||
|
const { pushMessage } = useToast()
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const updateBinary = () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
client
|
||||||
|
.updateExecutable()
|
||||||
|
.then(() => pushMessage(i18n.t('toastUpdated'), 'success'))
|
||||||
|
.then(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
endIcon={isLoading ? <CircularProgress size={16} color='secondary' /> : <></>}
|
||||||
|
onClick={updateBinary}
|
||||||
|
>
|
||||||
|
{i18n.t('updateBinButton')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateBinaryButton
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Chip } from '@mui/material'
|
import { Chip } from '@mui/material'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { ytdlpRpcVersionState } from '../atoms/status'
|
import { ytdlpRpcVersionState } from '../atoms/status'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const VersionIndicator: React.FC = () => {
|
const VersionIndicator: React.FC = () => {
|
||||||
const version = useRecoilValue(ytdlpRpcVersionState)
|
const version = useAtomValue(ytdlpRpcVersionState)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { AlertColor } from '@mui/material'
|
import { AlertColor } from '@mui/material'
|
||||||
import { useRecoilState } from 'recoil'
|
|
||||||
import { toastListState } from '../atoms/toast'
|
import { toastListState } from '../atoms/toast'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
export const useToast = () => {
|
export const useToast = () => {
|
||||||
const [, setToasts] = useRecoilState(toastListState)
|
const setToasts = useSetAtom(toastListState)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pushMessage: (message: string, severity?: AlertColor) => {
|
pushMessage: (message: string, severity?: AlertColor) => {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useRecoilValue } from 'recoil'
|
import { useAtomValue } from 'jotai'
|
||||||
import { i18nBuilderState } from '../atoms/i18n'
|
import { i18nBuilderState } from '../atoms/i18n'
|
||||||
|
|
||||||
export const useI18n = () => {
|
export const useI18n = () => {
|
||||||
const instance = useRecoilValue(i18nBuilderState)
|
const instance = useAtomValue(i18nBuilderState)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
i18n: instance
|
i18n: instance,
|
||||||
|
t: instance.t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useRecoilValue } from 'recoil'
|
import { useAtomValue } from 'jotai'
|
||||||
import { rpcClientState } from '../atoms/rpc'
|
import { rpcClientState } from '../atoms/rpc'
|
||||||
|
|
||||||
export const useRPC = () => {
|
export const useRPC = () => {
|
||||||
const client = useRecoilValue(rpcClientState)
|
const client = useAtomValue(rpcClientState)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
client
|
client
|
||||||
|
|||||||
@@ -191,4 +191,11 @@ export class RPCClient {
|
|||||||
params: []
|
params: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateExecutable() {
|
||||||
|
return this.sendHTTP({
|
||||||
|
method: 'Service.UpdateExecutable',
|
||||||
|
params: []
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Alert, Snackbar } from "@mui/material"
|
import { Alert, Snackbar } from "@mui/material"
|
||||||
import { useRecoilState } from 'recoil'
|
|
||||||
import { Toast, toastListState } from '../atoms/toast'
|
import { Toast, toastListState } from '../atoms/toast'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
|
||||||
const Toaster: React.FC = () => {
|
const Toaster: React.FC = () => {
|
||||||
const [toasts, setToasts] = useRecoilState(toastListState)
|
const [toasts, setToasts] = useAtom(toastListState)
|
||||||
|
|
||||||
const deletePredicate = (t: Toast) => (Date.now() - t.createdAt) < 2000
|
const deletePredicate = (t: Toast) => (Date.now() - t.createdAt) < 2000
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,11 @@ export type RPCParams = {
|
|||||||
|
|
||||||
export type DLMetadata = {
|
export type DLMetadata = {
|
||||||
formats: Array<DLFormat>
|
formats: Array<DLFormat>
|
||||||
|
_type: string
|
||||||
best: DLFormat
|
best: DLFormat
|
||||||
thumbnail: string
|
thumbnail: string
|
||||||
title: string
|
title: string
|
||||||
|
entries: Array<DLMetadata>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DLFormat = {
|
export type DLFormat = {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import { matchW } from 'fp-ts/lib/TaskEither'
|
|||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { useEffect, useMemo, useState, useTransition } from 'react'
|
import { useEffect, useMemo, useState, useTransition } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
|
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
|
||||||
import { serverURL } from '../atoms/settings'
|
import { serverURL } from '../atoms/settings'
|
||||||
import { useObservable } from '../hooks/observable'
|
import { useObservable } from '../hooks/observable'
|
||||||
@@ -42,13 +41,14 @@ import { useI18n } from '../hooks/useI18n'
|
|||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
import { DirectoryEntry } from '../types'
|
import { DirectoryEntry } from '../types'
|
||||||
import { base64URLEncode, formatSize } from '../utils'
|
import { base64URLEncode, formatSize } from '../utils'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
export default function Downloaded() {
|
export default function Downloaded() {
|
||||||
const [menuPos, setMenuPos] = useState({ x: 0, y: 0 })
|
const [menuPos, setMenuPos] = useState({ x: 0, y: 0 })
|
||||||
const [showMenu, setShowMenu] = useState(false)
|
const [showMenu, setShowMenu] = useState(false)
|
||||||
const [currentFile, setCurrentFile] = useState<DirectoryEntry>()
|
const [currentFile, setCurrentFile] = useState<DirectoryEntry>()
|
||||||
|
|
||||||
const serverAddr = useRecoilValue(serverURL)
|
const serverAddr = useAtomValue(serverURL)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
Container
|
Container
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { loadingAtom } from '../atoms/ui'
|
import { loadingAtom } from '../atoms/ui'
|
||||||
import Downloads from '../components/Downloads'
|
import Downloads from '../components/Downloads'
|
||||||
import HomeActions from '../components/HomeActions'
|
import HomeActions from '../components/HomeActions'
|
||||||
import LoadingBackdrop from '../components/LoadingBackdrop'
|
import LoadingBackdrop from '../components/LoadingBackdrop'
|
||||||
import Splash from '../components/Splash'
|
import Splash from '../components/Splash'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const isLoading = useRecoilValue(loadingAtom)
|
const isLoading = useAtomValue(loadingAtom)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="xl" sx={{ mt: 2, mb: 8 }}>
|
<Container maxWidth="xl" sx={{ mt: 2, mb: 8 }}>
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import { matchW } from 'fp-ts/lib/TaskEither'
|
|||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useRecoilValue } from 'recoil'
|
|
||||||
import { serverURL } from '../atoms/settings'
|
import { serverURL } from '../atoms/settings'
|
||||||
import { useToast } from '../hooks/toast'
|
import { useToast } from '../hooks/toast'
|
||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
const LoginContainer = styled(Container)({
|
const LoginContainer = styled(Container)({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -43,7 +43,7 @@ export default function Login() {
|
|||||||
|
|
||||||
const [formHasError, setFormHasError] = useState(false)
|
const [formHasError, setFormHasError] = useState(false)
|
||||||
|
|
||||||
const url = useRecoilValue(serverURL)
|
const url = useAtomValue(serverURL)
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Container,
|
Container,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -18,8 +17,8 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
capitalize
|
capitalize
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
import { Suspense, useEffect, useMemo, useState } from 'react'
|
import { Suspense, useEffect, useMemo, useState } from 'react'
|
||||||
import { useRecoilState } from 'recoil'
|
|
||||||
import {
|
import {
|
||||||
Subject,
|
Subject,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
@@ -45,34 +44,33 @@ import {
|
|||||||
themeState
|
themeState
|
||||||
} from '../atoms/settings'
|
} from '../atoms/settings'
|
||||||
import CookiesTextField from '../components/CookiesTextField'
|
import CookiesTextField from '../components/CookiesTextField'
|
||||||
|
import UpdateBinaryButton from '../components/UpdateBinaryButton'
|
||||||
import { useToast } from '../hooks/toast'
|
import { useToast } from '../hooks/toast'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
import { useRPC } from '../hooks/useRPC'
|
|
||||||
import { validateDomain, validateIP } from '../utils'
|
import { validateDomain, validateIP } from '../utils'
|
||||||
|
|
||||||
// NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS
|
// NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const [reverseProxy, setReverseProxy] = useRecoilState(servedFromReverseProxyState)
|
const [reverseProxy, setReverseProxy] = useAtom(servedFromReverseProxyState)
|
||||||
const [baseURL, setBaseURL] = useRecoilState(servedFromReverseProxySubDirState)
|
const [baseURL, setBaseURL] = useAtom(servedFromReverseProxySubDirState)
|
||||||
|
|
||||||
const [formatSelection, setFormatSelection] = useRecoilState(formatSelectionState)
|
const [formatSelection, setFormatSelection] = useAtom(formatSelectionState)
|
||||||
const [pathOverriding, setPathOverriding] = useRecoilState(pathOverridingState)
|
const [pathOverriding, setPathOverriding] = useAtom(pathOverridingState)
|
||||||
const [fileRenaming, setFileRenaming] = useRecoilState(fileRenamingState)
|
const [fileRenaming, setFileRenaming] = useAtom(fileRenamingState)
|
||||||
const [enableArgs, setEnableArgs] = useRecoilState(enableCustomArgsState)
|
const [enableArgs, setEnableArgs] = useAtom(enableCustomArgsState)
|
||||||
|
|
||||||
const [serverAddr, setServerAddr] = useRecoilState(serverAddressState)
|
const [serverAddr, setServerAddr] = useAtom(serverAddressState)
|
||||||
const [serverPort, setServerPort] = useRecoilState(serverPortState)
|
const [serverPort, setServerPort] = useAtom(serverPortState)
|
||||||
|
|
||||||
const [pollingTime, setPollingTime] = useRecoilState(rpcPollingTimeState)
|
const [pollingTime, setPollingTime] = useAtom(rpcPollingTimeState)
|
||||||
const [language, setLanguage] = useRecoilState(languageState)
|
const [language, setLanguage] = useAtom(languageState)
|
||||||
const [appTitle, setApptitle] = useRecoilState(appTitleState)
|
const [appTitle, setApptitle] = useAtom(appTitleState)
|
||||||
|
|
||||||
const [theme, setTheme] = useRecoilState(themeState)
|
const [theme, setTheme] = useAtom(themeState)
|
||||||
|
|
||||||
const [invalidIP, setInvalidIP] = useState(false)
|
const [invalidIP, setInvalidIP] = useState(false)
|
||||||
|
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
const { client } = useRPC()
|
|
||||||
|
|
||||||
const { pushMessage } = useToast()
|
const { pushMessage } = useToast()
|
||||||
|
|
||||||
@@ -140,13 +138,6 @@ export default function Settings() {
|
|||||||
setTheme(event.target.value as Theme)
|
setTheme(event.target.value as Theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates yt-dlp binary via RPC
|
|
||||||
*/
|
|
||||||
const updateBinary = () => {
|
|
||||||
client.updateExecutable().then(() => pushMessage(i18n.t('toastUpdated'), 'success'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="xl" sx={{ mt: 4, mb: 8 }}>
|
<Container maxWidth="xl" sx={{ mt: 4, mb: 8 }}>
|
||||||
<Paper
|
<Paper
|
||||||
@@ -352,14 +343,8 @@ export default function Settings() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Stack direction="row">
|
<Stack direction="row" sx={{ pt: 2 }}>
|
||||||
<Button
|
<UpdateBinaryButton />
|
||||||
sx={{ mr: 1, mt: 3 }}
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => updateBinary()}
|
|
||||||
>
|
|
||||||
{i18n.t('updateBinButton')}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
12
go.mod
12
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/marcopeocchi/yt-dlp-web-ui
|
module github.com/marcopeocchi/yt-dlp-web-ui/v3
|
||||||
|
|
||||||
go 1.23
|
go 1.23
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/reactivex/rxgo/v2 v2.5.0
|
|
||||||
golang.org/x/oauth2 v0.23.0
|
golang.org/x/oauth2 v0.23.0
|
||||||
golang.org/x/sync v0.8.0
|
golang.org/x/sync v0.8.0
|
||||||
golang.org/x/sys v0.25.0
|
golang.org/x/sys v0.25.0
|
||||||
@@ -19,21 +18,16 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
|
||||||
github.com/stretchr/testify v1.9.0 // indirect
|
|
||||||
github.com/teivah/onecontext v1.3.0 // indirect
|
|
||||||
golang.org/x/crypto v0.27.0 // indirect
|
golang.org/x/crypto v0.27.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
|
||||||
modernc.org/libc v1.61.0 // indirect
|
modernc.org/libc v1.61.0 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
|||||||
53
go.sum
53
go.sum
@@ -1,18 +1,11 @@
|
|||||||
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
|
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
|
||||||
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
|
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
|
||||||
github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
@@ -42,78 +35,40 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
|||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/reactivex/rxgo/v2 v2.5.0 h1:FhPgHwX9vKdNQB2gq9EPt+EKk9QrrzoeztGbEEnZam4=
|
|
||||||
github.com/reactivex/rxgo/v2 v2.5.0/go.mod h1:bs4fVZxcb5ZckLIOeIeVH942yunJLWDABWGbrHAW+qU=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/teivah/onecontext v0.0.0-20200513185103-40f981bfd775/go.mod h1:XUZ4x3oGhWfiOnUvTslnKKs39AWUct3g3yJvXTQSJOQ=
|
|
||||||
github.com/teivah/onecontext v1.3.0 h1:tbikMhAlo6VhAuEGCvhc8HlTnpX4xTNPTOseWuhO1J0=
|
|
||||||
github.com/teivah/onecontext v1.3.0/go.mod h1:hoW1nmdPVK/0jrvGtcx8sCKYs2PiS4z0zzfdeuEVyb0=
|
|
||||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
|
|
||||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
|
||||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
|
||||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
|
||||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
|
||||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
|
||||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
|
||||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
|
||||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
modernc.org/ccgo/v4 v4.20.7 h1:skrinQsjxWfvj6nbC3ztZPJy+NuwmB3hV9zX/pthNYQ=
|
|
||||||
modernc.org/ccgo/v4 v4.20.7/go.mod h1:UOkI3JSG2zT4E2ioHlncSOZsXbuDCZLvPi3uMlZT5GY=
|
|
||||||
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
|
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
|
||||||
|
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||||
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
|
||||||
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
modernc.org/libc v1.59.9 h1:k+nNDDakwipimgmJ1D9H466LhFeSkaPPycAs1OZiDmY=
|
|
||||||
modernc.org/libc v1.59.9/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A=
|
|
||||||
modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s=
|
|
||||||
modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY=
|
|
||||||
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
|
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
|
||||||
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
|
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
@@ -124,8 +79,6 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
|||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
|
|
||||||
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
|
|
||||||
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
|
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
|
||||||
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
||||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
|||||||
14
main.go
14
main.go
@@ -8,10 +8,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/cli"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -23,6 +23,7 @@ var (
|
|||||||
downloaderPath string
|
downloaderPath string
|
||||||
sessionFilePath string
|
sessionFilePath string
|
||||||
localDatabasePath string
|
localDatabasePath string
|
||||||
|
frontendPath string
|
||||||
|
|
||||||
requireAuth bool
|
requireAuth bool
|
||||||
username string
|
username string
|
||||||
@@ -52,6 +53,7 @@ func init() {
|
|||||||
flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path")
|
flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path")
|
||||||
flag.StringVar(&sessionFilePath, "session", ".", "session file path")
|
flag.StringVar(&sessionFilePath, "session", ".", "session file path")
|
||||||
flag.StringVar(&localDatabasePath, "db", "local.db", "local database path")
|
flag.StringVar(&localDatabasePath, "db", "local.db", "local database path")
|
||||||
|
flag.StringVar(&frontendPath, "web", "", "frontend web resources path")
|
||||||
|
|
||||||
flag.BoolVar(&enableFileLogging, "fl", false, "enable outputting logs to a file")
|
flag.BoolVar(&enableFileLogging, "fl", false, "enable outputting logs to a file")
|
||||||
flag.StringVar(&logFile, "lf", "yt-dlp-webui.log", "set log file location")
|
flag.StringVar(&logFile, "lf", "yt-dlp-webui.log", "set log file location")
|
||||||
@@ -69,6 +71,10 @@ func main() {
|
|||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if frontendPath != "" {
|
||||||
|
frontend = os.DirFS(frontendPath)
|
||||||
|
}
|
||||||
|
|
||||||
c := config.Instance()
|
c := config.Instance()
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{ yt-dlp-web-ui-frontend, buildGoModule, lib, makeWrapper, yt-dlp, ... }:
|
{ yt-dlp-web-ui-frontend, buildGo123Module, lib, makeWrapper, yt-dlp, ... }:
|
||||||
let
|
let
|
||||||
fs = lib.fileset;
|
fs = lib.fileset;
|
||||||
common = import ./common.nix { inherit lib; };
|
common = import ./common.nix { inherit lib; };
|
||||||
in
|
in
|
||||||
buildGoModule {
|
buildGo123Module {
|
||||||
pname = "yt-dlp-web-ui";
|
pname = "yt-dlp-web-ui";
|
||||||
inherit (common) version;
|
inherit (common) version;
|
||||||
src = fs.toSource rec {
|
src = fs.toSource rec {
|
||||||
@@ -26,7 +26,7 @@ buildGoModule {
|
|||||||
# repo commons
|
# repo commons
|
||||||
../.github
|
../.github
|
||||||
../README.md
|
../README.md
|
||||||
../LICENSE.md
|
../LICENSE
|
||||||
../.gitignore
|
../.gitignore
|
||||||
../.vscode
|
../.vscode
|
||||||
]);
|
]);
|
||||||
@@ -44,7 +44,7 @@ buildGoModule {
|
|||||||
--prefix PATH : ${lib.makeBinPath [ yt-dlp ]}
|
--prefix PATH : ${lib.makeBinPath [ yt-dlp ]}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
vendorHash = "sha256-guM/U9DROJMx2ctPKBQis1YRhaf6fKvvwEWgswQKMG0=";
|
vendorHash = "sha256-c7IdCmYJEn5qJn3K8wt0qz3t0Nq9rbgWp1eONlCJOwM=";
|
||||||
|
|
||||||
meta = common.meta // {
|
meta = common.meta // {
|
||||||
mainProgram = "yt-dlp-web-ui";
|
mainProgram = "yt-dlp-web-ui";
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ type Config struct {
|
|||||||
QueueSize int `yaml:"queue_size"`
|
QueueSize int `yaml:"queue_size"`
|
||||||
LocalDatabasePath string `yaml:"local_database_path"`
|
LocalDatabasePath string `yaml:"local_database_path"`
|
||||||
SessionFilePath string `yaml:"session_file_path"`
|
SessionFilePath string `yaml:"session_file_path"`
|
||||||
path string
|
path string // private
|
||||||
|
|
||||||
UseOpenId bool `yaml:"use_openid"`
|
UseOpenId bool `yaml:"use_openid"`
|
||||||
OpenIdProviderURL string `yaml:"openid_provider_url"`
|
OpenIdProviderURL string `yaml:"openid_provider_url"`
|
||||||
OpenIdClientId string `yaml:"openid_client_id"`
|
OpenIdClientId string `yaml:"openid_client_id"`
|
||||||
OpenIdClientSecret string `yaml:"openid_client_secret"`
|
OpenIdClientSecret string `yaml:"openid_client_secret"`
|
||||||
OpenIdRedirectURL string `yaml:"openid_redirect_url"`
|
OpenIdRedirectURL string `yaml:"openid_redirect_url"`
|
||||||
|
FrontendPath string `yaml:"frontend_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var lockFilePath = filepath.Join(config.Instance().Dir(), ".db.lock")
|
var lockFilePath = filepath.Join(config.Instance().Dir(), ".db.lock")
|
||||||
|
|||||||
56
server/formats/parser.go
Normal file
56
server/formats/parser.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseURL(url string) (*Metadata, error) {
|
||||||
|
cmd := exec.Command(config.Instance().DownloaderPath, url, "-J")
|
||||||
|
|
||||||
|
stdout, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to retrieve metadata", slog.String("err", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info(
|
||||||
|
"retrieving metadata",
|
||||||
|
slog.String("caller", "getFormats"),
|
||||||
|
slog.String("url", url),
|
||||||
|
)
|
||||||
|
|
||||||
|
info := &Metadata{URL: url}
|
||||||
|
best := &Format{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
decodingError error
|
||||||
|
)
|
||||||
|
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
decodingError = json.Unmarshal(stdout, &info)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
decodingError = json.Unmarshal(stdout, &best)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if decodingError != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Best = *best
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
28
server/formats/types.go
Normal file
28
server/formats/types.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
// Used to deser the formats in the -J output
|
||||||
|
type Metadata struct {
|
||||||
|
Type string `json:"_type"`
|
||||||
|
Formats []Format `json:"formats"`
|
||||||
|
Best Format `json:"best"`
|
||||||
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Entries []Metadata `json:"entries"` // populated if url is playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Metadata) IsPlaylist() bool {
|
||||||
|
return m.Type == "playlist"
|
||||||
|
}
|
||||||
|
|
||||||
|
// A skimmed yt-dlp format node
|
||||||
|
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"`
|
||||||
|
Size float32 `json:"filesize_approx"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
}
|
||||||
@@ -17,8 +17,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TOKEN_COOKIE_NAME = "jwt-yt-dlp-webui"
|
const TOKEN_COOKIE_NAME = "jwt-yt-dlp-webui"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ type ProgressTemplate struct {
|
|||||||
Eta float32 `json:"eta"`
|
Eta float32 `json:"eta"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type PostprocessTemplate struct {
|
type PostprocessTemplate struct {
|
||||||
FilePath string `json:"filepath"`
|
FilePath string `json:"filepath"`
|
||||||
}
|
}
|
||||||
@@ -45,27 +44,6 @@ type DownloadInfo struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to deser the formats in the -J output
|
|
||||||
type DownloadFormats struct {
|
|
||||||
Formats []Format `json:"formats"`
|
|
||||||
Best Format `json:"best"`
|
|
||||||
Thumbnail string `json:"thumbnail"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
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"`
|
|
||||||
FPS float32 `json:"fps"`
|
|
||||||
Resolution string `json:"resolution"`
|
|
||||||
VCodec string `json:"vcodec"`
|
|
||||||
ACodec string `json:"acodec"`
|
|
||||||
Size float32 `json:"filesize_approx"`
|
|
||||||
Language string `json:"language"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// struct representing the response sent to the client
|
// struct representing the response sent to the client
|
||||||
// as JSON-RPC result field
|
// as JSON-RPC result field
|
||||||
type ProcessResponse struct {
|
type ProcessResponse struct {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTest() {
|
func setupTest() {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Monitor struct {
|
type Monitor struct {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// In-Memory Thread-Safe Key-Value Storage with optional persistence
|
// In-Memory Thread-Safe Key-Value Storage with optional persistence
|
||||||
@@ -111,7 +111,9 @@ func (m *MemoryDB) Persist() error {
|
|||||||
|
|
||||||
// Restore a persisted state
|
// Restore a persisted state
|
||||||
func (m *MemoryDB) Restore(mq *MessageQueue) {
|
func (m *MemoryDB) Restore(mq *MessageQueue) {
|
||||||
fd, err := os.Open("session.dat")
|
sf := filepath.Join(config.Instance().SessionFilePath, "session.dat")
|
||||||
|
|
||||||
|
fd, err := os.Open(sf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
evbus "github.com/asaskevich/EventBus"
|
evbus "github.com/asaskevich/EventBus"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"golang.org/x/sync/semaphore"
|
"golang.org/x/sync/semaphore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"os"
|
"os"
|
||||||
@@ -19,7 +18,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const downloadTemplate = `download:
|
const downloadTemplate = `download:
|
||||||
@@ -261,54 +260,6 @@ func (p *Process) Kill() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the available format for this URL
|
|
||||||
//
|
|
||||||
// TODO: Move out from process.go
|
|
||||||
func (p *Process) GetFormats() (DownloadFormats, error) {
|
|
||||||
cmd := exec.Command(config.Instance().DownloaderPath, p.Url, "-J")
|
|
||||||
|
|
||||||
stdout, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to retrieve metadata", slog.String("err", err.Error()))
|
|
||||||
return DownloadFormats{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info(
|
|
||||||
"retrieving metadata",
|
|
||||||
slog.String("caller", "getFormats"),
|
|
||||||
slog.String("url", p.Url),
|
|
||||||
)
|
|
||||||
|
|
||||||
info := DownloadFormats{URL: p.Url}
|
|
||||||
best := Format{}
|
|
||||||
|
|
||||||
var (
|
|
||||||
wg sync.WaitGroup
|
|
||||||
decodingError error
|
|
||||||
)
|
|
||||||
|
|
||||||
wg.Add(2)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
decodingError = json.Unmarshal(stdout, &info)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
decodingError = json.Unmarshal(stdout, &best)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if decodingError != nil {
|
|
||||||
return DownloadFormats{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
info.Best = best
|
|
||||||
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Process) GetFileName(o *DownloadOutput) error {
|
func (p *Process) GetFileName(o *DownloadOutput) error {
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
config.Instance().DownloaderPath,
|
config.Instance().DownloaderPath,
|
||||||
|
|||||||
@@ -3,21 +3,23 @@ package logging
|
|||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
File base logger with log-rotate capabilities.
|
implements io.Writer interface
|
||||||
The rotate process must be initiated from an external goroutine.
|
|
||||||
|
|
||||||
After rotation the previous logs file are compressed with gzip algorithm.
|
File base logger with log-rotate capabilities.
|
||||||
|
The rotate process must be initiated from an external goroutine.
|
||||||
|
|
||||||
The rotated log follows this naming: [filename].UTC time.gz
|
After rotation the previous logs file are compressed with gzip algorithm.
|
||||||
|
|
||||||
|
The rotated log follows this naming: [filename].UTC time.gz
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// implements io.Writer interface
|
|
||||||
type LogRotateWriter struct {
|
type LogRotateWriter struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
fd *os.File
|
fd *os.File
|
||||||
@@ -40,50 +42,41 @@ func (w *LogRotateWriter) Write(b []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *LogRotateWriter) Rotate() error {
|
func (w *LogRotateWriter) Rotate() error {
|
||||||
var err error
|
slog.Info("started log rotation")
|
||||||
|
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
|
|
||||||
gzFile, err := os.Create(w.filename + "." + time.Now().Format(time.RFC3339) + ".gz")
|
gzFile, err := os.Create(strings.TrimSuffix(w.filename, ".log") + "-" + time.Now().Format(time.RFC3339) + ".log.gz")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(w.fd)
|
zw := gzip.NewWriter(gzFile)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
w.mu.Unlock()
|
zw.Close()
|
||||||
w.gzipLog(gzFile, &data)
|
zw.Flush()
|
||||||
|
gzFile.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_, err = os.Stat(w.filename)
|
if _, err := os.Stat(w.filename); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if w.fd != nil {
|
fd, _ := os.Open(w.filename)
|
||||||
err = w.fd.Close()
|
io.Copy(zw, fd)
|
||||||
w.fd = nil
|
fd.Close()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Remove(w.filename)
|
w.fd.Close()
|
||||||
if err != nil {
|
|
||||||
|
if err := os.Remove(w.filename); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.fd, err = os.Create(w.filename)
|
w.fd, _ = os.Create(w.filename)
|
||||||
|
|
||||||
|
w.mu.Unlock()
|
||||||
|
slog.Info("ended log rotation")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LogRotateWriter) gzipLog(wr io.Writer, data *[]byte) error {
|
|
||||||
if _, err := gzip.NewWriter(wr).Write(*data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package logging
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
|
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
@@ -22,19 +22,29 @@ var upgrader = websocket.Upgrader{
|
|||||||
WriteBufferSize: 1000,
|
WriteBufferSize: 1000,
|
||||||
}
|
}
|
||||||
|
|
||||||
func webSocket(w http.ResponseWriter, r *http.Request) {
|
func webSocket(logger *ObservableLogger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
c, err := upgrader.Upgrade(w, r, nil)
|
c, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for msg := range logsObservable.Observe() {
|
logs := logger.Observe(r.Context())
|
||||||
c.WriteJSON(msg.V)
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.Context().Done():
|
||||||
|
return
|
||||||
|
case msg := <-logs:
|
||||||
|
c.WriteJSON(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sse(w http.ResponseWriter, r *http.Request) {
|
func sse(logger *ObservableLogger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
w.Header().Set("Connection", "keep-alive")
|
w.Header().Set("Connection", "keep-alive")
|
||||||
@@ -45,35 +55,41 @@ func sse(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for msg := range logsObservable.Observe() {
|
logs := logger.Observe(r.Context())
|
||||||
if msg.E != nil {
|
|
||||||
http.Error(w, msg.E.Error(), http.StatusInternalServerError)
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.Context().Done():
|
||||||
|
slog.Info("detaching from logger")
|
||||||
|
return
|
||||||
|
case msg, ok := <-logs:
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "closed logs channel", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var b bytes.Buffer
|
||||||
b bytes.Buffer
|
|
||||||
sb strings.Builder
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := json.NewEncoder(&b).Encode(msg.V); err != nil {
|
b.WriteString("event: log\n")
|
||||||
|
b.WriteString("data: ")
|
||||||
|
|
||||||
|
if err := json.NewEncoder(&b).Encode(msg); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString("event: log\n")
|
b.WriteRune('\n')
|
||||||
sb.WriteString("data: ")
|
b.WriteRune('\n')
|
||||||
sb.WriteString(b.String())
|
|
||||||
sb.WriteRune('\n')
|
|
||||||
sb.WriteRune('\n')
|
|
||||||
|
|
||||||
fmt.Fprint(w, sb.String())
|
io.Copy(w, &b)
|
||||||
|
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApplyRouter() func(chi.Router) {
|
func ApplyRouter(logger *ObservableLogger) func(chi.Router) {
|
||||||
return func(r chi.Router) {
|
return func(r chi.Router) {
|
||||||
if config.Instance().RequireAuth {
|
if config.Instance().RequireAuth {
|
||||||
r.Use(middlewares.Authenticated)
|
r.Use(middlewares.Authenticated)
|
||||||
@@ -81,7 +97,7 @@ func ApplyRouter() func(chi.Router) {
|
|||||||
if config.Instance().UseOpenId {
|
if config.Instance().UseOpenId {
|
||||||
r.Use(openid.Middleware)
|
r.Use(openid.Middleware)
|
||||||
}
|
}
|
||||||
r.Get("/ws", webSocket)
|
r.Get("/ws", webSocket(logger))
|
||||||
r.Get("/sse", sse)
|
r.Get("/sse", sse(logger))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,53 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"context"
|
||||||
|
|
||||||
"github.com/reactivex/rxgo/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Logger implementation using the observable pattern.
|
Logger implementation using the observable pattern.
|
||||||
Implements io.Writer interface.
|
Implements io.Writer interface.
|
||||||
|
|
||||||
The observable is an event source which drops everythigng unless there's
|
The observable is an event source which drops everythigng unless there's
|
||||||
a subscriber connected.
|
a subscriber connected.
|
||||||
|
|
||||||
The observer implementatios are a http ServerSentEvents handler and a
|
The observer implementatios are a http ServerSentEvents handler and a
|
||||||
websocket one in handler.go
|
websocket one in handler.go
|
||||||
*/
|
*/
|
||||||
|
type ObservableLogger struct {
|
||||||
var (
|
logsChan chan []byte
|
||||||
logsChan = make(chan rxgo.Item, 100)
|
}
|
||||||
logsObservable = rxgo.
|
|
||||||
FromEventSource(logsChan, rxgo.WithBackPressureStrategy(rxgo.Drop)).
|
|
||||||
BufferWithTime(rxgo.WithDuration(time.Millisecond * 500))
|
|
||||||
)
|
|
||||||
|
|
||||||
type ObservableLogger struct{}
|
|
||||||
|
|
||||||
func NewObservableLogger() *ObservableLogger {
|
func NewObservableLogger() *ObservableLogger {
|
||||||
return &ObservableLogger{}
|
return &ObservableLogger{
|
||||||
|
logsChan: make(chan []byte, 100),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ObservableLogger) Write(p []byte) (n int, err error) {
|
func (o *ObservableLogger) Write(p []byte) (n int, err error) {
|
||||||
logsChan <- rxgo.Of(string(p))
|
select {
|
||||||
|
case o.logsChan <- p:
|
||||||
n = len(p)
|
n = len(p)
|
||||||
err = nil
|
err = nil
|
||||||
|
|
||||||
return
|
return
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObservableLogger) Observe(ctx context.Context) <-chan string {
|
||||||
|
logs := make(chan string)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case logLine := <-o.logsChan:
|
||||||
|
logs <- string(logLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return logs
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package rest
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContainerArgs struct {
|
type ContainerArgs struct {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package rest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
|
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Container(args *ContainerArgs) *Handler {
|
func Container(args *ContainerArgs) *Handler {
|
||||||
@@ -34,6 +34,7 @@ func ApplyRouter(args *ContainerArgs) func(chi.Router) {
|
|||||||
r.Post("/cookies", h.SetCookies())
|
r.Post("/cookies", h.SetCookies())
|
||||||
r.Delete("/cookies", h.DeleteCookies())
|
r.Delete("/cookies", h.DeleteCookies())
|
||||||
r.Post("/template", h.AddTemplate())
|
r.Post("/template", h.AddTemplate())
|
||||||
|
r.Patch("/template", h.UpdateTemplate())
|
||||||
r.Get("/template/all", h.GetTemplates())
|
r.Get("/template/all", h.GetTemplates())
|
||||||
r.Delete("/template/{id}", h.DeleteTemplate())
|
r.Delete("/template/{id}", h.DeleteTemplate())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
@@ -34,9 +34,9 @@ func (h *Handler) Exec() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(id)
|
if err := json.NewEncoder(w).Encode(id); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,6 +61,7 @@ func (h *Handler) ExecPlaylist() http.HandlerFunc {
|
|||||||
|
|
||||||
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,13 +76,14 @@ func (h *Handler) ExecLivestream() http.HandlerFunc {
|
|||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.service.ExecLivestream(req)
|
h.service.ExecLivestream(req)
|
||||||
|
|
||||||
err := json.NewEncoder(w).Encode("ok")
|
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,9 +100,9 @@ func (h *Handler) Running() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(res)
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,21 +136,19 @@ func (h *Handler) SetCookies() http.HandlerFunc {
|
|||||||
|
|
||||||
req := new(internal.SetCookiesRequest)
|
req := new(internal.SetCookiesRequest)
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(req)
|
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.service.SetCookies(r.Context(), req.Cookies)
|
if err := h.service.SetCookies(r.Context(), req.Cookies); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode("ok")
|
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,15 +157,14 @@ func (h *Handler) DeleteCookies() http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
err := h.service.SetCookies(r.Context(), "")
|
if err := h.service.SetCookies(r.Context(), ""); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode("ok")
|
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,8 +177,7 @@ func (h *Handler) AddTemplate() http.HandlerFunc {
|
|||||||
|
|
||||||
req := new(internal.CustomTemplate)
|
req := new(internal.CustomTemplate)
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(req)
|
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -189,15 +187,14 @@ func (h *Handler) AddTemplate() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.service.SaveTemplate(r.Context(), req)
|
if err := h.service.SaveTemplate(r.Context(), req); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode("ok")
|
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,6 +218,33 @@ func (h *Handler) GetTemplates() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateTemplate() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
req := &internal.CustomTemplate{}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.service.UpdateTemplate(r.Context(), req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteTemplate() http.HandlerFunc {
|
func (h *Handler) DeleteTemplate() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
@@ -229,15 +253,14 @@ func (h *Handler) DeleteTemplate() http.HandlerFunc {
|
|||||||
|
|
||||||
id := chi.URLParam(r, "id")
|
id := chi.URLParam(r, "id")
|
||||||
|
|
||||||
err := h.service.DeleteTemplate(r.Context(), id)
|
if err := h.service.DeleteTemplate(r.Context(), id); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode("ok")
|
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,6 +289,7 @@ func (h *Handler) GetVersion() http.HandlerFunc {
|
|||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal/livestream"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -133,6 +133,22 @@ func (s *Service) GetTemplates(ctx context.Context) (*[]internal.CustomTemplate,
|
|||||||
return &templates, nil
|
return &templates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateTemplate(ctx context.Context, t *internal.CustomTemplate) (*internal.CustomTemplate, error) {
|
||||||
|
conn, err := s.db.Conn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
_, err = conn.ExecContext(ctx, "UPDATE templates SET name = ?, content = ? WHERE id = ?", t.Name, t.Content, t.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) DeleteTemplate(ctx context.Context, id string) error {
|
func (s *Service) DeleteTemplate(ctx context.Context, id string) error {
|
||||||
conn, err := s.db.Conn(ctx)
|
conn, err := s.db.Conn(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -148,7 +164,7 @@ func (s *Service) DeleteTemplate(ctx context.Context, id string) error {
|
|||||||
|
|
||||||
func (s *Service) GetVersion(ctx context.Context) (string, string, error) {
|
func (s *Service) GetVersion(ctx context.Context) (string, string, error) {
|
||||||
//TODO: load from realease properties file, or anything else outside code
|
//TODO: load from realease properties file, or anything else outside code
|
||||||
const CURRENT_RPC_VERSION = "3.2.1"
|
const CURRENT_RPC_VERSION = "3.2.2"
|
||||||
|
|
||||||
result := make(chan string, 1)
|
result := make(chan string, 1)
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package rpc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal/livestream"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
|
||||||
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
|
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dependency injection container.
|
// Dependency injection container.
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/formats"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal/livestream"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/sys"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/updater"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/sys"
|
||||||
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/updater"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -21,12 +22,6 @@ type Pending []string
|
|||||||
|
|
||||||
type NoArgs struct{}
|
type NoArgs struct{}
|
||||||
|
|
||||||
type Args struct {
|
|
||||||
Id string
|
|
||||||
URL string
|
|
||||||
Params []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec spawns a Process.
|
// Exec spawns a Process.
|
||||||
// The result of the execution is the newly spawned process Id.
|
// The result of the execution is the newly spawned process Id.
|
||||||
func (s *Service) Exec(args internal.DownloadRequest, result *string) error {
|
func (s *Service) Exec(args internal.DownloadRequest, result *string) error {
|
||||||
@@ -91,7 +86,7 @@ func (s *Service) KillAllLivestream(args NoArgs, result *struct{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Progess retrieves the Progress of a specific Process given its Id
|
// Progess retrieves the Progress of a specific Process given its Id
|
||||||
func (s *Service) Progess(args Args, progress *internal.DownloadProgress) error {
|
func (s *Service) Progess(args internal.DownloadRequest, progress *internal.DownloadProgress) error {
|
||||||
proc, err := s.db.Get(args.Id)
|
proc, err := s.db.Get(args.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -102,13 +97,20 @@ func (s *Service) Progess(args Args, progress *internal.DownloadProgress) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Progess retrieves available format for a given resource
|
// Progess retrieves available format for a given resource
|
||||||
func (s *Service) Formats(args Args, meta *internal.DownloadFormats) error {
|
func (s *Service) Formats(args internal.DownloadRequest, meta *formats.Metadata) error {
|
||||||
var (
|
var err error
|
||||||
err error
|
|
||||||
p = internal.Process{Url: args.URL}
|
metadata, err := formats.ParseURL(args.URL)
|
||||||
)
|
if err != nil && metadata == nil {
|
||||||
*meta, err = p.GetFormats()
|
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.IsPlaylist() {
|
||||||
|
go internal.PlaylistDetect(args, s.mq, s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
*meta = *metadata
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pending retrieves a slice of all Pending/Running processes ids
|
// Pending retrieves a slice of all Pending/Running processes ids
|
||||||
|
|||||||
@@ -19,16 +19,16 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/cors"
|
"github.com/go-chi/cors"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/dbutil"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/dbutil"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/handlers"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/handlers"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal/livestream"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/logging"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/logging"
|
||||||
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
|
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/rest"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/rest"
|
||||||
ytdlpRPC "github.com/marcopeocchi/yt-dlp-web-ui/server/rpc"
|
ytdlpRPC "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/rpc"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
@@ -47,13 +47,16 @@ type serverConfig struct {
|
|||||||
lm *livestream.Monitor
|
lm *livestream.Monitor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: change scope
|
||||||
|
var observableLogger = logging.NewObservableLogger()
|
||||||
|
|
||||||
func RunBlocking(rc *RunConfig) {
|
func RunBlocking(rc *RunConfig) {
|
||||||
mdb := internal.NewMemoryDB()
|
mdb := internal.NewMemoryDB()
|
||||||
|
|
||||||
// ---- LOGGING ---------------------------------------------------
|
// ---- LOGGING ---------------------------------------------------
|
||||||
logWriters := []io.Writer{
|
logWriters := []io.Writer{
|
||||||
os.Stdout,
|
os.Stdout,
|
||||||
logging.NewObservableLogger(), // for web-ui
|
observableLogger, // for web-ui
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := config.Instance()
|
conf := config.Instance()
|
||||||
@@ -65,6 +68,8 @@ func RunBlocking(rc *RunConfig) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer logger.Rotate()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
time.Sleep(time.Hour * 24)
|
time.Sleep(time.Hour * 24)
|
||||||
@@ -207,7 +212,7 @@ func newServer(c serverConfig) *http.Server {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
r.Route("/log", logging.ApplyRouter())
|
r.Route("/log", logging.ApplyRouter(observableLogger))
|
||||||
|
|
||||||
return &http.Server{Handler: r}
|
return &http.Server{Handler: r}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package updater
|
|||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update using the builtin function of yt-dlp
|
// Update using the builtin function of yt-dlp
|
||||||
|
|||||||
Reference in New Issue
Block a user