Compare commits

..

17 Commits

Author SHA1 Message Date
d1184bf939 removed unused import 2024-11-15 14:22:59 +01:00
a39b526d64 editable templates 2024-11-15 11:29:13 +01:00
6c9118f67e added dockerignore 2024-11-12 11:39:21 +01:00
34c78c7e2d ready for 3.2.2 2024-11-12 11:32:50 +01:00
Marco Piovanello
01c6edef74 jotai migration (#221) 2024-11-12 11:31:25 +01:00
4a87ea559a prevent downloading playlist with format selection 2024-11-10 15:32:17 +01:00
Andrási István
846fb294d0 Fix module name to match major version v3. Simplify makefile. (#213)
Co-authored-by: Marco Piovanello <35533749+marcopeocchi@users.noreply.github.com>
2024-11-10 13:59:40 +01:00
Dusk
baa25afa27 fix: manual installation (#220) 2024-11-10 08:53:30 +01:00
Néfix Estrada
b0dac0adda fix(nix): fix package build (#208) 2024-10-13 13:38:17 +02:00
Marco Piovanello
8c0e7b3cb8 updated memory_db.go
Closes #202
2024-09-18 21:25:06 +02:00
38da65a848 reduced log rotation memory usage 2024-09-18 17:50:14 +02:00
Marco Piovanello
64fbdbbbdf Dropping rxgo (#201)
* rxgo event source to channel with drop strategy

* code optimizations
2024-09-18 17:49:25 +02:00
Marco Piovanello
a00059ca88 enabled french and swedish (#199) 2024-09-18 13:08:57 +02:00
Oさん
7abccaec71 Update Japanese translation (#198) 2024-09-18 13:05:06 +02:00
Marco Piovanello
f075d3d531 Update README.md 2024-09-18 11:40:49 +02:00
Marco Piovanello
190d891578 Update README.md 2024-09-18 11:40:34 +02:00
645cc6f9a1 readme update 2024-09-18 11:30:09 +02:00
74 changed files with 898 additions and 852 deletions

28
.dockerignore Normal file
View 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
View File

@@ -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']

View File

@@ -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

View File

@@ -1,7 +1,10 @@
# 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 +16,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
![image](https://github.com/user-attachments/assets/fc43a3fb-ecf9-449d-b5cb-5d5635020c00)
![image](https://github.com/user-attachments/assets/3210f6ac-0dd8-403c-b839-3c24ff7d7d00)
![image](https://github.com/user-attachments/assets/16450a40-cda6-4c8b-9d20-8ec36282f6ed)
## 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 +128,29 @@ 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
Password required for auth
``` ```
### Config file ### Config file
@@ -155,17 +176,23 @@ 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
``` ```
### Systemd integration ### Systemd integration
@@ -214,13 +241,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.
@@ -243,7 +266,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)

View File

@@ -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": {

View File

@@ -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: {}

View File

@@ -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>
) )
} }

View File

@@ -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(() =>

View File

@@ -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

View File

@@ -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()
@@ -47,4 +37,4 @@ export const savedTemplatesState = selector<CustomTemplate[]>({
getOrElse(() => new Array<CustomTemplate>()) getOrElse(() => new Array<CustomTemplate>())
) )
} }
}) )

View File

@@ -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>())
) ))
})

View File

@@ -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,
})

View File

@@ -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()))
]
})

View File

@@ -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),
@@ -228,4 +163,4 @@ export const settingsState = selector<SettingsState>({
servedFromReverseProxy: get(servedFromReverseProxyState), servedFromReverseProxy: get(servedFromReverseProxyState),
appTitle: get(appTitleState) appTitle: get(appTitleState)
}) })
}) )

View File

@@ -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
)), )),
)() )())
})

View File

@@ -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: [],
})

View File

@@ -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)
}) )

View File

@@ -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()

View File

@@ -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',

View File

@@ -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

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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) => {

View File

@@ -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 (
<> <>

View File

@@ -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()

View File

@@ -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={{

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)
}) })

View File

@@ -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()

View File

@@ -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

View 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

View File

@@ -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>

View File

@@ -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 />,

View File

@@ -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' }}>

View File

@@ -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) => {

View File

@@ -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
} }
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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()

View File

@@ -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 }}>

View File

@@ -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()

View File

@@ -19,7 +19,6 @@ import {
capitalize capitalize
} from '@mui/material' } from '@mui/material'
import { Suspense, useEffect, useMemo, useState } from 'react' import { Suspense, useEffect, useMemo, useState } from 'react'
import { useRecoilState } from 'recoil'
import { import {
Subject, Subject,
debounceTime, debounceTime,
@@ -49,25 +48,26 @@ 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 { validateDomain, validateIP } from '../utils' import { validateDomain, validateIP } from '../utils'
import { useAtom } from 'jotai'
// 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)

12
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -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 (

View File

@@ -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";

View File

@@ -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
View 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
View 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"`
}

View File

@@ -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"
) )
/* /*

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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 (

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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"
) )

View File

@@ -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 {

View File

@@ -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,

View File

@@ -3,12 +3,16 @@ package logging
import ( import (
"compress/gzip" "compress/gzip"
"io" "io"
"log/slog"
"os" "os"
"strings"
"sync" "sync"
"time" "time"
) )
/* /*
implements io.Writer interface
File base logger with log-rotate capabilities. File base logger with log-rotate capabilities.
The rotate process must be initiated from an external goroutine. The rotate process must be initiated from an external goroutine.
@@ -16,8 +20,6 @@ import (
The rotated log follows this naming: [filename].UTC time.gz 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
}

View File

@@ -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))
} }
} }

View File

@@ -1,9 +1,7 @@
package logging package logging
import ( import (
"time" "context"
"github.com/reactivex/rxgo/v2"
) )
/* /*
@@ -16,25 +14,40 @@ import (
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
default:
return 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
}

View File

@@ -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"
) )

View File

@@ -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 {

View File

@@ -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())
} }

View File

@@ -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
} }
} }
} }

View File

@@ -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)

View File

@@ -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.

View File

@@ -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,15 +97,22 @@ 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
func (s *Service) Pending(args NoArgs, pending *Pending) error { func (s *Service) Pending(args NoArgs, pending *Pending) error {
*pending = *s.db.Keys() *pending = *s.db.Keys()

View File

@@ -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}
} }

View File

@@ -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"
) )

View File

@@ -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