diff --git a/frontend/src/Home.tsx b/frontend/src/Home.tsx index c63bea4..d87e077 100644 --- a/frontend/src/Home.tsx +++ b/frontend/src/Home.tsx @@ -9,6 +9,7 @@ import { Grid, IconButton, InputAdornment, + InputLabel, MenuItem, Paper, Select, @@ -49,9 +50,11 @@ export default function Home({ socket }: Props) { const [pickedAudioFormat, setPickedAudioFormat] = useState(''); const [pickedBestFormat, setPickedBestFormat] = useState(''); - const [downloadPath, setDownloadPath] = useState(0); + const [downloadPath, setDownloadPath] = useState(0); const [availableDownloadPaths, setAvailableDownloadPaths] = useState([]); + const [fileNameOverride, setFilenameOverride] = useState(''); + const [url, setUrl] = useState(''); const [workingUrl, setWorkingUrl] = useState(''); const [showBackdrop, setShowBackdrop] = useState(false); @@ -141,14 +144,17 @@ export default function Home({ socket }: Props) { url: immediate || url || workingUrl, path: availableDownloadPaths[downloadPath], params: cliArgs.toString() + toFormatArgs(codes), + renameTo: fileNameOverride, }) + setUrl('') setWorkingUrl('') + setFilenameOverride('') + setTimeout(() => { - const input = document.getElementById('urlInput') as HTMLInputElement; - input.value = ''; - setShowBackdrop(true); - setDownloadFormats(undefined); + resetInput() + setShowBackdrop(true) + setDownloadFormats(undefined) }, 250); } @@ -159,16 +165,17 @@ export default function Home({ socket }: Props) { socket.emit('send-url-format-selection', { url: url, }) + setWorkingUrl(url) setUrl('') - setPickedAudioFormat(''); - setPickedVideoFormat(''); - setPickedBestFormat(''); + setPickedAudioFormat('') + setPickedVideoFormat('') + setPickedBestFormat('') + setTimeout(() => { - const input = document.getElementById('urlInput') as HTMLInputElement; - input.value = ''; + resetInput() setShowBackdrop(true) - }, 250); + }, 250) } /** @@ -179,6 +186,14 @@ export default function Home({ socket }: Props) { setUrl(e.target.value) } + /** + * Update the filename override state whenever the input value changes + * @param e Input change event + */ + const handleFilenameOverrideChange = (e: React.ChangeEvent) => { + setFilenameOverride(e.target.value) + } + /** * Abort a specific download if id's provided, other wise abort all running ones. * @param id The download id / pid @@ -209,6 +224,16 @@ export default function Home({ socket }: Props) { 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 -------------------- */ const Input = styled('input')({ @@ -232,43 +257,63 @@ export default function Home({ socket }: Props) { flexDirection: 'column', }} > - - - - - - ), - }} - /> - - - - - - + + + + + ), + }} + /> + + + { + settings.fileRenaming ? + + + : + null + } + { + settings.pathOverriding ? + + + {i18n.t('customPath')} + + + : + null + } @@ -291,119 +336,121 @@ export default function Home({ socket }: Props) { - + {/* Format Selection grid */} - {downloadFormats ? - - - - - - {downloadFormats.title} - - {/* */} - - - - - {/* video only */} - - - Best quality - - - - - - {/* video only */} - {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? + { + downloadFormats ? + + + + + + {downloadFormats.title} + + {/* */} + + + + + {/* video only */} - Video data {downloadFormats.formats[1].acodec} + Best quality - : null - } - {downloadFormats.formats - .filter(format => format.acodec === 'none' && format.vcodec !== 'none') - .map((format, idx) => ( - - - - )) - } - {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? - - - Audio data - - - : null - } - {downloadFormats.formats - .filter(format => format.acodec !== 'none' && format.vcodec === 'none') - .map((format, idx) => ( - - - - )) - } - - - + - + + {/* video only */} + {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? + + + Video data {downloadFormats.formats[1].acodec} + + + : null + } + {downloadFormats.formats + .filter(format => format.acodec === 'none' && format.vcodec !== 'none') + .map((format, idx) => ( + + + + )) + } + {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? + + + Audio data + + + : null + } + {downloadFormats.formats + .filter(format => format.acodec !== 'none' && format.vcodec === 'none') + .map((format, idx) => ( + + + + )) + } + + + + + + - - - - : null} + + + : null + } - { /*Super big brain flatMap moment*/ + { Array .from(messageMap) .filter(flattened => [...flattened][0]) @@ -440,6 +487,6 @@ export default function Home({ socket }: Props) { message="Connected" onClose={() => setShowToast(false)} /> - + ); } \ No newline at end of file diff --git a/frontend/src/Settings.tsx b/frontend/src/Settings.tsx index 9d0cd8a..7ea39fe 100644 --- a/frontend/src/Settings.tsx +++ b/frontend/src/Settings.tsx @@ -25,8 +25,10 @@ import { CliArguments } from "./classes"; import { LanguageUnion, setCliArgs, + setFileRenaming, setFormatSelection, setLanguage, + setPathOverriding, setServerAddr, setServerPort, setTheme, @@ -158,10 +160,10 @@ export default function Settings({ socket }: Props) { - Language + {i18n.t('languageSelect')} Light @@ -230,6 +232,35 @@ export default function Settings({ socket }: Props) { } label={i18n.t('formatSelectionEnabler')} /> + + + {i18n.t('overridesAnchor')} + + + { + dispatch(setPathOverriding(!settings.pathOverriding)) + }} + /> + } + label={i18n.t('pathOverrideOption')} + /> + { + dispatch(setFileRenaming(!settings.fileRenaming)) + }} + /> + } + label={i18n.t('filenameOverrideOption')} + /> + + - {/* */} diff --git a/frontend/src/assets/i18n.yaml b/frontend/src/assets/i18n.yaml index 7343cb0..96fadcb 100644 --- a/frontend/src/assets/i18n.yaml +++ b/frontend/src/assets/i18n.yaml @@ -18,6 +18,13 @@ languages: toastConnected: 'Connected to ' toastUpdated: Updated yt-dlp binary! 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: urlInput: URL di YouTube o di qualsiasi altro servizio supportato statusTitle: Stato @@ -36,6 +43,13 @@ languages: toastConnected: 'Connesso a ' toastUpdated: yt-dlp aggiornato con successo! 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: urlInput: YouTube 或其他受支持服务的视频网址 statusTitle: 状态 @@ -54,6 +68,13 @@ languages: toastConnected: '已连接到 ' toastUpdated: 已更新 yt-dlp 可执行文件! 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: urlInput: YouTube or other supported service video url statusTitle: Status @@ -72,6 +93,13 @@ languages: toastConnected: 'Connected to ' toastUpdated: Updated yt-dlp binary! 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: urlInput: YouTube or other supported service video url statusTitle: Status @@ -90,6 +118,13 @@ languages: toastConnected: 'Connected to ' toastUpdated: Updated yt-dlp binary! 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: urlInput: YouTube나 다른 지원되는 사이트의 URL statusTitle: 상태 @@ -108,6 +143,13 @@ languages: toastConnected: '다음으로 연결됨 ' toastUpdated: yt-dlp 바이너리를 업데이트 했습니다 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: urlInput: YouTubeまたはサポート済み動画のURL statusTitle: 状態 @@ -126,3 +168,10 @@ languages: toastConnected: '接続中 ' toastUpdated: yt-dlpを更新しました! 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 \ No newline at end of file diff --git a/frontend/src/features/settings/settingsSlice.ts b/frontend/src/features/settings/settingsSlice.ts index 9660e42..e0a7cf8 100644 --- a/frontend/src/features/settings/settingsSlice.ts +++ b/frontend/src/features/settings/settingsSlice.ts @@ -1,6 +1,4 @@ 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 ThemeUnion = "light" | "dark" @@ -13,6 +11,8 @@ export interface SettingsState { cliArgs: string, formatSelection: boolean, ratelimit: string, + fileRenaming: boolean, + pathOverriding: boolean, } const initialState: SettingsState = { @@ -23,6 +23,8 @@ const initialState: SettingsState = { cliArgs: localStorage.getItem("cli-args") ?? "", formatSelection: localStorage.getItem("format-selection") === "true", ratelimit: localStorage.getItem("rate-limit") ?? "", + fileRenaming: localStorage.getItem("file-renaming") === "true", + pathOverriding: localStorage.getItem("path-overriding") === "true", } export const settingsSlice = createSlice({ @@ -57,9 +59,27 @@ export const settingsSlice = createSlice({ state.ratelimit = action.payload localStorage.setItem("rate-limit", action.payload) }, + setPathOverriding: (state, action: PayloadAction) => { + state.pathOverriding = action.payload + localStorage.setItem("path-overriding", action.payload.toString()) + }, + setFileRenaming: (state, action: PayloadAction) => { + 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 \ No newline at end of file diff --git a/server/src/core/Process.ts b/server/src/core/Process.ts index 73b4f0b..c14914f 100644 --- a/server/src/core/Process.ts +++ b/server/src/core/Process.ts @@ -23,6 +23,7 @@ class Process { private pid: number; private metadata?: IDownloadMetadata; private exePath = join(__dirname, 'yt-dlp'); + private customFileName?: string; private readonly template = `download: { @@ -34,13 +35,14 @@ class Process { .replace(/\s\s+/g, ' ') .replace('\n', ''); - constructor(url: string, params: Array, settings: any) { + constructor(url: string, params: Array, settings: any, customFileName?: string) { this.url = url; this.params = params || []; this.settings = settings this.stdout = undefined; this.pid = undefined; this.metadata = undefined; + this.customFileName = customFileName; } /** @@ -59,7 +61,7 @@ class Process { 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, '--no-colors', ] @@ -73,6 +75,10 @@ class Process { log.info('proc', `Spawned a new process, pid: ${this.pid}`) + if (callback) { + callback() + } + return this; } diff --git a/server/src/core/downloader.ts b/server/src/core/downloader.ts index 1910974..dd1d53b 100644 --- a/server/src/core/downloader.ts +++ b/server/src/core/downloader.ts @@ -62,12 +62,14 @@ export async function download(socket: Socket, payload: IPayload) { payload.params.split(' ') : payload.params; + const renameTo = payload.renameTo + const scopedSettings: ISettings = { ...settings, download_path: payload.path } - let p = new Process(url, params, scopedSettings); + let p = new Process(url, params, scopedSettings, renameTo); p.start().then(downloader => { mem_db.add(downloader) @@ -114,7 +116,7 @@ function streamProcess(process: Process, socket: Socket) { }); } - from(process.getStdout().removeAllListeners()) // stdout as observable + from(process.getStdout().removeAllListeners()) // stdout as observable .pipe( throttle(() => interval(500)), // discard events closer than 500ms map(stdout => formatter(String(stdout), process.getPid())) diff --git a/server/src/interfaces/IPayload.d.ts b/server/src/interfaces/IPayload.d.ts index e18ba11..ab6e953 100644 --- a/server/src/interfaces/IPayload.d.ts +++ b/server/src/interfaces/IPayload.d.ts @@ -4,9 +4,10 @@ export interface IPayload { url: string - params: Array | string, - path: string, - title?: string, - thumbnail?: string, - size?: string, + params: Array | string + path: string + title?: string + thumbnail?: string + size?: string + renameTo?: string } \ No newline at end of file