Compare commits
4 Commits
feat-persi
...
v3.2.6
| Author | SHA1 | Date | |
|---|---|---|---|
| e4362468f7 | |||
| 6880f60d14 | |||
|
|
5d4aa7e2a3 | ||
|
|
2845196bc7 |
1
.deploystack/docker-run.txt
Normal file
1
.deploystack/docker-run.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
docker run -d -p 3033:3033 -v /downloads:/downloads marcobaobao/yt-dlp-webui
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
result/
|
result/
|
||||||
result
|
result
|
||||||
dist
|
dist
|
||||||
|
.pnpm-store/
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
@@ -20,9 +21,11 @@ cookies.txt
|
|||||||
__debug*
|
__debug*
|
||||||
ui/
|
ui/
|
||||||
.idea
|
.idea
|
||||||
|
.idea/
|
||||||
frontend/.pnp.cjs
|
frontend/.pnp.cjs
|
||||||
frontend/.pnp.loader.mjs
|
frontend/.pnp.loader.mjs
|
||||||
frontend/.yarn/install-state.gz
|
frontend/.yarn/install-state.gz
|
||||||
.db.lock
|
.db.lock
|
||||||
livestreams.dat
|
livestreams.dat
|
||||||
.git
|
.vite/deps
|
||||||
|
archive.txt
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -29,4 +29,3 @@ frontend/.yarn/install-state.gz
|
|||||||
livestreams.dat
|
livestreams.dat
|
||||||
.vite/deps
|
.vite/deps
|
||||||
archive.txt
|
archive.txt
|
||||||
web_config.yml
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -28,7 +28,7 @@ docker pull ghcr.io/marcopiovanello/yt-dlp-web-ui:latest
|
|||||||
## Community stuff
|
## Community stuff
|
||||||
Feel free to join :)
|
Feel free to join :)
|
||||||
|
|
||||||
[](https://discord.gg/3Sj9ZZHv)
|
[](https://discord.gg/WRnVWr4y)
|
||||||
|
|
||||||
## Some screeshots
|
## Some screeshots
|
||||||

|

|
||||||
@@ -115,6 +115,16 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ⚡ One-Click Deploy
|
||||||
|
|
||||||
|
| Cloud Provider | Deploy Button |
|
||||||
|
|----------------|---------------|
|
||||||
|
| AWS | <a href="https://deploystack.io/deploy/marcopiovanello-yt-dlp-web-ui?provider=aws&language=cfn"><img src="https://raw.githubusercontent.com/deploystackio/deploy-templates/refs/heads/main/.assets/img/aws.svg" height="38"></a> |
|
||||||
|
| DigitalOcean | <a href="https://deploystack.io/deploy/marcopiovanello-yt-dlp-web-ui?provider=do&language=dop"><img src="https://raw.githubusercontent.com/deploystackio/deploy-templates/refs/heads/main/.assets/img/do.svg" height="38"></a> |
|
||||||
|
| Render | <a href="https://deploystack.io/deploy/marcopiovanello-yt-dlp-web-ui?provider=rnd&language=rnd"><img src="https://raw.githubusercontent.com/deploystackio/deploy-templates/refs/heads/main/.assets/img/rnd.svg" height="38"></a> |
|
||||||
|
|
||||||
|
<sub>Generated by <a href="https://deploystack.io/c/marcopiovanello-yt-dlp-web-ui" target="_blank">DeployStack.io</a></sub>
|
||||||
|
|
||||||
## [Prebuilt binaries](https://github.com/marcopiovanello/yt-dlp-web-ui/releases) installation
|
## [Prebuilt binaries](https://github.com/marcopiovanello/yt-dlp-web-ui/releases) installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -18,12 +18,11 @@
|
|||||||
"@mui/icons-material": "^6.2.0",
|
"@mui/icons-material": "^6.2.0",
|
||||||
"@mui/material": "^6.2.0",
|
"@mui/material": "^6.2.0",
|
||||||
"fp-ts": "^2.16.5",
|
"fp-ts": "^2.16.5",
|
||||||
"jotai": "^2.10.3",
|
|
||||||
"jotai-cache": "^0.5.0",
|
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
"react-virtuoso": "^4.7.11",
|
"react-virtuoso": "^4.7.11",
|
||||||
|
"jotai": "^2.10.3",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
12
frontend/pnpm-lock.yaml
generated
12
frontend/pnpm-lock.yaml
generated
@@ -32,9 +32,6 @@ importers:
|
|||||||
jotai:
|
jotai:
|
||||||
specifier: ^2.10.3
|
specifier: ^2.10.3
|
||||||
version: 2.10.3(@types/react@19.0.1)(react@19.0.0)
|
version: 2.10.3(@types/react@19.0.1)(react@19.0.0)
|
||||||
jotai-cache:
|
|
||||||
specifier: ^0.5.0
|
|
||||||
version: 0.5.0(jotai@2.10.3(@types/react@19.0.1)(react@19.0.0))
|
|
||||||
react:
|
react:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.0.0
|
version: 19.0.0
|
||||||
@@ -740,11 +737,6 @@ packages:
|
|||||||
is-core-module@2.12.1:
|
is-core-module@2.12.1:
|
||||||
resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==}
|
resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==}
|
||||||
|
|
||||||
jotai-cache@0.5.0:
|
|
||||||
resolution: {integrity: sha512-29pUuEfSXL7Ba6lxZmiNDARc73TspWzAzCy0jCkk2uEOnFJ6kaUBZTp/AZSwnIsh1ndfUfM9/QpbLU7uJAQL0A==}
|
|
||||||
peerDependencies:
|
|
||||||
jotai: '>=2.0.0'
|
|
||||||
|
|
||||||
jotai@2.10.3:
|
jotai@2.10.3:
|
||||||
resolution: {integrity: sha512-Nnf4IwrLhNfuz2JOQLI0V/AgwcpxvVy8Ec8PidIIDeRi4KCFpwTFIpHAAcU+yCgnw/oASYElq9UY0YdUUegsSA==}
|
resolution: {integrity: sha512-Nnf4IwrLhNfuz2JOQLI0V/AgwcpxvVy8Ec8PidIIDeRi4KCFpwTFIpHAAcU+yCgnw/oASYElq9UY0YdUUegsSA==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
@@ -1520,10 +1512,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has: 1.0.3
|
has: 1.0.3
|
||||||
|
|
||||||
jotai-cache@0.5.0(jotai@2.10.3(@types/react@19.0.1)(react@19.0.0)):
|
|
||||||
dependencies:
|
|
||||||
jotai: 2.10.3(@types/react@19.0.1)(react@19.0.0)
|
|
||||||
|
|
||||||
jotai@2.10.3(@types/react@19.0.1)(react@19.0.0):
|
jotai@2.10.3(@types/react@19.0.1)(react@19.0.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.1
|
'@types/react': 19.0.1
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import ListItemButton from '@mui/material/ListItemButton'
|
|||||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||||
import ListItemText from '@mui/material/ListItemText'
|
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 { grey } from '@mui/material/colors'
|
import { grey } from '@mui/material/colors'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { Link, Outlet } from 'react-router-dom'
|
import { Link, Outlet } from 'react-router-dom'
|
||||||
import { settingsState } from './atoms/settings'
|
import { settingsState } from './atoms/settings'
|
||||||
import AppBar from './components/AppBar'
|
import AppBar from './components/AppBar'
|
||||||
import { AppTitle } from './components/AppTitle'
|
|
||||||
import Drawer from './components/Drawer'
|
import Drawer from './components/Drawer'
|
||||||
import Footer from './components/Footer'
|
import Footer from './components/Footer'
|
||||||
import Logout from './components/Logout'
|
import Logout from './components/Logout'
|
||||||
@@ -76,7 +76,15 @@ export default function Layout() {
|
|||||||
>
|
>
|
||||||
<Menu />
|
<Menu />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<AppTitle />
|
<Typography
|
||||||
|
component="h1"
|
||||||
|
variant="h6"
|
||||||
|
color="inherit"
|
||||||
|
noWrap
|
||||||
|
sx={{ flexGrow: 1 }}
|
||||||
|
>
|
||||||
|
{settings.appTitle}
|
||||||
|
</Typography>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Drawer variant="permanent" open={open}>
|
<Drawer variant="permanent" open={open}>
|
||||||
|
|||||||
@@ -79,4 +79,5 @@ keys:
|
|||||||
The monitor job will be scheduled/triggered by a defined cron expression (defaults to every 5 minutes if left blank).
|
The monitor job will be scheduled/triggered by a defined cron expression (defaults to every 5 minutes if left blank).
|
||||||
cronExpressionLabel: 'Cron expression'
|
cronExpressionLabel: 'Cron expression'
|
||||||
editButtonLabel: 'Edit'
|
editButtonLabel: 'Edit'
|
||||||
newSubscriptionButton: New subscription
|
newSubscriptionButton: New subscription
|
||||||
|
clearCompletedButton: 'Clear completed'
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { getOrElse } from 'fp-ts/lib/Either'
|
import { getOrElse } from 'fp-ts/lib/Either'
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { atomWithCache } from 'jotai-cache'
|
import { atom } from 'jotai'
|
||||||
import { atomWithStorage } from 'jotai/utils'
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
import { CustomTemplate } from '../types'
|
import { CustomTemplate } from '../types'
|
||||||
import { serverSideCookiesState, serverURL } from './settings'
|
import { serverSideCookiesState, serverURL } from './settings'
|
||||||
|
|
||||||
export const cookiesTemplateState = atomWithCache<Promise<string>>(async (get) =>
|
export const cookiesTemplateState = atom<Promise<string>>(async (get) =>
|
||||||
await get(serverSideCookiesState)
|
await get(serverSideCookiesState)
|
||||||
? '--cookies=cookies.txt'
|
? '--cookies=cookies.txt'
|
||||||
: ''
|
: ''
|
||||||
@@ -22,7 +22,7 @@ export const filenameTemplateState = atomWithStorage(
|
|||||||
localStorage.getItem('lastFilenameTemplate') ?? ''
|
localStorage.getItem('lastFilenameTemplate') ?? ''
|
||||||
)
|
)
|
||||||
|
|
||||||
export const savedTemplatesState = atomWithCache<Promise<CustomTemplate[]>>(async (get) => {
|
export const savedTemplatesState = atom<Promise<CustomTemplate[]>>(async (get) => {
|
||||||
const task = ffetch<CustomTemplate[]>(`${get(serverURL)}/api/v1/template/all`)
|
const task = ffetch<CustomTemplate[]>(`${get(serverURL)}/api/v1/template/all`)
|
||||||
const either = await task()
|
const either = await task()
|
||||||
|
|
||||||
@@ -30,4 +30,5 @@ export const savedTemplatesState = atomWithCache<Promise<CustomTemplate[]>>(asyn
|
|||||||
either,
|
either,
|
||||||
getOrElse(() => new Array<CustomTemplate>())
|
getOrElse(() => new Array<CustomTemplate>())
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { matchW } from 'fp-ts/lib/TaskEither'
|
import { matchW } from 'fp-ts/lib/TaskEither'
|
||||||
import { atom } from 'jotai'
|
|
||||||
import { atomWithStorage } from 'jotai/utils'
|
|
||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
import { prefersDarkMode } from '../utils'
|
import { prefersDarkMode } from '../utils'
|
||||||
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
export const languages = [
|
export const languages = [
|
||||||
'catalan',
|
'catalan',
|
||||||
@@ -121,8 +121,7 @@ export const appTitleState = atomWithStorage(
|
|||||||
export const serverAddressAndPortState = atom((get) => {
|
export const serverAddressAndPortState = atom((get) => {
|
||||||
if (get(servedFromReverseProxySubDirState)) {
|
if (get(servedFromReverseProxySubDirState)) {
|
||||||
return `${get(serverAddressState)}/${get(servedFromReverseProxySubDirState)}/`
|
return `${get(serverAddressState)}/${get(servedFromReverseProxySubDirState)}/`
|
||||||
.replaceAll('"', '') // XXX: atomWithStorage uses JSON.stringify to serialize
|
.replaceAll('"', '') // TODO: atomWithStorage put extra double quotes on strings
|
||||||
.replaceAll('//', '/') // which puts extra double quotes.
|
|
||||||
}
|
}
|
||||||
if (get(servedFromReverseProxyState)) {
|
if (get(servedFromReverseProxyState)) {
|
||||||
return `${get(serverAddressState)}`
|
return `${get(serverAddressState)}`
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Typography } from '@mui/material'
|
|
||||||
import { useAtom } from 'jotai'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { appTitleState } from '../atoms/settings'
|
|
||||||
import useFetch from '../hooks/useFetch'
|
|
||||||
|
|
||||||
export const AppTitle: React.FC = () => {
|
|
||||||
const [appTitle, setAppTitle] = useAtom(appTitleState)
|
|
||||||
|
|
||||||
const { data } = useFetch<{ title: string }>('/webconfig')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data?.title) {
|
|
||||||
setAppTitle(
|
|
||||||
data.title.startsWith('"')
|
|
||||||
? data.title.substring(1, data.title.length - 1)
|
|
||||||
: data.title
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
component="h1"
|
|
||||||
variant="h6"
|
|
||||||
color="inherit"
|
|
||||||
noWrap
|
|
||||||
sx={{ flexGrow: 1 }}
|
|
||||||
>
|
|
||||||
{appTitle.startsWith('"') ? appTitle.substring(1, appTitle.length - 1) : appTitle}
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import AddCircleIcon from '@mui/icons-material/AddCircle'
|
import AddCircleIcon from '@mui/icons-material/AddCircle'
|
||||||
import BuildCircleIcon from '@mui/icons-material/BuildCircle'
|
import BuildCircleIcon from '@mui/icons-material/BuildCircle'
|
||||||
|
import ClearAllIcon from '@mui/icons-material/ClearAll'
|
||||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
||||||
import FolderZipIcon from '@mui/icons-material/FolderZip'
|
import FolderZipIcon from '@mui/icons-material/FolderZip'
|
||||||
import FormatListBulleted from '@mui/icons-material/FormatListBulleted'
|
import FormatListBulleted from '@mui/icons-material/FormatListBulleted'
|
||||||
@@ -42,6 +43,11 @@ const HomeSpeedDial: React.FC<Props> = ({ onDownloadOpen, onEditorOpen }) => {
|
|||||||
tooltipTitle={i18n.t('bulkDownload')}
|
tooltipTitle={i18n.t('bulkDownload')}
|
||||||
onClick={() => window.open(`${serverAddr}/archive/bulk?token=${localStorage.getItem('token')}`)}
|
onClick={() => window.open(`${serverAddr}/archive/bulk?token=${localStorage.getItem('token')}`)}
|
||||||
/>
|
/>
|
||||||
|
<SpeedDialAction
|
||||||
|
icon={<ClearAllIcon />}
|
||||||
|
tooltipTitle={i18n.t('clearCompletedButton')}
|
||||||
|
onClick={() => client.clearCompleted()}
|
||||||
|
/>
|
||||||
<SpeedDialAction
|
<SpeedDialAction
|
||||||
icon={<DeleteForeverIcon />}
|
icon={<DeleteForeverIcon />}
|
||||||
tooltipTitle={i18n.t('abortAllButton')}
|
tooltipTitle={i18n.t('abortAllButton')}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { TransitionProps } from '@mui/material/transitions'
|
import { TransitionProps } from '@mui/material/transitions'
|
||||||
import { matchW } from 'fp-ts/lib/TaskEither'
|
import { matchW } from 'fp-ts/lib/Either'
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { forwardRef, startTransition, useState } from 'react'
|
import { forwardRef, startTransition, useState } from 'react'
|
||||||
@@ -52,16 +52,21 @@ const SubscriptionsDialog: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
|
|
||||||
const baseURL = useAtomValue(serverURL)
|
const baseURL = useAtomValue(serverURL)
|
||||||
|
|
||||||
const submit = async (sub: Omit<Subscription, 'id'>) => pipe(
|
const submit = async (sub: Omit<Subscription, 'id'>) => {
|
||||||
ffetch<void>(`${baseURL}/subscriptions`, {
|
const task = ffetch<void>(`${baseURL}/subscriptions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(sub)
|
body: JSON.stringify(sub)
|
||||||
}),
|
})
|
||||||
matchW(
|
const either = await task()
|
||||||
(l) => pushMessage(l, 'error'),
|
|
||||||
(_) => onClose()
|
pipe(
|
||||||
|
either,
|
||||||
|
matchW(
|
||||||
|
(l) => pushMessage(l, 'error'),
|
||||||
|
(_) => onClose()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)()
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ import { serverURL } from '../atoms/settings'
|
|||||||
import { ffetch } from '../lib/httpClient'
|
import { ffetch } from '../lib/httpClient'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper hook for ffetch. Handles data retrieval and cancellation signals.
|
|
||||||
* If R type is set to void it doesn't perform deserialization.
|
|
||||||
* @param resource path of the resource. serverURL is prepended
|
|
||||||
* @returns JSON decoded value, eventual error and refetcher as an object to destruct.
|
|
||||||
*/
|
|
||||||
const useFetch = <R>(resource: string) => {
|
const useFetch = <R>(resource: string) => {
|
||||||
const base = useAtomValue(serverURL)
|
const base = useAtomValue(serverURL)
|
||||||
|
|
||||||
@@ -32,10 +26,7 @@ const useFetch = <R>(resource: string) => {
|
|||||||
)()
|
)()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController()
|
|
||||||
fetcher()
|
fetcher()
|
||||||
|
|
||||||
return () => controller.abort()
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return { data, error, fetcher }
|
return { data, error, fetcher }
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { tryCatch } from 'fp-ts/TaskEither'
|
import { tryCatch } from 'fp-ts/TaskEither'
|
||||||
import * as J from 'fp-ts/Json'
|
|
||||||
import * as E from 'fp-ts/Either'
|
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
|
||||||
|
|
||||||
async function fetcher(url: string, opt?: RequestInit, controller?: AbortController): Promise<string> {
|
async function fetcher<T>(url: string, opt?: RequestInit): Promise<T> {
|
||||||
const jwt = localStorage.getItem('token')
|
const jwt = localStorage.getItem('token')
|
||||||
|
|
||||||
if (opt && !opt.headers) {
|
if (opt && !opt.headers) {
|
||||||
@@ -17,25 +14,17 @@ async function fetcher(url: string, opt?: RequestInit, controller?: AbortControl
|
|||||||
headers: {
|
headers: {
|
||||||
...opt?.headers,
|
...opt?.headers,
|
||||||
'X-Authentication': jwt ?? ''
|
'X-Authentication': jwt ?? ''
|
||||||
},
|
}
|
||||||
signal: controller?.signal
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw await res.text()
|
throw await res.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.text()
|
return res.json() as T
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ffetch = <T>(url: string, opt?: RequestInit, controller?: AbortController) => tryCatch(
|
export const ffetch = <T>(url: string, opt?: RequestInit) => tryCatch(
|
||||||
async () => pipe(
|
() => fetcher<T>(url, opt),
|
||||||
await fetcher(url, opt, controller),
|
|
||||||
J.parse,
|
|
||||||
E.match(
|
|
||||||
(l) => l as T,
|
|
||||||
(r) => r as T
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(e) => `error while fetching: ${e}`
|
(e) => `error while fetching: ${e}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -200,4 +200,11 @@ export class RPCClient {
|
|||||||
params: []
|
params: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clearCompleted() {
|
||||||
|
return this.sendHTTP({
|
||||||
|
method: 'Service.ClearCompleted',
|
||||||
|
params: []
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ export type RPCMethods =
|
|||||||
| "Service.ProgressLivestream"
|
| "Service.ProgressLivestream"
|
||||||
| "Service.KillLivestream"
|
| "Service.KillLivestream"
|
||||||
| "Service.KillAllLivestream"
|
| "Service.KillAllLivestream"
|
||||||
|
| "Service.ClearCompleted"
|
||||||
|
|
||||||
export type RPCRequest = {
|
export type RPCRequest = {
|
||||||
method: RPCMethods
|
method: RPCMethods
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
capitalize
|
capitalize
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
import { useAtom } from 'jotai'
|
||||||
import { matchW } from 'fp-ts/lib/TaskEither'
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
|
||||||
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'
|
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Subject,
|
Subject,
|
||||||
@@ -36,9 +34,9 @@ import {
|
|||||||
accentState,
|
accentState,
|
||||||
accents,
|
accents,
|
||||||
appTitleState,
|
appTitleState,
|
||||||
autoFileExtensionState,
|
|
||||||
enableCustomArgsState,
|
enableCustomArgsState,
|
||||||
fileRenamingState,
|
fileRenamingState,
|
||||||
|
autoFileExtensionState,
|
||||||
formatSelectionState,
|
formatSelectionState,
|
||||||
languageState,
|
languageState,
|
||||||
languages,
|
languages,
|
||||||
@@ -47,14 +45,12 @@ import {
|
|||||||
servedFromReverseProxySubDirState,
|
servedFromReverseProxySubDirState,
|
||||||
serverAddressState,
|
serverAddressState,
|
||||||
serverPortState,
|
serverPortState,
|
||||||
serverURL,
|
|
||||||
themeState
|
themeState
|
||||||
} from '../atoms/settings'
|
} from '../atoms/settings'
|
||||||
import CookiesTextField from '../components/CookiesTextField'
|
import CookiesTextField from '../components/CookiesTextField'
|
||||||
import UpdateBinaryButton from '../components/UpdateBinaryButton'
|
import UpdateBinaryButton from '../components/UpdateBinaryButton'
|
||||||
import { useToast } from '../hooks/toast'
|
import { useToast } from '../hooks/toast'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
import { ffetch } from '../lib/httpClient'
|
|
||||||
import Translator from '../lib/i18n'
|
import Translator from '../lib/i18n'
|
||||||
import { validateDomain, validateIP } from '../utils'
|
import { validateDomain, validateIP } from '../utils'
|
||||||
|
|
||||||
@@ -74,7 +70,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
const [pollingTime, setPollingTime] = useAtom(rpcPollingTimeState)
|
const [pollingTime, setPollingTime] = useAtom(rpcPollingTimeState)
|
||||||
const [language, setLanguage] = useAtom(languageState)
|
const [language, setLanguage] = useAtom(languageState)
|
||||||
const [appTitle, setAppTitle] = useAtom(appTitleState)
|
const [appTitle, setApptitle] = useAtom(appTitleState)
|
||||||
const [accent, setAccent] = useAtom(accentState)
|
const [accent, setAccent] = useAtom(accentState)
|
||||||
|
|
||||||
const [theme, setTheme] = useAtom(themeState)
|
const [theme, setTheme] = useAtom(themeState)
|
||||||
@@ -85,11 +81,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
const { pushMessage } = useToast()
|
const { pushMessage } = useToast()
|
||||||
|
|
||||||
// TODO: change name
|
|
||||||
const derivedServerURL = useAtomValue(serverURL)
|
|
||||||
|
|
||||||
const baseURL$ = useMemo(() => new Subject<string>(), [])
|
const baseURL$ = useMemo(() => new Subject<string>(), [])
|
||||||
const appTitle$ = useMemo(() => new Subject<string>(), [])
|
|
||||||
const serverAddr$ = useMemo(() => new Subject<string>(), [])
|
const serverAddr$ = useMemo(() => new Subject<string>(), [])
|
||||||
const serverPort$ = useMemo(() => new Subject<string>(), [])
|
const serverPort$ = useMemo(() => new Subject<string>(), [])
|
||||||
|
|
||||||
@@ -142,25 +134,6 @@ export default function Settings() {
|
|||||||
return () => sub.unsubscribe()
|
return () => sub.unsubscribe()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// TODO: refactor out of component. maybe use withAtomEffect from jotai/effect package.
|
|
||||||
useEffect(() => {
|
|
||||||
const sub = appTitle$
|
|
||||||
.pipe(debounceTime(500))
|
|
||||||
.subscribe(title => {
|
|
||||||
pipe(
|
|
||||||
ffetch(`${derivedServerURL}/webconfig/title`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify(title)
|
|
||||||
}),
|
|
||||||
matchW(
|
|
||||||
(l) => pushMessage(l, 'error'),
|
|
||||||
(_) => setAppTitle(title)
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
})
|
|
||||||
return () => sub.unsubscribe()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Language toggler handler
|
* Language toggler handler
|
||||||
*/
|
*/
|
||||||
@@ -221,7 +194,7 @@ export default function Settings() {
|
|||||||
fullWidth
|
fullWidth
|
||||||
label={i18n.t('appTitle')}
|
label={i18n.t('appTitle')}
|
||||||
defaultValue={appTitle}
|
defaultValue={appTitle}
|
||||||
onChange={(e) => appTitle$.next(e.target.value)}
|
onChange={(e) => setApptitle(e.currentTarget.value)}
|
||||||
error={appTitle === ''}
|
error={appTitle === ''}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -245,7 +218,7 @@ export default function Settings() {
|
|||||||
{ value: 500, label: '500 ms' },
|
{ value: 500, label: '500 ms' },
|
||||||
{ value: 750, label: '750 ms' },
|
{ value: 750, label: '750 ms' },
|
||||||
{ value: 1000, label: '1000 ms' },
|
{ value: 1000, label: '1000 ms' },
|
||||||
{ value: 2000, label: '2 s' },
|
{ value: 2000, label: '2000 ms' },
|
||||||
]}
|
]}
|
||||||
onChange={(_, value) => typeof value === 'number'
|
onChange={(_, value) => typeof value === 'number'
|
||||||
? setPollingTime(value)
|
? setPollingTime(value)
|
||||||
@@ -394,7 +367,7 @@ export default function Settings() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={i18n.t('autoFileExtensionOption')}
|
label={i18n.t('autoFileExtensionOption')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/marcopiovanello/yt-dlp-web-ui/v3
|
module github.com/marcopiovanello/yt-dlp-web-ui/v3
|
||||||
|
|
||||||
go 1.23
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
|
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
package configurator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A singleton holding configuration of the frontend component
|
|
||||||
// with optional persistence on a file.
|
|
||||||
type AppConfig struct {
|
|
||||||
Title string `yaml:"title" json:"title"`
|
|
||||||
BaseURL string `yaml:"base_url" json:"base_url"`
|
|
||||||
Language string `yaml:"language" json:"language"`
|
|
||||||
RPCPollingTime int `yaml:"rpc_polling_time" json:"rpc_polling_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Configurator struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
Config AppConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
instance *Configurator
|
|
||||||
instanceOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
func Instance() *Configurator {
|
|
||||||
instanceOnce.Do(func() {
|
|
||||||
if instance == nil {
|
|
||||||
instance = &Configurator{}
|
|
||||||
|
|
||||||
// TODO: move out of initialization
|
|
||||||
err := instance.Load()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed initializating configurator", slog.Any("err", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Configurator) Load() error {
|
|
||||||
fd, err := getConfigurationFile()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fd.Close()
|
|
||||||
|
|
||||||
if err := yaml.NewDecoder(fd).Decode(&c.Config); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Configurator) Persist() error {
|
|
||||||
fd, err := getConfigurationFile()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fd.Close()
|
|
||||||
|
|
||||||
if err := yaml.NewEncoder(fd).Encode(c.Config); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Configurator) setAppConfig(ac *AppConfig) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
// TODO: better validaitons
|
|
||||||
if ac.BaseURL != "" {
|
|
||||||
c.Config.BaseURL = ac.BaseURL
|
|
||||||
}
|
|
||||||
if ac.Language != "" {
|
|
||||||
c.Config.Language = ac.Language
|
|
||||||
}
|
|
||||||
if ac.Title != "" {
|
|
||||||
c.Config.Title = ac.Title
|
|
||||||
}
|
|
||||||
if ac.RPCPollingTime >= 250 && ac.RPCPollingTime <= 2000 {
|
|
||||||
c.Config.RPCPollingTime = ac.RPCPollingTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfigurationFile() (*os.File, error) {
|
|
||||||
fd, err := os.OpenFile(
|
|
||||||
filepath.Join(config.Instance().Dir(), "web_config.yml"),
|
|
||||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fd, nil
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package configurator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
// App configurator REST handlers
|
|
||||||
|
|
||||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(Instance().Config); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
var req AppConfig
|
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Instance().setAppConfig(&req)
|
|
||||||
|
|
||||||
if err := Instance().Persist(); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode("ok")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setAppTitle(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
var req string
|
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
editField(w, func(c *AppConfig) {
|
|
||||||
if req != "" {
|
|
||||||
c.Title = req
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode("ok")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setBaseURL(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
var req string
|
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
editField(w, func(c *AppConfig) {
|
|
||||||
if req != "" {
|
|
||||||
c.BaseURL = req
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode("ok")
|
|
||||||
}
|
|
||||||
|
|
||||||
func editField(w http.ResponseWriter, editFunc func(c *AppConfig)) {
|
|
||||||
editFunc(&Instance().Config)
|
|
||||||
|
|
||||||
if err := Instance().Persist(); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApplyRouter() func(r chi.Router) {
|
|
||||||
return func(r chi.Router) {
|
|
||||||
r.Get("/", GetConfig)
|
|
||||||
r.Post("/", SetConfig)
|
|
||||||
r.Patch("/title", setAppTitle)
|
|
||||||
r.Patch("/baseURL", setBaseURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"container/heap"
|
"container/heap"
|
||||||
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoadBalancer struct {
|
type LoadBalancer struct {
|
||||||
@@ -9,7 +10,29 @@ type LoadBalancer struct {
|
|||||||
done chan *Worker
|
done chan *Worker
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LoadBalancer) Balance(work chan Process) {
|
func NewLoadBalancer(numWorker int) *LoadBalancer {
|
||||||
|
var pool Pool
|
||||||
|
|
||||||
|
doneChan := make(chan *Worker)
|
||||||
|
|
||||||
|
for i := range numWorker {
|
||||||
|
w := &Worker{
|
||||||
|
requests: make(chan *Process, 1),
|
||||||
|
index: i,
|
||||||
|
}
|
||||||
|
go w.Work(doneChan)
|
||||||
|
pool = append(pool, w)
|
||||||
|
|
||||||
|
slog.Info("spawned worker", slog.Int("index", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LoadBalancer{
|
||||||
|
pool: pool,
|
||||||
|
done: doneChan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LoadBalancer) Balance(work chan *Process) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case req := <-work:
|
case req := <-work:
|
||||||
@@ -20,7 +43,7 @@ func (b *LoadBalancer) Balance(work chan Process) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LoadBalancer) dispatch(req Process) {
|
func (b *LoadBalancer) dispatch(req *Process) {
|
||||||
w := heap.Pop(&b.pool).(*Worker)
|
w := heap.Pop(&b.pool).(*Worker)
|
||||||
w.requests <- req
|
w.requests <- req
|
||||||
w.pending++
|
w.pending++
|
||||||
|
|||||||
@@ -141,26 +141,13 @@ func (l *LiveStream) monitorStartTime(r io.Reader) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRIES = 5
|
scanner.Scan()
|
||||||
/*
|
|
||||||
if it's waiting a livestream the 5th line will indicate the time to live
|
|
||||||
its a dumb and not robust method.
|
|
||||||
|
|
||||||
example:
|
for !strings.Contains(scanner.Text(), "Waiting for") {
|
||||||
[youtube] Extracting URL: https://www.youtube.com/watch?v=IQVbGfVVjgY
|
|
||||||
[youtube] IQVbGfVVjgY: Downloading webpage
|
|
||||||
[youtube] IQVbGfVVjgY: Downloading ios player API JSON
|
|
||||||
[youtube] IQVbGfVVjgY: Downloading web creator player API JSON
|
|
||||||
WARNING: [youtube] This live event will begin in 27 minutes. <- STDERR, ignore
|
|
||||||
[wait] Waiting for 00:27:15 - Press Ctrl+C to try now <- 5th line
|
|
||||||
*/
|
|
||||||
for range TRIES {
|
|
||||||
scanner.Scan()
|
scanner.Scan()
|
||||||
|
|
||||||
if strings.Contains(scanner.Text(), "Waiting for") {
|
|
||||||
waitTimeScanner()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waitTimeScanner()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LiveStream) WaitTime() <-chan time.Duration {
|
func (l *LiveStream) WaitTime() <-chan time.Duration {
|
||||||
|
|||||||
@@ -9,15 +9,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func setupTest() {
|
func setupTest() {
|
||||||
config.Instance().DownloaderPath = "yt-dlp"
|
config.Instance().DownloaderPath = "build/yt-dlp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const URL = "https://www.youtube.com/watch?v=pwoAyLGOysU"
|
||||||
|
|
||||||
func TestLivestream(t *testing.T) {
|
func TestLivestream(t *testing.T) {
|
||||||
setupTest()
|
setupTest()
|
||||||
|
|
||||||
done := make(chan *LiveStream)
|
done := make(chan *LiveStream)
|
||||||
|
|
||||||
ls := New("https://www.youtube.com/watch?v=LSm1daKezcE", done, &internal.MessageQueue{}, &internal.MemoryDB{})
|
ls := New(URL, done, &internal.MessageQueue{}, &internal.MemoryDB{})
|
||||||
go ls.Start()
|
go ls.Start()
|
||||||
|
|
||||||
time.AfterFunc(time.Second*20, func() {
|
time.AfterFunc(time.Second*20, func() {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func PlaylistDetect(req DownloadRequest, mq *MessageQueue, db *MemoryDB) error {
|
|||||||
return errors.New("probably not a valid URL")
|
return errors.New("probably not a valid URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Type == "playlist" {
|
if m.IsPlaylist() {
|
||||||
entries := slices.CompactFunc(slices.Compact(m.Entries), func(a common.DownloadInfo, b common.DownloadInfo) bool {
|
entries := slices.CompactFunc(slices.Compact(m.Entries), func(a common.DownloadInfo, b common.DownloadInfo) bool {
|
||||||
return a.URL == b.URL
|
return a.URL == b.URL
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
|
// Pool implements heap.Interface interface as a standard priority queue
|
||||||
type Pool []*Worker
|
type Pool []*Worker
|
||||||
|
|
||||||
func (h Pool) Len() int { return len(h) }
|
func (h Pool) Len() int { return len(h) }
|
||||||
func (h Pool) Less(i, j int) bool { return h[i].index < h[j].index }
|
func (h Pool) Less(i, j int) bool { return h[i].pending < h[j].pending }
|
||||||
func (h Pool) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
|
||||||
func (h *Pool) Push(x any) { *h = append(*h, x.(*Worker)) }
|
func (h Pool) Swap(i, j int) {
|
||||||
|
h[i], h[j] = h[j], h[i]
|
||||||
|
h[i].index = i
|
||||||
|
h[j].index = j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Pool) Push(x any) { *h = append(*h, x.(*Worker)) }
|
||||||
|
|
||||||
func (h *Pool) Pop() any {
|
func (h *Pool) Pop() any {
|
||||||
old := *h
|
old := *h
|
||||||
n := len(old)
|
n := len(old)
|
||||||
x := old[n-1]
|
x := old[n-1]
|
||||||
|
old[n-1] = nil
|
||||||
*h = old[0 : n-1]
|
*h = old[0 : n-1]
|
||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
type Worker struct {
|
type Worker struct {
|
||||||
requests chan Process // downloads to do
|
requests chan *Process // downloads to do
|
||||||
pending int // downloads pending
|
pending int // downloads pending
|
||||||
index int // index in the heap
|
index int // index in the heap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Worker) Work(done chan *Worker) {
|
func (w *Worker) Work(done chan *Worker) {
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ type Metadata struct {
|
|||||||
PlaylistTitle string `json:"title"`
|
PlaylistTitle string `json:"title"`
|
||||||
Type string `json:"_type"`
|
Type string `json:"_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Metadata) IsPlaylist() bool { return m.Type == "playlist" }
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ func (s *Service) KillAll(args NoArgs, killed *string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("succesfully killed process", slog.String("id", proc.Id))
|
slog.Info("succesfully killed process", slog.String("id", proc.Id))
|
||||||
|
proc = nil // gc helper
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -195,6 +196,35 @@ func (s *Service) Clear(args string, killed *string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Removes completed processes
|
||||||
|
func (s *Service) ClearCompleted(cleared *string) error {
|
||||||
|
var (
|
||||||
|
keys = s.db.Keys()
|
||||||
|
removeFunc = func(p *internal.Process) error {
|
||||||
|
defer s.db.Delete(p.Id)
|
||||||
|
|
||||||
|
if p.Progress.Status != internal.StatusCompleted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Kill()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, key := range *keys {
|
||||||
|
proc, err := s.db.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := removeFunc(proc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// FreeSpace gets the available from package sys util
|
// FreeSpace gets the available from package sys util
|
||||||
func (s *Service) FreeSpace(args NoArgs, free *uint64) error {
|
func (s *Service) FreeSpace(args NoArgs, free *uint64) error {
|
||||||
freeSpace, err := sys.FreeSpace()
|
freeSpace, err := sys.FreeSpace()
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive"
|
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive"
|
||||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archiver"
|
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archiver"
|
||||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
|
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
|
||||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/configurator"
|
|
||||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/dbutil"
|
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/dbutil"
|
||||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/filebrowser"
|
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/filebrowser"
|
||||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
|
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
|
||||||
@@ -236,9 +235,6 @@ func newServer(c serverConfig) *http.Server {
|
|||||||
// Subscriptions
|
// Subscriptions
|
||||||
r.Route("/subscriptions", subscription.Container(c.db, cronTaskRunner).ApplyRouter())
|
r.Route("/subscriptions", subscription.Container(c.db, cronTaskRunner).ApplyRouter())
|
||||||
|
|
||||||
// Frontend config store
|
|
||||||
r.Route("/webconfig", configurator.ApplyRouter())
|
|
||||||
|
|
||||||
return &http.Server{Handler: r}
|
return &http.Server{Handler: r}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ func (h *RestHandler) Delete() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user