format selection enabled

This commit is contained in:
2022-06-03 15:52:39 +02:00
parent 92158cc599
commit 368595c08f
16 changed files with 223 additions and 33 deletions

2
.gitignore vendored
View File

@@ -11,4 +11,4 @@ src/server/core/yt-dlp
*.ytdl *.ytdl
*.part *.part
*.db *.db
*.DS_Store .DS_Store

View File

@@ -33,6 +33,8 @@ Refactoring and JSDoc.
04/01/22: Background jobs now are retrieved!! It's still rudimentary but it leverages on yt-dlp resume feature 04/01/22: Background jobs now are retrieved!! It's still rudimentary but it leverages on yt-dlp resume feature
05/05/22: Material UI update 05/05/22: Material UI update
03/06/22: The most requested feature finally implemented: Format Selection!!
``` ```
@@ -49,15 +51,30 @@ The avaible settings are currently only:
- Switch theme - Switch theme
- Extract audio - Extract audio
- Switch language - Switch language
- Optional format selection
<img src="https://i.imgur.com/2zPs8FH.png"> <img src="https://i.imgur.com/2zPs8FH.png">
<img src="https://i.imgur.com/b4Jhkfk.png"> <img src="https://i.imgur.com/b4Jhkfk.png">
<img src="https://i.imgur.com/knjLa8c.png"> <img src="https://i.imgur.com/knjLa8c.png">
## Format selection
![](https://i.ibb.co/fNxDHJd/localhost-1234-2.png)
This feature is disabled by default as this WebUI/Wrapper/Software/Bunch of Code is intended to be used to retrieve the best quality automatically.
To enable it go to the settings page:
![](https://i.ibb.co/YdXRwKc/localhost-1234-3.png)
And set it :D
Future releases will have: Future releases will have:
- ~~Multi download~~ *done* - ~~Multi download~~ *done*
- ~~Exctract audio~~ *done* - ~~Exctract audio~~ *done*
- Format selection *in-progess* - ~~Format selection~~ *done*
- Download archive
- ARM Build
## Troubleshooting ## Troubleshooting
- **It says that it isn't connected/ip in the footer is not defined.** - **It says that it isn't connected/ip in the footer is not defined.**
@@ -105,8 +122,3 @@ node dist/main.js
- Well, yes (until now). - Well, yes (until now).
- **Why is it so slow to start a download?** - **Why is it so slow to start a download?**
- I genuinely don't know. I know that standalone yt-dlp is slow to start up even on my M1 Mac, so.... - I genuinely don't know. I know that standalone yt-dlp is slow to start up even on my M1 Mac, so....
## Todo list
- ~~retrieve background tasks~~
- format selection
- better ui/ux

View File

@@ -237,8 +237,8 @@ function AppContent() {
> >
<Toolbar /> <Toolbar />
<Routes> <Routes>
<Route path="/" element={<Home socket={socket}></Home>}></Route> <Route path="/" element={<Home></Home>}></Route>
<Route path="/settings" element={<Settings socket={socket}></Settings>}></Route> <Route path="/settings" element={<Settings></Settings>}></Route>
<Route path="/downloaded" element={<ArchivedDownloads></ArchivedDownloads>}></Route> <Route path="/downloaded" element={<ArchivedDownloads></ArchivedDownloads>}></Route>
</Routes> </Routes>
</Box> </Box>

View File

@@ -1,18 +1,16 @@
import { Backdrop, Button, CircularProgress, Container, Grid, Paper, Snackbar, TextField, } from "@mui/material"; import { Backdrop, Button, ButtonGroup, CircularProgress, Container, Grid, Paper, Skeleton, Snackbar, TextField, Typography, } from "@mui/material";
import React, { Fragment, useEffect, useState } from "react"; import React, { Fragment, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import { StackableResult } from "./components/StackableResult"; import { StackableResult } from "./components/StackableResult";
import { connected, disconnected, downloading, finished } from "./features/status/statusSlice"; import { connected, disconnected, downloading, finished } from "./features/status/statusSlice";
import { IDLInfo, IDLInfoBase, IDownloadInfo, IMessage } from "./interfaces"; import { IDLInfo, IDLInfoBase, IDownloadInfo, IMessage } from "./interfaces";
import { RootState } from "./stores/store"; import { RootState } from "./stores/store";
import { updateInStateMap, } from "./utils"; import { toFormatArgs, updateInStateMap, } from "./utils";
type Props = { let socket: Socket;
socket: Socket
}
export default function Home({ socket }: Props) { export default function Home() {
// redux state // redux state
const settings = useSelector((state: RootState) => state.settings) const settings = useSelector((state: RootState) => state.settings)
const status = useSelector((state: RootState) => state.status) const status = useSelector((state: RootState) => state.status)
@@ -22,11 +20,23 @@ export default function Home({ socket }: Props) {
const [progressMap, setProgressMap] = useState(new Map<number, number>()); const [progressMap, setProgressMap] = useState(new Map<number, number>());
const [messageMap, setMessageMap] = useState(new Map<number, IMessage>()); const [messageMap, setMessageMap] = useState(new Map<number, IMessage>());
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLInfoBase>()); const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLInfoBase>());
const [downloadFormats, setDownloadFormats] = useState<IDownloadInfo>();
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
const [pickedBestFormat, setPickedBestFormat] = useState('');
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [workingUrl, setWorkingUrl] = useState('');
const [showBackdrop, setShowBackdrop] = useState(false); const [showBackdrop, setShowBackdrop] = useState(false);
/* -------------------- Effects -------------------- */ /* -------------------- Effects -------------------- */
useEffect(() => {
socket = io(`http://${localStorage.getItem('server-addr') || 'localhost'}:3022`);
return () => {
socket.disconnect()
};
}, [])
/* WebSocket connect event handler*/ /* WebSocket connect event handler*/
useEffect(() => { useEffect(() => {
socket.on('connect', () => { socket.on('connect', () => {
@@ -53,6 +63,7 @@ export default function Home({ socket }: Props) {
socket.on('available-formats', (data: IDownloadInfo) => { socket.on('available-formats', (data: IDownloadInfo) => {
setShowBackdrop(false) setShowBackdrop(false)
console.log(data) console.log(data)
setDownloadFormats(data);
}) })
}, []) }, [])
@@ -89,11 +100,37 @@ export default function Home({ socket }: Props) {
* Retrive url from input, cli-arguments from checkboxes and emits via WebSocket * Retrive url from input, cli-arguments from checkboxes and emits via WebSocket
*/ */
const sendUrl = () => { const sendUrl = () => {
const codes = new Array<string>();
if (pickedVideoFormat !== '') codes.push(pickedVideoFormat);
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
if (pickedBestFormat !== '') codes.push(pickedBestFormat);
socket.emit('send-url', { socket.emit('send-url', {
url: url, url: url || workingUrl,
params: settings.cliArgs.toString(), params: settings.cliArgs.toString() + toFormatArgs(codes),
}) })
setUrl('') setUrl('')
setWorkingUrl('')
setTimeout(() => {
const input = document.getElementById('urlInput') as HTMLInputElement;
input.value = '';
setShowBackdrop(true);
setDownloadFormats(null);
}, 250);
}
/**
* Retrive url from input and display the formats selection view
*/
const sendUrlFormatSelection = () => {
socket.emit('send-url-format-selection', {
url: url,
})
setWorkingUrl(url)
setUrl('')
setPickedAudioFormat('');
setPickedVideoFormat('');
setPickedBestFormat('');
setTimeout(() => { setTimeout(() => {
const input = document.getElementById('urlInput') as HTMLInputElement; const input = document.getElementById('urlInput') as HTMLInputElement;
input.value = ''; input.value = '';
@@ -120,6 +157,7 @@ export default function Home({ socket }: Props) {
socket.emit('abort', { pid: id }) socket.emit('abort', { pid: id })
return return
} }
setDownloadFormats(null)
socket.emit('abort-all') socket.emit('abort-all')
} }
@@ -145,13 +183,14 @@ export default function Home({ socket }: Props) {
label={settings.i18n.t('urlInput')} label={settings.i18n.t('urlInput')}
variant="outlined" variant="outlined"
onChange={handleUrlChange} onChange={handleUrlChange}
disabled={settings.formatSelection && downloadFormats != null}
/> />
<Grid container spacing={1} pt={2}> <Grid container spacing={1} pt={2}>
<Grid item> <Grid item>
<Button <Button
variant="contained" variant="contained"
disabled={url === ''} disabled={url === ''}
onClick={() => sendUrl()} onClick={() => settings.formatSelection ? sendUrlFormatSelection() : sendUrl()}
> >
{settings.i18n.t('startButton')} {settings.i18n.t('startButton')}
</Button> </Button>
@@ -168,6 +207,110 @@ export default function Home({ socket }: Props) {
</Paper> </Paper>
</Grid> </Grid>
</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}
</Button>
</Grid>
{/* video only */}
<Grid item xs={12}>
<Typography variant="body1" component="div">
Video 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={() => {
setPickedVideoFormat(format.format_id)
setPickedBestFormat('')
}}
disabled={pickedVideoFormat === format.format_id}
>
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
</Button>
</Grid>
))
}
<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={() => {
setPickedAudioFormat(format.format_id)
setPickedBestFormat('')
}}
disabled={pickedAudioFormat === format.format_id}
>
{format.format_note} - {format.vcodec === 'none' ? format.acodec : format.vcodec}
</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}
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}> <Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
{ /*Super big brain flatMap moment*/ { /*Super big brain flatMap moment*/
Array Array

View File

@@ -19,17 +19,15 @@ import {
} from "@mui/material"; } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { Socket } from "socket.io-client"; import { io } from "socket.io-client";
import { LanguageUnion, setCliArgs, setLanguage, setServerAddr, setTheme, ThemeUnion } from "./features/settings/settingsSlice"; import { LanguageUnion, setCliArgs, setFormatSelection, setLanguage, setServerAddr, setTheme, ThemeUnion } from "./features/settings/settingsSlice";
import { alreadyUpdated, updated } from "./features/status/statusSlice"; import { alreadyUpdated, updated } from "./features/status/statusSlice";
import { RootState } from "./stores/store"; import { RootState } from "./stores/store";
import { validateDomain, validateIP } from "./utils"; import { validateDomain, validateIP } from "./utils";
type Props = { const socket = io(`http://${localStorage.getItem('server-addr') || 'localhost'}:3022`)
socket: Socket
}
export default function Settings({ socket }: Props) { export default function Settings() {
const settings = useSelector((state: RootState) => state.settings) const settings = useSelector((state: RootState) => state.settings)
const status = useSelector((state: RootState) => state.status) const status = useSelector((state: RootState) => state.status)
const dispatch = useDispatch() const dispatch = useDispatch()
@@ -155,6 +153,15 @@ export default function Settings({ socket }: Props) {
} }
label={settings.i18n.t('extractAudioCheckbox')} label={settings.i18n.t('extractAudioCheckbox')}
/> />
<FormControlLabel
control={
<Switch
defaultChecked={settings.formatSelection}
onChange={() => dispatch(setFormatSelection(!settings.formatSelection))}
/>
}
label={settings.i18n.t('formatSelectionEnabler')}
/>
<Grid> <Grid>
<Stack direction="row"> <Stack direction="row">
<Button <Button

View File

@@ -16,6 +16,7 @@ languages:
bgReminder: Once you close this page the download will continue in the background. bgReminder: Once you close this page the download will continue in the background.
toastConnected: 'Connected to ' toastConnected: 'Connected to '
toastUpdated: Updated yt-dlp binary! toastUpdated: Updated yt-dlp binary!
formatSelectionEnabler: Enable video/audio formats selection
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

View File

@@ -1,5 +1,5 @@
import { EightK, FourK, Hd, Sd } from "@mui/icons-material"; import { EightK, FourK, Hd, Sd } from "@mui/icons-material";
import { Button, Card, CardActionArea, CardActions, CardContent, CardMedia, Chip, Grid, LinearProgress, Skeleton, Stack, Typography } from "@mui/material"; import { Button, Card, CardActionArea, CardActions, CardContent, CardMedia, Chip, LinearProgress, Skeleton, Stack, Typography } from "@mui/material";
import { IMessage } from "../interfaces"; import { IMessage } from "../interfaces";
import { ellipsis } from "../utils"; import { ellipsis } from "../utils";

View File

@@ -10,7 +10,8 @@ export interface SettingsState {
language: LanguageUnion, language: LanguageUnion,
theme: ThemeUnion, theme: ThemeUnion,
cliArgs: CliArguments, cliArgs: CliArguments,
i18n: I18nBuilder i18n: I18nBuilder,
formatSelection: boolean
} }
const initialState: SettingsState = { const initialState: SettingsState = {
@@ -19,6 +20,7 @@ const initialState: SettingsState = {
theme: (localStorage.getItem("theme") || "light") as ThemeUnion, theme: (localStorage.getItem("theme") || "light") as ThemeUnion,
cliArgs: localStorage.getItem("cli-args") ? new CliArguments().fromString(localStorage.getItem("cli-args")) : new CliArguments(false, true), cliArgs: localStorage.getItem("cli-args") ? new CliArguments().fromString(localStorage.getItem("cli-args")) : new CliArguments(false, true),
i18n: new I18nBuilder((localStorage.getItem("language") || "english")), i18n: new I18nBuilder((localStorage.getItem("language") || "english")),
formatSelection: localStorage.getItem("format-selection") === "true",
} }
export const settingsSlice = createSlice({ export const settingsSlice = createSlice({
@@ -42,9 +44,13 @@ export const settingsSlice = createSlice({
state.theme = action.payload state.theme = action.payload
localStorage.setItem("theme", action.payload) localStorage.setItem("theme", action.payload)
}, },
setFormatSelection: (state, action: PayloadAction<boolean>) => {
state.formatSelection = action.payload
localStorage.setItem("format-selection", action.payload.toString())
},
} }
}) })
export const { setLanguage, setCliArgs, setTheme, setServerAddr } = settingsSlice.actions export const { setLanguage, setCliArgs, setTheme, setServerAddr, setFormatSelection } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

View File

@@ -27,6 +27,7 @@ export interface IDownloadInfoSection {
fps: number, fps: number,
resolution: string, resolution: string,
vcodec: string, vcodec: string,
acodec: string,
} }
export interface IDLInfo { export interface IDLInfo {

View File

@@ -67,6 +67,8 @@ export const updateInStateMap = (k: number, v: any, target: Map<number, any>, ca
callback(new Map(target.set(k, v))); callback(new Map(target.set(k, v)));
} }
export function updateInStateArray<T>(v: T, target: Array<T>, callback: Function) { }
/** /**
* Pre like function * Pre like function
* @param data * @param data
@@ -75,3 +77,14 @@ export const updateInStateMap = (k: number, v: any, target: Map<number, any>, ca
export function buildMessage(data: IMessage) { export function buildMessage(data: IMessage) {
return `operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`; return `operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`;
} }
export function toFormatArgs(codes: string[]): string {
if (codes.length > 1) {
return codes.reduce((v, a) => ` -f ${v}+${a}`)
}
if (codes.length === 1) {
return ` -f ${codes[0]}`;
}
return '';
}

View File

@@ -47,6 +47,8 @@
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"parcel": "^2.5.0", "parcel": "^2.5.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"typescript": "^4.6.4" "typescript": "^4.6.4"
}, },
"pnpm": { "pnpm": {

View File

@@ -83,14 +83,16 @@ class Process {
fps: format.fps ?? '', fps: format.fps ?? '',
resolution: format.resolution ?? '', resolution: format.resolution ?? '',
vcodec: format.vcodec ?? '', vcodec: format.vcodec ?? '',
acodec: format.acodec ?? '',
} }
}), }).filter((format: IDownloadInfoSection) => format.format_note !== 'storyboard'),
best: { best: {
format_id: json.format_id ?? '', format_id: json.format_id ?? '',
format_note: json.format_note ?? '', format_note: json.format_note ?? '',
fps: json.fps ?? '', fps: json.fps ?? '',
resolution: json.resolution ?? '', resolution: json.resolution ?? '',
vcodec: json.vcodec ?? '', vcodec: json.vcodec ?? '',
acodec: json.acodec ?? '',
}, },
thumbnail: json.thumbnail, thumbnail: json.thumbnail,
title: json.title, title: json.title,

View File

@@ -31,7 +31,6 @@ catch (e) {
export async function getFormatsAndInfo(socket: Socket, url: string) { export async function getFormatsAndInfo(socket: Socket, url: string) {
let p = new Process(url, [], settings); let p = new Process(url, [], settings);
const formats = await p.getInfo(); const formats = await p.getInfo();
console.log(formats)
socket.emit('available-formats', formats) socket.emit('available-formats', formats)
p = null; p = null;
} }
@@ -102,7 +101,7 @@ export async function retrieveDownload(socket: Socket) {
coldRestart = false; coldRestart = false;
let downloads = []; let downloads = [];
// sanitize // sanitize
downloads = [... new Set(downloads.filter(el => el !== undefined))]; downloads = [...new Set(downloads.filter(el => el !== undefined))];
log.info('dl', `Cold restart, retrieving ${downloads.length} jobs`) log.info('dl', `Cold restart, retrieving ${downloads.length} jobs`)
for (const entry of downloads) { for (const entry of downloads) {
if (entry) { if (entry) {

View File

@@ -2,7 +2,7 @@ import { stat, createReadStream } from 'fs';
import { lookup } from 'mime-types'; import { lookup } from 'mime-types';
export function streamer(ctx: any, next: any) { export function streamer(ctx: any, next: any) {
const filepath = '/Users/marco/dev/homebrew/yt-dlp-web-ui/downloads/AleXa (알렉사) Voting Open in American Song Contest Grand Final!.webm' const filepath = ''
stat(filepath, (err, stat) => { stat(filepath, (err, stat) => {
if (err) { if (err) {
ctx.response.status = 404; ctx.response.status = 404;

View File

@@ -11,4 +11,5 @@ interface IDownloadInfoSection {
fps: number, fps: number,
resolution: string, resolution: string,
vcodec: string, vcodec: string,
acodec: string,
} }

View File

@@ -48,9 +48,12 @@ io.on('connection', socket => {
socket.on('send-url', (args) => { socket.on('send-url', (args) => {
logger('ws', args?.url) logger('ws', args?.url)
//if (args.url) getFormatsAndInfo(socket, args?.url)
download(socket, args) download(socket, args)
}) })
socket.on('send-url-format-selection', (args) => {
logger('ws', args?.url)
if (args.url) getFormatsAndInfo(socket, args?.url)
})
socket.on('abort', (args) => { socket.on('abort', (args) => {
abortDownload(socket, args) abortDownload(socket, args)
}) })