import { io } from "socket.io-client"; import React, { useState, useEffect, useRef, Fragment } from "react"; import { Container, Row, Col, InputGroup, FormControl, Button, ButtonGroup, } from "react-bootstrap"; import { StopFill, GearFill, Translate } from "react-bootstrap-icons"; import { buildMessage, updateInStateMap, validateDomain, validateIP } from "./utils"; import { IDLInfo, IDLInfoBase, IMessage } from "./interfaces"; import { MessageToast } from "./components/MessageToast"; import { StackableResult } from "./components/StackableResult"; import { CliArguments } from "./classes"; import { I18nBuilder } from "./i18n"; import './App.css'; import { Footer } from "./components/Footer"; const socket = io(`http://${localStorage.getItem('server-addr') || 'localhost'}:3022`) export function App() { const [progressMap, setProgressMap] = useState(new Map()); const [messageMap, setMessageMap] = useState(new Map()); const [downloadInfoMap, setDownloadInfoMap] = useState(new Map()); const [halt, setHalt] = useState(false); const [url, setUrl] = useState(''); const [showToast, setShowToast] = useState(false); const [invalidIP, setInvalidIP] = useState(false); const [connected, setConnected] = useState(false); const [updatedBin, setUpdatedBin] = useState(false); const [showSettings, setShowSettings] = useState(false); const [showLanguages, setShowLanguages] = useState(false); const [freeDiskSpace, setFreeDiskSpace] = useState(''); const [darkMode, setDarkMode] = useState(localStorage.getItem('theme') === 'dark'); const [language, setLanguage] = useState(localStorage.getItem('language') || 'english'); const xaInput = useRef(null); const mtInput = useRef(null); /* -------------------- Init ----------------------- */ const cliArgs = new CliArguments(); if (!localStorage.getItem('cliArgs')) { localStorage.setItem('cliArgs', '') } cliArgs.fromString(localStorage.getItem('cliArgs')) const i18n = new I18nBuilder(language); /* -------------------- Effects -------------------- */ /* WebSocket connect event handler*/ useEffect(() => { socket.on('connect', () => { setConnected(true) setShowToast(true) socket.emit('fetch-jobs') socket.emit('disk-space') }) return () => { socket.disconnect() } }, []) /* Ask server for pending jobs / background jobs */ useEffect(() => { socket.on('pending-jobs', () => { socket.emit('retrieve-jobs') }) }, []) /* Handle download information sent by server */ useEffect(() => { socket.on('info', (data: IDLInfo) => { updateInStateMap(data.pid, data.info, downloadInfoMap, setDownloadInfoMap); }) }, []) /* Handle per-download progress */ useEffect(() => { socket.on('progress', (data: IMessage) => { if (data.status === 'Done!' || data.status === 'Aborted') { setHalt(false); updateInStateMap(data.pid, 'Done!', messageMap, setMessageMap); updateInStateMap(data.pid, 0, progressMap, setProgressMap); socket.emit('disk-space') return; } updateInStateMap(data.pid, buildMessage(data), messageMap, setMessageMap); if (data.progress) { updateInStateMap(data.pid, Math.ceil(Number(data.progress.replace('%', ''))), progressMap, setProgressMap) } }) }, []) /* Handle yt-dlp update success */ useEffect(() => { socket.on('updated', () => { setUpdatedBin(true) setHalt(false) }) }, []) /* Theme changer */ useEffect(() => { darkMode ? document.body.classList.add('dark') : document.body.classList.remove('dark'); }, [darkMode]) /* Get disk free space */ useEffect(() => { socket.on('free-space', (res: string) => { setFreeDiskSpace(res) }) }, []) /* Change language */ useEffect(() => { i18n.setLanguage(language) }, [language]) /* -------------------- component functions -------------------- */ /** * Retrive url from input, cli-arguments from checkboxes and emits via WebSocket */ const sendUrl = () => { setHalt(true) socket.emit('send-url', { url: url, params: cliArgs.toString(), }) setUrl('') const input = document.getElementById('urlInput') as HTMLInputElement; input.value = ''; } /** * Update the url state whenever the input value changes * @param e Input change event */ const handleUrlChange = (e: React.ChangeEvent) => { setUrl(e.target.value) } /** * Update the server ip address state and localstorage whenever the input value changes. * Validate the ip-addr then set. * @param e Input change event */ const handleAddrChange = (e: React.ChangeEvent) => { const input = e.target.value; if (validateIP(input)) { setInvalidIP(false) localStorage.setItem('server-addr', input) } else if (validateDomain(input)) { setInvalidIP(false) localStorage.setItem('server-addr', input) } else { setInvalidIP(true) } } /** * Abort a specific download if id's provided, other wise abort all running ones. * @param id The download id / pid * @returns void */ const abort = (id?: number) => { if (id) { updateInStateMap(id, null, downloadInfoMap, setDownloadInfoMap, true) socket.emit('abort', { pid: id }) return } socket.emit('abort-all') setHalt(false) } /** * Send via WebSocket a message in order to update the yt-dlp binary from server */ const updateBinary = () => { setHalt(true) socket.emit('update-bin') } /** * Theme toggler handler */ const toggleTheme = () => { if (darkMode) { localStorage.setItem('theme', 'light') setDarkMode(false) } else { localStorage.setItem('theme', 'dark') setDarkMode(true) } } /** * Handle extract audio checkbox */ const setExtractAudio = () => { if (cliArgs.extractAudio) { xaInput.current.checked = false; cliArgs.extractAudio = false; const lStorageItem = localStorage.getItem('cliArgs'); localStorage.setItem('cliArgs', lStorageItem.replace('-x ', '')); } else { xaInput.current.checked = true; cliArgs.extractAudio = true; const lStorageItem = localStorage.getItem('cliArgs'); localStorage.setItem('cliArgs', lStorageItem.concat('-x ', '')); } } /** * Handle no modified time header checkbox */ const setNoMTime = () => { if (cliArgs.noMTime) { mtInput.current.checked = false; cliArgs.noMTime = false; const lStorageItem = localStorage.getItem('cliArgs'); localStorage.setItem('cliArgs', lStorageItem.replace('--no-mtime ', '')); } else { mtInput.current.checked = true; cliArgs.noMTime = true; const lStorageItem = localStorage.getItem('cliArgs'); localStorage.setItem('cliArgs', lStorageItem.concat('--no-mtime ', '')); } } /** * Language toggler handler */ const handleLanguageChage = (event: React.ChangeEvent) => { setLanguage(event.target.value); setShowLanguages(false); localStorage.setItem('language', event.target.value); } return (

yt-dlp WebUI

{ !Array.from(messageMap).length ?
{i18n.t('statusTitle')}
{i18n.t('statusReady')}
: null } { /*Super big brain flatMap moment*/ Array.from(messageMap).flatMap(message => ( { /* Message[0] => key, the pid which is shared with the progress and download Maps Message[1] => value, the actual formatted message sent from server */ } {message[0] && message[1] && message[1] !== 'Done!' ? : null } )) }
setShowSettings(!showSettings)}> {' '} setShowLanguages(!showLanguages)}> {showLanguages ? : null }
{showSettings ?
{i18n.t('serverAddressTitle')}
ws:// :3022

{' '}
: null }
{i18n.t('bgReminder')}
<> {i18n.t('toastConnected')}{localStorage.getItem('server-addr') || 'localhost'} {i18n.t('toastUpdated')}
) }