Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9cb018132 | |||
|
|
ac077ea1e1 | ||
| f29d719df0 | |||
|
|
6adfa71fde | ||
| 0946d374e3 | |||
|
|
f68c29f838 | ||
|
|
c46e39e736 | ||
|
|
2885d6b5d8 | ||
|
|
ab7932ae92 |
27
README.md
27
README.md
@@ -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/).
|
||||
|
||||
27
examples/docker-compose-nginx/app.conf
Normal file
27
examples/docker-compose-nginx/app.conf
Normal file
@@ -0,0 +1,27 @@
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://app:3033;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
client_max_body_size 20000m;
|
||||
proxy_connect_timeout 5000;
|
||||
proxy_send_timeout 5000;
|
||||
proxy_read_timeout 5000;
|
||||
send_timeout 5000;
|
||||
}
|
||||
}
|
||||
15
examples/docker-compose-nginx/docker-compose.yml
Normal file
15
examples/docker-compose-nginx/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
app:
|
||||
image: marcobaobao/yt-dlp-webui
|
||||
volumes:
|
||||
- ./downloads:/downloads
|
||||
restart: unless-stopped
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./app.conf:/etc/nginx/conf.d/app.conf
|
||||
depends_on:
|
||||
- app
|
||||
ports:
|
||||
- 80:80
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yt-dlp-webui",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.3",
|
||||
"description": "Frontend compontent of yt-dlp-webui",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -10,30 +10,29 @@
|
||||
"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",
|
||||
"jotai": "^2.10.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
1668
frontend/pnpm-lock.yaml
generated
1668
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { grey } from '@mui/material/colors'
|
||||
import { grey, red } from '@mui/material/colors'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Link, Outlet } from 'react-router-dom'
|
||||
import { settingsState } from './atoms/settings'
|
||||
@@ -29,6 +29,7 @@ import ThemeToggler from './components/ThemeToggler'
|
||||
import { useI18n } from './hooks/useI18n'
|
||||
import Toaster from './providers/ToasterProvider'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { getAccentValue } from './utils'
|
||||
|
||||
export default function Layout() {
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -40,11 +41,14 @@ export default function Layout() {
|
||||
createTheme({
|
||||
palette: {
|
||||
mode: settings.theme,
|
||||
primary: {
|
||||
main: getAccentValue(settings.accent)
|
||||
},
|
||||
background: {
|
||||
default: settings.theme === 'light' ? grey[50] : '#121212'
|
||||
},
|
||||
},
|
||||
}), [settings.theme]
|
||||
}), [settings.theme, settings.accent]
|
||||
)
|
||||
|
||||
const toggleDrawer = () => setOpen(state => !state)
|
||||
|
||||
@@ -65,6 +65,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
german:
|
||||
urlInput: Video URL
|
||||
statusTitle: Status
|
||||
@@ -126,6 +127,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
french:
|
||||
urlInput: URL vidéo de YouTube ou d'un autre service pris en charge
|
||||
statusTitle: Statut
|
||||
@@ -189,6 +191,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
italian:
|
||||
urlInput: URL Video (uno per linea)
|
||||
statusTitle: Stato
|
||||
@@ -249,6 +252,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
chinese:
|
||||
urlInput: 视频 URL
|
||||
statusTitle: 状态
|
||||
@@ -310,6 +314,7 @@ languages:
|
||||
如果直播已经开始,那么依然可以下载,但是不会记录下载进度。
|
||||
直播开始后,将会转移到下载页面
|
||||
livestreamExperimentalWarning: 实验性功能,可能存在未知Bug,请谨慎使用
|
||||
accentSelect: 'Accent'
|
||||
spanish:
|
||||
urlInput: URL de YouTube u otro servicio compatible
|
||||
statusTitle: Estado
|
||||
@@ -369,6 +374,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
russian:
|
||||
urlInput: URL-адрес YouTube или любого другого поддерживаемого сервиса
|
||||
statusTitle: Статус
|
||||
@@ -428,6 +434,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
korean:
|
||||
urlInput: YouTube나 다른 지원되는 사이트의 URL
|
||||
statusTitle: 상태
|
||||
@@ -487,6 +494,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
japanese:
|
||||
urlInput: YouTubeまたはサポート済み動画のURL
|
||||
statusTitle: 状態
|
||||
@@ -547,6 +555,7 @@ languages:
|
||||
すでに開始されているライブストリームが提供された場合、ダウンロードは継続されますが進行状況は追跡されません。
|
||||
ライブストリームが開始されると、ダウンロードページに移動されます。
|
||||
livestreamExperimentalWarning: この機能は実験的なものです。何かが壊れるかもしれません!
|
||||
accentSelect: 'Accent'
|
||||
catalan:
|
||||
urlInput: URL de YouTube o d'un altre servei compatible
|
||||
statusTitle: Estat
|
||||
@@ -606,6 +615,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
ukrainian:
|
||||
urlInput: URL-адреса YouTube або будь-якого іншого підтримуваного сервісу
|
||||
statusTitle: Статус
|
||||
@@ -665,6 +675,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
polish:
|
||||
urlInput: Adres URL YouTube lub innej obsługiwanej usługi
|
||||
statusTitle: Status
|
||||
@@ -724,6 +735,7 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
swedish:
|
||||
urlInput: Videolänk (en per rad)
|
||||
statusTitle: Status
|
||||
@@ -789,3 +801,4 @@ languages:
|
||||
If an already started livestream is provided it will be still downloaded but its progress will not be tracked.
|
||||
Once started the livestream will be migrated to the downloads page.
|
||||
livestreamExperimentalWarning: This feature is still experimental. Something might break!
|
||||
accentSelect: 'Accent'
|
||||
|
||||
@@ -22,8 +22,8 @@ export const filenameTemplateState = atomWithStorage(
|
||||
localStorage.getItem('lastFilenameTemplate') ?? ''
|
||||
)
|
||||
|
||||
export const downloadTemplateState = atom<string>((get) =>
|
||||
`${get(customArgsState)} ${get(cookiesTemplateState)}`
|
||||
export const downloadTemplateState = atom<Promise<string>>(async (get) =>
|
||||
`${get(customArgsState)} ${await get(cookiesTemplateState)}`
|
||||
.replace(/ +/g, ' ')
|
||||
.trim()
|
||||
)
|
||||
|
||||
@@ -26,11 +26,15 @@ export type Language = (typeof languages)[number]
|
||||
export type Theme = 'light' | 'dark' | 'system'
|
||||
export type ThemeNarrowed = 'light' | 'dark'
|
||||
|
||||
export const accents = ['default', 'red'] as const
|
||||
export type Accent = (typeof accents)[number]
|
||||
|
||||
export interface SettingsState {
|
||||
serverAddr: string
|
||||
serverPort: number
|
||||
language: Language
|
||||
theme: ThemeNarrowed
|
||||
accent: Accent
|
||||
cliArgs: string
|
||||
formatSelection: boolean
|
||||
fileRenaming: boolean
|
||||
@@ -146,7 +150,11 @@ const themeSelector = atom<ThemeNarrowed>((get) => {
|
||||
return 'dark'
|
||||
}
|
||||
return 'light'
|
||||
}
|
||||
})
|
||||
|
||||
export const accentState = atomWithStorage<Accent>(
|
||||
'accent-color',
|
||||
localStorage.getItem('accent-color') as Accent ?? 'default',
|
||||
)
|
||||
|
||||
export const settingsState = atom<SettingsState>((get) => ({
|
||||
@@ -154,6 +162,7 @@ export const settingsState = atom<SettingsState>((get) => ({
|
||||
serverPort: get(serverPortState),
|
||||
language: get(languageState),
|
||||
theme: get(themeSelector),
|
||||
accent: get(accentState),
|
||||
cliArgs: get(latestCliArgumentsState),
|
||||
formatSelection: get(formatSelectionState),
|
||||
fileRenaming: get(fileRenamingState),
|
||||
|
||||
@@ -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,18 +30,22 @@ import {
|
||||
useState,
|
||||
useTransition
|
||||
} from 'react'
|
||||
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 { useToast } from '../hooks/toast'
|
||||
import LoadingBackdrop from './LoadingBackdrop'
|
||||
import { useAtom, useAtomValue } from 'jotai'
|
||||
|
||||
const Transition = forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
|
||||
67
frontend/src/components/TemplateTextField.tsx
Normal file
67
frontend/src/components/TemplateTextField.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { FC, useState } from 'react'
|
||||
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
TextField
|
||||
} from '@mui/material'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { CustomTemplate } from '../types'
|
||||
|
||||
interface Props {
|
||||
template: CustomTemplate
|
||||
onChange: (template: CustomTemplate) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
const TemplateTextField: FC<Props> = ({ template, onChange, onDelete }) => {
|
||||
const { i18n } = useI18n()
|
||||
|
||||
const [editedTemplate, setEditedTemplate] = useState(template)
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
key={template.id}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
<Grid item xs={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={i18n.t('templatesEditorNameLabel')}
|
||||
defaultValue={template.name}
|
||||
onChange={(e) => setEditedTemplate({ ...editedTemplate, name: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={i18n.t('templatesEditorContentLabel')}
|
||||
defaultValue={template.content}
|
||||
onChange={(e) => setEditedTemplate({ ...editedTemplate, content: e.target.value })}
|
||||
InputProps={{
|
||||
endAdornment: <div style={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={() => onChange(editedTemplate)}>
|
||||
<EditIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={() => onDelete(editedTemplate.id)}>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default TemplateTextField
|
||||
@@ -1,6 +1,7 @@
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import {
|
||||
Alert,
|
||||
AppBar,
|
||||
@@ -26,6 +27,7 @@ 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 & {
|
||||
@@ -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>
|
||||
|
||||
33
frontend/src/components/UpdateBinaryButton.tsx
Normal file
33
frontend/src/components/UpdateBinaryButton.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Button, CircularProgress } from '@mui/material'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
import { useState } from 'react'
|
||||
import { useToast } from '../hooks/toast'
|
||||
|
||||
const UpdateBinaryButton: React.FC = () => {
|
||||
const { i18n } = useI18n()
|
||||
const { client } = useRPC()
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const updateBinary = () => {
|
||||
setIsLoading(true)
|
||||
client
|
||||
.updateExecutable()
|
||||
.then(() => pushMessage(i18n.t('toastUpdated'), 'success'))
|
||||
.then(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="contained"
|
||||
endIcon={isLoading ? <CircularProgress size={16} color='secondary' /> : <></>}
|
||||
onClick={updateBinary}
|
||||
>
|
||||
{i18n.t('updateBinButton')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateBinaryButton
|
||||
@@ -5,6 +5,7 @@ export const useI18n = () => {
|
||||
const instance = useAtomValue(i18nBuilderState)
|
||||
|
||||
return {
|
||||
i18n: instance
|
||||
i18n: instance,
|
||||
t: instance.t
|
||||
}
|
||||
}
|
||||
@@ -191,4 +191,11 @@ export class RPCClient {
|
||||
params: []
|
||||
})
|
||||
}
|
||||
|
||||
public updateExecutable() {
|
||||
return this.sendHTTP({
|
||||
method: 'Service.UpdateExecutable',
|
||||
params: []
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { blue, red } from '@mui/material/colors'
|
||||
import { pipe } from 'fp-ts/lib/function'
|
||||
import { Accent } from './atoms/settings'
|
||||
import type { RPCResponse } from "./types"
|
||||
import { ProcessStatus } from './types'
|
||||
|
||||
@@ -79,4 +81,15 @@ export const base64URLEncode = (s: string) => pipe(
|
||||
s => String.fromCodePoint(...new TextEncoder().encode(s)),
|
||||
btoa,
|
||||
encodeURIComponent
|
||||
)
|
||||
)
|
||||
|
||||
export const getAccentValue = (accent: Accent) => {
|
||||
switch (accent) {
|
||||
case 'default':
|
||||
return blue[700]
|
||||
case 'red':
|
||||
return red[600]
|
||||
default:
|
||||
return blue[700]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Container,
|
||||
FormControl,
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
Typography,
|
||||
capitalize
|
||||
} from '@mui/material'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Subject,
|
||||
@@ -28,8 +28,11 @@ import {
|
||||
} from 'rxjs'
|
||||
import { rpcPollingTimeState } from '../atoms/rpc'
|
||||
import {
|
||||
Accent,
|
||||
Language,
|
||||
Theme,
|
||||
accentState,
|
||||
accents,
|
||||
appTitleState,
|
||||
enableCustomArgsState,
|
||||
fileRenamingState,
|
||||
@@ -44,11 +47,10 @@ 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'
|
||||
import { useAtom } from 'jotai'
|
||||
|
||||
// NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS
|
||||
export default function Settings() {
|
||||
@@ -66,13 +68,13 @@ export default function Settings() {
|
||||
const [pollingTime, setPollingTime] = useAtom(rpcPollingTimeState)
|
||||
const [language, setLanguage] = useAtom(languageState)
|
||||
const [appTitle, setApptitle] = useAtom(appTitleState)
|
||||
const [accent, setAccent] = useAtom(accentState)
|
||||
|
||||
const [theme, setTheme] = useAtom(themeState)
|
||||
|
||||
const [invalidIP, setInvalidIP] = useState(false)
|
||||
|
||||
const { i18n } = useI18n()
|
||||
const { client } = useRPC()
|
||||
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
@@ -140,13 +142,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
|
||||
@@ -257,7 +252,7 @@ export default function Settings() {
|
||||
Appearance
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{i18n.t('languageSelect')}</InputLabel>
|
||||
<Select
|
||||
@@ -287,6 +282,22 @@ export default function Settings() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{i18n.t('accentSelect')}</InputLabel>
|
||||
<Select
|
||||
defaultValue={accent}
|
||||
label={i18n.t('accentSelect')}
|
||||
onChange={(e) => setAccent(e.target.value as Accent)}
|
||||
>
|
||||
{accents.map((accent) => (
|
||||
<MenuItem key={accent} value={accent}>
|
||||
{capitalize(accent)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography variant="h6" color="primary" sx={{ mt: 2, mb: 0.5 }}>
|
||||
General download settings
|
||||
@@ -352,14 +363,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>
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
|
||||
6
main.go
6
main.go
@@ -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()
|
||||
|
||||
{
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.2"
|
||||
const CURRENT_RPC_VERSION = "3.2.3"
|
||||
|
||||
result := make(chan string, 1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user