Compare commits

...

20 Commits

Author SHA1 Message Date
Marco
3859c80214 code refactoring, added file download (#118) 2023-12-27 15:08:51 +01:00
Marco
c5535fad71 jwt in headers+localstorage instead of httpOnly cookie (#117) 2023-12-27 14:32:08 +01:00
Marco
f7ba203ed0 deprecating docker armv7 builds 2023-12-06 13:22:06 +01:00
3ba8c455df fix archive files list 2023-12-03 13:33:03 +01:00
8c166147b0 code refactor 2023-12-03 12:12:19 +01:00
70a8d27d22 fixed redirect when auth is enabled 2023-12-03 11:53:15 +01:00
0ab9f15184 updated deps, new cors rules, code refactor 2023-12-03 11:13:37 +01:00
phuslu
1636191f0d support bind address, fix https://github.com/marcopeocchi/yt-dlp-web-ui/issues/108 (#109) 2023-11-27 08:41:33 +01:00
Marco
f478754b6f fixed double ext and playlist title detection (#106) 2023-11-22 11:14:58 +01:00
fe6519773e typescript bumped to 5.3.2 2023-11-21 18:07:18 +01:00
17779995c3 archive bugfix 2023-11-21 18:07:04 +01:00
12f6b6bf10 code refactoring 2023-11-21 13:43:59 +01:00
3f1f67b2c6 fix 'Don't set file modification time' behavior
closes #103
2023-11-21 13:43:51 +01:00
ec3a5ad1ee code refactoring, removed react helmet 2023-11-21 13:12:41 +01:00
c56e3a106b code refactoring 2023-11-19 13:52:51 +01:00
Michael M. Chang
710a8537e0 better reverse proxy detection (#101) 2023-11-04 15:00:28 +01:00
Marco
a52225323c Update archive.go 2023-11-02 11:19:07 +01:00
1d9dabd397 frontend performance optimizations 2023-11-02 10:42:59 +01:00
f49f072963 code refactoring 2023-11-02 10:18:32 +01:00
00b3fccbdc optimizations, added captions 2023-11-02 10:05:34 +01:00
45 changed files with 5204 additions and 399 deletions

View File

@@ -68,7 +68,7 @@ jobs:
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels}} labels: ${{ steps.meta.outputs.labels}}

3
.gitignore vendored
View File

@@ -1,6 +1,4 @@
dist dist
package-lock.json
pnpm-lock.yaml
.pnpm-debug.log .pnpm-debug.log
node_modules node_modules
.env .env
@@ -15,3 +13,4 @@ yt-dlp-webui
session.dat session.dat
config.yml config.yml
cookies.txt cookies.txt
__debug*

View File

@@ -167,6 +167,8 @@ Usage yt-dlp-webui:
yt-dlp executable path (default "yt-dlp") yt-dlp executable path (default "yt-dlp")
-out string -out string
Where files will be saved (default ".") Where files will be saved (default ".")
-host string
Host where server will listen at (default "0.0.0.0")
-port int -port int
Port where server will listen at (default 3033) Port where server will listen at (default 3033)
-qs int -qs int

3249
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
}, },
"author": "marcopeocchi", "author": "marcopeocchi",
"license": "MPL-2.0", "license": "MPL-2.0",
"private": true,
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
@@ -17,7 +18,6 @@
"fp-ts": "^2.16.1", "fp-ts": "^2.16.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-router-dom": "^6.17.0", "react-router-dom": "^6.17.0",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
@@ -30,7 +30,7 @@
"@types/react-helmet": "^6.1.8", "@types/react-helmet": "^6.1.8",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.4.0", "@vitejs/plugin-react-swc": "^3.4.0",
"typescript": "^5.2.2", "typescript": "^5.3.2",
"vite": "^4.5.0" "vite": "^4.5.0"
} }
} }

1334
frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,7 @@ import ListItemText from '@mui/material/ListItemText'
import Toolbar from '@mui/material/Toolbar' import Toolbar from '@mui/material/Toolbar'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { grey } from '@mui/material/colors' import { grey } from '@mui/material/colors'
import { useMemo, useState } from 'react' import { Suspense, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Link, Outlet } from 'react-router-dom' import { Link, Outlet } from 'react-router-dom'
import { useRecoilValue } from 'recoil' import { useRecoilValue } from 'recoil'
import { settingsState } from './atoms/settings' import { settingsState } from './atoms/settings'
@@ -28,6 +27,7 @@ import FreeSpaceIndicator from './components/FreeSpaceIndicator'
import Logout from './components/Logout' import Logout from './components/Logout'
import SocketSubscriber from './components/SocketSubscriber' import SocketSubscriber from './components/SocketSubscriber'
import ThemeToggler from './components/ThemeToggler' import ThemeToggler from './components/ThemeToggler'
import { useI18n } from './hooks/useI18n'
import Toaster from './providers/ToasterProvider' import Toaster from './providers/ToasterProvider'
export default function Layout() { export default function Layout() {
@@ -50,14 +50,11 @@ export default function Layout() {
const toggleDrawer = () => setOpen(state => !state) const toggleDrawer = () => setOpen(state => !state)
const { i18n } = useI18n()
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<SocketSubscriber> <SocketSubscriber />
<Helmet>
<title>
{settings.appTitle}
</title>
</Helmet>
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<CssBaseline /> <CssBaseline />
<AppBar position="absolute" open={open}> <AppBar position="absolute" open={open}>
@@ -83,15 +80,19 @@ export default function Layout() {
> >
{settings.appTitle} {settings.appTitle}
</Typography> </Typography>
<Suspense fallback={i18n.t('loadingLabel')}>
<FreeSpaceIndicator /> <FreeSpaceIndicator />
</Suspense>
<div style={{ <div style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexWrap: 'wrap', flexWrap: 'wrap',
marginLeft: '4px',
gap: 3,
}}> }}>
<SettingsEthernet /> <SettingsEthernet />
<span> <span>
&nbsp;{isConnected ? settings.serverAddr : 'not connected'} {isConnected ? settings.serverAddr : i18n.t('notConnectedText')}
</span> </span>
</div> </div>
</Toolbar> </Toolbar>
@@ -121,7 +122,7 @@ export default function Layout() {
<ListItemIcon> <ListItemIcon>
<Dashboard /> <Dashboard />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Home" /> <ListItemText primary={i18n.t('homeButtonLabel')} />
</ListItemButton> </ListItemButton>
</Link> </Link>
<Link to={'/archive'} style={ <Link to={'/archive'} style={
@@ -134,7 +135,7 @@ export default function Layout() {
<ListItemIcon> <ListItemIcon>
<DownloadIcon /> <DownloadIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Archive" /> <ListItemText primary={i18n.t('archiveButtonLabel')} />
</ListItemButton> </ListItemButton>
</Link> </Link>
<Link to={'/settings'} style={ <Link to={'/settings'} style={
@@ -147,7 +148,7 @@ export default function Layout() {
<ListItemIcon> <ListItemIcon>
<SettingsIcon /> <SettingsIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Settings" /> <ListItemText primary={i18n.t('settingsButtonLabel')} />
</ListItemButton> </ListItemButton>
</Link> </Link>
<ThemeToggler /> <ThemeToggler />
@@ -167,7 +168,6 @@ export default function Layout() {
</Box> </Box>
</Box> </Box>
<Toaster /> <Toaster />
</SocketSubscriber>
</ThemeProvider> </ThemeProvider>
) )
} }

View File

@@ -35,6 +35,13 @@ languages:
playlistCheckbox: Download playlist (it will take time, after submitting you may close this window) playlistCheckbox: Download playlist (it will take time, after submitting you may close this window)
restartAppMessage: Needs a page reload to take effect restartAppMessage: Needs a page reload to take effect
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title appTitle: App title
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor
@@ -75,6 +82,15 @@ languages:
playlistCheckbox: Télécharger la liste de lecture (cela prendra du temps, vous pouvez fermer cette fenêtre après l'avoir validée) playlistCheckbox: Télécharger la liste de lecture (cela prendra du temps, vous pouvez fermer cette fenêtre après l'avoir validée)
restartAppMessage: Nécessite un rechargement de la page pour prendre effet restartAppMessage: Nécessite un rechargement de la page pour prendre effet
servedFromReverseProxyCheckbox: Est derrière un sous-dossier de proxy inverse servedFromReverseProxyCheckbox: Est derrière un sous-dossier de proxy inverse
notConnectedText: not connected
settingsLabel: Settings
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: Nom de l'application appTitle: Nom de l'application
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor
@@ -114,6 +130,13 @@ languages:
playlistCheckbox: Download playlist (richiederà tempo, puoi chiudere la finestra dopo l'inoltro) playlistCheckbox: Download playlist (richiederà tempo, puoi chiudere la finestra dopo l'inoltro)
restartAppMessage: La finestra deve essere ricaricata perché abbia effetto restartAppMessage: La finestra deve essere ricaricata perché abbia effetto
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: Nuovo download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: Titolo applicazione appTitle: Titolo applicazione
savedTemplates: Template salvati savedTemplates: Template salvati
templatesEditor: Editor template templatesEditor: Editor template
@@ -154,6 +177,13 @@ languages:
playlistCheckbox: 下载播放列表(可能需要一段时间,提交后可以关闭页面等待) playlistCheckbox: 下载播放列表(可能需要一段时间,提交后可以关闭页面等待)
restartAppMessage: 需要刷新页面才能生效 restartAppMessage: 需要刷新页面才能生效
servedFromReverseProxyCheckbox: 处于反向代理的子目录后 servedFromReverseProxyCheckbox: 处于反向代理的子目录后
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App 标题 appTitle: App 标题
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor
@@ -192,6 +222,13 @@ languages:
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window) playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title appTitle: App title
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor
@@ -230,6 +267,13 @@ languages:
clipboardAction: URL скопирован в буфер обмена clipboardAction: URL скопирован в буфер обмена
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window) playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title appTitle: App title
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor
@@ -268,6 +312,13 @@ languages:
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window) playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title appTitle: App title
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor
@@ -307,6 +358,13 @@ languages:
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window) playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title appTitle: App title
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor
@@ -345,6 +403,13 @@ languages:
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window) playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title appTitle: App title
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor
@@ -383,6 +448,13 @@ languages:
clipboardAction: URL скопійовано в буфер обміну clipboardAction: URL скопійовано в буфер обміну
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window) playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title appTitle: App title
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor
@@ -421,6 +493,13 @@ languages:
clipboardAction: Adres URL zostanie skopiowany do schowka clipboardAction: Adres URL zostanie skopiowany do schowka
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window) playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
newDownloadButton: New download
homeButtonLabel: Home
archiveButtonLabel: Archive
settingsButtonLabel: Settings
rpcAuthenticationLabel: RPC authentication
themeTogglerLabel: Theme toggler
loadingLabel: Loading...
appTitle: App title appTitle: App title
savedTemplates: Saved templates savedTemplates: Saved templates
templatesEditor: Templates editor templatesEditor: Templates editor

View File

@@ -47,6 +47,5 @@ export const savedTemplatesState = selector<CustomTemplate[]>({
either, either,
getOrElse(() => new Array<CustomTemplate>()) getOrElse(() => new Array<CustomTemplate>())
) )
}, }
dangerouslyAllowMutability: true
}) })

View File

@@ -5,8 +5,10 @@ import { rpcHTTPEndpoint, rpcWebSocketEndpoint } from './settings'
export const rpcClientState = selector({ export const rpcClientState = selector({
key: 'rpcClientState', key: 'rpcClientState',
get: ({ get }) => get: ({ get }) =>
new RPCClient(get(rpcHTTPEndpoint), get(rpcWebSocketEndpoint)), new RPCClient(
set: ({ get }) => get(rpcHTTPEndpoint),
new RPCClient(get(rpcHTTPEndpoint), get(rpcWebSocketEndpoint)), get(rpcWebSocketEndpoint),
localStorage.getItem('token') ?? ''
),
dangerouslyAllowMutability: true, dangerouslyAllowMutability: true,
}) })

View File

@@ -73,7 +73,7 @@ export const serverPortState = atom<number>({
export const latestCliArgumentsState = atom<string>({ export const latestCliArgumentsState = atom<string>({
key: 'latestCliArgumentsState', key: 'latestCliArgumentsState',
default: localStorage.getItem('cli-args') || '', default: localStorage.getItem('cli-args') || '--no-mtime',
effects: [ effects: [
({ onSet }) => ({ onSet }) =>
onSet(a => localStorage.setItem('cli-args', a.toString())) onSet(a => localStorage.setItem('cli-args', a.toString()))
@@ -127,7 +127,7 @@ export const listViewState = atom({
export const servedFromReverseProxyState = atom({ export const servedFromReverseProxyState = atom({
key: 'servedFromReverseProxyState', key: 'servedFromReverseProxyState',
default: localStorage.getItem('reverseProxy') === "true", default: localStorage.getItem('reverseProxy') === "true" || window.location.port == "",
effects: [ effects: [
({ onSet }) => ({ onSet }) =>
onSet(a => localStorage.setItem('reverseProxy', a.toString())) onSet(a => localStorage.setItem('reverseProxy', a.toString()))

View File

@@ -23,18 +23,20 @@ export const isDownloadingState = atom({
default: false default: false
}) })
// export const freeSpaceBytesState = selector({ export const freeSpaceBytesState = selector({
// key: 'freeSpaceBytesState', key: 'freeSpaceBytesState',
// get: async ({ get }) => { get: async ({ get }) => {
// const res = await get(rpcClientState).freeSpace() const res = await get(rpcClientState).freeSpace()
// return res.result .catch(() => ({ result: 0 }))
// } return res.result
// }) }
})
export const availableDownloadPathsState = selector({ export const availableDownloadPathsState = selector({
key: 'availableDownloadPathsState', key: 'availableDownloadPathsState',
get: async ({ get }) => { get: async ({ get }) => {
const res = await get(rpcClientState).directoryTree() const res = await get(rpcClientState).directoryTree()
.catch(() => ({ result: [] }))
return res.result return res.result
} }
}) })

View File

@@ -18,49 +18,49 @@ const validateCookie = (cookie: string) => pipe(
cookie => cookie.replaceAll('\t', ' '), cookie => cookie.replaceAll('\t', ' '),
cookie => cookie.split(' '), cookie => cookie.split(' '),
E.of, E.of,
E.chain( E.flatMap(
E.fromPredicate( E.fromPredicate(
f => f.length === 7, f => f.length === 7,
() => `missing parts` () => `missing parts`
) )
), ),
E.chain( E.flatMap(
E.fromPredicate( E.fromPredicate(
f => f[0].length > 0, f => f[0].length > 0,
() => 'missing domain' () => 'missing domain'
) )
), ),
E.chain( E.flatMap(
E.fromPredicate( E.fromPredicate(
f => f[1] === 'TRUE' || f[1] === 'FALSE', f => f[1] === 'TRUE' || f[1] === 'FALSE',
() => `invalid include subdomains` () => `invalid include subdomains`
) )
), ),
E.chain( E.flatMap(
E.fromPredicate( E.fromPredicate(
f => f[2].length > 0, f => f[2].length > 0,
() => 'invalid path' () => 'invalid path'
) )
), ),
E.chain( E.flatMap(
E.fromPredicate( E.fromPredicate(
f => f[3] === 'TRUE' || f[3] === 'FALSE', f => f[3] === 'TRUE' || f[3] === 'FALSE',
() => 'invalid secure flag' () => 'invalid secure flag'
) )
), ),
E.chain( E.flatMap(
E.fromPredicate( E.fromPredicate(
f => isFinite(Number(f[4])), f => isFinite(Number(f[4])),
() => 'invalid expiration' () => 'invalid expiration'
) )
), ),
E.chain( E.flatMap(
E.fromPredicate( E.fromPredicate(
f => f[5].length > 0, f => f[5].length > 0,
() => 'invalid name' () => 'invalid name'
) )
), ),
E.chain( E.flatMap(
E.fromPredicate( E.fromPredicate(
f => f[6].length > 0, f => f[6].length > 0,
() => 'invalid value' () => 'invalid value'

View File

@@ -15,6 +15,7 @@ import {
Stack, Stack,
Typography Typography
} from '@mui/material' } from '@mui/material'
import { useCallback } from 'react'
import { RPCResult } from '../types' import { RPCResult } from '../types'
import { ellipsis, formatSpeedMiB, mapProcessStatus, roundMiB } from '../utils' import { ellipsis, formatSpeedMiB, mapProcessStatus, roundMiB } from '../utils'
@@ -34,11 +35,17 @@ const Resolution: React.FC<{ resolution?: string }> = ({ resolution }) => {
} }
const DownloadCard: React.FC<Props> = ({ download, onStop, onCopy }) => { const DownloadCard: React.FC<Props> = ({ download, onStop, onCopy }) => {
const isCompleted = () => download.progress.percentage === '-1' const isCompleted = useCallback(
() => download.progress.percentage === '-1',
[download.progress.percentage]
)
const percentageToNumber = () => isCompleted() const percentageToNumber = useCallback(
() => isCompleted()
? 100 ? 100
: Number(download.progress.percentage.replace('%', '')) : Number(download.progress.percentage.replace('%', '')),
[download.progress.percentage, isCompleted]
)
return ( return (
<Card> <Card>

View File

@@ -22,7 +22,10 @@ import Toolbar from '@mui/material/Toolbar'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { TransitionProps } from '@mui/material/transitions' import { TransitionProps } from '@mui/material/transitions'
import { import {
FC,
Suspense,
forwardRef, forwardRef,
useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@@ -55,11 +58,7 @@ type Props = {
onDownloadStart: (url: string) => void onDownloadStart: (url: string) => void
} }
export default function DownloadDialog({ const DownloadDialog: FC<Props> = ({ open, onClose, onDownloadStart }) => {
open,
onClose,
onDownloadStart
}: Props) {
const settings = useRecoilValue(settingsState) const settings = useRecoilValue(settingsState)
const isConnected = useRecoilValue(connectedState) const isConnected = useRecoilValue(connectedState)
const availableDownloadPaths = useRecoilValue(availableDownloadPathsState) const availableDownloadPaths = useRecoilValue(availableDownloadPathsState)
@@ -94,6 +93,10 @@ export default function DownloadDialog({
const [isPending, startTransition] = useTransition() const [isPending, startTransition] = useTransition()
useEffect(() => {
setCustomArgs('')
}, [open])
/** /**
* Retrive url from input, cli-arguments from checkboxes and emits via WebSocket * Retrive url from input, cli-arguments from checkboxes and emits via WebSocket
*/ */
@@ -117,7 +120,7 @@ export default function DownloadDialog({
setTimeout(() => { setTimeout(() => {
resetInput() resetInput()
setDownloadFormats(undefined) setDownloadFormats(undefined)
onDownloadStart(url) onDownloadStart(immediate || url || workingUrl)
}, 250) }, 250)
} }
@@ -309,7 +312,9 @@ export default function DownloadDialog({
</Grid> </Grid>
} }
</Grid> </Grid>
<Suspense>
<ExtraDownloadOptions /> <ExtraDownloadOptions />
</Suspense>
<Grid container spacing={1} pt={2} justifyContent="space-between"> <Grid container spacing={1} pt={2} justifyContent="space-between">
<Grid item> <Grid item>
<Button <Button
@@ -369,3 +374,5 @@ export default function DownloadDialog({
</Dialog> </Dialog>
) )
} }
export default DownloadDialog

View File

@@ -20,13 +20,11 @@ const DownloadsCardView: React.FC = () => {
{ {
downloads.map(download => ( downloads.map(download => (
<Grid item xs={4} sm={8} md={6} key={download.id}> <Grid item xs={4} sm={8} md={6} key={download.id}>
<>
<DownloadCard <DownloadCard
download={download} download={download}
onStop={() => abort(download.id)} onStop={() => abort(download.id)}
onCopy={() => pushMessage(i18n.t('clipboardAction'), 'info')} onCopy={() => pushMessage(i18n.t('clipboardAction'), 'info')}
/> />
</>
</Grid> </Grid>
)) ))
} }

View File

@@ -1,4 +1,4 @@
import { Autocomplete, Box, TextField, Typography } from '@mui/material' import { Autocomplete, Box, TextField } from '@mui/material'
import { useRecoilState, useRecoilValue } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { customArgsState, savedTemplatesState } from '../atoms/downloadTemplate' import { customArgsState, savedTemplatesState } from '../atoms/downloadTemplate'
import { useI18n } from '../hooks/useI18n' import { useI18n } from '../hooks/useI18n'

View File

@@ -1,26 +1,21 @@
import StorageIcon from '@mui/icons-material/Storage' import StorageIcon from '@mui/icons-material/Storage'
import { useEffect, useState } from 'react' import { useRecoilValue } from 'recoil'
import { freeSpaceBytesState } from '../atoms/status'
import { formatGiB } from '../utils' import { formatGiB } from '../utils'
import { useRPC } from '../hooks/useRPC'
const FreeSpaceIndicator = () => { const FreeSpaceIndicator = () => {
const [freeSpace, setFreeSpace] = useState(0) const freeSpace = useRecoilValue(freeSpaceBytesState)
const { client } = useRPC()
useEffect(() => {
client.freeSpace().then(r => setFreeSpace(r.result))
}, [client])
return ( return (
<div style={{ <div style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: 3
}}> }}>
<StorageIcon /> <StorageIcon />
<span> <span>
&nbsp;{formatGiB(freeSpace)}&nbsp; {formatGiB(freeSpace)}
</span> </span>
</div> </div>
) )

View File

@@ -1,9 +1,9 @@
import { useState } from 'react' import { Suspense, useState } from 'react'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { loadingAtom } from '../atoms/ui' import { loadingAtom } from '../atoms/ui'
import { useToast } from '../hooks/toast'
import DownloadDialog from './DownloadDialog' import DownloadDialog from './DownloadDialog'
import HomeSpeedDial from './HomeSpeedDial' import HomeSpeedDial from './HomeSpeedDial'
import { useToast } from '../hooks/toast'
import TemplatesEditor from './TemplatesEditor' import TemplatesEditor from './TemplatesEditor'
const HomeActions: React.FC = () => { const HomeActions: React.FC = () => {
@@ -20,6 +20,7 @@ const HomeActions: React.FC = () => {
onDownloadOpen={() => setOpenDownload(true)} onDownloadOpen={() => setOpenDownload(true)}
onEditorOpen={() => setOpenEditor(true)} onEditorOpen={() => setOpenEditor(true)}
/> />
<Suspense>
<DownloadDialog <DownloadDialog
open={openDownload} open={openDownload}
onClose={() => { onClose={() => {
@@ -32,6 +33,7 @@ const HomeActions: React.FC = () => {
setIsLoading(true) setIsLoading(true)
}} }}
/> />
</Suspense>
<TemplatesEditor <TemplatesEditor
open={openEditor} open={openEditor}
onClose={() => setOpenEditor(false)} onClose={() => setOpenEditor(false)}

View File

@@ -48,7 +48,7 @@ const HomeSpeedDial: React.FC<Props> = ({ onDownloadOpen, onEditorOpen }) => {
/> />
<SpeedDialAction <SpeedDialAction
icon={<AddCircleIcon />} icon={<AddCircleIcon />}
tooltipTitle={i18n.t('newDownload')} tooltipTitle={i18n.t('newDownloadButton')}
onClick={onDownloadOpen} onClick={onDownloadOpen}
/> />
</SpeedDial> </SpeedDial>

View File

@@ -1,26 +1,27 @@
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
import LogoutIcon from '@mui/icons-material/Logout' import LogoutIcon from '@mui/icons-material/Logout'
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useRecoilValue } from 'recoil' import { useRecoilValue } from 'recoil'
import { serverURL } from '../atoms/settings' import { serverURL } from '../atoms/settings'
import { useI18n } from '../hooks/useI18n'
export default function Logout() { export default function Logout() {
const navigate = useNavigate() const navigate = useNavigate()
const url = useRecoilValue(serverURL) const url = useRecoilValue(serverURL)
const logout = async () => { const logout = async () => {
const res = await fetch(`${url}/auth/logout`) localStorage.removeItem('token')
if (res.ok) {
navigate('/login') navigate('/login')
} }
}
const { i18n } = useI18n()
return ( return (
<ListItemButton onClick={logout}> <ListItemButton onClick={logout}>
<ListItemIcon> <ListItemIcon>
<LogoutIcon /> <LogoutIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="RPC authentication" /> <ListItemText primary={i18n.t('rpcAuthenticationLabel')} />
</ListItemButton> </ListItemButton>
) )
} }

View File

@@ -1,7 +1,8 @@
import * as O from 'fp-ts/Option' import * as O from 'fp-ts/Option'
import { useEffect, useMemo } from 'react' import { useEffect, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { useRecoilState, useRecoilValue } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { share, take, timer } from 'rxjs' import { take, timer } from 'rxjs'
import { downloadsState } from '../atoms/downloads' import { downloadsState } from '../atoms/downloads'
import { serverAddressAndPortState } from '../atoms/settings' import { serverAddressAndPortState } from '../atoms/settings'
import { connectedState } from '../atoms/status' import { connectedState } from '../atoms/status'
@@ -13,7 +14,7 @@ import { datetimeCompareFunc, isRPCResponse } from '../utils'
interface Props extends React.HTMLAttributes<HTMLBaseElement> { } interface Props extends React.HTMLAttributes<HTMLBaseElement> { }
const SocketSubscriber: React.FC<Props> = ({ children }) => { const SocketSubscriber: React.FC<Props> = () => {
const [connected, setIsConnected] = useRecoilState(connectedState) const [connected, setIsConnected] = useRecoilState(connectedState)
const [, setDownloads] = useRecoilState(downloadsState) const [, setDownloads] = useRecoilState(downloadsState)
@@ -23,19 +24,24 @@ const SocketSubscriber: React.FC<Props> = ({ children }) => {
const { client } = useRPC() const { client } = useRPC()
const { pushMessage } = useToast() const { pushMessage } = useToast()
const sharedSocket$ = useMemo(() => client.socket$.pipe(share()), []) const navigate = useNavigate()
const socketOnce$ = useMemo(() => sharedSocket$.pipe(take(1)), [])
useSubscription(socketOnce$, () => { const socketOnce$ = useMemo(() => client.socket$.pipe(take(1)), [])
useEffect(() => {
if (!connected) {
socketOnce$.subscribe(() => {
setIsConnected(true) setIsConnected(true)
pushMessage( pushMessage(
`${i18n.t('toastConnected')} (${serverAddressAndPort})`, `${i18n.t('toastConnected')} (${serverAddressAndPort})`,
"success" "success"
) )
}) })
}
}, [connected])
useSubscription( useSubscription(
sharedSocket$, client.socket$,
event => { event => {
if (!isRPCResponse(event)) { return } if (!isRPCResponse(event)) { return }
if (!Array.isArray(event.result)) { return } if (!Array.isArray(event.result)) { return }
@@ -50,7 +56,6 @@ const SocketSubscriber: React.FC<Props> = ({ children }) => {
) )
) )
} }
setDownloads(O.none) setDownloads(O.none)
}, },
err => { err => {
@@ -58,19 +63,20 @@ const SocketSubscriber: React.FC<Props> = ({ children }) => {
pushMessage( pushMessage(
`${i18n.t('rpcConnErr')} (${serverAddressAndPort})`, `${i18n.t('rpcConnErr')} (${serverAddressAndPort})`,
"error" "error"
) ),
navigate(`/error`)
} }
) )
useEffect(() => { useEffect(() => {
if (connected) { if (connected) {
timer(0, 1000).subscribe(() => client.running()) const sub = timer(0, 1000).subscribe(() => client.running())
}
}, [connected])
return ( return () => sub.unsubscribe()
<>{children}</> }
) }, [connected, client])
return null
} }
export default SocketSubscriber export default SocketSubscriber

View File

@@ -25,7 +25,7 @@ export default function Splash() {
const { i18n } = useI18n() const { i18n } = useI18n()
const activeDownloads = useRecoilValue(activeDownloadsState) const activeDownloads = useRecoilValue(activeDownloadsState)
if (!activeDownloads || activeDownloads.length !== 0) { if (activeDownloads.length !== 0) {
return null return null
} }

View File

@@ -4,6 +4,7 @@ import BrightnessAuto from '@mui/icons-material/BrightnessAuto'
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material' import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { Theme, themeState } from '../atoms/settings' import { Theme, themeState } from '../atoms/settings'
import { useI18n } from '../hooks/useI18n'
const ThemeToggler: React.FC = () => { const ThemeToggler: React.FC = () => {
const [theme, setTheme] = useRecoilState(themeState) const [theme, setTheme] = useRecoilState(themeState)
@@ -17,6 +18,8 @@ const ThemeToggler: React.FC = () => {
const themes: Theme[] = ['system', 'light', 'dark'] const themes: Theme[] = ['system', 'light', 'dark']
const currentTheme = themes.indexOf(theme) const currentTheme = themes.indexOf(theme)
const { i18n } = useI18n()
return ( return (
<ListItemButton onClick={() => { <ListItemButton onClick={() => {
setTheme(themes[(currentTheme + 1) % themes.length]) setTheme(themes[(currentTheme + 1) % themes.length])
@@ -24,7 +27,7 @@ const ThemeToggler: React.FC = () => {
<ListItemIcon> <ListItemIcon>
{actions[theme]} {actions[theme]}
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Toggle theme" /> <ListItemText primary={i18n.t('themeTogglerLabel')} />
</ListItemButton> </ListItemButton>
) )
} }

View File

@@ -1,12 +1,10 @@
export class CliArguments { export class CliArguments {
private _extractAudio: boolean private _extractAudio: boolean
private _noMTime: boolean private _noMTime: boolean
private _proxy: string
constructor(extractAudio = false, noMTime = true) { constructor(extractAudio = false, noMTime = true) {
this._extractAudio = extractAudio this._extractAudio = extractAudio
this._noMTime = noMTime this._noMTime = noMTime
this._proxy = ""
} }
public get extractAudio(): boolean { public get extractAudio(): boolean {
@@ -46,7 +44,14 @@ export class CliArguments {
return args.trim() return args.trim()
} }
private reset() {
this._extractAudio = false
this._noMTime = false
}
public fromString(str: string): CliArguments { public fromString(str: string): CliArguments {
this.reset()
if (str) { if (str) {
if (str.includes('-x')) { if (str.includes('-x')) {
this._extractAudio = true this._extractAudio = true

View File

@@ -7,7 +7,7 @@ export const ffetch = <T>(url: string, opt?: RequestInit) => tryCatch(
const fetcher = async <T>(url: string, opt?: RequestInit) => { const fetcher = async <T>(url: string, opt?: RequestInit) => {
const res = await fetch(url, opt) const jwt = localStorage.getItem('token')
if (opt && !opt.headers) { if (opt && !opt.headers) {
opt.headers = { opt.headers = {
@@ -15,6 +15,12 @@ const fetcher = async <T>(url: string, opt?: RequestInit) => {
} }
} }
if (opt?.headers) {
opt.headers = { ...opt.headers, 'X-Authentication': jwt ?? '' }
}
const res = await fetch(url, opt)
if (!res.ok) { if (!res.ok) {
throw await res.text() throw await res.text()
} }

View File

@@ -4,9 +4,10 @@ import i18n from "../assets/i18n.yaml"
export default class I18nBuilder { export default class I18nBuilder {
private language: string private language: string
private textMap = i18n.languages private textMap = i18n.languages
private current: string[]
constructor(language: string) { constructor(language: string) {
this.language = language this.setLanguage(language)
} }
getLanguage(): string { getLanguage(): string {
@@ -15,13 +16,12 @@ export default class I18nBuilder {
setLanguage(language: string): void { setLanguage(language: string): void {
this.language = language this.language = language
this.current = this.textMap[this.language]
} }
t(key: string): string { t(key: string): string {
const map = this.textMap[this.language] if (this.current) {
if (map) { return this.current[key] ?? 'caption not defined'
const translation = map[key]
return translation ?? 'caption not defined'
} }
return 'caption not defined' return 'caption not defined'
} }

View File

@@ -15,15 +15,19 @@ export class RPCClient {
private seq: number private seq: number
private httpEndpoint: string private httpEndpoint: string
private readonly _socket$: WebSocketSubject<any> private readonly _socket$: WebSocketSubject<any>
private readonly token?: string
constructor(httpEndpoint: string, webSocketEndpoint: string) { constructor(httpEndpoint: string, webSocketEndpoint: string, token?: string) {
this.seq = 0 this.seq = 0
this.httpEndpoint = httpEndpoint this.httpEndpoint = httpEndpoint
this._socket$ = webSocket<any>(webSocketEndpoint) this._socket$ = webSocket<any>({
url: token ? `${webSocketEndpoint}?token=${token}` : webSocketEndpoint
})
this.token = token
} }
public get socket$(): Observable<RPCResponse<RPCResult[]>> { public get socket$(): Observable<RPCResponse<RPCResult[]>> {
return this._socket$.asObservable() return this._socket$
} }
private incrementSeq() { private incrementSeq() {
@@ -47,6 +51,9 @@ export class RPCClient {
private async sendHTTP<T>(req: RPCRequest) { private async sendHTTP<T>(req: RPCRequest) {
const res = await fetch(this.httpEndpoint, { const res = await fetch(this.httpEndpoint, {
method: 'POST', method: 'POST',
headers: {
'X-Authentication': this.token ?? ''
},
body: JSON.stringify({ body: JSON.stringify({
...req, ...req,
id: this.incrementSeq(), id: this.incrementSeq(),

View File

@@ -57,6 +57,14 @@ export const router = createHashRouter([
</Suspense > </Suspense >
) )
}, },
{
path: '/error',
element: (
<Suspense fallback={<CircularProgress />}>
<ErrorBoundary />
</Suspense >
)
},
] ]
}, },
]) ])

View File

@@ -9,6 +9,7 @@ import {
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
IconButton,
List, List,
ListItem, ListItem,
ListItemButton, ListItemButton,
@@ -39,6 +40,8 @@ import { useI18n } from '../hooks/useI18n'
import { ffetch } from '../lib/httpClient' import { ffetch } from '../lib/httpClient'
import { DeleteRequest, DirectoryEntry } from '../types' import { DeleteRequest, DirectoryEntry } from '../types'
import { base64URLEncode, roundMiB } from '../utils' import { base64URLEncode, roundMiB } from '../utils'
import DownloadIcon from '@mui/icons-material/Download'
export default function Downloaded() { export default function Downloaded() {
const serverAddr = useRecoilValue(serverURL) const serverAddr = useRecoilValue(serverURL)
@@ -69,7 +72,7 @@ export default function Downloaded() {
pushMessage(e, 'error') pushMessage(e, 'error')
navigate('/login') navigate('/login')
}, },
(d) => files$.next(d), (d) => files$.next(d ?? []),
) )
)() )()
@@ -108,8 +111,8 @@ export default function Downloaded() {
path: upperLevel, path: upperLevel,
shaSum: '', shaSum: '',
size: 0, size: 0,
}, ...r] }, ...r.filter(f => f.name !== '')]
: r : r.filter(f => f.name !== '')
) )
) )
)() )()
@@ -119,7 +122,7 @@ export default function Downloaded() {
combineLatestWith(selected$), combineLatestWith(selected$),
map(([data, selected]) => data.map(x => ({ map(([data, selected]) => data.map(x => ({
...x, ...x,
selected: selected.includes(x.name) selected: selected.includes(x.path)
}))), }))),
share() share()
), []) ), [])
@@ -155,7 +158,13 @@ export default function Downloaded() {
const onFileClick = (path: string) => startTransition(() => { const onFileClick = (path: string) => startTransition(() => {
const encoded = base64URLEncode(path) const encoded = base64URLEncode(path)
window.open(`${serverAddr}/archive/d/${encoded}`) window.open(`${serverAddr}/archive/v/${encoded}?token=${localStorage.getItem('token')}`)
})
const downloadFile = (path: string) => startTransition(() => {
const encoded = base64URLEncode(path)
window.open(`${serverAddr}/archive/d/${encoded}?token=${localStorage.getItem('token')}`)
}) })
const onFolderClick = (path: string) => startTransition(() => { const onFolderClick = (path: string) => startTransition(() => {
@@ -192,11 +201,20 @@ export default function Downloaded() {
{roundMiB(file.size)} {roundMiB(file.size)}
</Typography> </Typography>
} }
{!file.isDirectory && <Checkbox {!file.isDirectory && <>
<IconButton
size='small'
onClick={() => downloadFile(file.path)}
sx={{ marginLeft: 1.5 }}
>
<DownloadIcon />
</IconButton>
<Checkbox
edge="end" edge="end"
checked={file.selected} checked={file.selected}
onChange={() => addSelected(file.name)} onChange={() => addSelected(file.path)}
/>} />
</>}
</div> </div>
} }
disablePadding disablePadding

View File

@@ -54,7 +54,7 @@ export default function Login() {
} }
const login = async () => { const login = async () => {
const task = ffetch(`${url}/auth/login`, { const task = ffetch<string>(`${url}/auth/login`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -63,17 +63,20 @@ export default function Login() {
username, username,
password, password,
}), }),
redirect: 'follow'
}) })
pipe( pipe(
task, task,
matchW( matchW(
(l) => { (error) => {
setFormHasError(true) setFormHasError(true)
pushMessage(l, 'error') pushMessage(error, 'error')
}, },
() => navigateAndReload() (token) => {
console.log(token)
localStorage.setItem('token', token)
navigateAndReload()
}
) )
)() )()
} }

36
go.mod
View File

@@ -3,32 +3,32 @@ module github.com/marcopeocchi/yt-dlp-web-ui
go 1.20 go 1.20
require ( require (
github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/chi/v5 v5.0.11
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/golang-jwt/jwt/v5 v5.0.0 github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.3.1 github.com/google/uuid v1.5.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.1
github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa
golang.org/x/sys v0.13.0 golang.org/x/sys v0.15.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.28.0
) )
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/mod v0.3.0 // indirect golang.org/x/mod v0.14.0 // indirect
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect golang.org/x/net v0.19.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/tools v0.16.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/ccgo/v3 v3.16.15 // indirect
modernc.org/libc v1.24.1 // indirect modernc.org/libc v1.38.0 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.6.0 // indirect modernc.org/memory v1.7.2 // indirect
modernc.org/opt v0.1.3 // indirect modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.26.0 // indirect modernc.org/strutil v1.2.0 // indirect
modernc.org/strutil v1.1.3 // indirect modernc.org/token v1.1.0 // indirect
modernc.org/token v1.0.1 // indirect
) )

102
go.sum
View File

@@ -1,72 +1,62 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa h1:uaAQLGhN4SesB9inOQ1Q6EH+BwTWHQOvwhR0TIJvnYc= github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa h1:uaAQLGhN4SesB9inOQ1Q6EH+BwTWHQOvwhR0TIJvnYc=
github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa/go.mod h1:RvfVo/6Sbnfra9kkvIxDW8NYOOaYsHjF0DdtMCs9cdo= github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa/go.mod h1:RvfVo/6Sbnfra9kkvIxDW8NYOOaYsHjF0DdtMCs9cdo=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/libc v1.38.0 h1:o4Lpk0zNDSdsjfEXnF1FGXWQ9PDi1NOdWcLP5n13FGo=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/libc v1.38.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw= modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=

View File

@@ -14,6 +14,7 @@ import (
) )
var ( var (
host string
port int port int
queueSize int queueSize int
configFile string configFile string
@@ -36,6 +37,7 @@ var (
func init() { func init() {
flag.StringVar(&host, "host", "0.0.0.0", "Host where server will listen at")
flag.IntVar(&port, "port", 3033, "Port where server will listen at") flag.IntVar(&port, "port", 3033, "Port where server will listen at")
flag.IntVar(&queueSize, "qs", runtime.NumCPU(), "Download queue size") flag.IntVar(&queueSize, "qs", runtime.NumCPU(), "Download queue size")
@@ -61,6 +63,7 @@ func main() {
c := config.Instance() c := config.Instance()
c.Host = host
c.Port = port c.Port = port
c.QueueSize = queueSize c.QueueSize = queueSize
c.DownloadPath = downloadPath c.DownloadPath = downloadPath
@@ -76,5 +79,5 @@ func main() {
log.Println(cli.BgRed, "config", cli.Reset, "no config file found") log.Println(cli.BgRed, "config", cli.Reset, "no config file found")
} }
server.RunBlocking(port, frontend, localDatabasePath) server.RunBlocking(host, port, frontend, localDatabasePath)
} }

View File

@@ -8,6 +8,7 @@ import (
) )
type Config struct { type Config struct {
Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
DownloadPath string `yaml:"downloadPath"` DownloadPath string `yaml:"downloadPath"`
DownloaderPath string `yaml:"downloaderPath"` DownloaderPath string `yaml:"downloaderPath"`

View File

@@ -3,6 +3,7 @@ package handlers
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@@ -20,20 +21,19 @@ type DirectoryEntry struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
Size int64 `json:"size"` Size int64 `json:"size"`
SHASum string `json:"shaSum"`
ModTime time.Time `json:"modTime"` ModTime time.Time `json:"modTime"`
IsVideo bool `json:"isVideo"` IsVideo bool `json:"isVideo"`
IsDirectory bool `json:"isDirectory"` IsDirectory bool `json:"isDirectory"`
} }
func walkDir(root string) (*[]DirectoryEntry, error) { func walkDir(root string) (*[]DirectoryEntry, error) {
files := []DirectoryEntry{}
dirs, err := os.ReadDir(root) dirs, err := os.ReadDir(root)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var files []DirectoryEntry
for _, d := range dirs { for _, d := range dirs {
if !utils.IsValidEntry(d) { if !utils.IsValidEntry(d) {
continue continue
@@ -50,7 +50,6 @@ func walkDir(root string) (*[]DirectoryEntry, error) {
Path: path, Path: path,
Name: d.Name(), Name: d.Name(),
Size: info.Size(), Size: info.Size(),
SHASum: utils.ShaSumString(path),
IsVideo: utils.IsVideo(d), IsVideo: utils.IsVideo(d),
IsDirectory: d.IsDir(), IsDirectory: d.IsDir(),
ModTime: info.ModTime(), ModTime: info.ModTime(),
@@ -69,8 +68,7 @@ func ListDownloaded(w http.ResponseWriter, r *http.Request) {
root := config.Instance().DownloadPath root := config.Instance().DownloadPath
req := new(ListRequest) req := new(ListRequest)
err := json.NewDecoder(r.Body).Decode(&req) if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
@@ -88,9 +86,8 @@ func ListDownloaded(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(files)
if err != nil { if err := json.NewEncoder(w).Encode(files); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
} }
@@ -100,21 +97,13 @@ type DeleteRequest = DirectoryEntry
func DeleteFile(w http.ResponseWriter, r *http.Request) { func DeleteFile(w http.ResponseWriter, r *http.Request) {
req := new(DeleteRequest) req := new(DeleteRequest)
err := json.NewDecoder(r.Body).Decode(&req) if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
sum := utils.ShaSumString(req.Path) if err := os.Remove(req.Path); err != nil {
if sum != req.SHASum { http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, "shasum mismatch", http.StatusBadRequest)
return
}
err = os.Remove(req.Path)
if err != nil {
http.Error(w, "shasum mismatch", http.StatusBadRequest)
return return
} }
@@ -142,18 +131,60 @@ func SendFile(w http.ResponseWriter, r *http.Request) {
return return
} }
decodedStr := string(decoded) filename := string(decoded)
root := config.Instance().DownloadPath root := config.Instance().DownloadPath
// TODO: further path / file validations // TODO: further path / file validations
if strings.Contains(filepath.Dir(decodedStr), root) { if strings.Contains(filepath.Dir(filename), root) {
w.Header().Add( http.ServeFile(w, r, filename)
"Content-Disposition", return
"inline; filename="+filepath.Base(decodedStr), }
)
w.WriteHeader(http.StatusUnauthorized)
http.ServeFile(w, r, decodedStr) }
func DownloadFile(w http.ResponseWriter, r *http.Request) {
path := chi.URLParam(r, "id")
if path == "" {
http.Error(w, "inexistent path", http.StatusBadRequest)
return
}
path, err := url.QueryUnescape(path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
decoded, err := base64.StdEncoding.DecodeString(path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
filename := string(decoded)
root := config.Instance().DownloadPath
if strings.Contains(filepath.Dir(filename), root) {
w.Header().Add(
"Content-Disposition",
"inline; filename="+filepath.Base(filename),
)
w.Header().Set(
"Content-Type",
"application/octet-stream",
)
fd, err := os.Open(filename)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.Copy(w, fd)
} }
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)

View File

@@ -30,7 +30,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
) )
if username != req.Username || password != req.Password { if username != req.Username || password != req.Password {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, "invalid username or password", http.StatusBadRequest)
return return
} }
@@ -47,16 +47,10 @@ func Login(w http.ResponseWriter, r *http.Request) {
return return
} }
cookie := &http.Cookie{ if err := json.NewEncoder(w).Encode(tokenString); err != nil {
Name: utils.TOKEN_COOKIE_NAME, http.Error(w, err.Error(), http.StatusInternalServerError)
HttpOnly: true, return
Secure: false,
Expires: expiresAt, // 30 days
Value: tokenString,
Path: "/",
} }
http.SetCookie(w, cookie)
} }
func Logout(w http.ResponseWriter, r *http.Request) { func Logout(w http.ResponseWriter, r *http.Request) {

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"log" "log"
"os/exec" "os/exec"
"strings"
"time" "time"
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli" "github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
@@ -14,6 +15,7 @@ import (
type metadata struct { type metadata struct {
Entries []DownloadInfo `json:"entries"` Entries []DownloadInfo `json:"entries"`
Count int `json:"playlist_count"` Count int `json:"playlist_count"`
PlaylistTitle string `json:"title"`
Type string `json:"_type"` Type string `json:"_type"`
} }
@@ -57,6 +59,15 @@ func PlaylistDetect(req DownloadRequest, mq *MessageQueue, db *MemoryDB) error {
for i, meta := range m.Entries { for i, meta := range m.Entries {
delta := time.Second.Microseconds() * int64(i+1) delta := time.Second.Microseconds() * int64(i+1)
// detect playlist title from metadata since each playlist entry will be
// treated as an individual download
req.Rename = strings.Replace(
req.Rename,
"%(playlist_title)s",
m.PlaylistTitle,
1,
)
proc := &Process{ proc := &Process{
Url: meta.OriginalURL, Url: meta.OriginalURL,
Progress: DownloadProgress{}, Progress: DownloadProgress{},

View File

@@ -84,9 +84,11 @@ func (p *Process) Start() {
} }
if p.Output.Filename != "" { if p.Output.Filename != "" {
out.Filename = p.Output.Filename + ".%(ext)s" out.Filename = p.Output.Filename
} }
buildFilename(&p.Output)
params := append([]string{ params := append([]string{
strings.Split(p.Url, "?list")[0], //no playlist strings.Split(p.Url, "?list")[0], //no playlist
"--newline", "--newline",
@@ -298,3 +300,16 @@ func (p *Process) SetMetadata() error {
func (p *Process) getShortId() string { func (p *Process) getShortId() string {
return strings.Split(p.Id, "-")[0] return strings.Split(p.Id, "-")[0]
} }
func buildFilename(o *DownloadOutput) {
if o.Filename != "" && strings.Contains(o.Filename, ".%(ext)s") {
o.Filename += ".%(ext)s"
}
o.Filename = strings.Replace(
o.Filename,
".%(ext)s.%(ext)s",
".%(ext)s",
1,
)
}

View File

@@ -1,36 +1,21 @@
package middlewares package middlewares
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/utils"
) )
func Authenticated(next http.Handler) http.Handler { func validateToken(tokenValue string) error {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if tokenValue == "" {
if !config.Instance().RequireAuth { return errors.New("invalid token")
next.ServeHTTP(w, r)
return
} }
cookie, err := r.Cookie(utils.TOKEN_COOKIE_NAME) token, _ := jwt.Parse(tokenValue, func(t *jwt.Token) (interface{}, error) {
if err != nil {
http.Error(w, "invalid token", http.StatusBadRequest)
return
}
if cookie == nil {
http.Error(w, "invalid token", http.StatusBadRequest)
return
}
token, _ := jwt.Parse(cookie.Value, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
} }
@@ -41,16 +26,28 @@ func Authenticated(next http.Handler) http.Handler {
expiresAt, err := time.Parse(time.RFC3339, claims["expiresAt"].(string)) expiresAt, err := time.Parse(time.RFC3339, claims["expiresAt"].(string))
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return err
return
} }
if time.Now().After(expiresAt) { if time.Now().After(expiresAt) {
http.Error(w, "token expired", http.StatusBadRequest) return errors.New("token expired")
return
} }
} else { } else {
http.Error(w, "invalid token", http.StatusBadRequest) return errors.New("invalid token")
}
return nil
}
func Authenticated(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Authentication")
if token == "" {
token = r.URL.Query().Get("token")
}
if err := validateToken(token); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }

View File

@@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal" "github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware" middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
) )
@@ -20,7 +21,9 @@ func ApplyRouter(db *sql.DB, mdb *internal.MemoryDB, mq *internal.MessageQueue)
h := Container(db, mdb, mq) h := Container(db, mdb, mq)
return func(r chi.Router) { return func(r chi.Router) {
if config.Instance().RequireAuth {
r.Use(middlewares.Authenticated) r.Use(middlewares.Authenticated)
}
r.Post("/exec", h.Exec()) r.Post("/exec", h.Exec())
r.Get("/running", h.Running()) r.Get("/running", h.Running())
r.Post("/cookies", h.SetCookies()) r.Post("/cookies", h.SetCookies())

View File

@@ -85,6 +85,8 @@ func (s *Service) GetTemplates(ctx context.Context) (*[]internal.CustomTemplate,
return nil, err return nil, err
} }
defer rows.Close()
templates := make([]internal.CustomTemplate, 0) templates := make([]internal.CustomTemplate, 0)
for rows.Next() { for rows.Next() {

View File

@@ -2,6 +2,7 @@ package rpc
import ( import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal" "github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware" middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
) )
@@ -17,7 +18,9 @@ func Container(db *internal.MemoryDB, mq *internal.MessageQueue) *Service {
// RPC service must be registered before applying this router! // RPC service must be registered before applying this router!
func ApplyRouter() func(chi.Router) { func ApplyRouter() func(chi.Router) {
return func(r chi.Router) { return func(r chi.Router) {
if config.Instance().RequireAuth {
r.Use(middlewares.Authenticated) r.Use(middlewares.Authenticated)
}
r.Get("/ws", WebSocket) r.Get("/ws", WebSocket)
r.Post("/http", Post) r.Post("/http", Post)
} }

View File

@@ -2,6 +2,7 @@ package rpc
import ( import (
"io" "io"
"log"
"net/http" "net/http"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@@ -29,6 +30,7 @@ func WebSocket(w http.ResponseWriter, r *http.Request) {
mtype, reader, err := c.NextReader() mtype, reader, err := c.NextReader()
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
break break
} }
@@ -37,6 +39,7 @@ func WebSocket(w http.ResponseWriter, r *http.Request) {
writer, err := c.NextWriter(mtype) writer, err := c.NextWriter(mtype)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
break break
} }

View File

@@ -16,6 +16,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/dbutils" "github.com/marcopeocchi/yt-dlp-web-ui/server/dbutils"
"github.com/marcopeocchi/yt-dlp-web-ui/server/handlers" "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"
@@ -28,13 +29,14 @@ import (
type serverConfig struct { type serverConfig struct {
frontend fs.FS frontend fs.FS
host string
port int port int
mdb *internal.MemoryDB mdb *internal.MemoryDB
db *sql.DB db *sql.DB
mq *internal.MessageQueue mq *internal.MessageQueue
} }
func RunBlocking(port int, frontend fs.FS, dbPath string) { func RunBlocking(host string, port int, frontend fs.FS, dbPath string) {
var mdb internal.MemoryDB var mdb internal.MemoryDB
mdb.Restore() mdb.Restore()
@@ -53,6 +55,7 @@ func RunBlocking(port int, frontend fs.FS, dbPath string) {
srv := newServer(serverConfig{ srv := newServer(serverConfig{
frontend: frontend, frontend: frontend,
host: host,
port: port, port: port,
mdb: &mdb, mdb: &mdb,
mq: mq, mq: mq,
@@ -71,7 +74,21 @@ func newServer(c serverConfig) *http.Server {
r := chi.NewRouter() r := chi.NewRouter()
r.Use(cors.AllowAll().Handler) corsMiddleware := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowedHeaders: []string{"*"},
AllowCredentials: true,
})
r.Use(corsMiddleware.Handler)
r.Use(middleware.Logger) r.Use(middleware.Logger)
app := http.FileServer(http.FS(c.frontend)) app := http.FileServer(http.FS(c.frontend))
@@ -80,10 +97,13 @@ func newServer(c serverConfig) *http.Server {
// Archive routes // Archive routes
r.Route("/archive", func(r chi.Router) { r.Route("/archive", func(r chi.Router) {
if config.Instance().RequireAuth {
r.Use(middlewares.Authenticated) r.Use(middlewares.Authenticated)
}
r.Post("/downloaded", handlers.ListDownloaded) r.Post("/downloaded", handlers.ListDownloaded)
r.Post("/delete", handlers.DeleteFile) r.Post("/delete", handlers.DeleteFile)
r.Get("/d/{id}", handlers.SendFile) r.Get("/d/{id}", handlers.DownloadFile)
r.Get("/v/{id}", handlers.SendFile)
}) })
// Authentication routes // Authentication routes
@@ -99,7 +119,7 @@ func newServer(c serverConfig) *http.Server {
r.Route("/api/v1", rest.ApplyRouter(c.db, c.mdb, c.mq)) r.Route("/api/v1", rest.ApplyRouter(c.db, c.mdb, c.mq))
return &http.Server{ return &http.Server{
Addr: fmt.Sprintf(":%d", c.port), Addr: fmt.Sprintf("%s:%d", c.host, c.port),
Handler: r, Handler: r,
} }
} }