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
fe:
cd frontend && pnpm build
cd frontend && pnpm install && pnpm build
dev:
cd frontend && pnpm dev
cd frontend && pnpm install && pnpm dev
all:
$(MAKE) fe && cd ..
all: fe
CGO_ENABLED=0 go build -o yt-dlp-webui main.go
multiarch:
$(MAKE) fe
multiarch: fe
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=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
A not so terrible web ui for yt-dlp.
@@ -151,6 +155,8 @@ Usage yt-dlp-webui:
session file path (default ".")
-user string
Username required for auth
-web string
frontend web resources path
```
### Config file
@@ -180,7 +186,7 @@ password: my_random_secret
queue_size: 4 # min. 2
# [optional] Full path to the yt-dlp (default: "yt-dlp")
downloaderPath: /usr/local/bin/yt-dlp
#downloaderPath: /usr/local/bin/yt-dlp
# [optional] Enable file based logging with rotation (default: false)
#enable_file_logging: false
@@ -193,6 +199,9 @@ downloaderPath: /usr/local/bin/yt-dlp
# [optional] Path where the sqlite database will be created/opened (default: "./local.db")
#local_database_path
# [optional] Path where a custom frontend will be loaded (instead of the embedded one)
#frontend_path: ./web/solid-frontend
```
### Systemd integration
@@ -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.
## 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
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/).

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",
"version": "3.2.1",
"version": "3.2.3",
"description": "Frontend compontent of yt-dlp-webui",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"author": "marcopeocchi",
"license": "MPL-2.0",
"license": "GPL-3.0-only",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.0.13",
"@fontsource/roboto-mono": "^5.0.18",
"@mui/icons-material": "^5.15.16",
"@mui/material": "^5.15.16",
"@mui/icons-material": "^6.2.0",
"@mui/material": "^6.2.0",
"fp-ts": "^2.16.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^6.23.1",
"react-virtuoso": "^4.7.11",
"recoil": "^0.7.7",
"jotai": "^2.10.3",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0",
"@types/node": "^20.14.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.2.18",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@types/react-helmet": "^6.1.11",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.7.0",
"million": "^3.1.11",
"typescript": "^5.4.5",
"vite": "^5.2.11"
"@vitejs/plugin-react-swc": "^3.7.2",
"typescript": "^5.7.2",
"vite": "^6.0.3"
}
}

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 { RecoilRoot } from 'recoil'
import { Provider } from 'jotai'
import { router } from './router'
export function App() {
return (
<RecoilRoot>
<Provider>
<RouterProvider router={router} />
</RecoilRoot>
</Provider>
)
}

View File

@@ -19,7 +19,6 @@ import Typography from '@mui/material/Typography'
import { grey } from '@mui/material/colors'
import { useMemo, useState } from 'react'
import { Link, Outlet } from 'react-router-dom'
import { useRecoilValue } from 'recoil'
import { settingsState } from './atoms/settings'
import AppBar from './components/AppBar'
import Drawer from './components/Drawer'
@@ -29,11 +28,12 @@ import SocketSubscriber from './components/SocketSubscriber'
import ThemeToggler from './components/ThemeToggler'
import { useI18n } from './hooks/useI18n'
import Toaster from './providers/ToasterProvider'
import { useAtomValue } from 'jotai'
export default function Layout() {
const [open, setOpen] = useState(false)
const settings = useRecoilValue(settingsState)
const settings = useAtomValue(settingsState)
const mode = settings.theme
const theme = useMemo(() =>

View File

@@ -1,50 +1,40 @@
import { getOrElse } from 'fp-ts/lib/Either'
import { pipe } from 'fp-ts/lib/function'
import { atom, selector } from 'recoil'
import { ffetch } from '../lib/httpClient'
import { CustomTemplate } from '../types'
import { serverSideCookiesState, serverURL } from './settings'
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
export const cookiesTemplateState = selector({
key: 'cookiesTemplateState',
get: ({ get }) => get(serverSideCookiesState)
export const cookiesTemplateState = atom<Promise<string>>(async (get) =>
await get(serverSideCookiesState)
? '--cookies=cookies.txt'
: ''
})
)
export const customArgsState = atom({
key: 'customArgsState',
default: localStorage.getItem('customArgs') ?? '',
effects: [
({ onSet }) => onSet(e => localStorage.setItem('customArgs', e))
]
})
export const customArgsState = atomWithStorage(
'customArgs',
localStorage.getItem('customArgs') ?? ''
)
export const filenameTemplateState = atom({
key: 'filenameTemplateState',
default: localStorage.getItem('lastFilenameTemplate') ?? '',
effects: [
({ onSet }) => onSet(e => localStorage.setItem('lastFilenameTemplate', e))
]
})
export const filenameTemplateState = atomWithStorage(
'lastFilenameTemplate',
localStorage.getItem('lastFilenameTemplate') ?? ''
)
export const downloadTemplateState = selector({
key: 'downloadTemplateState',
get: ({ get }) =>
`${get(customArgsState)} ${get(cookiesTemplateState)}`
.replace(/ +/g, ' ')
.trim()
})
export const downloadTemplateState = atom<Promise<string>>(async (get) =>
`${get(customArgsState)} ${await get(cookiesTemplateState)}`
.replace(/ +/g, ' ')
.trim()
)
export const savedTemplatesState = selector<CustomTemplate[]>({
key: 'savedTemplatesState',
get: async ({ get }) => {
const task = ffetch<CustomTemplate[]>(`${get(serverURL)}/api/v1/template/all`)
const either = await task()
export const savedTemplatesState = atom<Promise<CustomTemplate[]>>(async (get) => {
const task = ffetch<CustomTemplate[]>(`${get(serverURL)}/api/v1/template/all`)
const either = await task()
return pipe(
either,
getOrElse(() => new Array<CustomTemplate>())
)
}
})
return pipe(
either,
getOrElse(() => new Array<CustomTemplate>())
)
}
)

View File

@@ -1,22 +1,13 @@
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/lib/function'
import { atom, selector } from 'recoil'
import { RPCResult } from '../types'
import { atom } from 'jotai'
export const downloadsState = atom<O.Option<RPCResult[]>>({
key: 'downloadsState',
default: O.none
})
export const downloadsState = atom<O.Option<RPCResult[]>>(O.none)
export const loadingDownloadsState = selector<boolean>({
key: 'loadingDownloadsState',
get: ({ get }) => O.isNone(get(downloadsState))
})
export const loadingDownloadsState = atom<boolean>((get) => O.isNone(get(downloadsState)))
export const activeDownloadsState = selector<RPCResult[]>({
key: 'activeDownloadsState',
get: ({ get }) => pipe(
get(downloadsState),
O.getOrElse(() => new Array<RPCResult>())
)
})
export const activeDownloadsState = atom<RPCResult[]>((get) => pipe(
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 { languageState } from './settings'
export const i18nBuilderState = selector({
key: 'i18nBuilderState',
get: ({ get }) => new I18nBuilder(get(languageState)),
dangerouslyAllowMutability: true,
})
export const i18nBuilderState = atom((get) => new I18nBuilder(get(languageState)))

View File

@@ -1,23 +1,17 @@
import { atom, selector } from 'recoil'
import { atom } from 'jotai'
import { RPCClient } from '../lib/rpcClient'
import { rpcHTTPEndpoint, rpcWebSocketEndpoint } from './settings'
import { atomWithStorage } from 'jotai/utils'
export const rpcClientState = selector({
key: 'rpcClientState',
get: ({ get }) =>
new RPCClient(
get(rpcHTTPEndpoint),
get(rpcWebSocketEndpoint),
localStorage.getItem('token') ?? ''
),
dangerouslyAllowMutability: true,
})
export const rpcClientState = atom((get) =>
new RPCClient(
get(rpcHTTPEndpoint),
get(rpcWebSocketEndpoint),
localStorage.getItem('token') ?? ''
),
)
export const rpcPollingTimeState = atom({
key: 'rpcPollingTimeState',
default: Number(localStorage.getItem('rpc-polling-time')) || 1000,
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('rpc-polling-time', a.toString()))
]
})
export const rpcPollingTimeState = atomWithStorage(
'rpc-polling-time',
Number(localStorage.getItem('rpc-polling-time')) || 1000
)

View File

@@ -1,8 +1,9 @@
import { pipe } from 'fp-ts/lib/function'
import { matchW } from 'fp-ts/lib/TaskEither'
import { atom, selector } from 'recoil'
import { ffetch } from '../lib/httpClient'
import { prefersDarkMode } from '../utils'
import { atomWithStorage } from 'jotai/utils'
import { atom } from 'jotai'
export const languages = [
'english',
@@ -40,194 +41,126 @@ export interface SettingsState {
appTitle: string
}
export const languageState = atom<Language>({
key: 'languageState',
default: localStorage.getItem('language') as Language || 'english',
effects: [
({ onSet }) =>
onSet(l => localStorage.setItem('language', l.toString()))
]
})
export const languageState = atomWithStorage<Language>(
'language',
localStorage.getItem('language') as Language || 'english'
)
export const themeState = atom<Theme>({
key: 'themeStateState',
default: localStorage.getItem('theme') as Theme || 'system',
effects: [
({ onSet }) =>
onSet(l => localStorage.setItem('theme', l.toString()))
]
})
export const themeState = atomWithStorage<Theme>(
'theme',
localStorage.getItem('theme') as Theme || 'system'
)
export const serverAddressState = atom<string>({
key: 'serverAddressState',
default: localStorage.getItem('server-addr') || window.location.hostname,
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('server-addr', a.toString()))
]
})
export const serverAddressState = atomWithStorage<string>(
'server-addr',
localStorage.getItem('server-addr') || window.location.hostname
)
export const serverPortState = atom<number>({
key: 'serverPortState',
default: Number(localStorage.getItem('server-port')) || Number(window.location.port),
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('server-port', a.toString()))
]
})
export const serverPortState = atomWithStorage<number>(
'server-port',
Number(localStorage.getItem('server-port')) || Number(window.location.port)
)
export const latestCliArgumentsState = atom<string>({
key: 'latestCliArgumentsState',
default: localStorage.getItem('cli-args') || '--no-mtime',
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('cli-args', a.toString()))
]
})
export const latestCliArgumentsState = atomWithStorage<string>(
'cli-args',
localStorage.getItem('cli-args') || '--no-mtime'
)
export const formatSelectionState = atom({
key: 'formatSelectionState',
default: localStorage.getItem('format-selection') === "true",
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('format-selection', a.toString()))
]
})
export const formatSelectionState = atomWithStorage(
'format-selection',
localStorage.getItem('format-selection') === 'true'
)
export const fileRenamingState = atom({
key: 'fileRenamingState',
default: localStorage.getItem('file-renaming') === "true",
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('file-renaming', a.toString()))
]
})
export const fileRenamingState = atomWithStorage(
'file-renaming',
localStorage.getItem('file-renaming') === 'true'
)
export const pathOverridingState = atom({
key: 'pathOverridingState',
default: localStorage.getItem('path-overriding') === "true",
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('path-overriding', a.toString()))
]
})
export const pathOverridingState = atomWithStorage(
'path-overriding',
localStorage.getItem('path-overriding') === 'true'
)
export const enableCustomArgsState = atom({
key: 'enableCustomArgsState',
default: localStorage.getItem('enable-custom-args') === "true",
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('enable-custom-args', a.toString()))
]
})
export const enableCustomArgsState = atomWithStorage(
'enable-custom-args',
localStorage.getItem('enable-custom-args') === 'true'
)
export const listViewState = atom({
key: 'listViewState',
default: localStorage.getItem('listview') === "true",
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('listview', a.toString()))
]
})
export const listViewState = atomWithStorage(
'listview',
localStorage.getItem('listview') === 'true'
)
export const servedFromReverseProxyState = atom({
key: 'servedFromReverseProxyState',
default: localStorage.getItem('reverseProxy') === "true" || window.location.port == "",
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('reverseProxy', a.toString()))
]
})
export const servedFromReverseProxyState = atomWithStorage(
'reverseProxy',
localStorage.getItem('reverseProxy') === 'true' || window.location.port == ''
)
export const servedFromReverseProxySubDirState = atom<string>({
key: 'servedFromReverseProxySubDirState',
default: localStorage.getItem('reverseProxySubDir') ?? '',
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('reverseProxySubDir', a))
]
})
export const servedFromReverseProxySubDirState = atomWithStorage<string>(
'reverseProxySubDir',
localStorage.getItem('reverseProxySubDir') ?? ''
)
export const appTitleState = atom({
key: 'appTitleState',
default: localStorage.getItem('appTitle') ?? 'yt-dlp Web UI',
effects: [
({ onSet }) =>
onSet(a => localStorage.setItem('appTitle', a.toString()))
]
})
export const appTitleState = atomWithStorage(
'appTitle',
localStorage.getItem('appTitle') ?? 'yt-dlp Web UI'
)
export const serverAddressAndPortState = selector({
key: 'serverAddressAndPortState',
get: ({ get }) => {
if (get(servedFromReverseProxySubDirState)) {
return `${get(serverAddressState)}/${get(servedFromReverseProxySubDirState)}/`
}
if (get(servedFromReverseProxyState)) {
return `${get(serverAddressState)}`
}
return `${get(serverAddressState)}:${get(serverPortState)}`
export const serverAddressAndPortState = atom((get) => {
if (get(servedFromReverseProxySubDirState)) {
return `${get(serverAddressState)}/${get(servedFromReverseProxySubDirState)}/`
}
})
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`
if (get(servedFromReverseProxyState)) {
return `${get(serverAddressState)}`
}
return `${get(serverAddressState)}:${get(serverPortState)}`
})
export const rpcHTTPEndpoint = selector({
key: 'rpcHTTPEndpoint',
get: ({ get }) => {
const proto = window.location.protocol
return `${proto}//${get(serverAddressAndPortState)}/rpc/http`
export const serverURL = atom((get) =>
`${window.location.protocol}//${get(serverAddressAndPortState)}`
)
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>({
key: 'serverSideCookiesState',
get: async ({ get }) => await pipe(
ffetch<Readonly<{ cookies: string }>>(`${get(serverURL)}/api/v1/cookies`),
matchW(
() => '',
(r) => r.cookies
)
)()
export const settingsState = atom<SettingsState>((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)
})
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 { of } from 'fp-ts/lib/Task'
import { getOrElse } from 'fp-ts/lib/TaskEither'
import { atom, selector } from 'recoil'
import { ffetch } from '../lib/httpClient'
import { RPCVersion } from '../types'
import { rpcClientState } from './rpc'
import { serverURL } from './settings'
import { atom } from 'jotai'
export const connectedState = atom({
key: 'connectedState',
default: false
export const connectedState = atom(false)
export const freeSpaceBytesState = atom(async (get) => {
const res = await get(rpcClientState)
.freeSpace()
.catch(() => ({ result: 0 }))
return res.result
})
export const freeSpaceBytesState = selector({
key: 'freeSpaceBytesState',
get: async ({ get }) => {
const res = await get(rpcClientState).freeSpace()
.catch(() => ({ result: 0 }))
return res.result
}
export const availableDownloadPathsState = atom(async (get) => {
const res = await get(rpcClientState).directoryTree()
.catch(() => ({ result: [] }))
return res.result
})
export const availableDownloadPathsState = selector({
key: 'availableDownloadPathsState',
get: async ({ get }) => {
const res = await get(rpcClientState).directoryTree()
.catch(() => ({ result: [] }))
return res.result
}
})
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
)),
)()
})
export const ytdlpRpcVersionState = atom<Promise<RPCVersion>>(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 { atom } from 'recoil'
import { atom } from 'jotai'
export type Toast = {
open: boolean,
@@ -9,7 +9,4 @@ export type Toast = {
severity?: AlertColor
}
export const toastListState = atom<Toast[]>({
key: 'toastListState',
default: [],
})
export const toastListState = atom<Toast[]>([])

View File

@@ -1,14 +1,10 @@
import { atom, selector } from 'recoil'
import { atom } from 'jotai'
import { activeDownloadsState } from './downloads'
export const loadingAtom = atom({
key: 'loadingAtom',
default: true
})
export const loadingAtom = atom(true)
export const totalDownloadSpeedState = selector<number>({
key: 'totalDownloadSpeedState',
get: ({ get }) => get(activeDownloadsState)
export const totalDownloadSpeedState = atom<number>((get) =>
get(activeDownloadsState)
.map(d => d.progress.speed)
.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 { pipe } from 'fp-ts/lib/function'
import { useMemo } from 'react'
import { useRecoilValue } from 'recoil'
import { Subject, debounceTime, distinctUntilChanged } from 'rxjs'
import { serverSideCookiesState, serverURL } from '../atoms/settings'
import { useSubscription } from '../hooks/observable'
import { useToast } from '../hooks/toast'
import { ffetch } from '../lib/httpClient'
import { useAtomValue } from 'jotai'
const validateCookie = (cookie: string) => pipe(
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 CookiesTextField: React.FC = () => {
const serverAddr = useRecoilValue(serverURL)
const savedCookies = useRecoilValue(serverSideCookiesState)
const serverAddr = useAtomValue(serverURL)
const savedCookies = useAtomValue(serverSideCookiesState)
const { pushMessage } = useToast()

View File

@@ -16,10 +16,10 @@ import {
Typography
} from '@mui/material'
import { useCallback } from 'react'
import { useRecoilValue } from 'recoil'
import { serverURL } from '../atoms/settings'
import { RPCResult } from '../types'
import { base64URLEncode, ellipsis, formatSize, formatSpeedMiB, mapProcessStatus } from '../utils'
import { useAtomValue } from 'jotai'
type Props = {
download: RPCResult
@@ -37,7 +37,7 @@ const Resolution: React.FC<{ resolution?: string }> = ({ resolution }) => {
}
const DownloadCard: React.FC<Props> = ({ download, onStop, onCopy }) => {
const serverAddr = useRecoilValue(serverURL)
const serverAddr = useAtomValue(serverURL)
const isCompleted = useCallback(
() => download.progress.percentage === '-1',

View File

@@ -2,7 +2,6 @@ import { FileUpload } from '@mui/icons-material'
import CloseIcon from '@mui/icons-material/Close'
import {
Autocomplete,
Backdrop,
Box,
Button,
Checkbox,
@@ -21,6 +20,7 @@ import Slide from '@mui/material/Slide'
import Toolbar from '@mui/material/Toolbar'
import Typography from '@mui/material/Typography'
import { TransitionProps } from '@mui/material/transitions'
import { useAtom, useAtomValue } from 'jotai'
import {
FC,
Suspense,
@@ -30,16 +30,22 @@ import {
useState,
useTransition
} from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { customArgsState, downloadTemplateState, filenameTemplateState, savedTemplatesState } from '../atoms/downloadTemplate'
import {
customArgsState,
downloadTemplateState,
filenameTemplateState,
savedTemplatesState
} from '../atoms/downloadTemplate'
import { settingsState } from '../atoms/settings'
import { availableDownloadPathsState, connectedState } from '../atoms/status'
import FormatsGrid from '../components/FormatsGrid'
import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n'
import { useRPC } from '../hooks/useRPC'
import type { DLMetadata } from '../types'
import { toFormatArgs } from '../utils'
import ExtraDownloadOptions from './ExtraDownloadOptions'
import LoadingBackdrop from './LoadingBackdrop'
const Transition = forwardRef(function Transition(
props: TransitionProps & {
@@ -57,22 +63,23 @@ type Props = {
}
const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
const settings = useRecoilValue(settingsState)
const isConnected = useRecoilValue(connectedState)
const availableDownloadPaths = useRecoilValue(availableDownloadPathsState)
const downloadTemplate = useRecoilValue(downloadTemplateState)
const savedTemplates = useRecoilValue(savedTemplatesState)
const settings = useAtomValue(settingsState)
const isConnected = useAtomValue(connectedState)
const availableDownloadPaths = useAtomValue(availableDownloadPathsState)
const downloadTemplate = useAtomValue(downloadTemplateState)
const savedTemplates = useAtomValue(savedTemplatesState)
const [downloadFormats, setDownloadFormats] = useState<DLMetadata>()
const [pickedVideoFormat, setPickedVideoFormat] = useState('')
const [pickedAudioFormat, setPickedAudioFormat] = 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 [filenameTemplate, setFilenameTemplate] = useRecoilState(
const [filenameTemplate, setFilenameTemplate] = useAtom(
filenameTemplateState
)
@@ -82,6 +89,7 @@ const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
const { i18n } = useI18n()
const { client } = useRPC()
const { pushMessage } = useToast()
const urlInputRef = useRef<HTMLInputElement>(null)
const customFilenameInputRef = useRef<HTMLInputElement>(null)
@@ -129,11 +137,28 @@ const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
setPickedVideoFormat('')
setPickedBestFormat('')
if (isPlaylist) {
pushMessage('Format selection on playlist is not supported', 'warning')
resetInput()
onClose()
return
}
setIsFormatsLoading(true)
client.formats(url)
?.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)
resetInput()
})
.then(() => setIsFormatsLoading(false))
}
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -175,10 +200,7 @@ const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
onClose={onClose}
TransitionComponent={Transition}
>
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={isPending}
/>
<LoadingBackdrop isLoading={isPending || isFormatsLoading} />
<AppBar sx={{ position: 'relative' }}>
<Toolbar>
<IconButton

View File

@@ -1,5 +1,5 @@
import { useAtom, useAtomValue } from 'jotai'
import { useEffect } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { loadingDownloadsState } from '../atoms/downloads'
import { listViewState } from '../atoms/settings'
import { loadingAtom } from '../atoms/ui'
@@ -7,10 +7,10 @@ import DownloadsGridView from './DownloadsGridView'
import DownloadsTableView from './DownloadsTableView'
const Downloads: React.FC = () => {
const tableView = useRecoilValue(listViewState)
const loadingDownloads = useRecoilValue(loadingDownloadsState)
const tableView = useAtomValue(listViewState)
const loadingDownloads = useAtomValue(loadingDownloadsState)
const [isLoading, setIsLoading] = useRecoilState(loadingAtom)
const [isLoading, setIsLoading] = useAtom(loadingAtom)
useEffect(() => {
if (loadingDownloads) {

View File

@@ -1,5 +1,5 @@
import { Grid } from '@mui/material'
import { useRecoilValue } from 'recoil'
import { useAtomValue } from 'jotai'
import { activeDownloadsState } from '../atoms/downloads'
import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n'
@@ -8,7 +8,7 @@ import { ProcessStatus, RPCResult } from '../types'
import DownloadCard from './DownloadCard'
const DownloadsGridView: React.FC = () => {
const downloads = useRecoilValue(activeDownloadsState)
const downloads = useAtomValue(activeDownloadsState)
const { i18n } = useI18n()
const { client } = useRPC()

View File

@@ -20,12 +20,12 @@ import {
} from "@mui/material"
import { forwardRef } from 'react'
import { TableComponents, TableVirtuoso } from 'react-virtuoso'
import { useRecoilValue } from 'recoil'
import { activeDownloadsState } from '../atoms/downloads'
import { serverURL } from '../atoms/settings'
import { useRPC } from '../hooks/useRPC'
import { ProcessStatus, RPCResult } from '../types'
import { base64URLEncode, formatSize, formatSpeedMiB } from "../utils"
import { useAtomValue } from 'jotai'
const columns = [
{
@@ -119,8 +119,8 @@ function fixedHeaderContent() {
}
const DownloadsTableView: React.FC = () => {
const downloads = useRecoilValue(activeDownloadsState)
const serverAddr = useRecoilValue(serverURL)
const downloads = useAtomValue(activeDownloadsState)
const serverAddr = useAtomValue(serverURL)
const { client } = useRPC()
const viewFile = (path: string) => {

View File

@@ -1,13 +1,13 @@
import { Autocomplete, Box, TextField, Typography } from '@mui/material'
import { useRecoilState, useRecoilValue } from 'recoil'
import { customArgsState, savedTemplatesState } from '../atoms/downloadTemplate'
import { useI18n } from '../hooks/useI18n'
import { useAtom, useAtomValue } from 'jotai'
const ExtraDownloadOptions: React.FC = () => {
const { i18n } = useI18n()
const customTemplates = useRecoilValue(savedTemplatesState)
const [, setCustomArgs] = useRecoilState(customArgsState)
const customTemplates = useAtomValue(savedTemplatesState)
const [, setCustomArgs] = useAtom(customArgsState)
return (
<>

View File

@@ -2,7 +2,6 @@ import DownloadIcon from '@mui/icons-material/Download'
import SettingsEthernet from '@mui/icons-material/SettingsEthernet'
import { AppBar, CircularProgress, Divider, Toolbar } from '@mui/material'
import { Suspense } from 'react'
import { useRecoilValue } from 'recoil'
import { settingsState } from '../atoms/settings'
import { connectedState } from '../atoms/status'
import { totalDownloadSpeedState } from '../atoms/ui'
@@ -10,11 +9,12 @@ import { useI18n } from '../hooks/useI18n'
import { formatSpeedMiB } from '../utils'
import FreeSpaceIndicator from './FreeSpaceIndicator'
import VersionIndicator from './VersionIndicator'
import { useAtomValue } from 'jotai'
const Footer: React.FC = () => {
const settings = useRecoilValue(settingsState)
const isConnected = useRecoilValue(connectedState)
const totalDownloadSpeed = useRecoilValue(totalDownloadSpeedState)
const settings = useAtomValue(settingsState)
const isConnected = useAtomValue(connectedState)
const totalDownloadSpeed = useAtomValue(totalDownloadSpeedState)
const mode = settings.theme
const { i18n } = useI18n()

View File

@@ -1,10 +1,10 @@
import StorageIcon from '@mui/icons-material/Storage'
import { useRecoilValue } from 'recoil'
import { freeSpaceBytesState } from '../atoms/status'
import { formatSize } from '../utils'
import { useAtomValue } from 'jotai'
const FreeSpaceIndicator = () => {
const freeSpace = useRecoilValue(freeSpaceBytesState)
const freeSpace = useAtomValue(freeSpaceBytesState)
return (
<div style={{

View File

@@ -1,5 +1,5 @@
import { useSetAtom } from 'jotai'
import { Suspense, useState } from 'react'
import { useRecoilState } from 'recoil'
import { loadingAtom } from '../atoms/ui'
import { useToast } from '../hooks/toast'
import DownloadDialog from './DownloadDialog'
@@ -7,7 +7,7 @@ import HomeSpeedDial from './HomeSpeedDial'
import TemplatesEditor from './TemplatesEditor'
const HomeActions: React.FC = () => {
const [, setIsLoading] = useRecoilState(loadingAtom)
const setIsLoading = useSetAtom(loadingAtom)
const [openDownload, setOpenDownload] = useState(false)
const [openEditor, setOpenEditor] = useState(false)

View File

@@ -9,7 +9,7 @@ import {
SpeedDialAction,
SpeedDialIcon
} from '@mui/material'
import { useRecoilState, useRecoilValue } from 'recoil'
import { useAtom, useAtomValue } from 'jotai'
import { listViewState, serverURL } from '../atoms/settings'
import { useI18n } from '../hooks/useI18n'
import { useRPC } from '../hooks/useRPC'
@@ -20,8 +20,8 @@ type Props = {
}
const HomeSpeedDial: React.FC<Props> = ({ onDownloadOpen, onEditorOpen }) => {
const serverAddr = useRecoilValue(serverURL)
const [listView, setListView] = useRecoilState(listViewState)
const serverAddr = useAtomValue(serverURL)
const [listView, setListView] = useAtom(listViewState)
const { i18n } = useI18n()
const { client } = useRPC()

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useRecoilValue } from 'recoil'
import { serverURL } from '../atoms/settings'
import { useI18n } from '../hooks/useI18n'
import { useAtomValue } from 'jotai'
const token = localStorage.getItem('token')
@@ -11,7 +11,7 @@ const LogTerminal: React.FC = () => {
const boxRef = useRef<HTMLDivElement>(null)
const serverAddr = useRecoilValue(serverURL)
const serverAddr = useAtomValue(serverURL)
const { i18n } = useI18n()

View File

@@ -1,7 +1,6 @@
import * as O from 'fp-ts/Option'
import { useEffect, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { useRecoilState, useRecoilValue } from 'recoil'
import { take, timer } from 'rxjs'
import { downloadsState } from '../atoms/downloads'
import { rpcPollingTimeState } from '../atoms/rpc'
@@ -12,15 +11,16 @@ import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n'
import { useRPC } from '../hooks/useRPC'
import { datetimeCompareFunc, isRPCResponse } from '../utils'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
interface Props extends React.HTMLAttributes<HTMLBaseElement> { }
const SocketSubscriber: React.FC<Props> = () => {
const [connected, setIsConnected] = useRecoilState(connectedState)
const [, setDownloads] = useRecoilState(downloadsState)
const [connected, setIsConnected] = useAtom(connectedState)
const setDownloads = useSetAtom(downloadsState)
const serverAddressAndPort = useRecoilValue(serverAddressAndPortState)
const rpcPollingTime = useRecoilValue(rpcPollingTimeState)
const serverAddressAndPort = useAtomValue(serverAddressAndPortState)
const rpcPollingTime = useAtomValue(rpcPollingTimeState)
const { i18n } = useI18n()
const { client } = useRPC()

View File

@@ -1,8 +1,8 @@
import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
import { Container, SvgIcon, Typography, styled } from '@mui/material'
import { useRecoilValue } from 'recoil'
import { activeDownloadsState } from '../atoms/downloads'
import { useI18n } from '../hooks/useI18n'
import { useAtomValue } from 'jotai'
const FlexContainer = styled(Container)({
display: 'flex',
@@ -23,7 +23,7 @@ const Title = styled(Typography)({
export default function Splash() {
const { i18n } = useI18n()
const activeDownloads = useRecoilValue(activeDownloadsState)
const activeDownloads = useAtomValue(activeDownloadsState)
if (activeDownloads.length !== 0) {
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 CloseIcon from '@mui/icons-material/Close'
import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import {
Alert,
AppBar,
@@ -20,12 +21,13 @@ import { TransitionProps } from '@mui/material/transitions'
import { matchW } from 'fp-ts/lib/Either'
import { pipe } from 'fp-ts/lib/function'
import { forwardRef, useEffect, useState, useTransition } from 'react'
import { useRecoilValue } from 'recoil'
import { serverURL } from '../atoms/settings'
import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n'
import { ffetch } from '../lib/httpClient'
import { CustomTemplate } from '../types'
import { useAtomValue } from 'jotai'
import TemplateTextField from './TemplateTextField'
const Transition = forwardRef(function Transition(
props: TransitionProps & {
@@ -45,7 +47,7 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
const [templateName, setTemplateName] = useState('')
const [templateContent, setTemplateContent] = useState('')
const serverAddr = useRecoilValue(serverURL)
const serverAddr = useAtomValue(serverURL)
const [isPending, startTransition] = useTransition()
const [templates, setTemplates] = useState<CustomTemplate[]>([])
@@ -55,11 +57,11 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
useEffect(() => {
if (open) {
getTemplates()
fetchTemplates()
}
}, [open])
const getTemplates = async () => {
const fetchTemplates = async () => {
const task = ffetch<CustomTemplate[]>(`${serverAddr}/api/v1/template/all`)
const either = await task()
@@ -89,7 +91,7 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
(l) => pushMessage(l, 'warning'),
() => {
pushMessage('Added template')
getTemplates()
fetchTemplates()
setTemplateName('')
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 task = ffetch<unknown>(`${serverAddr}/api/v1/template/${id}`, {
method: 'DELETE',
@@ -110,7 +132,7 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
(l) => pushMessage(l, 'warning'),
() => {
pushMessage('Deleted template')
getTemplates()
fetchTemplates()
}
)
)
@@ -188,38 +210,12 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
</Grid>
</Grid>
{templates.map(template => (
<Grid
container
spacing={2}
justifyContent="center"
alignItems="center"
<TemplateTextField
key={template.id}
sx={{ mt: 1 }}
>
<Grid item xs={3}>
<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>
template={template}
onChange={updateTemplate}
onDelete={deleteTemplate}
/>
))}
</Paper>
</Grid>

View File

@@ -2,12 +2,12 @@ import Brightness4 from '@mui/icons-material/Brightness4'
import Brightness5 from '@mui/icons-material/Brightness5'
import BrightnessAuto from '@mui/icons-material/BrightnessAuto'
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
import { useRecoilState } from 'recoil'
import { Theme, themeState } from '../atoms/settings'
import { useI18n } from '../hooks/useI18n'
import { useAtom } from 'jotai'
const ThemeToggler: React.FC = () => {
const [theme, setTheme] = useRecoilState(themeState)
const [theme, setTheme] = useAtom(themeState)
const actions: Record<Theme, React.ReactNode> = {
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 { useRecoilValue } from 'recoil'
import { ytdlpRpcVersionState } from '../atoms/status'
import { useAtomValue } from 'jotai'
const VersionIndicator: React.FC = () => {
const version = useRecoilValue(ytdlpRpcVersionState)
const version = useAtomValue(ytdlpRpcVersionState)
return (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>

View File

@@ -1,9 +1,9 @@
import { AlertColor } from '@mui/material'
import { useRecoilState } from 'recoil'
import { toastListState } from '../atoms/toast'
import { useSetAtom } from 'jotai'
export const useToast = () => {
const [, setToasts] = useRecoilState(toastListState)
const setToasts = useSetAtom(toastListState)
return {
pushMessage: (message: string, severity?: AlertColor) => {

View File

@@ -1,10 +1,11 @@
import { useRecoilValue } from 'recoil'
import { useAtomValue } from 'jotai'
import { i18nBuilderState } from '../atoms/i18n'
export const useI18n = () => {
const instance = useRecoilValue(i18nBuilderState)
const instance = useAtomValue(i18nBuilderState)
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'
export const useRPC = () => {
const client = useRecoilValue(rpcClientState)
const client = useAtomValue(rpcClientState)
return {
client

View File

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

View File

@@ -1,10 +1,10 @@
import { Alert, Snackbar } from "@mui/material"
import { useRecoilState } from 'recoil'
import { Toast, toastListState } from '../atoms/toast'
import { useEffect } from 'react'
import { useAtom } from 'jotai'
const Toaster: React.FC = () => {
const [toasts, setToasts] = useRecoilState(toastListState)
const [toasts, setToasts] = useAtom(toastListState)
const deletePredicate = (t: Toast) => (Date.now() - t.createdAt) < 2000

View File

@@ -69,9 +69,11 @@ export type RPCParams = {
export type DLMetadata = {
formats: Array<DLFormat>
_type: string
best: DLFormat
thumbnail: string
title: string
entries: Array<DLMetadata>
}
export type DLFormat = {

View File

@@ -33,7 +33,6 @@ import { matchW } from 'fp-ts/lib/TaskEither'
import { pipe } from 'fp-ts/lib/function'
import { useEffect, useMemo, useState, useTransition } from 'react'
import { useNavigate } from 'react-router-dom'
import { useRecoilValue } from 'recoil'
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
import { serverURL } from '../atoms/settings'
import { useObservable } from '../hooks/observable'
@@ -42,13 +41,14 @@ import { useI18n } from '../hooks/useI18n'
import { ffetch } from '../lib/httpClient'
import { DirectoryEntry } from '../types'
import { base64URLEncode, formatSize } from '../utils'
import { useAtomValue } from 'jotai'
export default function Downloaded() {
const [menuPos, setMenuPos] = useState({ x: 0, y: 0 })
const [showMenu, setShowMenu] = useState(false)
const [currentFile, setCurrentFile] = useState<DirectoryEntry>()
const serverAddr = useRecoilValue(serverURL)
const serverAddr = useAtomValue(serverURL)
const navigate = useNavigate()
const { i18n } = useI18n()

View File

@@ -1,15 +1,15 @@
import {
Container
} from '@mui/material'
import { useRecoilValue } from 'recoil'
import { loadingAtom } from '../atoms/ui'
import Downloads from '../components/Downloads'
import HomeActions from '../components/HomeActions'
import LoadingBackdrop from '../components/LoadingBackdrop'
import Splash from '../components/Splash'
import { useAtomValue } from 'jotai'
export default function Home() {
const isLoading = useRecoilValue(loadingAtom)
const isLoading = useAtomValue(loadingAtom)
return (
<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 { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useRecoilValue } from 'recoil'
import { serverURL } from '../atoms/settings'
import { useToast } from '../hooks/toast'
import { ffetch } from '../lib/httpClient'
import { useAtomValue } from 'jotai'
const LoginContainer = styled(Container)({
display: 'flex',
@@ -43,7 +43,7 @@ export default function Login() {
const [formHasError, setFormHasError] = useState(false)
const url = useRecoilValue(serverURL)
const url = useAtomValue(serverURL)
const navigate = useNavigate()

View File

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

View File

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

14
main.go
View File

@@ -8,10 +8,10 @@ import (
"os"
"runtime"
"github.com/marcopeocchi/yt-dlp-web-ui/server"
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/cli"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
)
var (
@@ -23,6 +23,7 @@ var (
downloaderPath string
sessionFilePath string
localDatabasePath string
frontendPath string
requireAuth bool
username string
@@ -52,6 +53,7 @@ func init() {
flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path")
flag.StringVar(&sessionFilePath, "session", ".", "session file 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.StringVar(&logFile, "lf", "yt-dlp-webui.log", "set log file location")
@@ -69,6 +71,10 @@ func main() {
log.Fatalln(err)
}
if frontendPath != "" {
frontend = os.DirFS(frontendPath)
}
c := config.Instance()
{

View File

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

View File

@@ -6,7 +6,7 @@ import (
"os"
"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")

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"
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
)
/*

View File

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

View File

@@ -10,7 +10,6 @@ type ProgressTemplate struct {
Eta float32 `json:"eta"`
}
type PostprocessTemplate struct {
FilePath string `json:"filepath"`
}
@@ -45,27 +44,6 @@ type DownloadInfo struct {
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
// as JSON-RPC result field
type ProcessResponse struct {

View File

@@ -10,8 +10,8 @@ import (
"strings"
"time"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
)
const (

View File

@@ -4,8 +4,8 @@ import (
"testing"
"time"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
)
func setupTest() {

View File

@@ -7,8 +7,8 @@ import (
"os"
"path/filepath"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
)
type Monitor struct {

View File

@@ -8,7 +8,7 @@ import (
"sync"
"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

View File

@@ -6,7 +6,7 @@ import (
"log/slog"
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"
)

View File

@@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
)
type metadata struct {

View File

@@ -11,7 +11,6 @@ import (
"log/slog"
"regexp"
"slices"
"sync"
"syscall"
"os"
@@ -19,12 +18,12 @@ import (
"strings"
"time"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
)
const downloadTemplate = `download:
{
"eta":%(progress.eta)s,
"eta":%(progress.eta)s,
"percentage":"%(progress._percent_str)s",
"speed":%(progress.speed)s
}`
@@ -225,7 +224,7 @@ func (p *Process) Complete() {
Speed: 0,
ETA: 0,
}
// for safety, if the filename is not set, set it with original function
if p.Output.SavedFilePath == "" {
p.GetFileName(&p.Output)
@@ -261,54 +260,6 @@ func (p *Process) Kill() error {
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 {
cmd := exec.Command(
config.Instance().DownloaderPath,

View File

@@ -9,9 +9,9 @@ import (
"github.com/go-chi/chi/v5"
"github.com/gorilla/websocket"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
)
var upgrader = websocket.Upgrader{

View File

@@ -4,7 +4,7 @@ import (
"context"
"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"
)

View File

@@ -3,7 +3,7 @@ package rest
import (
"database/sql"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
)
type ContainerArgs struct {

View File

@@ -2,9 +2,9 @@ package rest
import (
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
)
func Container(args *ContainerArgs) *Handler {
@@ -34,6 +34,7 @@ func ApplyRouter(args *ContainerArgs) func(chi.Router) {
r.Post("/cookies", h.SetCookies())
r.Delete("/cookies", h.DeleteCookies())
r.Post("/template", h.AddTemplate())
r.Patch("/template", h.UpdateTemplate())
r.Get("/template/all", h.GetTemplates())
r.Delete("/template/{id}", h.DeleteTemplate())
}

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"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 {
@@ -34,9 +34,9 @@ func (h *Handler) Exec() http.HandlerFunc {
return
}
err = json.NewEncoder(w).Encode(id)
if err != nil {
if err := json.NewEncoder(w).Encode(id); err != nil {
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 {
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
h.service.ExecLivestream(req)
err := json.NewEncoder(w).Encode("ok")
if err != nil {
if err := json.NewEncoder(w).Encode("ok"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
@@ -98,9 +100,9 @@ func (h *Handler) Running() http.HandlerFunc {
return
}
err = json.NewEncoder(w).Encode(res)
if err != nil {
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
@@ -134,21 +136,19 @@ func (h *Handler) SetCookies() http.HandlerFunc {
req := new(internal.SetCookiesRequest)
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = h.service.SetCookies(r.Context(), req.Cookies)
if err != nil {
if err := h.service.SetCookies(r.Context(), req.Cookies); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode("ok")
if err != nil {
if err := json.NewEncoder(w).Encode("ok"); err != nil {
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) {
w.Header().Set("Content-Type", "application/json")
err := h.service.SetCookies(r.Context(), "")
if err != nil {
if err := h.service.SetCookies(r.Context(), ""); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode("ok")
if err != nil {
if err := json.NewEncoder(w).Encode("ok"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
@@ -178,8 +177,7 @@ func (h *Handler) AddTemplate() http.HandlerFunc {
req := new(internal.CustomTemplate)
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@@ -189,15 +187,14 @@ func (h *Handler) AddTemplate() http.HandlerFunc {
return
}
err = h.service.SaveTemplate(r.Context(), req)
if err != nil {
if err := h.service.SaveTemplate(r.Context(), req); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode("ok")
if err != nil {
if err := json.NewEncoder(w).Encode("ok"); err != nil {
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 {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
@@ -229,15 +253,14 @@ func (h *Handler) DeleteTemplate() http.HandlerFunc {
id := chi.URLParam(r, "id")
err := h.service.DeleteTemplate(r.Context(), id)
if err != nil {
if err := h.service.DeleteTemplate(r.Context(), id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode("ok")
if err != nil {
if err := json.NewEncoder(w).Encode("ok"); err != nil {
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}

View File

@@ -10,9 +10,9 @@ import (
"time"
"github.com/google/uuid"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal/livestream"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
)
type Service struct {
@@ -133,6 +133,22 @@ func (s *Service) GetTemplates(ctx context.Context) (*[]internal.CustomTemplate,
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 {
conn, err := s.db.Conn(ctx)
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) {
//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)

View File

@@ -2,11 +2,11 @@ package rpc
import (
"github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal/livestream"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
)
// Dependency injection container.

View File

@@ -4,10 +4,11 @@ import (
"errors"
"log/slog"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal/livestream"
"github.com/marcopeocchi/yt-dlp-web-ui/server/sys"
"github.com/marcopeocchi/yt-dlp-web-ui/server/updater"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/formats"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/sys"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/updater"
)
type Service struct {
@@ -21,12 +22,6 @@ type Pending []string
type NoArgs struct{}
type Args struct {
Id string
URL string
Params []string
}
// Exec spawns a Process.
// The result of the execution is the newly spawned process Id.
func (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
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)
if err != nil {
return err
@@ -102,13 +97,20 @@ func (s *Service) Progess(args Args, progress *internal.DownloadProgress) error
}
// Progess retrieves available format for a given resource
func (s *Service) Formats(args Args, meta *internal.DownloadFormats) error {
var (
err error
p = internal.Process{Url: args.URL}
)
*meta, err = p.GetFormats()
return err
func (s *Service) Formats(args internal.DownloadRequest, meta *formats.Metadata) error {
var err error
metadata, err := formats.ParseURL(args.URL)
if err != nil && metadata == nil {
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

View File

@@ -19,16 +19,16 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/dbutil"
"github.com/marcopeocchi/yt-dlp-web-ui/server/handlers"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal/livestream"
"github.com/marcopeocchi/yt-dlp-web-ui/server/logging"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
"github.com/marcopeocchi/yt-dlp-web-ui/server/rest"
ytdlpRPC "github.com/marcopeocchi/yt-dlp-web-ui/server/rpc"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/dbutil"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/handlers"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/logging"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/openid"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/rest"
ytdlpRPC "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/rpc"
_ "modernc.org/sqlite"
)

View File

@@ -4,8 +4,8 @@ import (
"os"
"path/filepath"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
"golang.org/x/sys/unix"
)

View File

@@ -3,7 +3,7 @@ package updater
import (
"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