filename override, addressing #22

This commit is contained in:
2023-01-07 12:04:07 +01:00
parent 97eb686eb1
commit 3754750f61
7 changed files with 324 additions and 169 deletions

View File

@@ -9,6 +9,7 @@ import {
Grid, Grid,
IconButton, IconButton,
InputAdornment, InputAdornment,
InputLabel,
MenuItem, MenuItem,
Paper, Paper,
Select, Select,
@@ -49,9 +50,11 @@ export default function Home({ socket }: Props) {
const [pickedAudioFormat, setPickedAudioFormat] = useState(''); const [pickedAudioFormat, setPickedAudioFormat] = useState('');
const [pickedBestFormat, setPickedBestFormat] = useState(''); const [pickedBestFormat, setPickedBestFormat] = useState('');
const [downloadPath, setDownloadPath] = useState<number>(0); const [downloadPath, setDownloadPath] = useState(0);
const [availableDownloadPaths, setAvailableDownloadPaths] = useState<string[]>([]); const [availableDownloadPaths, setAvailableDownloadPaths] = useState<string[]>([]);
const [fileNameOverride, setFilenameOverride] = useState('');
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [workingUrl, setWorkingUrl] = useState(''); const [workingUrl, setWorkingUrl] = useState('');
const [showBackdrop, setShowBackdrop] = useState(false); const [showBackdrop, setShowBackdrop] = useState(false);
@@ -141,14 +144,17 @@ export default function Home({ socket }: Props) {
url: immediate || url || workingUrl, url: immediate || url || workingUrl,
path: availableDownloadPaths[downloadPath], path: availableDownloadPaths[downloadPath],
params: cliArgs.toString() + toFormatArgs(codes), params: cliArgs.toString() + toFormatArgs(codes),
renameTo: fileNameOverride,
}) })
setUrl('') setUrl('')
setWorkingUrl('') setWorkingUrl('')
setFilenameOverride('')
setTimeout(() => { setTimeout(() => {
const input = document.getElementById('urlInput') as HTMLInputElement; resetInput()
input.value = ''; setShowBackdrop(true)
setShowBackdrop(true); setDownloadFormats(undefined)
setDownloadFormats(undefined);
}, 250); }, 250);
} }
@@ -159,16 +165,17 @@ export default function Home({ socket }: Props) {
socket.emit('send-url-format-selection', { socket.emit('send-url-format-selection', {
url: url, url: url,
}) })
setWorkingUrl(url) setWorkingUrl(url)
setUrl('') setUrl('')
setPickedAudioFormat(''); setPickedAudioFormat('')
setPickedVideoFormat(''); setPickedVideoFormat('')
setPickedBestFormat(''); setPickedBestFormat('')
setTimeout(() => { setTimeout(() => {
const input = document.getElementById('urlInput') as HTMLInputElement; resetInput()
input.value = '';
setShowBackdrop(true) setShowBackdrop(true)
}, 250); }, 250)
} }
/** /**
@@ -179,6 +186,14 @@ export default function Home({ socket }: Props) {
setUrl(e.target.value) setUrl(e.target.value)
} }
/**
* Update the filename override state whenever the input value changes
* @param e Input change event
*/
const handleFilenameOverrideChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilenameOverride(e.target.value)
}
/** /**
* Abort a specific download if id's provided, other wise abort all running ones. * Abort a specific download if id's provided, other wise abort all running ones.
* @param id The download id / pid * @param id The download id / pid
@@ -209,6 +224,16 @@ export default function Home({ socket }: Props) {
reader.readAsDataURL(urlList[0]) reader.readAsDataURL(urlList[0])
} }
const resetInput = () => {
const input = document.getElementById('urlInput') as HTMLInputElement;
input.value = '';
const filename = document.getElementById('customFilenameInput') as HTMLInputElement;
if (filename) {
filename.value = '';
}
}
/* -------------------- styled components -------------------- */ /* -------------------- styled components -------------------- */
const Input = styled('input')({ const Input = styled('input')({
@@ -232,8 +257,7 @@ export default function Home({ socket }: Props) {
flexDirection: 'column', flexDirection: 'column',
}} }}
> >
<Grid container spacing={1}> <Grid container>
<Grid item xs={10}>
<TextField <TextField
fullWidth fullWidth
id="urlInput" id="urlInput"
@@ -255,20 +279,41 @@ export default function Home({ socket }: Props) {
}} }}
/> />
</Grid> </Grid>
<Grid item xs={2}> <Grid container spacing={1} sx={{ mt: 1 }}>
{
settings.fileRenaming ?
<Grid item xs={8}>
<TextField
id="customFilenameInput"
fullWidth
label={i18n.t('customFilename')}
variant="outlined"
onChange={handleFilenameOverrideChange}
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
/>
</Grid> :
null
}
{
settings.pathOverriding ?
<Grid item xs={4}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>{i18n.t('customPath')}</InputLabel>
<Select <Select
label={i18n.t('customPath')}
defaultValue={0} defaultValue={0}
variant={'outlined'}
value={downloadPath} value={downloadPath}
onChange={(e) => setDownloadPath(Number(e.target.value))} onChange={(e) => setDownloadPath(Number(e.target.value))}
> >
{availableDownloadPaths.map((val: string, idx: number) => ( {availableDownloadPaths.map((val: string, idx: number) => (
<MenuItem key={idx} value={idx}>{val}</MenuItem> <MenuItem key={idx} value={idx}>{val}</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
</Grid> </Grid> :
null
}
</Grid> </Grid>
<Grid container spacing={1} pt={2}> <Grid container spacing={1} pt={2}>
<Grid item> <Grid item>
@@ -293,7 +338,8 @@ export default function Home({ socket }: Props) {
</Grid> </Grid>
</Grid > </Grid >
{/* Format Selection grid */} {/* Format Selection grid */}
{downloadFormats ? <Grid container spacing={2} mt={2}> {
downloadFormats ? <Grid container spacing={2} mt={2}>
<Grid item xs={12}> <Grid item xs={12}>
<Paper <Paper
sx={{ sx={{
@@ -401,9 +447,10 @@ export default function Home({ socket }: Props) {
</Grid> </Grid>
</Paper> </Paper>
</Grid> </Grid>
</Grid> : null} </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*/ {
Array Array
.from<any>(messageMap) .from<any>(messageMap)
.filter(flattened => [...flattened][0]) .filter(flattened => [...flattened][0])

View File

@@ -25,8 +25,10 @@ import { CliArguments } from "./classes";
import { import {
LanguageUnion, LanguageUnion,
setCliArgs, setCliArgs,
setFileRenaming,
setFormatSelection, setFormatSelection,
setLanguage, setLanguage,
setPathOverriding,
setServerAddr, setServerAddr,
setServerPort, setServerPort,
setTheme, setTheme,
@@ -158,10 +160,10 @@ export default function Settings({ socket }: Props) {
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Language</InputLabel> <InputLabel>{i18n.t('languageSelect')}</InputLabel>
<Select <Select
defaultValue={settings.language} defaultValue={settings.language}
label="Language" label={i18n.t('languageSelect')}
onChange={handleLanguageChange} onChange={handleLanguageChange}
> >
<MenuItem value="english">English</MenuItem> <MenuItem value="english">English</MenuItem>
@@ -176,10 +178,10 @@ export default function Settings({ socket }: Props) {
</Grid> </Grid>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Theme</InputLabel> <InputLabel>{i18n.t('themeSelect')}</InputLabel>
<Select <Select
defaultValue={settings.theme} defaultValue={settings.theme}
label="Theme" label={i18n.t('themeSelect')}
onChange={handleThemeChange} onChange={handleThemeChange}
> >
<MenuItem value="light">Light</MenuItem> <MenuItem value="light">Light</MenuItem>
@@ -230,6 +232,35 @@ export default function Settings({ socket }: Props) {
} }
label={i18n.t('formatSelectionEnabler')} label={i18n.t('formatSelectionEnabler')}
/> />
<Grid>
<Typography variant="h6" color="primary" sx={{ mt: 2, mb: 0.5 }}>
{i18n.t('overridesAnchor')}
</Typography>
<Stack direction="column">
<FormControlLabel
control={
<Switch
defaultChecked={settings.pathOverriding}
onChange={() => {
dispatch(setPathOverriding(!settings.pathOverriding))
}}
/>
}
label={i18n.t('pathOverrideOption')}
/>
<FormControlLabel
control={
<Switch
defaultChecked={settings.fileRenaming}
onChange={() => {
dispatch(setFileRenaming(!settings.fileRenaming))
}}
/>
}
label={i18n.t('filenameOverrideOption')}
/>
</Stack>
</Grid>
<Grid> <Grid>
<Stack direction="row"> <Stack direction="row">
<Button <Button
@@ -239,7 +270,6 @@ export default function Settings({ socket }: Props) {
> >
{i18n.t('updateBinButton')} {i18n.t('updateBinButton')}
</Button> </Button>
{/* <Button sx={{ mr: 1, mt: 1 }} variant="outlined">Primary</Button> */}
</Stack> </Stack>
</Grid> </Grid>
</FormGroup> </FormGroup>

View File

@@ -18,6 +18,13 @@ languages:
toastConnected: 'Connected to ' toastConnected: 'Connected to '
toastUpdated: Updated yt-dlp binary! toastUpdated: Updated yt-dlp binary!
formatSelectionEnabler: Enable video/audio formats selection formatSelectionEnabler: Enable video/audio formats selection
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filemame (leave blank to use default)
customPath: Custom path
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
@@ -36,6 +43,13 @@ languages:
toastConnected: 'Connesso a ' toastConnected: 'Connesso a '
toastUpdated: yt-dlp aggiornato con successo! toastUpdated: yt-dlp aggiornato con successo!
formatSelectionEnabler: Abilita la selezione dei formati audio/video formatSelectionEnabler: Abilita la selezione dei formati audio/video
themeSelect: 'Tema'
languageSelect: 'Lingua'
overridesAnchor: Sovrascritture
pathOverrideOption: Abilita sovrascrittura percorso di output
filenameOverrideOption: Abilita sovrascrittura del nome del file di output
customFilename: Custom filemame (leave blank to use default)
customPath: Custom path
chinese: chinese:
urlInput: YouTube 或其他受支持服务的视频网址 urlInput: YouTube 或其他受支持服务的视频网址
statusTitle: 状态 statusTitle: 状态
@@ -54,6 +68,13 @@ languages:
toastConnected: '已连接到 ' toastConnected: '已连接到 '
toastUpdated: 已更新 yt-dlp 可执行文件! toastUpdated: 已更新 yt-dlp 可执行文件!
formatSelectionEnabler: 启用视频/音频格式选择 formatSelectionEnabler: 启用视频/音频格式选择
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filemame (leave blank to use default)
customPath: Custom path
spanish: spanish:
urlInput: YouTube or other supported service video url urlInput: YouTube or other supported service video url
statusTitle: Status statusTitle: Status
@@ -72,6 +93,13 @@ languages:
toastConnected: 'Connected to ' toastConnected: 'Connected to '
toastUpdated: Updated yt-dlp binary! toastUpdated: Updated yt-dlp binary!
formatSelectionEnabler: Enable video/audio formats selection formatSelectionEnabler: Enable video/audio formats selection
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filemame (leave blank to use default)
customPath: Custom path
russian: russian:
urlInput: YouTube or other supported service video url urlInput: YouTube or other supported service video url
statusTitle: Status statusTitle: Status
@@ -90,6 +118,13 @@ languages:
toastConnected: 'Connected to ' toastConnected: 'Connected to '
toastUpdated: Updated yt-dlp binary! toastUpdated: Updated yt-dlp binary!
formatSelectionEnabler: Enable video/audio formats selection formatSelectionEnabler: Enable video/audio formats selection
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filemame (leave blank to use default)
customPath: Custom path
korean: korean:
urlInput: YouTube나 다른 지원되는 사이트의 URL urlInput: YouTube나 다른 지원되는 사이트의 URL
statusTitle: 상태 statusTitle: 상태
@@ -108,6 +143,13 @@ languages:
toastConnected: '다음으로 연결됨 ' toastConnected: '다음으로 연결됨 '
toastUpdated: yt-dlp 바이너리를 업데이트 했습니다 toastUpdated: yt-dlp 바이너리를 업데이트 했습니다
formatSelectionEnabler: 비디오/오디오 포멧 옵션 표시 formatSelectionEnabler: 비디오/오디오 포멧 옵션 표시
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filemame (leave blank to use default)
customPath: Custom path
japanese: japanese:
urlInput: YouTubeまたはサポート済み動画のURL urlInput: YouTubeまたはサポート済み動画のURL
statusTitle: 状態 statusTitle: 状態
@@ -126,3 +168,10 @@ languages:
toastConnected: '接続中 ' toastConnected: '接続中 '
toastUpdated: yt-dlpを更新しました! toastUpdated: yt-dlpを更新しました!
formatSelectionEnabler: 選択可能な動画/音源 formatSelectionEnabler: 選択可能な動画/音源
themeSelect: 'Theme'
languageSelect: 'Language'
overridesAnchor: Overrides
pathOverrideOption: Enable output path overriding
filenameOverrideOption: Enable output file name overriding
customFilename: Custom filemame (leave blank to use default)
customPath: Custom path

View File

@@ -1,6 +1,4 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit" import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { CliArguments } from "../../classes"
import { I18nBuilder } from "../../i18n"
export type LanguageUnion = "english" | "chinese" | "russian" | "italian" | "spanish" | "korean" | "japanese" export type LanguageUnion = "english" | "chinese" | "russian" | "italian" | "spanish" | "korean" | "japanese"
export type ThemeUnion = "light" | "dark" export type ThemeUnion = "light" | "dark"
@@ -13,6 +11,8 @@ export interface SettingsState {
cliArgs: string, cliArgs: string,
formatSelection: boolean, formatSelection: boolean,
ratelimit: string, ratelimit: string,
fileRenaming: boolean,
pathOverriding: boolean,
} }
const initialState: SettingsState = { const initialState: SettingsState = {
@@ -23,6 +23,8 @@ const initialState: SettingsState = {
cliArgs: localStorage.getItem("cli-args") ?? "", cliArgs: localStorage.getItem("cli-args") ?? "",
formatSelection: localStorage.getItem("format-selection") === "true", formatSelection: localStorage.getItem("format-selection") === "true",
ratelimit: localStorage.getItem("rate-limit") ?? "", ratelimit: localStorage.getItem("rate-limit") ?? "",
fileRenaming: localStorage.getItem("file-renaming") === "true",
pathOverriding: localStorage.getItem("path-overriding") === "true",
} }
export const settingsSlice = createSlice({ export const settingsSlice = createSlice({
@@ -57,9 +59,27 @@ export const settingsSlice = createSlice({
state.ratelimit = action.payload state.ratelimit = action.payload
localStorage.setItem("rate-limit", action.payload) localStorage.setItem("rate-limit", action.payload)
}, },
setPathOverriding: (state, action: PayloadAction<boolean>) => {
state.pathOverriding = action.payload
localStorage.setItem("path-overriding", action.payload.toString())
},
setFileRenaming: (state, action: PayloadAction<boolean>) => {
state.fileRenaming = action.payload
localStorage.setItem("file-renaming", action.payload.toString())
},
} }
}) })
export const { setLanguage, setCliArgs, setTheme, setServerAddr, setServerPort, setFormatSelection, setRateLimit } = settingsSlice.actions export const {
setLanguage,
setCliArgs,
setTheme,
setServerAddr,
setServerPort,
setFormatSelection,
setRateLimit,
setFileRenaming,
setPathOverriding,
} = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

View File

@@ -23,6 +23,7 @@ class Process {
private pid: number; private pid: number;
private metadata?: IDownloadMetadata; private metadata?: IDownloadMetadata;
private exePath = join(__dirname, 'yt-dlp'); private exePath = join(__dirname, 'yt-dlp');
private customFileName?: string;
private readonly template = `download: private readonly template = `download:
{ {
@@ -34,13 +35,14 @@ class Process {
.replace(/\s\s+/g, ' ') .replace(/\s\s+/g, ' ')
.replace('\n', ''); .replace('\n', '');
constructor(url: string, params: Array<string>, settings: any) { constructor(url: string, params: Array<string>, settings: any, customFileName?: string) {
this.url = url; this.url = url;
this.params = params || []; this.params = params || [];
this.settings = settings this.settings = settings
this.stdout = undefined; this.stdout = undefined;
this.pid = undefined; this.pid = undefined;
this.metadata = undefined; this.metadata = undefined;
this.customFileName = customFileName;
} }
/** /**
@@ -59,7 +61,7 @@ class Process {
const ytldp = spawn(this.exePath, const ytldp = spawn(this.exePath,
[ [
'-o', `${this.settings?.download_path || 'downloads/'}%(title)s.%(ext)s`, '-o', `${this.settings?.download_path || 'downloads/'}${this.customFileName || '%(title)s'}.%(ext)s`,
'--progress-template', this.template, '--progress-template', this.template,
'--no-colors', '--no-colors',
] ]
@@ -73,6 +75,10 @@ class Process {
log.info('proc', `Spawned a new process, pid: ${this.pid}`) log.info('proc', `Spawned a new process, pid: ${this.pid}`)
if (callback) {
callback()
}
return this; return this;
} }

View File

@@ -62,12 +62,14 @@ export async function download(socket: Socket, payload: IPayload) {
payload.params.split(' ') : payload.params.split(' ') :
payload.params; payload.params;
const renameTo = payload.renameTo
const scopedSettings: ISettings = { const scopedSettings: ISettings = {
...settings, ...settings,
download_path: payload.path download_path: payload.path
} }
let p = new Process(url, params, scopedSettings); let p = new Process(url, params, scopedSettings, renameTo);
p.start().then(downloader => { p.start().then(downloader => {
mem_db.add(downloader) mem_db.add(downloader)

View File

@@ -4,9 +4,10 @@
export interface IPayload { export interface IPayload {
url: string url: string
params: Array<string> | string, params: Array<string> | string
path: string, path: string
title?: string, title?: string
thumbnail?: string, thumbnail?: string
size?: string, size?: string
renameTo?: string
} }