Compare commits

..

14 Commits

Author SHA1 Message Date
5a47c2d3d8 upgraded react to v19 2024-12-12 09:38:37 +01:00
f29d719df0 fixed never awaited cookies template promise 2024-12-12 09:33:09 +01:00
Marco Piovanello
6adfa71fde custom path based frontend (#231) 2024-12-05 10:00:15 +01:00
0946d374e3 more examples 2024-11-23 09:53:44 +01:00
Marco Piovanello
f68c29f838 Update README.md 2024-11-19 11:43:42 +01:00
Marco Piovanello
c46e39e736 code refactoring (#227) 2024-11-19 11:36:36 +01:00
Marco Piovanello
2885d6b5d8 Update README.md 2024-11-17 08:58:08 +01:00
Marco Piovanello
ab7932ae92 Editable templates (#225)
* editable templates

* removed unused import
2024-11-15 14:24:44 +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
74 changed files with 1407 additions and 1733 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

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,3 +1,7 @@
> [!NOTE]
> A poll is up to decide the future of yt-dlp-web-ui frontend! If you're interested you can take part.
> https://github.com/marcopeocchi/yt-dlp-web-ui/discussions/223
# yt-dlp Web UI # yt-dlp Web UI
A not so terrible web ui for yt-dlp. A not so terrible web ui for yt-dlp.
@@ -151,6 +155,8 @@ Usage yt-dlp-webui:
session file path (default ".") session file path (default ".")
-user string -user string
Username required for auth Username required for auth
-web string
frontend web resources path
``` ```
### Config file ### Config file
@@ -180,7 +186,7 @@ password: my_random_secret
queue_size: 4 # min. 2 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) # [optional] Enable file based logging with rotation (default: false)
#enable_file_logging: false #enable_file_logging: false
@@ -193,6 +199,9 @@ downloaderPath: /usr/local/bin/yt-dlp
# [optional] Path where the sqlite database will be created/opened (default: "./local.db") # [optional] Path where the sqlite database will be created/opened (default: "./local.db")
#local_database_path #local_database_path
# [optional] Path where a custom frontend will be loaded (instead of the embedded one)
#frontend_path: ./web/solid-frontend
``` ```
### Systemd integration ### Systemd integration
@@ -258,6 +267,22 @@ It is **planned** to also expose a **gRPC** server.
For more information open an issue on GitHub and I will provide more info ASAP. For more information open an issue on GitHub and I will provide more info ASAP.
## Custom frontend
To load a custom frontend you need to specify its path either in the config file ([see config file](#config-file)) or via flags.
The frontend needs to follow this structure:
```
path/to/my/frontend
├── assets
│ ├── js-chunk-1.js (example)
│ ├── js-chunk-2.js (example)
│ ├── style.css (example)
└── index.html
```
`assets` is where the resources will be loaded.
`index.html` is the entrypoint.
## Nix ## Nix
This repo adds support for Nix(OS) in various ways through a `flake-parts` flake. This repo adds support for Nix(OS) in various ways through a `flake-parts` flake.
For more info, please refer to the [official documentation](https://nixos.org/learn/). For more info, please refer to the [official documentation](https://nixos.org/learn/).

View File

@@ -0,0 +1,27 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://app:3033;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
client_max_body_size 20000m;
proxy_connect_timeout 5000;
proxy_send_timeout 5000;
proxy_read_timeout 5000;
send_timeout 5000;
}
}

View File

@@ -0,0 +1,15 @@
services:
app:
image: marcobaobao/yt-dlp-webui
volumes:
- ./downloads:/downloads
restart: unless-stopped
nginx:
image: nginx:alpine
restart: unless-stopped
volumes:
- ./app.conf:/etc/nginx/conf.d/app.conf
depends_on:
- app
ports:
- 80:80

View File

@@ -1,39 +1,38 @@
{ {
"name": "yt-dlp-webui", "name": "yt-dlp-webui",
"version": "3.2.1", "version": "3.2.3",
"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.14.0",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.0.13", "@fontsource/roboto": "^5.0.13",
"@fontsource/roboto-mono": "^5.0.18", "@fontsource/roboto-mono": "^5.0.18",
"@mui/icons-material": "^5.15.16", "@mui/icons-material": "^6.2.0",
"@mui/material": "^5.15.16", "@mui/material": "^6.2.0",
"fp-ts": "^2.16.5", "fp-ts": "^2.16.5",
"react": "^18.3.1", "react": "^19.0.0",
"react-dom": "^18.3.1", "react-dom": "^19.0.0",
"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.3",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },
"devDependencies": { "devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0", "@modyfi/vite-plugin-yaml": "^1.1.0",
"@types/node": "^20.14.2", "@types/node": "^20.14.2",
"@types/react": "^18.3.3", "@types/react": "^19.0.1",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^19.0.2",
"@types/react-helmet": "^6.1.11", "@types/react-helmet": "^6.1.11",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.7.0", "@vitejs/plugin-react-swc": "^3.7.2",
"million": "^3.1.11", "typescript": "^5.7.2",
"typescript": "^5.4.5", "vite": "^6.0.3"
"vite": "^5.2.11"
} }
} }

1697
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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

@@ -1,50 +1,40 @@
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<Promise<string>>(async (get) =>
key: 'downloadTemplateState', `${get(customArgsState)} ${await get(cookiesTemplateState)}`
get: ({ get }) => .replace(/ +/g, ' ')
`${get(customArgsState)} ${get(cookiesTemplateState)}` .trim()
.replace(/ +/g, ' ') )
.trim()
})
export const savedTemplatesState = selector<CustomTemplate[]>({ export const savedTemplatesState = atom<Promise<CustomTemplate[]>>(async (get) => {
key: 'savedTemplatesState', const task = ffetch<CustomTemplate[]>(`${get(serverURL)}/api/v1/template/all`)
get: async ({ get }) => { const either = await task()
const task = ffetch<CustomTemplate[]>(`${get(serverURL)}/api/v1/template/all`)
const either = await task()
return pipe( return pipe(
either, either,
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(downloadsState),
get: ({ get }) => pipe( O.getOrElse(() => new Array<RPCResult>())
get(downloadsState), ))
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', new RPCClient(
get: ({ get }) => get(rpcHTTPEndpoint),
new RPCClient( get(rpcWebSocketEndpoint),
get(rpcHTTPEndpoint), localStorage.getItem('token') ?? ''
get(rpcWebSocketEndpoint), ),
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,8 +1,9 @@
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',
@@ -40,194 +41,126 @@ 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', if (get(servedFromReverseProxySubDirState)) {
get: ({ get }) => { return `${get(serverAddressState)}/${get(servedFromReverseProxySubDirState)}/`
if (get(servedFromReverseProxySubDirState)) {
return `${get(serverAddressState)}/${get(servedFromReverseProxySubDirState)}/`
}
if (get(servedFromReverseProxyState)) {
return `${get(serverAddressState)}`
}
return `${get(serverAddressState)}:${get(serverPortState)}`
} }
}) if (get(servedFromReverseProxyState)) {
return `${get(serverAddressState)}`
export const serverURL = selector({
key: 'serverURL',
get: ({ get }) =>
`${window.location.protocol}//${get(serverAddressAndPortState)}`
})
export const rpcWebSocketEndpoint = selector({
key: 'rpcWebSocketEndpoint',
get: ({ get }) => {
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
return `${proto}//${get(serverAddressAndPortState)}/rpc/ws`
} }
return `${get(serverAddressState)}:${get(serverPortState)}`
}) })
export const rpcHTTPEndpoint = selector({ export const serverURL = atom((get) =>
key: 'rpcHTTPEndpoint', `${window.location.protocol}//${get(serverAddressAndPortState)}`
get: ({ get }) => { )
const proto = window.location.protocol
return `${proto}//${get(serverAddressAndPortState)}/rpc/http` export const rpcWebSocketEndpoint = atom((get) => {
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
return `${proto}//${get(serverAddressAndPortState)}/rpc/ws`
}
)
export const rpcHTTPEndpoint = atom((get) => {
const proto = window.location.protocol
return `${proto}//${get(serverAddressAndPortState)}/rpc/http`
}
)
export const serverSideCookiesState = atom<Promise<string>>(async (get) => await pipe(
ffetch<Readonly<{ cookies: string }>>(`${get(serverURL)}/api/v1/cookies`),
matchW(
() => '',
(r) => r.cookies
)
)())
const themeSelector = atom<ThemeNarrowed>((get) => {
const theme = get(themeState)
if ((theme === 'system' && prefersDarkMode()) || theme === 'dark') {
return 'dark'
} }
}) return 'light'
}
)
export const serverSideCookiesState = selector<string>({ export const settingsState = atom<SettingsState>((get) => ({
key: 'serverSideCookiesState', serverAddr: get(serverAddressState),
get: async ({ get }) => await pipe( serverPort: get(serverPortState),
ffetch<Readonly<{ cookies: string }>>(`${get(serverURL)}/api/v1/cookies`), language: get(languageState),
matchW( theme: get(themeSelector),
() => '', cliArgs: get(latestCliArgumentsState),
(r) => r.cookies formatSelection: get(formatSelectionState),
) fileRenaming: get(fileRenamingState),
)() pathOverriding: get(pathOverridingState),
enableCustomArgs: get(enableCustomArgsState),
listView: get(listViewState),
servedFromReverseProxy: get(servedFromReverseProxyState),
appTitle: get(appTitleState)
}) })
)
const themeSelector = selector<ThemeNarrowed>({
key: 'themeSelector',
get: ({ get }) => {
const theme = get(themeState)
if ((theme === 'system' && prefersDarkMode()) || theme === 'dark') {
return 'dark'
}
return 'light'
}
})
export const settingsState = selector<SettingsState>({
key: 'settingsState',
get: ({ get }) => ({
serverAddr: get(serverAddressState),
serverPort: get(serverPortState),
language: get(languageState),
theme: get(themeSelector),
cliArgs: get(latestCliArgumentsState),
formatSelection: get(formatSelectionState),
fileRenaming: get(fileRenamingState),
pathOverriding: get(pathOverridingState),
enableCustomArgs: get(enableCustomArgsState),
listView: get(listViewState),
servedFromReverseProxy: get(servedFromReverseProxyState),
appTitle: get(appTitleState)
})
})

View File

@@ -1,45 +1,34 @@
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 = atom(async (get) => {
const res = await get(rpcClientState)
.freeSpace()
.catch(() => ({ result: 0 }))
return res.result
}) })
export const freeSpaceBytesState = selector({ export const availableDownloadPathsState = atom(async (get) => {
key: 'freeSpaceBytesState', const res = await get(rpcClientState).directoryTree()
get: async ({ get }) => { .catch(() => ({ result: [] }))
const res = await get(rpcClientState).freeSpace() return res.result
.catch(() => ({ result: 0 }))
return res.result
}
}) })
export const availableDownloadPathsState = selector({ export const ytdlpRpcVersionState = atom<Promise<RPCVersion>>(async (get) => await pipe(
key: 'availableDownloadPathsState', ffetch<RPCVersion>(`${get(serverURL)}/api/v1/version`),
get: async ({ get }) => { getOrElse(() => pipe(
const res = await get(rpcClientState).directoryTree() {
.catch(() => ({ result: [] })) rpcVersion: 'unknown version',
return res.result ytdlpVersion: 'unknown version',
} },
}) of
)),
export const ytdlpRpcVersionState = selector<RPCVersion>({ )())
key: 'ytdlpRpcVersionState',
get: async ({ get }) => await pipe(
ffetch<RPCVersion>(`${get(serverURL)}/api/v1/version`),
getOrElse(() => pipe(
{
rpcVersion: 'unknown version',
ytdlpVersion: 'unknown version',
},
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

@@ -2,7 +2,6 @@ import { FileUpload } from '@mui/icons-material'
import CloseIcon from '@mui/icons-material/Close' import CloseIcon from '@mui/icons-material/Close'
import { import {
Autocomplete, Autocomplete,
Backdrop,
Box, Box,
Button, Button,
Checkbox, Checkbox,
@@ -21,6 +20,7 @@ import Slide from '@mui/material/Slide'
import Toolbar from '@mui/material/Toolbar' import Toolbar from '@mui/material/Toolbar'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { TransitionProps } from '@mui/material/transitions' import { TransitionProps } from '@mui/material/transitions'
import { useAtom, useAtomValue } from 'jotai'
import { import {
FC, FC,
Suspense, Suspense,
@@ -30,16 +30,22 @@ import {
useState, useState,
useTransition useTransition
} from 'react' } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil' import {
import { customArgsState, downloadTemplateState, filenameTemplateState, savedTemplatesState } from '../atoms/downloadTemplate' 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'
import FormatsGrid from '../components/FormatsGrid' import FormatsGrid from '../components/FormatsGrid'
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 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 LoadingBackdrop from './LoadingBackdrop'
const Transition = forwardRef(function Transition( const Transition = forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
@@ -57,22 +63,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 +89,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 +137,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 +200,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()

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

@@ -0,0 +1,33 @@
import { Button, CircularProgress } from '@mui/material'
import { useI18n } from '../hooks/useI18n'
import { useRPC } from '../hooks/useRPC'
import { useState } from 'react'
import { useToast } from '../hooks/toast'
const UpdateBinaryButton: React.FC = () => {
const { i18n } = useI18n()
const { client } = useRPC()
const { pushMessage } = useToast()
const [isLoading, setIsLoading] = useState(false)
const updateBinary = () => {
setIsLoading(true)
client
.updateExecutable()
.then(() => pushMessage(i18n.t('toastUpdated'), 'success'))
.then(() => setIsLoading(false))
}
return (
<Button
variant="contained"
endIcon={isLoading ? <CircularProgress size={16} color='secondary' /> : <></>}
onClick={updateBinary}
>
{i18n.t('updateBinButton')}
</Button>
)
}
export default UpdateBinaryButton

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

@@ -191,4 +191,11 @@ export class RPCClient {
params: [] params: []
}) })
} }
public updateExecutable() {
return this.sendHTTP({
method: 'Service.UpdateExecutable',
params: []
})
}
} }

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

@@ -1,5 +1,4 @@
import { import {
Button,
Checkbox, Checkbox,
Container, Container,
FormControl, FormControl,
@@ -18,8 +17,8 @@ import {
Typography, Typography,
capitalize capitalize
} from '@mui/material' } from '@mui/material'
import { useAtom } from 'jotai'
import { Suspense, useEffect, useMemo, useState } from 'react' import { Suspense, useEffect, useMemo, useState } from 'react'
import { useRecoilState } from 'recoil'
import { import {
Subject, Subject,
debounceTime, debounceTime,
@@ -45,34 +44,33 @@ import {
themeState themeState
} from '../atoms/settings' } from '../atoms/settings'
import CookiesTextField from '../components/CookiesTextField' import CookiesTextField from '../components/CookiesTextField'
import UpdateBinaryButton from '../components/UpdateBinaryButton'
import { useToast } from '../hooks/toast' import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n' import { useI18n } from '../hooks/useI18n'
import { useRPC } from '../hooks/useRPC'
import { validateDomain, validateIP } from '../utils' import { validateDomain, validateIP } from '../utils'
// NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS // NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS
export default function Settings() { export default function Settings() {
const [reverseProxy, setReverseProxy] = useRecoilState(servedFromReverseProxyState) const [reverseProxy, setReverseProxy] = useAtom(servedFromReverseProxyState)
const [baseURL, setBaseURL] = useRecoilState(servedFromReverseProxySubDirState) const [baseURL, setBaseURL] = useAtom(servedFromReverseProxySubDirState)
const [formatSelection, setFormatSelection] = useRecoilState(formatSelectionState) const [formatSelection, setFormatSelection] = useAtom(formatSelectionState)
const [pathOverriding, setPathOverriding] = useRecoilState(pathOverridingState) const [pathOverriding, setPathOverriding] = useAtom(pathOverridingState)
const [fileRenaming, setFileRenaming] = useRecoilState(fileRenamingState) const [fileRenaming, setFileRenaming] = useAtom(fileRenamingState)
const [enableArgs, setEnableArgs] = useRecoilState(enableCustomArgsState) const [enableArgs, setEnableArgs] = useAtom(enableCustomArgsState)
const [serverAddr, setServerAddr] = useRecoilState(serverAddressState) const [serverAddr, setServerAddr] = useAtom(serverAddressState)
const [serverPort, setServerPort] = useRecoilState(serverPortState) const [serverPort, setServerPort] = useAtom(serverPortState)
const [pollingTime, setPollingTime] = useRecoilState(rpcPollingTimeState) const [pollingTime, setPollingTime] = useAtom(rpcPollingTimeState)
const [language, setLanguage] = useRecoilState(languageState) const [language, setLanguage] = useAtom(languageState)
const [appTitle, setApptitle] = useRecoilState(appTitleState) const [appTitle, setApptitle] = useAtom(appTitleState)
const [theme, setTheme] = useRecoilState(themeState) const [theme, setTheme] = useAtom(themeState)
const [invalidIP, setInvalidIP] = useState(false) const [invalidIP, setInvalidIP] = useState(false)
const { i18n } = useI18n() const { i18n } = useI18n()
const { client } = useRPC()
const { pushMessage } = useToast() const { pushMessage } = useToast()
@@ -140,13 +138,6 @@ export default function Settings() {
setTheme(event.target.value as Theme) setTheme(event.target.value as Theme)
} }
/**
* Updates yt-dlp binary via RPC
*/
const updateBinary = () => {
client.updateExecutable().then(() => pushMessage(i18n.t('toastUpdated'), 'success'))
}
return ( return (
<Container maxWidth="xl" sx={{ mt: 4, mb: 8 }}> <Container maxWidth="xl" sx={{ mt: 4, mb: 8 }}>
<Paper <Paper
@@ -352,14 +343,8 @@ export default function Settings() {
</Suspense> </Suspense>
</Grid> </Grid>
<Grid> <Grid>
<Stack direction="row"> <Stack direction="row" sx={{ pt: 2 }}>
<Button <UpdateBinaryButton />
sx={{ mr: 1, mt: 3 }}
variant="contained"
onClick={() => updateBinary()}
>
{i18n.t('updateBinButton')}
</Button>
</Stack> </Stack>
</Grid> </Grid>
</Paper> </Paper>

View File

@@ -1,12 +1,10 @@
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc'
import million from 'million/compiler'
import ViteYaml from '@modyfi/vite-plugin-yaml' import ViteYaml from '@modyfi/vite-plugin-yaml'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
export default defineConfig(() => { export default defineConfig(() => {
return { return {
plugins: [ plugins: [
million.vite({ auto: true }),
react(), react(),
ViteYaml(), ViteYaml(),
], ],

2
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

14
main.go
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 (
@@ -23,6 +23,7 @@ var (
downloaderPath string downloaderPath string
sessionFilePath string sessionFilePath string
localDatabasePath string localDatabasePath string
frontendPath string
requireAuth bool requireAuth bool
username string username string
@@ -52,6 +53,7 @@ func init() {
flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path") flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path")
flag.StringVar(&sessionFilePath, "session", ".", "session file path") flag.StringVar(&sessionFilePath, "session", ".", "session file path")
flag.StringVar(&localDatabasePath, "db", "local.db", "local database path") flag.StringVar(&localDatabasePath, "db", "local.db", "local database path")
flag.StringVar(&frontendPath, "web", "", "frontend web resources path")
flag.BoolVar(&enableFileLogging, "fl", false, "enable outputting logs to a file") flag.BoolVar(&enableFileLogging, "fl", false, "enable outputting logs to a file")
flag.StringVar(&logFile, "lf", "yt-dlp-webui.log", "set log file location") flag.StringVar(&logFile, "lf", "yt-dlp-webui.log", "set log file location")
@@ -69,6 +71,10 @@ func main() {
log.Fatalln(err) log.Fatalln(err)
} }
if frontendPath != "" {
frontend = os.DirFS(frontendPath)
}
c := config.Instance() c := config.Instance()
{ {

View File

@@ -9,26 +9,26 @@ import (
) )
type Config struct { type Config struct {
LogPath string `yaml:"log_path"` LogPath string `yaml:"log_path"`
EnableFileLogging bool `yaml:"enable_file_logging"` EnableFileLogging bool `yaml:"enable_file_logging"`
BaseURL string `yaml:"base_url"` BaseURL string `yaml:"base_url"`
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
DownloadPath string `yaml:"downloadPath"` DownloadPath string `yaml:"downloadPath"`
DownloaderPath string `yaml:"downloaderPath"` DownloaderPath string `yaml:"downloaderPath"`
RequireAuth bool `yaml:"require_auth"` RequireAuth bool `yaml:"require_auth"`
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"password"` Password string `yaml:"password"`
QueueSize int `yaml:"queue_size"` QueueSize int `yaml:"queue_size"`
LocalDatabasePath string `yaml:"local_database_path"` LocalDatabasePath string `yaml:"local_database_path"`
SessionFilePath string `yaml:"session_file_path"` SessionFilePath string `yaml:"session_file_path"`
path string path string // private
UseOpenId bool `yaml:"use_openid"` UseOpenId bool `yaml:"use_openid"`
OpenIdProviderURL string `yaml:"openid_provider_url"` OpenIdProviderURL string `yaml:"openid_provider_url"`
OpenIdClientId string `yaml:"openid_client_id"` OpenIdClientId string `yaml:"openid_client_id"`
OpenIdClientSecret string `yaml:"openid_client_secret"` OpenIdClientSecret string `yaml:"openid_client_secret"`
OpenIdRedirectURL string `yaml:"openid_redirect_url"` OpenIdRedirectURL string `yaml:"openid_redirect_url"`
FrontendPath string `yaml:"frontend_path"`
} }
var ( var (

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

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,12 +18,12 @@ 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:
{ {
"eta":%(progress.eta)s, "eta":%(progress.eta)s,
"percentage":"%(progress._percent_str)s", "percentage":"%(progress._percent_str)s",
"speed":%(progress.speed)s "speed":%(progress.speed)s
}` }`
@@ -225,7 +224,7 @@ func (p *Process) Complete() {
Speed: 0, Speed: 0,
ETA: 0, ETA: 0,
} }
// for safety, if the filename is not set, set it with original function // for safety, if the filename is not set, set it with original function
if p.Output.SavedFilePath == "" { if p.Output.SavedFilePath == "" {
p.GetFileName(&p.Output) p.GetFileName(&p.Output)
@@ -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

@@ -9,9 +9,9 @@ import (
"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{

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,13 +97,20 @@ func (s *Service) Progess(args Args, progress *internal.DownloadProgress) error
} }
// Progess retrieves available format for a given resource // Progess retrieves available format for a given resource
func (s *Service) Formats(args Args, meta *internal.DownloadFormats) error { func (s *Service) Formats(args internal.DownloadRequest, meta *formats.Metadata) error {
var ( var err error
err error
p = internal.Process{Url: args.URL} metadata, err := formats.ParseURL(args.URL)
) if err != nil && metadata == nil {
*meta, err = p.GetFormats() return err
return err }
if metadata.IsPlaylist() {
go internal.PlaylistDetect(args, s.mq, s.db)
}
*meta = *metadata
return nil
} }
// Pending retrieves a slice of all Pending/Running processes ids // Pending retrieves a slice of all Pending/Running processes ids

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

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