Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eb2831bc6 | ||
| 9361d9ce29 | |||
| d100092f35 | |||
| d64303ccfa | |||
| 6688bc3977 | |||
| 600475f603 | |||
| da4aaeac84 | |||
|
|
2d75030cbc | ||
|
|
38bc66cd03 | ||
| 3efbb9d464 | |||
| fa4ba8a211 | |||
|
|
75ec95041d | ||
| 1b4c8c751b | |||
|
|
4710db25ee |
@@ -1,15 +1,17 @@
|
||||
node_modules
|
||||
downloads
|
||||
dist
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
.pnpm-debug.log
|
||||
.parcel-cache
|
||||
.git
|
||||
src/server/core/*.exe
|
||||
src/server/core/yt-dlp
|
||||
node_modules
|
||||
.env
|
||||
*.mp4
|
||||
*.ytdl
|
||||
*.part
|
||||
*.db
|
||||
build/
|
||||
downloads
|
||||
.DS_Store
|
||||
build/
|
||||
yt-dlp-webui
|
||||
session.dat
|
||||
config.yml
|
||||
cookies.txt
|
||||
|
||||
6
.github/workflows/docker-publish.yml
vendored
6
.github/workflows/docker-publish.yml
vendored
@@ -58,6 +58,8 @@ jobs:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/yt-dlp-webui
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
@@ -67,8 +69,8 @@ jobs:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/yt-dlp-webui:latest
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels}}
|
||||
|
||||
- name: Sign the published Docker image
|
||||
env:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ build/
|
||||
yt-dlp-webui
|
||||
session.dat
|
||||
config.yml
|
||||
cookies.txt
|
||||
|
||||
7
Makefile
7
Makefile
@@ -6,11 +6,10 @@ all:
|
||||
CGO_ENABLED=0 go build -o yt-dlp-webui main.go
|
||||
|
||||
multiarch:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o yt-dlp-webui_linux-arm main.go
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o yt-dlp-webui_linux-arm64 main.go
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o yt-dlp-webui_linux-amd64 main.go
|
||||
mkdir -p build
|
||||
mv yt-dlp-webui* build
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o build/yt-dlp-webui_linux-arm main.go
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o build/yt-dlp-webui_linux-arm64 main.go
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/yt-dlp-webui_linux-amd64 main.go
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
@@ -14,25 +14,23 @@
|
||||
"@fontsource/roboto": "^5.0.6",
|
||||
"@mui/icons-material": "^5.11.16",
|
||||
"@mui/material": "^5.13.5",
|
||||
"fp-ts": "^2.16.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-router-dom": "^6.13.0",
|
||||
"react-router-dom": "^6.17.0",
|
||||
"recoil": "^0.7.7",
|
||||
"rxjs": "^7.8.1",
|
||||
"uuid": "^9.0.0"
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/react": "^18.2.13",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/node": "^20.8.7",
|
||||
"@types/react": "^18.2.29",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"@types/react-helmet": "^6.1.8",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"buffer": "^6.0.3",
|
||||
"typescript": "^5.1.3",
|
||||
"vite": "^4.4.7"
|
||||
"@vitejs/plugin-react-swc": "^3.4.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0"
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@ languages:
|
||||
statusReady: Prêt
|
||||
selectFormatButton: Sélectionner le format
|
||||
startButton: Démarrer
|
||||
abortAllButton: Abort All
|
||||
updateBinButton: Mettre à jour le binaire yt-dlp
|
||||
abortAllButton: Tout arrêter
|
||||
updateBinButton: Mettre à jour l'exécutable yt-dlp
|
||||
darkThemeButton: Thème sombre
|
||||
lightThemeButton: Thème clair
|
||||
settingsAnchor: Paramètres
|
||||
@@ -51,15 +51,15 @@ languages:
|
||||
serverPortTitle: Port
|
||||
extractAudioCheckbox: Extraire l'audio
|
||||
noMTimeCheckbox: Ne pas définir le temps de modification du fichier
|
||||
bgReminder: Une fois que vous aurez fermé cette page, le téléchargement se poursuivra en arrière-plan.
|
||||
bgReminder: Une fois que vous aurez fermé cette page, le téléchargement continuera en arrière-plan.
|
||||
toastConnected: 'Connecté à '
|
||||
toastUpdated: Mise à jour du binaire yt-dlp !
|
||||
formatSelectionEnabler: Active la sélection des formats vidéo/audio
|
||||
themeSelect: 'Theme'
|
||||
toastUpdated: L'exécutable yt-dlp a été mis à jour !
|
||||
formatSelectionEnabler: Activer la sélection des formats vidéo/audio
|
||||
themeSelect: 'Thème'
|
||||
languageSelect: 'Langue'
|
||||
overridesAnchor: Surcharges
|
||||
pathOverrideOption: Activation de la surcharge du chemin de sortie
|
||||
filenameOverrideOption: Active la surcharge du nom du fichier de sortie
|
||||
overridesAnchor: Remplacer
|
||||
pathOverrideOption: Activer le remplacement du chemin de sortie
|
||||
filenameOverrideOption: Activer le remplacement du nom du fichier de sortie
|
||||
customFilename: Nom de fichier personnalisé (laisser vide pour utiliser le nom par défaut)
|
||||
customPath: Chemin personnalisé
|
||||
customArgs: Activer les args personnalisés yt-dlp (grand pouvoir = grandes responsabilités)
|
||||
@@ -71,7 +71,7 @@ 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)
|
||||
restartAppMessage: Nécessite un rechargement de la page pour prendre effet
|
||||
servedFromReverseProxyCheckbox: Est derrière un sous-dossier de proxy inverse
|
||||
appTitle: App title
|
||||
appTitle: Nom de l'application
|
||||
italian:
|
||||
urlInput: URL di YouTube o di qualsiasi altro servizio supportato
|
||||
statusTitle: Stato
|
||||
@@ -139,9 +139,10 @@ languages:
|
||||
splashText: 没有正在进行的下载
|
||||
archiveTitle: 归档
|
||||
clipboardAction: 复制 URL 到剪贴板
|
||||
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
|
||||
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
|
||||
appTitle: App title
|
||||
playlistCheckbox: 下载播放列表(可能需要一段时间,提交后可以关闭页面等待)
|
||||
restartAppMessage: 需要刷新页面才能生效
|
||||
servedFromReverseProxyCheckbox: 处于反向代理的子目录后
|
||||
appTitle: App 标题
|
||||
spanish:
|
||||
urlInput: URL de YouTube u otro servicio compatible
|
||||
statusTitle: Estado
|
||||
|
||||
@@ -172,6 +172,15 @@ export const rpcHTTPEndpoint = selector({
|
||||
}
|
||||
})
|
||||
|
||||
export const cookiesState = atom({
|
||||
key: 'cookiesState',
|
||||
default: localStorage.getItem('yt-dlp-cookies') ?? '',
|
||||
effects: [
|
||||
({ onSet }) =>
|
||||
onSet(c => localStorage.setItem('yt-dlp-cookies', c))
|
||||
]
|
||||
})
|
||||
|
||||
export const themeSelector = selector<ThemeNarrowed>({
|
||||
key: 'themeSelector',
|
||||
get: ({ get }) => {
|
||||
|
||||
@@ -2,5 +2,5 @@ import { atom } from 'recoil'
|
||||
|
||||
export const loadingAtom = atom({
|
||||
key: 'loadingAtom',
|
||||
default: false
|
||||
default: true
|
||||
})
|
||||
161
frontend/src/components/CookiesTextField.tsx
Normal file
161
frontend/src/components/CookiesTextField.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { TextField } from '@mui/material'
|
||||
import * as A from 'fp-ts/Array'
|
||||
import * as E from 'fp-ts/Either'
|
||||
import * as O from 'fp-ts/Option'
|
||||
import { pipe } from 'fp-ts/lib/function'
|
||||
import { useMemo } from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { Subject, debounceTime, distinctUntilChanged } from 'rxjs'
|
||||
import { downloadTemplateState } from '../atoms/downloadTemplate'
|
||||
import { cookiesState, serverURL } from '../atoms/settings'
|
||||
import { useSubscription } from '../hooks/observable'
|
||||
import { useToast } from '../hooks/toast'
|
||||
import { ffetch } from '../lib/httpClient'
|
||||
|
||||
const validateCookie = (cookie: string) => pipe(
|
||||
cookie,
|
||||
cookie => cookie.replace(/\s\s+/g, ' '),
|
||||
cookie => cookie.replaceAll('\t', ' '),
|
||||
cookie => cookie.split(' '),
|
||||
E.of,
|
||||
E.chain(
|
||||
E.fromPredicate(
|
||||
f => f.length === 7,
|
||||
() => `missing parts`
|
||||
)
|
||||
),
|
||||
E.chain(
|
||||
E.fromPredicate(
|
||||
f => f[0].length > 0,
|
||||
() => 'missing domain'
|
||||
)
|
||||
),
|
||||
E.chain(
|
||||
E.fromPredicate(
|
||||
f => f[1] === 'TRUE' || f[1] === 'FALSE',
|
||||
() => `invalid include subdomains`
|
||||
)
|
||||
),
|
||||
E.chain(
|
||||
E.fromPredicate(
|
||||
f => f[2].length > 0,
|
||||
() => 'invalid path'
|
||||
)
|
||||
),
|
||||
E.chain(
|
||||
E.fromPredicate(
|
||||
f => f[3] === 'TRUE' || f[3] === 'FALSE',
|
||||
() => 'invalid secure flag'
|
||||
)
|
||||
),
|
||||
E.chain(
|
||||
E.fromPredicate(
|
||||
f => isFinite(Number(f[4])),
|
||||
() => 'invalid expiration'
|
||||
)
|
||||
),
|
||||
E.chain(
|
||||
E.fromPredicate(
|
||||
f => f[5].length > 0,
|
||||
() => 'invalid name'
|
||||
)
|
||||
),
|
||||
E.chain(
|
||||
E.fromPredicate(
|
||||
f => f[6].length > 0,
|
||||
() => 'invalid value'
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
const CookiesTextField: React.FC = () => {
|
||||
const serverAddr = useRecoilValue(serverURL)
|
||||
const [customArgs, setCustomArgs] = useRecoilState(downloadTemplateState)
|
||||
const [savedCookies, setSavedCookies] = useRecoilState(cookiesState)
|
||||
|
||||
const { pushMessage } = useToast()
|
||||
const flag = '--cookies=cookies.txt'
|
||||
|
||||
const cookies$ = useMemo(() => new Subject<string>(), [])
|
||||
|
||||
const submitCookies = (cookies: string) =>
|
||||
ffetch(`${serverAddr}/api/v1/cookies`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
cookies
|
||||
})
|
||||
})()
|
||||
|
||||
const validateNetscapeCookies = (cookies: string) => pipe(
|
||||
cookies,
|
||||
cookies => cookies.split('\n'),
|
||||
cookies => cookies.filter(f => !f.startsWith('\n')), // empty lines
|
||||
cookies => cookies.filter(f => !f.startsWith('# ')), // comments
|
||||
cookies => cookies.filter(Boolean), // empty lines
|
||||
A.map(validateCookie),
|
||||
A.mapWithIndex((i, either) => pipe(
|
||||
either,
|
||||
E.matchW(
|
||||
(l) => pushMessage(`Error in line ${i + 1}: ${l}`, 'warning'),
|
||||
() => E.isRight(either)
|
||||
),
|
||||
)),
|
||||
A.filter(Boolean),
|
||||
A.match(
|
||||
() => false,
|
||||
(c) => {
|
||||
pushMessage(`Valid ${c.length} Netscape cookies`, 'info')
|
||||
return true
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
useSubscription(
|
||||
cookies$.pipe(
|
||||
debounceTime(650),
|
||||
distinctUntilChanged()
|
||||
),
|
||||
(cookies) => pipe(
|
||||
cookies,
|
||||
cookies => {
|
||||
setSavedCookies(cookies)
|
||||
return cookies
|
||||
},
|
||||
validateNetscapeCookies,
|
||||
O.fromPredicate(f => f === true),
|
||||
O.match(
|
||||
() => {
|
||||
if (customArgs.includes(flag)) {
|
||||
setCustomArgs(a => a.replace(flag, ''))
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
pipe(
|
||||
await submitCookies(cookies),
|
||||
E.match(
|
||||
(l) => pushMessage(`${l}`, 'error'),
|
||||
() => pushMessage(`Saved Netscape cookies`, 'success')
|
||||
)
|
||||
)
|
||||
if (!customArgs.includes(flag)) {
|
||||
setCustomArgs(a => `${a} ${flag}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="Netscape Cookies"
|
||||
multiline
|
||||
maxRows={20}
|
||||
minRows={4}
|
||||
fullWidth
|
||||
defaultValue={savedCookies}
|
||||
onChange={(e) => cookies$.next(e.currentTarget.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CookiesTextField
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
TextField,
|
||||
styled
|
||||
TextField
|
||||
} from '@mui/material'
|
||||
import AppBar from '@mui/material/AppBar'
|
||||
import Dialog from '@mui/material/Dialog'
|
||||
@@ -23,7 +22,6 @@ import Slide from '@mui/material/Slide'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { TransitionProps } from '@mui/material/transitions'
|
||||
import { Buffer } from 'buffer'
|
||||
import {
|
||||
forwardRef,
|
||||
useMemo,
|
||||
@@ -31,7 +29,8 @@ import {
|
||||
useState,
|
||||
useTransition
|
||||
} from 'react'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { downloadTemplateState } from '../atoms/downloadTemplate'
|
||||
import { settingsState } from '../atoms/settings'
|
||||
import { availableDownloadPathsState, connectedState } from '../atoms/status'
|
||||
import FormatsGrid from '../components/FormatsGrid'
|
||||
@@ -53,7 +52,7 @@ const Transition = forwardRef(function Transition(
|
||||
type Props = {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onDownloadStart: () => void
|
||||
onDownloadStart: (url: string) => void
|
||||
}
|
||||
|
||||
export default function DownloadDialog({
|
||||
@@ -72,7 +71,7 @@ export default function DownloadDialog({
|
||||
const [pickedAudioFormat, setPickedAudioFormat] = useState('')
|
||||
const [pickedBestFormat, setPickedBestFormat] = useState('')
|
||||
|
||||
const [customArgs, setCustomArgs] = useState('')
|
||||
const [customArgs, setCustomArgs] = useRecoilState(downloadTemplateState)
|
||||
const [downloadPath, setDownloadPath] = useState(0)
|
||||
|
||||
const [fileNameOverride, setFilenameOverride] = useState('')
|
||||
@@ -121,7 +120,7 @@ export default function DownloadDialog({
|
||||
setTimeout(() => {
|
||||
resetInput()
|
||||
setDownloadFormats(undefined)
|
||||
onDownloadStart()
|
||||
onDownloadStart(url)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
@@ -168,19 +167,18 @@ export default function DownloadDialog({
|
||||
localStorage.setItem("last-input-args", e.target.value)
|
||||
}
|
||||
|
||||
const parseUrlListFile = (event: any) => {
|
||||
const urlList = event.target.files
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('load', $event => {
|
||||
const base64 = $event.target?.result!.toString().split(',')[1]
|
||||
Buffer.from(base64!, 'base64')
|
||||
.toString()
|
||||
.trimEnd()
|
||||
.split('\n')
|
||||
.filter(_url => isValidURL(_url))
|
||||
.forEach(_url => sendUrl(_url))
|
||||
})
|
||||
reader.readAsDataURL(urlList[0])
|
||||
const parseUrlListFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.currentTarget.files
|
||||
if (!files || files.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const file = await files[0].text()
|
||||
|
||||
file
|
||||
.split('\n')
|
||||
.filter(u => isValidURL(u))
|
||||
.forEach(u => sendUrl(u))
|
||||
}
|
||||
|
||||
const resetInput = () => {
|
||||
@@ -190,12 +188,6 @@ export default function DownloadDialog({
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- styled components -------------------- */
|
||||
|
||||
const Input = styled('input')({
|
||||
display: 'none',
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
@@ -248,11 +240,12 @@ export default function DownloadDialog({
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<label htmlFor="icon-button-file">
|
||||
<Input
|
||||
<input
|
||||
hidden
|
||||
id="icon-button-file"
|
||||
type="file"
|
||||
accept=".txt"
|
||||
onChange={parseUrlListFile}
|
||||
onChange={e => parseUrlListFile(e)}
|
||||
/>
|
||||
<IconButton
|
||||
color="primary"
|
||||
|
||||
@@ -10,13 +10,13 @@ const Downloads: React.FC = () => {
|
||||
const listView = useRecoilValue(listViewState)
|
||||
const active = useRecoilValue(activeDownloadsState)
|
||||
|
||||
const [, setIsLoading] = useRecoilState(loadingAtom)
|
||||
const [isLoading, setIsLoading] = useRecoilState(loadingAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
setIsLoading(true)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [active?.length])
|
||||
}, [active?.length, isLoading])
|
||||
|
||||
if (listView) {
|
||||
return (
|
||||
|
||||
@@ -3,11 +3,14 @@ import { useRecoilState } from 'recoil'
|
||||
import { loadingAtom } from '../atoms/ui'
|
||||
import DownloadDialog from './DownloadDialog'
|
||||
import HomeSpeedDial from './HomeSpeedDial'
|
||||
import { useToast } from '../hooks/toast'
|
||||
|
||||
const HomeActions: React.FC = () => {
|
||||
const [, setIsLoading] = useRecoilState(loadingAtom)
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
return (
|
||||
<>
|
||||
<HomeSpeedDial
|
||||
@@ -19,7 +22,8 @@ const HomeActions: React.FC = () => {
|
||||
setOpenDialog(false)
|
||||
setIsLoading(true)
|
||||
}}
|
||||
onDownloadStart={() => {
|
||||
onDownloadStart={(url) => {
|
||||
pushMessage(`Requested ${url}`, 'info')
|
||||
setOpenDialog(false)
|
||||
setIsLoading(true)
|
||||
}}
|
||||
|
||||
@@ -8,7 +8,7 @@ const LoadingBackdrop: React.FC = () => {
|
||||
return (
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={!isLoading}
|
||||
open={isLoading}
|
||||
>
|
||||
<CircularProgress color="primary" />
|
||||
</Backdrop>
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
export async function ffetch<T>(
|
||||
url: string,
|
||||
onSuccess: (res: T) => void,
|
||||
onError: (err: string) => void,
|
||||
opt?: RequestInit,
|
||||
) {
|
||||
import { tryCatch } from 'fp-ts/TaskEither'
|
||||
import { flow } from 'fp-ts/lib/function'
|
||||
|
||||
export const ffetch = <T>(url: string, opt?: RequestInit) => flow(
|
||||
tryCatch(
|
||||
() => fetcher<T>(url, opt),
|
||||
(e) => `error while fetching: ${e}`
|
||||
)
|
||||
)
|
||||
|
||||
const fetcher = async <T>(url: string, opt?: RequestInit) => {
|
||||
const res = await fetch(url, opt)
|
||||
if (!res.ok) {
|
||||
onError(await res.text())
|
||||
return
|
||||
|
||||
if (opt && !opt.headers) {
|
||||
opt.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
onSuccess(await res.json() as T)
|
||||
|
||||
if (!res.ok) {
|
||||
throw await res.text()
|
||||
}
|
||||
return res.json() as T
|
||||
}
|
||||
@@ -10,11 +10,11 @@ const Toaster: React.FC = () => {
|
||||
if (toasts.length > 0) {
|
||||
const closer = setInterval(() => {
|
||||
setToasts(t => t.map(t => ({ ...t, open: false })))
|
||||
}, 1500)
|
||||
}, 2000)
|
||||
|
||||
const cleaner = setInterval(() => {
|
||||
setToasts(t => t.filter((x) => (Date.now() - x.createdAt) < 1500))
|
||||
}, 1750)
|
||||
setToasts(t => t.filter((x) => (Date.now() - x.createdAt) < 2000))
|
||||
}, 2250)
|
||||
|
||||
return () => {
|
||||
clearInterval(closer)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { pipe } from 'fp-ts/lib/function'
|
||||
import type { RPCResponse } from "./types"
|
||||
|
||||
/**
|
||||
@@ -10,15 +11,6 @@ export function validateIP(ipAddr: string): boolean {
|
||||
return ipRegex.test(ipAddr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a domain via regex.
|
||||
* The validation pass if the domain respects the following formats:
|
||||
* - localhost
|
||||
* - domain.tld
|
||||
* - dir.domain.tld
|
||||
* @param domainName
|
||||
* @returns domain validity test
|
||||
*/
|
||||
export function validateDomain(url: string): boolean {
|
||||
const urlRegex = /(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
|
||||
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
||||
@@ -28,17 +20,6 @@ export function validateDomain(url: string): boolean {
|
||||
return urlRegex.test(url) || name === 'localhost' && slugRegex.test(slug)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a domain via regex.
|
||||
* Exapmples
|
||||
* - http://example.com
|
||||
* - https://example.com
|
||||
* - http://www.example.com
|
||||
* - https://www.example.com
|
||||
* - http://10.0.0.1/[something]/[something-else]
|
||||
* @param url
|
||||
* @returns url validity test
|
||||
*/
|
||||
export function isValidURL(url: string): boolean {
|
||||
let urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
|
||||
return urlRegex.test(url)
|
||||
@@ -93,4 +74,11 @@ export function mapProcessStatus(status: number) {
|
||||
}
|
||||
|
||||
export const prefersDarkMode = () =>
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
export const base64URLEncode = (s: string) => pipe(
|
||||
s,
|
||||
s => String.fromCodePoint(...new TextEncoder().encode(s)),
|
||||
btoa,
|
||||
encodeURIComponent
|
||||
)
|
||||
@@ -26,23 +26,26 @@ import FolderIcon from '@mui/icons-material/Folder'
|
||||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
|
||||
import VideoFileIcon from '@mui/icons-material/VideoFile'
|
||||
|
||||
import { Buffer } from 'buffer'
|
||||
import { matchW } from 'fp-ts/lib/TaskEither'
|
||||
import { pipe } from 'fp-ts/lib/function'
|
||||
import { useEffect, useMemo, useState, useTransition } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
|
||||
import { serverURL } from '../atoms/settings'
|
||||
import { useObservable } from '../hooks/observable'
|
||||
import { useToast } from '../hooks/toast'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { ffetch } from '../lib/httpClient'
|
||||
import { DeleteRequest, DirectoryEntry } from '../types'
|
||||
import { roundMiB } from '../utils'
|
||||
import { base64URLEncode, roundMiB } from '../utils'
|
||||
|
||||
export default function Downloaded() {
|
||||
const serverAddr = useRecoilValue(serverURL)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { i18n } = useI18n()
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
|
||||
@@ -51,20 +54,24 @@ export default function Downloaded() {
|
||||
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const fetcher = () => ffetch<DirectoryEntry[]>(
|
||||
`${serverAddr}/archive/downloaded`,
|
||||
(d) => files$.next(d),
|
||||
() => navigate('/login'),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
const fetcher = () => pipe(
|
||||
ffetch<DirectoryEntry[]>(
|
||||
`${serverAddr}/archive/downloaded`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
subdir: '',
|
||||
})
|
||||
}
|
||||
),
|
||||
matchW(
|
||||
(e) => {
|
||||
pushMessage(e)
|
||||
navigate('/login')
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subdir: '',
|
||||
})
|
||||
}
|
||||
)
|
||||
(d) => files$.next(d),
|
||||
)
|
||||
)()
|
||||
|
||||
const fetcherSubfolder = (sub: string) => {
|
||||
const folders = sub.startsWith('/')
|
||||
@@ -138,7 +145,9 @@ export default function Downloaded() {
|
||||
}, [serverAddr])
|
||||
|
||||
const onFileClick = (path: string) => startTransition(() => {
|
||||
window.open(`${serverAddr}/archive/d/${Buffer.from(path).toString('hex')}`)
|
||||
const encoded = base64URLEncode(path)
|
||||
|
||||
window.open(`${serverAddr}/archive/d/${encoded}`)
|
||||
})
|
||||
|
||||
const onFolderClick = (path: string) => startTransition(() => {
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
serverPortState,
|
||||
themeState
|
||||
} from '../atoms/settings'
|
||||
import CookiesTextField from '../components/CookiesTextField'
|
||||
import { useToast } from '../hooks/toast'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
@@ -298,6 +299,12 @@ export default function Settings() {
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid sx={{ mr: 1, mt: 3 }}>
|
||||
<Typography variant="h6" color="primary" sx={{ mb: 2 }}>
|
||||
Cookies
|
||||
</Typography>
|
||||
<CookiesTextField />
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Stack direction="row">
|
||||
<Button
|
||||
|
||||
3
go.mod
3
go.mod
@@ -4,11 +4,12 @@ go 1.20
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa
|
||||
golang.org/x/sys v0.12.0
|
||||
golang.org/x/sys v0.13.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
8
go.sum
8
go.sum
@@ -1,21 +1,21 @@
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/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=
|
||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
13
main.go
13
main.go
@@ -14,11 +14,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
port int
|
||||
queueSize int
|
||||
configFile string
|
||||
downloadPath string
|
||||
downloaderPath string
|
||||
port int
|
||||
queueSize int
|
||||
configFile string
|
||||
downloadPath string
|
||||
downloaderPath string
|
||||
sessionFilePath string
|
||||
|
||||
requireAuth bool
|
||||
username string
|
||||
@@ -40,6 +41,7 @@ func init() {
|
||||
flag.StringVar(&configFile, "conf", "./config.yml", "Config file path")
|
||||
flag.StringVar(&downloadPath, "out", ".", "Where files will be saved")
|
||||
flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path")
|
||||
flag.StringVar(&sessionFilePath, "session", ".", "session file path")
|
||||
|
||||
flag.BoolVar(&requireAuth, "auth", false, "Enable RPC authentication")
|
||||
flag.StringVar(&username, "user", userFromEnv, "Username required for auth")
|
||||
@@ -61,6 +63,7 @@ func main() {
|
||||
c.QueueSize(queueSize)
|
||||
c.DownloadPath(downloadPath)
|
||||
c.DownloaderPath(downloaderPath)
|
||||
c.SessionFilePath(sessionFilePath)
|
||||
|
||||
c.RequireAuth(requireAuth)
|
||||
c.Username(username)
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
var lock sync.Mutex
|
||||
|
||||
type serverConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
DownloadPath string `yaml:"downloadPath"`
|
||||
DownloaderPath string `yaml:"downloaderPath"`
|
||||
RequireAuth bool `yaml:"require_auth"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
QueueSize int `yaml:"queue_size"`
|
||||
Port int `yaml:"port"`
|
||||
DownloadPath string `yaml:"downloadPath"`
|
||||
DownloaderPath string `yaml:"downloaderPath"`
|
||||
RequireAuth bool `yaml:"require_auth"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
QueueSize int `yaml:"queue_size"`
|
||||
SessionFilePath string `yaml:"session_file_path"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
@@ -68,6 +69,10 @@ func (c *config) QueueSize(size int) {
|
||||
c.cfg.QueueSize = size
|
||||
}
|
||||
|
||||
func (c *config) SessionFilePath(path string) {
|
||||
c.cfg.SessionFilePath = path
|
||||
}
|
||||
|
||||
var instance *config
|
||||
|
||||
func Instance() *config {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -129,7 +130,13 @@ func SendFile(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
decoded, err := hex.DecodeString(path)
|
||||
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
|
||||
@@ -141,10 +148,10 @@ func SendFile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// TODO: further path / file validations
|
||||
if strings.Contains(filepath.Dir(decodedStr), root) {
|
||||
// ctx.Response().Header.Set(
|
||||
// "Content-Disposition",
|
||||
// "inline; filename="+filepath.Base(decodedStr),
|
||||
// )
|
||||
w.Header().Add(
|
||||
"Content-Disposition",
|
||||
"inline; filename="+filepath.Base(decodedStr),
|
||||
)
|
||||
|
||||
http.ServeFile(w, r, decodedStr)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,12 @@ func Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := config.Instance().GetConfig()
|
||||
var (
|
||||
username = config.Instance().GetConfig().Username
|
||||
password = config.Instance().GetConfig().Password
|
||||
)
|
||||
|
||||
if cfg.Username != req.Username || cfg.Password != req.Password {
|
||||
if username != req.Username || password != req.Password {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,3 +71,8 @@ type DownloadRequest struct {
|
||||
Rename string `json:"rename"`
|
||||
Params []string `json:"params"`
|
||||
}
|
||||
|
||||
// struct representing request of creating a netscape cookies file
|
||||
type SetCookiesRequest struct {
|
||||
Cookies string `json:"cookies"`
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
||||
)
|
||||
|
||||
// In-Memory Thread-Safe Key-Value Storage with optional persistence
|
||||
@@ -93,7 +95,12 @@ func (m *MemoryDB) All() *[]ProcessResponse {
|
||||
func (m *MemoryDB) Persist() {
|
||||
running := m.All()
|
||||
|
||||
fd, err := os.Create("session.dat")
|
||||
sessionFile := filepath.Join(
|
||||
config.Instance().GetConfig().SessionFilePath,
|
||||
"session.dat",
|
||||
)
|
||||
|
||||
fd, err := os.Create(sessionFile)
|
||||
if err != nil {
|
||||
log.Println(cli.Red, "Failed to persist session", cli.Reset)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
@@ -17,7 +18,10 @@ type metadata struct {
|
||||
}
|
||||
|
||||
func PlaylistDetect(req DownloadRequest, mq *MessageQueue, db *MemoryDB) error {
|
||||
cmd := exec.Command(cfg.GetConfig().DownloaderPath, req.URL, "-J")
|
||||
var (
|
||||
downloader = config.Instance().GetConfig().DownloaderPath
|
||||
cmd = exec.Command(downloader, req.URL, "-J")
|
||||
)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
@@ -50,7 +54,9 @@ func PlaylistDetect(req DownloadRequest, mq *MessageQueue, db *MemoryDB) error {
|
||||
cli.BgGreen, "Playlist detected", cli.Reset, m.Count, "entries",
|
||||
)
|
||||
|
||||
for _, meta := range m.Entries {
|
||||
for i, meta := range m.Entries {
|
||||
delta := time.Second.Microseconds() * int64(i+1)
|
||||
|
||||
proc := &Process{
|
||||
Url: meta.OriginalURL,
|
||||
Progress: DownloadProgress{},
|
||||
@@ -60,7 +66,7 @@ func PlaylistDetect(req DownloadRequest, mq *MessageQueue, db *MemoryDB) error {
|
||||
}
|
||||
|
||||
proc.Info.URL = meta.OriginalURL
|
||||
proc.Info.CreatedAt = time.Now().Add(time.Second)
|
||||
proc.Info.CreatedAt = time.Now().Add(time.Duration(delta))
|
||||
|
||||
db.Set(proc)
|
||||
proc.SetPending()
|
||||
|
||||
@@ -27,10 +27,6 @@ const template = `download:
|
||||
"speed":%(progress.speed)s
|
||||
}`
|
||||
|
||||
var (
|
||||
cfg = config.Instance()
|
||||
)
|
||||
|
||||
const (
|
||||
StatusPending = iota
|
||||
StatusDownloading
|
||||
@@ -75,7 +71,7 @@ func (p *Process) Start() {
|
||||
})
|
||||
|
||||
out := DownloadOutput{
|
||||
Path: cfg.GetConfig().DownloadPath,
|
||||
Path: config.Instance().GetConfig().DownloadPath,
|
||||
Filename: "%(title)s.%(ext)s",
|
||||
}
|
||||
|
||||
@@ -97,7 +93,7 @@ func (p *Process) Start() {
|
||||
}, p.Params...)
|
||||
|
||||
// ----------------- main block ----------------- //
|
||||
cmd := exec.Command(cfg.GetConfig().DownloaderPath, params...)
|
||||
cmd := exec.Command(config.Instance().GetConfig().DownloaderPath, params...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
r, err := cmd.StdoutPipe()
|
||||
@@ -192,7 +188,7 @@ func (p *Process) Kill() error {
|
||||
|
||||
// Returns the available format for this URL
|
||||
func (p *Process) GetFormatsSync() (DownloadFormats, error) {
|
||||
cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.Url, "-J")
|
||||
cmd := exec.Command(config.Instance().GetConfig().DownloaderPath, p.Url, "-J")
|
||||
stdout, err := cmd.Output()
|
||||
|
||||
if err != nil {
|
||||
@@ -245,7 +241,7 @@ func (p *Process) SetPending() {
|
||||
}
|
||||
|
||||
func (p *Process) SetMetadata() error {
|
||||
cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.Url, "-J")
|
||||
cmd := exec.Command(config.Instance().GetConfig().DownloaderPath, p.Url, "-J")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
|
||||
@@ -21,5 +21,6 @@ func ApplyRouter(db *internal.MemoryDB, mq *internal.MessageQueue) func(chi.Rout
|
||||
r.Use(middlewares.Authenticated)
|
||||
r.Post("/exec", h.Exec())
|
||||
r.Get("/running", h.Running())
|
||||
r.Post("/cookies", h.SetCookies())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func (h *Handler) Exec() http.HandlerFunc {
|
||||
|
||||
req := internal.DownloadRequest{}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err := json.NewDecoder(r.Body).DecodeContext(r.Context(), &req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func (h *Handler) Exec() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(id)
|
||||
err = json.NewEncoder(w).EncodeContext(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -48,7 +48,34 @@ func (h *Handler) Running() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(res)
|
||||
err = json.NewEncoder(w).EncodeContext(r.Context(), res)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) SetCookies() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
req := new(internal.SetCookiesRequest)
|
||||
|
||||
err := json.NewDecoder(r.Body).DecodeContext(r.Context(), req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.service.SetCookies(r.Context(), req.Cookies)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).EncodeContext(r.Context(), "ok")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package rest
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
||||
)
|
||||
@@ -36,3 +37,15 @@ func (s *Service) Running(ctx context.Context) (*[]internal.ProcessResponse, err
|
||||
return s.db.All(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SetCookies(ctx context.Context, cookies string) error {
|
||||
fd, err := os.Create("cookies.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer fd.Close()
|
||||
fd.WriteString(cookies)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/handlers"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
||||
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
|
||||
@@ -54,7 +55,7 @@ func newServer(c serverConfig) *http.Server {
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middlewares.CORS)
|
||||
r.Use(cors.AllowAll().Handler)
|
||||
r.Use(middleware.Logger)
|
||||
|
||||
app := http.FileServer(http.FS(c.frontend))
|
||||
|
||||
Reference in New Issue
Block a user