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
|
dist
|
||||||
package-lock.json
|
package-lock.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
.parcel-cache
|
node_modules
|
||||||
.git
|
|
||||||
src/server/core/*.exe
|
|
||||||
src/server/core/yt-dlp
|
|
||||||
.env
|
.env
|
||||||
*.mp4
|
*.mp4
|
||||||
*.ytdl
|
*.ytdl
|
||||||
|
*.part
|
||||||
*.db
|
*.db
|
||||||
|
downloads
|
||||||
|
.DS_Store
|
||||||
build/
|
build/
|
||||||
|
yt-dlp-webui
|
||||||
|
session.dat
|
||||||
|
config.yml
|
||||||
|
cookies.txt
|
||||||
|
|||||||
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -58,6 +58,8 @@ jobs:
|
|||||||
images: |
|
images: |
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/yt-dlp-webui
|
docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/yt-dlp-webui
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
@@ -67,7 +69,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/yt-dlp-webui:latest
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels}}
|
labels: ${{ steps.meta.outputs.labels}}
|
||||||
|
|
||||||
- name: Sign the published Docker image
|
- name: Sign the published Docker image
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ build/
|
|||||||
yt-dlp-webui
|
yt-dlp-webui
|
||||||
session.dat
|
session.dat
|
||||||
config.yml
|
config.yml
|
||||||
|
cookies.txt
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -6,11 +6,10 @@ all:
|
|||||||
CGO_ENABLED=0 go build -o yt-dlp-webui main.go
|
CGO_ENABLED=0 go build -o yt-dlp-webui main.go
|
||||||
|
|
||||||
multiarch:
|
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
|
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:
|
clean:
|
||||||
rm -rf build
|
rm -rf build
|
||||||
@@ -14,25 +14,23 @@
|
|||||||
"@fontsource/roboto": "^5.0.6",
|
"@fontsource/roboto": "^5.0.6",
|
||||||
"@mui/icons-material": "^5.11.16",
|
"@mui/icons-material": "^5.11.16",
|
||||||
"@mui/material": "^5.13.5",
|
"@mui/material": "^5.13.5",
|
||||||
|
"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-helmet": "^6.1.0",
|
||||||
"react-router-dom": "^6.13.0",
|
"react-router-dom": "^6.17.0",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1"
|
||||||
"uuid": "^9.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.8.7",
|
||||||
"@types/react": "^18.2.13",
|
"@types/react": "^18.2.29",
|
||||||
"@types/react-dom": "^18.2.6",
|
"@types/react-dom": "^18.2.14",
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@types/react-helmet": "^6.1.8",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/uuid": "^9.0.2",
|
"@vitejs/plugin-react-swc": "^3.4.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"typescript": "^5.2.2",
|
||||||
"buffer": "^6.0.3",
|
"vite": "^4.5.0"
|
||||||
"typescript": "^5.1.3",
|
|
||||||
"vite": "^4.4.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,8 +42,8 @@ languages:
|
|||||||
statusReady: Prêt
|
statusReady: Prêt
|
||||||
selectFormatButton: Sélectionner le format
|
selectFormatButton: Sélectionner le format
|
||||||
startButton: Démarrer
|
startButton: Démarrer
|
||||||
abortAllButton: Abort All
|
abortAllButton: Tout arrêter
|
||||||
updateBinButton: Mettre à jour le binaire yt-dlp
|
updateBinButton: Mettre à jour l'exécutable yt-dlp
|
||||||
darkThemeButton: Thème sombre
|
darkThemeButton: Thème sombre
|
||||||
lightThemeButton: Thème clair
|
lightThemeButton: Thème clair
|
||||||
settingsAnchor: Paramètres
|
settingsAnchor: Paramètres
|
||||||
@@ -51,15 +51,15 @@ languages:
|
|||||||
serverPortTitle: Port
|
serverPortTitle: Port
|
||||||
extractAudioCheckbox: Extraire l'audio
|
extractAudioCheckbox: Extraire l'audio
|
||||||
noMTimeCheckbox: Ne pas définir le temps de modification du fichier
|
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é à '
|
toastConnected: 'Connecté à '
|
||||||
toastUpdated: Mise à jour du binaire yt-dlp !
|
toastUpdated: L'exécutable yt-dlp a été mis à jour !
|
||||||
formatSelectionEnabler: Active la sélection des formats vidéo/audio
|
formatSelectionEnabler: Activer la sélection des formats vidéo/audio
|
||||||
themeSelect: 'Theme'
|
themeSelect: 'Thème'
|
||||||
languageSelect: 'Langue'
|
languageSelect: 'Langue'
|
||||||
overridesAnchor: Surcharges
|
overridesAnchor: Remplacer
|
||||||
pathOverrideOption: Activation de la surcharge du chemin de sortie
|
pathOverrideOption: Activer le remplacement du chemin de sortie
|
||||||
filenameOverrideOption: Active la surcharge du nom du fichier 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)
|
customFilename: Nom de fichier personnalisé (laisser vide pour utiliser le nom par défaut)
|
||||||
customPath: Chemin personnalisé
|
customPath: Chemin personnalisé
|
||||||
customArgs: Activer les args personnalisés yt-dlp (grand pouvoir = grandes responsabilités)
|
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)
|
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
|
||||||
appTitle: App title
|
appTitle: Nom de l'application
|
||||||
italian:
|
italian:
|
||||||
urlInput: URL di YouTube o di qualsiasi altro servizio supportato
|
urlInput: URL di YouTube o di qualsiasi altro servizio supportato
|
||||||
statusTitle: Stato
|
statusTitle: Stato
|
||||||
@@ -139,9 +139,10 @@ languages:
|
|||||||
splashText: 没有正在进行的下载
|
splashText: 没有正在进行的下载
|
||||||
archiveTitle: 归档
|
archiveTitle: 归档
|
||||||
clipboardAction: 复制 URL 到剪贴板
|
clipboardAction: 复制 URL 到剪贴板
|
||||||
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
|
playlistCheckbox: 下载播放列表(可能需要一段时间,提交后可以关闭页面等待)
|
||||||
servedFromReverseProxyCheckbox: Is behind a reverse proxy subfolder
|
restartAppMessage: 需要刷新页面才能生效
|
||||||
appTitle: App title
|
servedFromReverseProxyCheckbox: 处于反向代理的子目录后
|
||||||
|
appTitle: App 标题
|
||||||
spanish:
|
spanish:
|
||||||
urlInput: URL de YouTube u otro servicio compatible
|
urlInput: URL de YouTube u otro servicio compatible
|
||||||
statusTitle: Estado
|
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>({
|
export const themeSelector = selector<ThemeNarrowed>({
|
||||||
key: 'themeSelector',
|
key: 'themeSelector',
|
||||||
get: ({ get }) => {
|
get: ({ get }) => {
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { atom } from 'recoil'
|
|||||||
|
|
||||||
export const loadingAtom = atom({
|
export const loadingAtom = atom({
|
||||||
key: 'loadingAtom',
|
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,
|
MenuItem,
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
TextField,
|
TextField
|
||||||
styled
|
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import AppBar from '@mui/material/AppBar'
|
import AppBar from '@mui/material/AppBar'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from '@mui/material/Dialog'
|
||||||
@@ -23,7 +22,6 @@ import Slide from '@mui/material/Slide'
|
|||||||
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 { TransitionProps } from '@mui/material/transitions'
|
import { TransitionProps } from '@mui/material/transitions'
|
||||||
import { Buffer } from 'buffer'
|
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -31,7 +29,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
useTransition
|
useTransition
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useRecoilValue } from 'recoil'
|
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||||
|
import { downloadTemplateState } from '../atoms/downloadTemplate'
|
||||||
import { settingsState } from '../atoms/settings'
|
import { settingsState } from '../atoms/settings'
|
||||||
import { availableDownloadPathsState, connectedState } from '../atoms/status'
|
import { availableDownloadPathsState, connectedState } from '../atoms/status'
|
||||||
import FormatsGrid from '../components/FormatsGrid'
|
import FormatsGrid from '../components/FormatsGrid'
|
||||||
@@ -53,7 +52,7 @@ const Transition = forwardRef(function Transition(
|
|||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onDownloadStart: () => void
|
onDownloadStart: (url: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DownloadDialog({
|
export default function DownloadDialog({
|
||||||
@@ -72,7 +71,7 @@ export default function DownloadDialog({
|
|||||||
const [pickedAudioFormat, setPickedAudioFormat] = useState('')
|
const [pickedAudioFormat, setPickedAudioFormat] = useState('')
|
||||||
const [pickedBestFormat, setPickedBestFormat] = useState('')
|
const [pickedBestFormat, setPickedBestFormat] = useState('')
|
||||||
|
|
||||||
const [customArgs, setCustomArgs] = useState('')
|
const [customArgs, setCustomArgs] = useRecoilState(downloadTemplateState)
|
||||||
const [downloadPath, setDownloadPath] = useState(0)
|
const [downloadPath, setDownloadPath] = useState(0)
|
||||||
|
|
||||||
const [fileNameOverride, setFilenameOverride] = useState('')
|
const [fileNameOverride, setFilenameOverride] = useState('')
|
||||||
@@ -121,7 +120,7 @@ export default function DownloadDialog({
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resetInput()
|
resetInput()
|
||||||
setDownloadFormats(undefined)
|
setDownloadFormats(undefined)
|
||||||
onDownloadStart()
|
onDownloadStart(url)
|
||||||
}, 250)
|
}, 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,19 +167,18 @@ export default function DownloadDialog({
|
|||||||
localStorage.setItem("last-input-args", e.target.value)
|
localStorage.setItem("last-input-args", e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseUrlListFile = (event: any) => {
|
const parseUrlListFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const urlList = event.target.files
|
const files = e.currentTarget.files
|
||||||
const reader = new FileReader()
|
if (!files || files.length < 1) {
|
||||||
reader.addEventListener('load', $event => {
|
return
|
||||||
const base64 = $event.target?.result!.toString().split(',')[1]
|
}
|
||||||
Buffer.from(base64!, 'base64')
|
|
||||||
.toString()
|
const file = await files[0].text()
|
||||||
.trimEnd()
|
|
||||||
|
file
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(_url => isValidURL(_url))
|
.filter(u => isValidURL(u))
|
||||||
.forEach(_url => sendUrl(_url))
|
.forEach(u => sendUrl(u))
|
||||||
})
|
|
||||||
reader.readAsDataURL(urlList[0])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetInput = () => {
|
const resetInput = () => {
|
||||||
@@ -190,12 +188,6 @@ export default function DownloadDialog({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------- styled components -------------------- */
|
|
||||||
|
|
||||||
const Input = styled('input')({
|
|
||||||
display: 'none',
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -248,11 +240,12 @@ export default function DownloadDialog({
|
|||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<label htmlFor="icon-button-file">
|
<label htmlFor="icon-button-file">
|
||||||
<Input
|
<input
|
||||||
|
hidden
|
||||||
id="icon-button-file"
|
id="icon-button-file"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".txt"
|
accept=".txt"
|
||||||
onChange={parseUrlListFile}
|
onChange={e => parseUrlListFile(e)}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ const Downloads: React.FC = () => {
|
|||||||
const listView = useRecoilValue(listViewState)
|
const listView = useRecoilValue(listViewState)
|
||||||
const active = useRecoilValue(activeDownloadsState)
|
const active = useRecoilValue(activeDownloadsState)
|
||||||
|
|
||||||
const [, setIsLoading] = useRecoilState(loadingAtom)
|
const [isLoading, setIsLoading] = useRecoilState(loadingAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (active) {
|
if (active) {
|
||||||
setIsLoading(true)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [active?.length])
|
}, [active?.length, isLoading])
|
||||||
|
|
||||||
if (listView) {
|
if (listView) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { useRecoilState } from 'recoil'
|
|||||||
import { loadingAtom } from '../atoms/ui'
|
import { loadingAtom } from '../atoms/ui'
|
||||||
import DownloadDialog from './DownloadDialog'
|
import DownloadDialog from './DownloadDialog'
|
||||||
import HomeSpeedDial from './HomeSpeedDial'
|
import HomeSpeedDial from './HomeSpeedDial'
|
||||||
|
import { useToast } from '../hooks/toast'
|
||||||
|
|
||||||
const HomeActions: React.FC = () => {
|
const HomeActions: React.FC = () => {
|
||||||
const [, setIsLoading] = useRecoilState(loadingAtom)
|
const [, setIsLoading] = useRecoilState(loadingAtom)
|
||||||
const [openDialog, setOpenDialog] = useState(false)
|
const [openDialog, setOpenDialog] = useState(false)
|
||||||
|
|
||||||
|
const { pushMessage } = useToast()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HomeSpeedDial
|
<HomeSpeedDial
|
||||||
@@ -19,7 +22,8 @@ const HomeActions: React.FC = () => {
|
|||||||
setOpenDialog(false)
|
setOpenDialog(false)
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
}}
|
}}
|
||||||
onDownloadStart={() => {
|
onDownloadStart={(url) => {
|
||||||
|
pushMessage(`Requested ${url}`, 'info')
|
||||||
setOpenDialog(false)
|
setOpenDialog(false)
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const LoadingBackdrop: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Backdrop
|
<Backdrop
|
||||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
open={!isLoading}
|
open={isLoading}
|
||||||
>
|
>
|
||||||
<CircularProgress color="primary" />
|
<CircularProgress color="primary" />
|
||||||
</Backdrop>
|
</Backdrop>
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
export async function ffetch<T>(
|
import { tryCatch } from 'fp-ts/TaskEither'
|
||||||
url: string,
|
import { flow } from 'fp-ts/lib/function'
|
||||||
onSuccess: (res: T) => void,
|
|
||||||
onError: (err: string) => void,
|
export const ffetch = <T>(url: string, opt?: RequestInit) => flow(
|
||||||
opt?: RequestInit,
|
tryCatch(
|
||||||
) {
|
() => fetcher<T>(url, opt),
|
||||||
|
(e) => `error while fetching: ${e}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetcher = async <T>(url: string, opt?: RequestInit) => {
|
||||||
const res = await fetch(url, opt)
|
const res = await fetch(url, opt)
|
||||||
|
|
||||||
|
if (opt && !opt.headers) {
|
||||||
|
opt.headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
onError(await res.text())
|
throw await res.text()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
onSuccess(await res.json() as T)
|
return res.json() as T
|
||||||
}
|
}
|
||||||
@@ -10,11 +10,11 @@ const Toaster: React.FC = () => {
|
|||||||
if (toasts.length > 0) {
|
if (toasts.length > 0) {
|
||||||
const closer = setInterval(() => {
|
const closer = setInterval(() => {
|
||||||
setToasts(t => t.map(t => ({ ...t, open: false })))
|
setToasts(t => t.map(t => ({ ...t, open: false })))
|
||||||
}, 1500)
|
}, 2000)
|
||||||
|
|
||||||
const cleaner = setInterval(() => {
|
const cleaner = setInterval(() => {
|
||||||
setToasts(t => t.filter((x) => (Date.now() - x.createdAt) < 1500))
|
setToasts(t => t.filter((x) => (Date.now() - x.createdAt) < 2000))
|
||||||
}, 1750)
|
}, 2250)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(closer)
|
clearInterval(closer)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { pipe } from 'fp-ts/lib/function'
|
||||||
import type { RPCResponse } from "./types"
|
import type { RPCResponse } from "./types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,15 +11,6 @@ export function validateIP(ipAddr: string): boolean {
|
|||||||
return ipRegex.test(ipAddr)
|
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 {
|
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 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]+)*$/
|
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)
|
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 {
|
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()!@:%_\+.~#?&\/\/=]*)/
|
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)
|
return urlRegex.test(url)
|
||||||
@@ -94,3 +75,10 @@ export function mapProcessStatus(status: number) {
|
|||||||
|
|
||||||
export const prefersDarkMode = () =>
|
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 InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
|
||||||
import VideoFileIcon from '@mui/icons-material/VideoFile'
|
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 { useEffect, useMemo, useState, useTransition } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useRecoilValue } from 'recoil'
|
import { useRecoilValue } from 'recoil'
|
||||||
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
|
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
|
||||||
import { serverURL } from '../atoms/settings'
|
import { serverURL } from '../atoms/settings'
|
||||||
import { useObservable } from '../hooks/observable'
|
import { useObservable } from '../hooks/observable'
|
||||||
|
import { useToast } from '../hooks/toast'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
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 { roundMiB } from '../utils'
|
import { base64URLEncode, roundMiB } from '../utils'
|
||||||
|
|
||||||
export default function Downloaded() {
|
export default function Downloaded() {
|
||||||
const serverAddr = useRecoilValue(serverURL)
|
const serverAddr = useRecoilValue(serverURL)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { i18n } = useI18n()
|
const { i18n } = useI18n()
|
||||||
|
const { pushMessage } = useToast()
|
||||||
|
|
||||||
const [openDialog, setOpenDialog] = useState(false)
|
const [openDialog, setOpenDialog] = useState(false)
|
||||||
|
|
||||||
@@ -51,20 +54,24 @@ export default function Downloaded() {
|
|||||||
|
|
||||||
const [isPending, startTransition] = useTransition()
|
const [isPending, startTransition] = useTransition()
|
||||||
|
|
||||||
const fetcher = () => ffetch<DirectoryEntry[]>(
|
const fetcher = () => pipe(
|
||||||
|
ffetch<DirectoryEntry[]>(
|
||||||
`${serverAddr}/archive/downloaded`,
|
`${serverAddr}/archive/downloaded`,
|
||||||
(d) => files$.next(d),
|
|
||||||
() => navigate('/login'),
|
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
subdir: '',
|
subdir: '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
),
|
||||||
|
matchW(
|
||||||
|
(e) => {
|
||||||
|
pushMessage(e)
|
||||||
|
navigate('/login')
|
||||||
|
},
|
||||||
|
(d) => files$.next(d),
|
||||||
)
|
)
|
||||||
|
)()
|
||||||
|
|
||||||
const fetcherSubfolder = (sub: string) => {
|
const fetcherSubfolder = (sub: string) => {
|
||||||
const folders = sub.startsWith('/')
|
const folders = sub.startsWith('/')
|
||||||
@@ -138,7 +145,9 @@ export default function Downloaded() {
|
|||||||
}, [serverAddr])
|
}, [serverAddr])
|
||||||
|
|
||||||
const onFileClick = (path: string) => startTransition(() => {
|
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(() => {
|
const onFolderClick = (path: string) => startTransition(() => {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
serverPortState,
|
serverPortState,
|
||||||
themeState
|
themeState
|
||||||
} from '../atoms/settings'
|
} from '../atoms/settings'
|
||||||
|
import CookiesTextField from '../components/CookiesTextField'
|
||||||
import { useToast } from '../hooks/toast'
|
import { useToast } from '../hooks/toast'
|
||||||
import { useI18n } from '../hooks/useI18n'
|
import { useI18n } from '../hooks/useI18n'
|
||||||
import { useRPC } from '../hooks/useRPC'
|
import { useRPC } from '../hooks/useRPC'
|
||||||
@@ -298,6 +299,12 @@ export default function Settings() {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid sx={{ mr: 1, mt: 3 }}>
|
||||||
|
<Typography variant="h6" color="primary" sx={{ mb: 2 }}>
|
||||||
|
Cookies
|
||||||
|
</Typography>
|
||||||
|
<CookiesTextField />
|
||||||
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Stack direction="row">
|
<Stack direction="row">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -4,11 +4,12 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.0.10
|
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/goccy/go-json v0.10.2
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||||
github.com/google/uuid v1.3.1
|
github.com/google/uuid v1.3.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa
|
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
|
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 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
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 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
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 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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 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=
|
||||||
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 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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 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=
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -19,6 +19,7 @@ var (
|
|||||||
configFile string
|
configFile string
|
||||||
downloadPath string
|
downloadPath string
|
||||||
downloaderPath string
|
downloaderPath string
|
||||||
|
sessionFilePath string
|
||||||
|
|
||||||
requireAuth bool
|
requireAuth bool
|
||||||
username string
|
username string
|
||||||
@@ -40,6 +41,7 @@ func init() {
|
|||||||
flag.StringVar(&configFile, "conf", "./config.yml", "Config file path")
|
flag.StringVar(&configFile, "conf", "./config.yml", "Config file path")
|
||||||
flag.StringVar(&downloadPath, "out", ".", "Where files will be saved")
|
flag.StringVar(&downloadPath, "out", ".", "Where files will be saved")
|
||||||
flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path")
|
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.BoolVar(&requireAuth, "auth", false, "Enable RPC authentication")
|
||||||
flag.StringVar(&username, "user", userFromEnv, "Username required for auth")
|
flag.StringVar(&username, "user", userFromEnv, "Username required for auth")
|
||||||
@@ -61,6 +63,7 @@ func main() {
|
|||||||
c.QueueSize(queueSize)
|
c.QueueSize(queueSize)
|
||||||
c.DownloadPath(downloadPath)
|
c.DownloadPath(downloadPath)
|
||||||
c.DownloaderPath(downloaderPath)
|
c.DownloaderPath(downloaderPath)
|
||||||
|
c.SessionFilePath(sessionFilePath)
|
||||||
|
|
||||||
c.RequireAuth(requireAuth)
|
c.RequireAuth(requireAuth)
|
||||||
c.Username(username)
|
c.Username(username)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type serverConfig struct {
|
|||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
QueueSize int `yaml:"queue_size"`
|
QueueSize int `yaml:"queue_size"`
|
||||||
|
SessionFilePath string `yaml:"session_file_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
@@ -68,6 +69,10 @@ func (c *config) QueueSize(size int) {
|
|||||||
c.cfg.QueueSize = size
|
c.cfg.QueueSize = size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *config) SessionFilePath(path string) {
|
||||||
|
c.cfg.SessionFilePath = path
|
||||||
|
}
|
||||||
|
|
||||||
var instance *config
|
var instance *config
|
||||||
|
|
||||||
func Instance() *config {
|
func Instance() *config {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -129,7 +130,13 @@ func SendFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -141,10 +148,10 @@ func SendFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// TODO: further path / file validations
|
// TODO: further path / file validations
|
||||||
if strings.Contains(filepath.Dir(decodedStr), root) {
|
if strings.Contains(filepath.Dir(decodedStr), root) {
|
||||||
// ctx.Response().Header.Set(
|
w.Header().Add(
|
||||||
// "Content-Disposition",
|
"Content-Disposition",
|
||||||
// "inline; filename="+filepath.Base(decodedStr),
|
"inline; filename="+filepath.Base(decodedStr),
|
||||||
// )
|
)
|
||||||
|
|
||||||
http.ServeFile(w, r, decodedStr)
|
http.ServeFile(w, r, decodedStr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,12 @@ func Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,3 +71,8 @@ type DownloadRequest struct {
|
|||||||
Rename string `json:"rename"`
|
Rename string `json:"rename"`
|
||||||
Params []string `json:"params"`
|
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"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
|
"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
|
// In-Memory Thread-Safe Key-Value Storage with optional persistence
|
||||||
@@ -93,7 +95,12 @@ func (m *MemoryDB) All() *[]ProcessResponse {
|
|||||||
func (m *MemoryDB) Persist() {
|
func (m *MemoryDB) Persist() {
|
||||||
running := m.All()
|
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 {
|
if err != nil {
|
||||||
log.Println(cli.Red, "Failed to persist session", cli.Reset)
|
log.Println(cli.Red, "Failed to persist session", cli.Reset)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
|
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
|
||||||
|
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
@@ -17,7 +18,10 @@ type metadata struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PlaylistDetect(req DownloadRequest, mq *MessageQueue, db *MemoryDB) error {
|
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()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
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",
|
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{
|
proc := &Process{
|
||||||
Url: meta.OriginalURL,
|
Url: meta.OriginalURL,
|
||||||
Progress: DownloadProgress{},
|
Progress: DownloadProgress{},
|
||||||
@@ -60,7 +66,7 @@ func PlaylistDetect(req DownloadRequest, mq *MessageQueue, db *MemoryDB) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
proc.Info.URL = meta.OriginalURL
|
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)
|
db.Set(proc)
|
||||||
proc.SetPending()
|
proc.SetPending()
|
||||||
|
|||||||
@@ -27,10 +27,6 @@ const template = `download:
|
|||||||
"speed":%(progress.speed)s
|
"speed":%(progress.speed)s
|
||||||
}`
|
}`
|
||||||
|
|
||||||
var (
|
|
||||||
cfg = config.Instance()
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StatusPending = iota
|
StatusPending = iota
|
||||||
StatusDownloading
|
StatusDownloading
|
||||||
@@ -75,7 +71,7 @@ func (p *Process) Start() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
out := DownloadOutput{
|
out := DownloadOutput{
|
||||||
Path: cfg.GetConfig().DownloadPath,
|
Path: config.Instance().GetConfig().DownloadPath,
|
||||||
Filename: "%(title)s.%(ext)s",
|
Filename: "%(title)s.%(ext)s",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +93,7 @@ func (p *Process) Start() {
|
|||||||
}, p.Params...)
|
}, p.Params...)
|
||||||
|
|
||||||
// ----------------- main block ----------------- //
|
// ----------------- main block ----------------- //
|
||||||
cmd := exec.Command(cfg.GetConfig().DownloaderPath, params...)
|
cmd := exec.Command(config.Instance().GetConfig().DownloaderPath, params...)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
|
||||||
r, err := cmd.StdoutPipe()
|
r, err := cmd.StdoutPipe()
|
||||||
@@ -192,7 +188,7 @@ func (p *Process) Kill() error {
|
|||||||
|
|
||||||
// Returns the available format for this URL
|
// Returns the available format for this URL
|
||||||
func (p *Process) GetFormatsSync() (DownloadFormats, error) {
|
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()
|
stdout, err := cmd.Output()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -245,7 +241,7 @@ func (p *Process) SetPending() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Process) SetMetadata() error {
|
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}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
|||||||
@@ -21,5 +21,6 @@ func ApplyRouter(db *internal.MemoryDB, mq *internal.MessageQueue) func(chi.Rout
|
|||||||
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func (h *Handler) Exec() http.HandlerFunc {
|
|||||||
|
|
||||||
req := internal.DownloadRequest{}
|
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)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ func (h *Handler) Exec() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(id)
|
err = json.NewEncoder(w).EncodeContext(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,34 @@ func (h *Handler) Running() http.HandlerFunc {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package rest
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
"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
|
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"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"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/handlers"
|
||||||
"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"
|
||||||
@@ -54,7 +55,7 @@ func newServer(c serverConfig) *http.Server {
|
|||||||
|
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
r.Use(middlewares.CORS)
|
r.Use(cors.AllowAll().Handler)
|
||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
|
|
||||||
app := http.FileServer(http.FS(c.frontend))
|
app := http.FileServer(http.FS(c.frontend))
|
||||||
|
|||||||
Reference in New Issue
Block a user