diff --git a/frontend/src/assets/i18n.yaml b/frontend/src/assets/i18n.yaml index ad4c283..fd8f302 100644 --- a/frontend/src/assets/i18n.yaml +++ b/frontend/src/assets/i18n.yaml @@ -1,7 +1,7 @@ --- languages: english: - urlInput: Video URL + urlInput: Video URL (one per line) statusTitle: Status statusReady: Ready selectFormatButton: Select format @@ -150,7 +150,7 @@ languages: logsTitle: 'Logs' awaitingLogs: 'Awaiting logs...' italian: - urlInput: URL Video + urlInput: URL Video (uno per linea) statusTitle: Stato startButton: Inizia statusReady: Pronto diff --git a/frontend/src/atoms/ui.ts b/frontend/src/atoms/ui.ts index 00d8e6c..3ba1c19 100644 --- a/frontend/src/atoms/ui.ts +++ b/frontend/src/atoms/ui.ts @@ -1,6 +1,12 @@ import { atom } from 'recoil' +import { RPCResult } from '../types' export const loadingAtom = atom({ key: 'loadingAtom', default: true +}) + +export const optimisticDownloadsState = atom({ + key: 'optimisticDownloadsState', + default: [] }) \ No newline at end of file diff --git a/frontend/src/components/DownloadDialog.tsx b/frontend/src/components/DownloadDialog.tsx index 2c12a08..dae1a4b 100644 --- a/frontend/src/components/DownloadDialog.tsx +++ b/frontend/src/components/DownloadDialog.tsx @@ -80,7 +80,6 @@ const DownloadDialog: FC = ({ open, onClose, onDownloadStart }) => { ) const [url, setUrl] = useState('') - const [workingUrl, setWorkingUrl] = useState('') const [isPlaylist, setIsPlaylist] = useState(false) @@ -103,35 +102,36 @@ const DownloadDialog: FC = ({ open, onClose, onDownloadStart }) => { /** * Retrive url from input, cli-arguments from checkboxes and emits via WebSocket */ - const sendUrl = (immediate?: string) => { - const codes = new Array() - if (pickedVideoFormat !== '') codes.push(pickedVideoFormat) - if (pickedAudioFormat !== '') codes.push(pickedAudioFormat) - if (pickedBestFormat !== '') codes.push(pickedBestFormat) + const sendUrl = async (immediate?: string) => { + for (const line of url.split('\n')) { + const codes = new Array() + if (pickedVideoFormat !== '') codes.push(pickedVideoFormat) + if (pickedAudioFormat !== '') codes.push(pickedAudioFormat) + if (pickedBestFormat !== '') codes.push(pickedBestFormat) - client.download({ - url: immediate || url || workingUrl, - args: `${argsBuilder.toString()} ${toFormatArgs(codes)} ${downloadTemplate}`, - pathOverride: downloadPath ?? '', - renameTo: settings.fileRenaming ? filenameTemplate : '', - playlist: isPlaylist, - }) + await new Promise(r => setTimeout(r, 200)) + await client.download({ + url: immediate || line, + args: `${argsBuilder.toString()} ${toFormatArgs(codes)} ${downloadTemplate}`, + pathOverride: downloadPath ?? '', + renameTo: settings.fileRenaming ? filenameTemplate : '', + playlist: isPlaylist, + }) + + setTimeout(() => { + resetInput() + setDownloadFormats(undefined) + onDownloadStart(immediate || line) + }, 250) + } setUrl('') - setWorkingUrl('') - - setTimeout(() => { - resetInput() - setDownloadFormats(undefined) - onDownloadStart(immediate || url || workingUrl) - }, 250) } /** * Retrive url from input and display the formats selection view */ const sendUrlFormatSelection = () => { - setWorkingUrl(url) setUrl('') setPickedAudioFormat('') setPickedVideoFormat('') @@ -220,6 +220,7 @@ const DownloadDialog: FC = ({ open, onClose, onDownloadStart }) => { > { const settings = useRecoilValue(settingsState) @@ -30,8 +30,12 @@ const Footer: React.FC = () => { fontSize: 14, display: 'flex', gap: 1, justifyContent: 'space-between' }}> -
v3.0.6
-
+
+
RPC v3.0.6
+
+ +
+
{ const [, setIsLoading] = useRecoilState(loadingAtom) + const [optimistic, setOptimistic] = useRecoilState(optimisticDownloadsState) const [openDownload, setOpenDownload] = useState(false) const [openEditor, setOpenEditor] = useState(false) const { pushMessage } = useToast() + // it's stupid because it will be overriden on the next server tick + const handleOptimisticUpdate = (url: string) => setOptimistic([ + ...optimistic, { + id: url, + info: { + created_at: new Date().toISOString(), + thumbnail: '', + title: url, + url: url + }, + progress: { + eta: Number.MAX_SAFE_INTEGER, + percentage: '0%', + process_status: 0, + speed: 0 + } + } + ]) + return ( <> { setOpenDownload(false) setIsLoading(true) }} + // TODO: handle optimistic UI update onDownloadStart={(url) => { + handleOptimisticUpdate(url) pushMessage(`Requested ${url}`, 'info') setOpenDownload(false) setIsLoading(true) diff --git a/frontend/src/components/SocketSubscriber.tsx b/frontend/src/components/SocketSubscriber.tsx index 43a9f4c..2ad6e1a 100644 --- a/frontend/src/components/SocketSubscriber.tsx +++ b/frontend/src/components/SocketSubscriber.tsx @@ -52,7 +52,7 @@ const SocketSubscriber: React.FC = () => { .filter(f => !!f.info.url).sort((a, b) => datetimeCompareFunc( b.info.created_at, a.info.created_at, - )) + )), ) ) } diff --git a/frontend/src/components/VersionIndicator.tsx b/frontend/src/components/VersionIndicator.tsx new file mode 100644 index 0000000..9c6fd5c --- /dev/null +++ b/frontend/src/components/VersionIndicator.tsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react' +import { useRecoilValue } from 'recoil' +import { serverURL } from '../atoms/settings' +import { CircularProgress } from '@mui/material' +import { useToast } from '../hooks/toast' + +const VersionIndicator: React.FC = () => { + const serverAddr = useRecoilValue(serverURL) + + const [version, setVersion] = useState('') + const { pushMessage } = useToast() + + const fetchVersion = async () => { + const res = await fetch(`${serverAddr}/api/v1/version`) + + if (!res.ok) { + return pushMessage(await res.text(), 'error') + } + + setVersion(await res.json()) + } + + useEffect(() => { + fetchVersion() + }, []) + + return ( + version + ?
yt-dlp v{version}
+ : + ) +} + +export default VersionIndicator \ No newline at end of file diff --git a/server/rest/container.go b/server/rest/container.go index a85db8b..6395d71 100644 --- a/server/rest/container.go +++ b/server/rest/container.go @@ -26,6 +26,7 @@ func ApplyRouter(db *sql.DB, mdb *internal.MemoryDB, mq *internal.MessageQueue) } r.Post("/exec", h.Exec()) r.Get("/running", h.Running()) + r.Get("/version", h.GetVersion()) r.Post("/cookies", h.SetCookies()) r.Post("/template", h.AddTemplate()) r.Get("/template/all", h.GetTemplates()) diff --git a/server/rest/handlers.go b/server/rest/handlers.go index e1db849..213cc24 100644 --- a/server/rest/handlers.go +++ b/server/rest/handlers.go @@ -158,3 +158,21 @@ func (h *Handler) DeleteTemplate() http.HandlerFunc { } } } + +func (h *Handler) GetVersion() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + w.Header().Set("Content-Type", "application/json") + + version, err := h.service.GetVersion(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err := json.NewEncoder(w).Encode(version); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} diff --git a/server/rest/service.go b/server/rest/service.go index 84b9baa..6047d8d 100644 --- a/server/rest/service.go +++ b/server/rest/service.go @@ -6,8 +6,11 @@ import ( "errors" "log/slog" "os" + "os/exec" + "time" "github.com/google/uuid" + "github.com/marcopeocchi/yt-dlp-web-ui/server/config" "github.com/marcopeocchi/yt-dlp-web-ui/server/internal" ) @@ -118,3 +121,23 @@ func (s *Service) DeleteTemplate(ctx context.Context, id string) error { return err } + +func (s *Service) GetVersion(ctx context.Context) (string, error) { + ch := make(chan string, 1) + + c, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + cmd := exec.CommandContext(c, config.Instance().DownloaderPath, "--version") + go func() { + stdout, _ := cmd.Output() + ch <- string(stdout) + }() + + select { + case <-c.Done(): + return "", errors.New("requesting yt-dlp version took too long") + case res := <-ch: + return res, nil + } +}