Code refactoring and bump deps

This commit is contained in:
2023-04-13 11:13:40 +02:00
parent 7f602f1e20
commit 621164589f
16 changed files with 455 additions and 344 deletions

View File

@@ -1,36 +1,38 @@
import { ThemeProvider } from "@emotion/react";
import { ThemeProvider } from '@emotion/react'
import {
ChevronLeft,
Dashboard,
// Download,
Menu, Settings as SettingsIcon,
FormatListBulleted,
Menu,
SettingsEthernet,
Settings as SettingsIcon,
Storage
} from "@mui/icons-material";
} from '@mui/icons-material'
import {
Box,
CircularProgress,
createTheme, CssBaseline,
CssBaseline,
Divider,
IconButton, List,
ListItemIcon, ListItemText, Toolbar,
Typography
} from "@mui/material";
import { grey } from "@mui/material/colors";
import ListItemButton from '@mui/material/ListItemButton';
import { lazy, Suspense, useMemo, useState } from "react";
import { Provider, useDispatch, useSelector } from "react-redux";
Typography,
createTheme
} from '@mui/material'
import ListItemButton from '@mui/material/ListItemButton'
import { grey } from '@mui/material/colors'
import { Suspense, lazy, useMemo, useState } from 'react'
import { Provider, useDispatch, useSelector } from 'react-redux'
import {
BrowserRouter as Router, Link, Route,
Link, Route,
BrowserRouter as Router,
Routes
} from 'react-router-dom';
import { AppBar } from "./components/AppBar";
import { Drawer } from "./components/Drawer";
import { toggleListView } from "./features/settings/settingsSlice";
import Home from "./Home";
import { RootState, store } from './stores/store';
import { formatGiB, getWebSocketEndpoint } from "./utils";
} from 'react-router-dom'
import Home from './Home'
import { AppBar } from './components/AppBar'
import { Drawer } from './components/Drawer'
import { toggleListView } from './features/settings/settingsSlice'
import { RootState, store } from './stores/store'
import { formatGiB, getWebSocketEndpoint } from './utils'
function AppContent() {
const [open, setOpen] = useState(false)
@@ -179,7 +181,7 @@ function AppContent() {
</Box>
</Router>
</ThemeProvider>
);
)
}
export function App() {
@@ -187,5 +189,5 @@ export function App() {
<Provider store={store}>
<AppContent />
</Provider>
);
)
}

View File

@@ -1,8 +1,8 @@
import { FileUpload } from "@mui/icons-material";
import { FileUpload } from '@mui/icons-material'
import {
Alert,
Backdrop,
Button,
ButtonGroup,
CircularProgress,
Container,
FormControl,
@@ -15,21 +15,21 @@ import {
Select,
Snackbar,
styled,
TextField,
Typography
} from "@mui/material";
import { Buffer } from 'buffer';
import { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { DownloadsCardView } from "./components/DownloadsCardView";
import { DownloadsListView } from "./components/DownloadsListView";
import { CliArguments } from "./features/core/argsParser";
import I18nBuilder from "./features/core/intl";
import { RPCClient } from "./features/core/rpcClient";
import { connected, setFreeSpace } from "./features/status/statusSlice";
import { RootState } from "./stores/store";
import { IDLMetadata, RPCResult } from "./types";
import { isValidURL, toFormatArgs } from "./utils";
TextField
} from '@mui/material'
import { Buffer } from 'buffer'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { DownloadsCardView } from './components/DownloadsCardView'
import { DownloadsListView } from './components/DownloadsListView'
import FormatsGrid from './components/FormatsGrid'
import { CliArguments } from './features/core/argsParser'
import I18nBuilder from './features/core/intl'
import { RPCClient } from './features/core/rpcClient'
import { connected, setFreeSpace } from './features/status/statusSlice'
import { RootState } from './stores/store'
import type { DLMetadata, RPCResult } from './types'
import { isValidURL, toFormatArgs } from './utils'
type Props = {
socket: WebSocket
@@ -42,30 +42,35 @@ export default function Home({ socket }: Props) {
const dispatch = useDispatch()
// ephemeral state
const [activeDownloads, setActiveDownloads] = useState<Array<RPCResult>>();
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
const [pickedBestFormat, setPickedBestFormat] = useState('');
const [activeDownloads, setActiveDownloads] = useState<Array<RPCResult>>()
const [downloadFormats, setDownloadFormats] = useState<DLMetadata>()
const [pickedVideoFormat, setPickedVideoFormat] = useState('')
const [pickedAudioFormat, setPickedAudioFormat] = useState('')
const [pickedBestFormat, setPickedBestFormat] = useState('')
const [customArgs, setCustomArgs] = useState('');
const [downloadPath, setDownloadPath] = useState(0);
const [availableDownloadPaths, setAvailableDownloadPaths] = useState<string[]>([]);
const [customArgs, setCustomArgs] = useState('')
const [downloadPath, setDownloadPath] = useState(0)
const [availableDownloadPaths, setAvailableDownloadPaths] = useState<string[]>([])
const [fileNameOverride, setFilenameOverride] = useState('');
const [fileNameOverride, setFilenameOverride] = useState('')
const [url, setUrl] = useState('');
const [workingUrl, setWorkingUrl] = useState('');
const [url, setUrl] = useState('')
const [workingUrl, setWorkingUrl] = useState('')
const [showBackdrop, setShowBackdrop] = useState(true);
const [showToast, setShowToast] = useState(true);
const [showBackdrop, setShowBackdrop] = useState(true)
const [showToast, setShowToast] = useState(true)
// memos
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
const client = useMemo(() => new RPCClient(socket), [settings.serverAddr, settings.serverPort])
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs])
// refs
const urlInputRef = useRef<HTMLInputElement>(null)
const customFilenameInputRef = useRef<HTMLInputElement>(null)
/* -------------------- Effects -------------------- */
/* WebSocket connect event handler*/
useEffect(() => {
socket.onopen = () => {
@@ -84,8 +89,7 @@ export default function Home({ socket }: Props) {
}, [status.connected])
useEffect(() => {
client.freeSpace()
.then(bytes => dispatch(setFreeSpace(bytes.result)))
client.freeSpace().then(bytes => dispatch(setFreeSpace(bytes.result)))
}, [])
useEffect(() => {
@@ -118,7 +122,19 @@ export default function Home({ socket }: Props) {
})
}, [])
/* -------------------- component functions -------------------- */
const [socketHasError, setSocketHasError] = useState(false)
useEffect(() => {
socket.onerror = () => {
setSocketHasError(true)
setShowBackdrop(false)
}
return () => {
socket.onerror = null
}
}, [socket])
/* -------------------- callbacks-------------------- */
/**
* Retrive url from input, cli-arguments from checkboxes and emits via WebSocket
@@ -222,12 +238,9 @@ export default function Home({ socket }: Props) {
}
const resetInput = () => {
const input = document.getElementById('urlInput') as HTMLInputElement;
input.value = '';
const filename = document.getElementById('customFilenameInput') as HTMLInputElement;
if (filename) {
filename.value = '';
urlInputRef.current!.value = '';
if (customFilenameInputRef.current) {
customFilenameInputRef.current!.value = '';
}
}
@@ -257,7 +270,7 @@ export default function Home({ socket }: Props) {
<Grid container>
<TextField
fullWidth
id="urlInput"
ref={urlInputRef}
label={i18n.t('urlInput')}
variant="outlined"
onChange={handleUrlChange}
@@ -278,54 +291,50 @@ export default function Home({ socket }: Props) {
</Grid>
<Grid container spacing={1} sx={{ mt: 1 }}>
{
settings.enableCustomArgs ?
<Grid item xs={12}>
<TextField
id="customArgsInput"
fullWidth
label={i18n.t('customArgsInput')}
variant="outlined"
onChange={handleCustomArgsChange}
value={customArgs}
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
/>
</Grid> :
null
settings.enableCustomArgs &&
<Grid item xs={12}>
<TextField
fullWidth
label={i18n.t('customArgsInput')}
variant="outlined"
onChange={handleCustomArgsChange}
value={customArgs}
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
/>
</Grid>
}
{
settings.fileRenaming ?
<Grid item xs={8}>
<TextField
id="customFilenameInput"
fullWidth
label={i18n.t('customFilename')}
variant="outlined"
value={fileNameOverride}
onChange={handleFilenameOverrideChange}
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
/>
</Grid> :
null
settings.fileRenaming &&
<Grid item xs={8}>
<TextField
ref={customFilenameInputRef}
fullWidth
label={i18n.t('customFilename')}
variant="outlined"
value={fileNameOverride}
onChange={handleFilenameOverrideChange}
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
/>
</Grid>
}
{
settings.pathOverriding ?
<Grid item xs={4}>
<FormControl fullWidth>
<InputLabel>{i18n.t('customPath')}</InputLabel>
<Select
label={i18n.t('customPath')}
defaultValue={0}
variant={'outlined'}
value={downloadPath}
onChange={(e) => setDownloadPath(Number(e.target.value))}
>
{availableDownloadPaths.map((val: string, idx: number) => (
<MenuItem key={idx} value={idx}>{val}</MenuItem>
))}
</Select>
</FormControl>
</Grid> :
null
settings.pathOverriding &&
<Grid item xs={4}>
<FormControl fullWidth>
<InputLabel>{i18n.t('customPath')}</InputLabel>
<Select
label={i18n.t('customPath')}
defaultValue={0}
variant={'outlined'}
value={downloadPath}
onChange={(e) => setDownloadPath(Number(e.target.value))}
>
{availableDownloadPaths.map((val: string, idx: number) => (
<MenuItem key={idx} value={idx}>{val}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
}
</Grid>
<Grid container spacing={1} pt={2}>
@@ -351,120 +360,31 @@ export default function Home({ socket }: Props) {
</Grid>
</Grid >
{/* Format Selection grid */}
{
downloadFormats ? <Grid container spacing={2} mt={2}>
<Grid item xs={12}>
<Paper
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
}}
>
<Grid container>
<Grid item xs={12}>
<Typography variant="h6" component="div" pb={1}>
{downloadFormats.title}
</Typography>
{/* <Skeleton variant="rectangular" height={180} /> */}
</Grid>
<Grid item xs={12} pb={1}>
<img src={downloadFormats.thumbnail} height={260} width="100%" style={{ objectFit: 'cover' }} />
</Grid>
{/* video only */}
<Grid item xs={12}>
<Typography variant="body1" component="div">
Best quality
</Typography>
</Grid>
<Grid item pr={2} py={1}>
<Button
variant="contained"
disabled={pickedBestFormat !== ''}
onClick={() => {
setPickedBestFormat(downloadFormats.best.format_id)
setPickedVideoFormat('')
setPickedAudioFormat('')
}}>
{downloadFormats.best.format_note || downloadFormats.best.format_id} - {downloadFormats.best.vcodec}+{downloadFormats.best.acodec}
&nbsp;({downloadFormats.best.resolution}{(downloadFormats.best.filesize_approx>0)?", ~"+Math.round(downloadFormats.best.filesize_approx/1024/1024)+" MiB":""})
</Button>
</Grid>
{/* video only */}
{downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ?
<Grid item xs={12}>
<Typography variant="body1" component="div">
Video data {downloadFormats.formats[1].acodec}
</Typography>
</Grid>
: null
}
{downloadFormats.formats
.filter(format => format.acodec === 'none' && format.vcodec !== 'none')
.map((format, idx) => (
<Grid item pr={2} py={1} key={idx}>
<Button
variant="contained"
onClick={() => {
setPickedVideoFormat(format.format_id)
setPickedBestFormat('')
}}
disabled={pickedVideoFormat === format.format_id}
>
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
&nbsp;({format.resolution}{(format.filesize_approx>0)?", ~"+Math.round(format.filesize_approx/1024/1024)+" MiB":""})
</Button>
</Grid>
))
}
{downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ?
<Grid item xs={12}>
<Typography variant="body1" component="div">
Audio data
</Typography>
</Grid>
: null
}
{downloadFormats.formats
.filter(format => format.acodec !== 'none' && format.vcodec === 'none')
.map((format, idx) => (
<Grid item pr={2} py={1} key={idx}>
<Button
variant="contained"
onClick={() => {
setPickedAudioFormat(format.format_id)
setPickedBestFormat('')
}}
disabled={pickedAudioFormat === format.format_id}
>
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
{(format.filesize_approx>0)?" (~"+Math.round(format.filesize_approx/1024/1024)+" MiB)":""}
</Button>
</Grid>
))
}
<Grid item xs={12} pt={2}>
<ButtonGroup disableElevation variant="contained">
<Button
onClick={() => sendUrl()}
disabled={!pickedBestFormat && !(pickedAudioFormat || pickedVideoFormat)}
> Download
</Button>
<Button
onClick={() => {
setPickedAudioFormat('');
setPickedVideoFormat('');
setPickedBestFormat('');
}}
> Clear
</Button>
</ButtonGroup>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid> : null
}
{downloadFormats && <FormatsGrid
downloadFormats={downloadFormats}
onBestQualitySelected={(id) => {
setPickedBestFormat(id)
setPickedVideoFormat('')
setPickedAudioFormat('')
}}
onVideoSelected={(id) => {
setPickedVideoFormat(id)
setPickedBestFormat('')
}}
onAudioSelected={(id) => {
setPickedAudioFormat(id)
setPickedBestFormat('')
}}
onClear={() => {
setPickedAudioFormat('');
setPickedVideoFormat('');
setPickedBestFormat('');
}}
onSubmit={sendUrl}
pickedBestFormat={pickedBestFormat}
pickedVideoFormat={pickedVideoFormat}
pickedAudioFormat={pickedAudioFormat}
/>}
{
settings.listView ?
<DownloadsListView downloads={activeDownloads ?? []} abortFunction={abort} /> :
@@ -473,9 +393,17 @@ export default function Home({ socket }: Props) {
<Snackbar
open={showToast === status.connected}
autoHideDuration={1500}
message="Connected"
onClose={() => setShowToast(false)}
/>
</Container >
>
<Alert variant="filled" severity="success">
{`Connected to (${settings.serverAddr}:${settings.serverPort})`}
</Alert>
</Snackbar>
<Snackbar open={socketHasError}>
<Alert variant="filled" severity="error">
{`${i18n.t('rpcConnErr')} (${settings.serverAddr}:${settings.serverPort})`}
</Alert>
</Snackbar>
</Container>
);
}

View File

@@ -16,15 +16,16 @@ import {
Switch,
TextField,
Typography
} from "@mui/material";
import { useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { debounceTime, distinctUntilChanged, map, of, takeWhile } from "rxjs";
import { CliArguments } from "./features/core/argsParser";
import I18nBuilder from "./features/core/intl";
import { RPCClient } from "./features/core/rpcClient";
} from '@mui/material'
import { useEffect, useMemo, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Subject, debounceTime, distinctUntilChanged, map, takeWhile } from 'rxjs'
import { CliArguments } from './features/core/argsParser'
import I18nBuilder from './features/core/intl'
import { RPCClient } from './features/core/rpcClient'
import {
LanguageUnion,
ThemeUnion,
setCliArgs,
setEnableCustomArgs,
setFileRenaming,
@@ -33,32 +34,31 @@ import {
setPathOverriding,
setServerAddr,
setServerPort,
setTheme,
ThemeUnion
} from "./features/settings/settingsSlice";
import { updated } from "./features/status/statusSlice";
import { RootState } from "./stores/store";
import { validateDomain, validateIP } from "./utils";
setTheme
} from './features/settings/settingsSlice'
import { updated } from './features/status/statusSlice'
import { RootState } from './stores/store'
import { validateDomain, validateIP } from './utils'
export default function Settings({ socket }: { socket: WebSocket }) {
const settings = useSelector((state: RootState) => state.settings)
const status = useSelector((state: RootState) => state.status)
const dispatch = useDispatch()
const status = useSelector((state: RootState) => state.status)
const settings = useSelector((state: RootState) => state.settings)
const [invalidIP, setInvalidIP] = useState(false);
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
const client = useMemo(() => new RPCClient(socket), [settings.serverAddr, settings.serverPort])
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs])
/**
* Update the server ip address state and localstorage whenever the input value changes.
* Validate the ip-addr then set.s
* @param event Input change event
*/
const handleAddrChange = (event: any) => {
const $serverAddr = of(event)
const client = useMemo(() => new RPCClient(socket), [])
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [])
const serverAddr$ = useMemo(() => new Subject<string>(), [])
const serverPort$ = useMemo(() => new Subject<string>(), [])
useEffect(() => {
const sub = serverAddr$
.pipe(
map(event => event.target.value),
debounceTime(500),
distinctUntilChanged()
)
@@ -73,24 +73,20 @@ export default function Settings({ socket }: { socket: WebSocket }) {
setInvalidIP(true)
}
})
return $serverAddr.unsubscribe()
}
return () => sub.unsubscribe()
}, [serverAddr$])
/**
* Set server port
*/
const handlePortChange = (event: any) => {
const $port = of(event)
useEffect(() => {
const sub = serverPort$
.pipe(
map(event => event.target.value),
map(val => Number(val)),
takeWhile(val => isFinite(val) && val <= 65535),
)
.subscribe(port => {
dispatch(setServerPort(port.toString()))
})
return $port.unsubscribe()
}
return () => sub.unsubscribe()
}, [])
/**
* Language toggler handler
@@ -107,7 +103,7 @@ export default function Settings({ socket }: { socket: WebSocket }) {
}
/**
* Send via WebSocket a message in order to update the yt-dlp binary from server
* Send via WebSocket a message to update yt-dlp binary
*/
const updateBinary = () => {
client.updateExecutable().then(() => dispatch(updated()))
@@ -136,7 +132,7 @@ export default function Settings({ socket }: { socket: WebSocket }) {
label={i18n.t('serverAddressTitle')}
defaultValue={settings.serverAddr}
error={invalidIP}
onChange={handleAddrChange}
onChange={(e) => serverAddr$.next(e.currentTarget.value)}
InputProps={{
startAdornment: <InputAdornment position="start">ws://</InputAdornment>,
}}
@@ -148,7 +144,7 @@ export default function Settings({ socket }: { socket: WebSocket }) {
fullWidth
label={i18n.t('serverPortTitle')}
defaultValue={settings.serverPort}
onChange={handlePortChange}
onChange={(e) => serverPort$.next(e.currentTarget.value)}
error={isNaN(Number(settings.serverPort)) || Number(settings.serverPort) > 65535}
sx={{ mb: 2 }}
/>
@@ -186,16 +182,6 @@ export default function Settings({ socket }: { socket: WebSocket }) {
</Select>
</FormControl>
</Grid>
{/* <Grid item xs={12} md={6}>
<TextField
fullWidth
label={'Max download speed' || i18n.t('serverPortTitle')}
defaultValue={settings.serverPort}
onChange={handlePortChange}
error={isNaN(Number(settings.serverPort)) || Number(settings.serverPort) > 65535}
sx={{ mb: 2 }}
/>
</Grid> */}
</Grid>
<FormControlLabel
control={

View File

@@ -28,6 +28,7 @@ languages:
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error while conencting to RPC server
italian:
urlInput: URL di YouTube o di qualsiasi altro servizio supportato
statusTitle: Stato
@@ -55,6 +56,7 @@ languages:
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error nella connessione al server RPC
chinese:
urlInput: YouTube 或其他受支持服务的视频网址
statusTitle: 状态
@@ -82,6 +84,7 @@ languages:
customPath: 自定义路径
customArgs: 启用自定义 yt-dlp 参数(能力越大 = 责任越大)
customArgsInput: 自定义 yt-dlp 参数
rpcConnErr: Error while conencting to RPC server
spanish:
urlInput: YouTube or other supported service video url
statusTitle: Status
@@ -109,6 +112,7 @@ languages:
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error while conencting to RPC server
russian:
urlInput: YouTube or other supported service video url
statusTitle: Status
@@ -136,6 +140,7 @@ languages:
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error while conencting to RPC server
korean:
urlInput: YouTube나 다른 지원되는 사이트의 URL
statusTitle: 상태
@@ -163,6 +168,7 @@ languages:
customPath: Custom path
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
customArgsInput: Custom yt-dlp arguments
rpcConnErr: Error while conencting to RPC server
japanese:
urlInput: YouTubeまたはサポート済み動画のURL
statusTitle: 状態
@@ -190,4 +196,5 @@ languages:
customFilename: (空白の場合は元のファイル名)
customPath: 保存先
customArgs: yt-dlpのオプションの有効化 (最適設定にする場合)
customArgsInput: yt-dlpのオプション
customArgsInput: yt-dlpのオプション
rpcConnErr: Error while conencting to RPC server

View File

@@ -0,0 +1,126 @@
import { Button, ButtonGroup, Grid, Paper, Typography } from "@mui/material"
import type { DLMetadata } from "../types"
type Props = {
downloadFormats: DLMetadata
onAudioSelected: (format: string) => void
onVideoSelected: (format: string) => void
onBestQualitySelected: (format: string) => void
onSubmit: () => void
onClear: () => void
pickedBestFormat: string
pickedAudioFormat: string
pickedVideoFormat: string
}
export default function FormatsGrid({
downloadFormats,
onAudioSelected,
onVideoSelected,
onBestQualitySelected,
onSubmit,
onClear,
pickedBestFormat,
pickedAudioFormat,
pickedVideoFormat,
}: Props) {
return (
<Grid container spacing={2} mt={2}>
<Grid item xs={12}>
<Paper
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
}}
>
<Grid container>
<Grid item xs={12}>
<Typography variant="h6" component="div" pb={1}>
{downloadFormats.title}
</Typography>
{/* <Skeleton variant="rectangular" height={180} /> */}
</Grid>
<Grid item xs={12} pb={1}>
<img src={downloadFormats.thumbnail} height={260} width="100%" style={{ objectFit: 'cover' }} />
</Grid>
{/* video only */}
<Grid item xs={12}>
<Typography variant="body1" component="div">
Best quality
</Typography>
</Grid>
<Grid item pr={2} py={1}>
<Button
variant="contained"
disabled={pickedBestFormat !== ''}
onClick={() => onBestQualitySelected(downloadFormats.best.format_id)}
>
{downloadFormats.best.format_note || downloadFormats.best.format_id} - {downloadFormats.best.vcodec}+{downloadFormats.best.acodec}
&nbsp;({downloadFormats.best.resolution}{(downloadFormats.best.filesize_approx > 0) ? ", ~" + Math.round(downloadFormats.best.filesize_approx / 1024 / 1024) + " MiB" : ""})
</Button>
</Grid>
{/* video only */}
{downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length &&
<Grid item xs={12}>
<Typography variant="body1" component="div">
Video data {downloadFormats.formats[1].acodec}
</Typography>
</Grid>
}
{downloadFormats.formats
.filter(format => format.acodec === 'none' && format.vcodec !== 'none')
.map((format, idx) => (
<Grid item pr={2} py={1} key={idx}>
<Button
variant="contained"
onClick={() => onVideoSelected(format.format_id)}
disabled={pickedVideoFormat === format.format_id}
>
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
&nbsp;({format.resolution}{(format.filesize_approx > 0) ? ", ~" + Math.round(format.filesize_approx / 1024 / 1024) + " MiB" : ""})
</Button>
</Grid>
))
}
{downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length &&
<Grid item xs={12}>
<Typography variant="body1" component="div">
Audio data
</Typography>
</Grid>
}
{downloadFormats.formats
.filter(format => format.acodec !== 'none' && format.vcodec === 'none')
.map((format, idx) => (
<Grid item pr={2} py={1} key={idx}>
<Button
variant="contained"
onClick={() => onAudioSelected(format.format_id)}
disabled={pickedAudioFormat === format.format_id}
>
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
{(format.filesize_approx > 0) ? " (~" + Math.round(format.filesize_approx / 1024 / 1024) + " MiB)" : ""}
</Button>
</Grid>
))
}
<Grid item xs={12} pt={2}>
<ButtonGroup disableElevation variant="contained">
<Button
onClick={() => onSubmit()}
disabled={!pickedBestFormat && !(pickedAudioFormat || pickedVideoFormat)}
> Download
</Button>
<Button
onClick={() => onClear()}
> Clear
</Button>
</ButtonGroup>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
)
}

View File

@@ -1,4 +1,4 @@
import type { RPCRequest, RPCResponse, IDLMetadata } from "../../types"
import type { RPCRequest, RPCResponse, DLMetadata } from "../../types"
import { getHttpRPCEndpoint } from '../../utils'
@@ -19,18 +19,17 @@ export class RPCClient {
this.socket.send(JSON.stringify(req))
}
private sendHTTP<T>(req: RPCRequest) {
return new Promise<RPCResponse<T>>((resolve) => {
fetch(getHttpRPCEndpoint(), {
method: 'POST',
body: JSON.stringify({
id: this.incrementSeq(),
...req
})
private async sendHTTP<T>(req: RPCRequest) {
const res = await fetch(getHttpRPCEndpoint(), {
method: 'POST',
body: JSON.stringify({
...req,
id: this.incrementSeq(),
})
.then(res => res.json())
.then(data => resolve(data))
})
const data: RPCResponse<T> = await res.json()
return data
}
public download(url: string, args: string, pathOverride = '', renameTo = '') {
@@ -50,7 +49,7 @@ export class RPCClient {
public formats(url: string) {
if (url) {
return this.sendHTTP<IDLMetadata>({
return this.sendHTTP<DLMetadata>({
id: this.incrementSeq(),
method: 'Service.Formats',
params: [{

View File

@@ -1,10 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
const root = ReactDOM.createRoot(document.getElementById('root')!)
const root = createRoot(document.getElementById('root')!)
root.render(
<React.StrictMode>
<App></App>
</React.StrictMode>
<StrictMode>
<App />
</StrictMode>
)

View File

@@ -45,14 +45,14 @@ export type RPCParams = {
Params?: string
}
export interface IDLMetadata {
formats: Array<IDLFormat>,
best: IDLFormat,
export interface DLMetadata {
formats: Array<DLFormat>,
best: DLFormat,
thumbnail: string,
title: string,
}
export interface IDLFormat {
export interface DLFormat {
format_id: string,
format_note: string,
fps: number,