From 746aaacea4248153490c68f83ec431c7041ce58d Mon Sep 17 00:00:00 2001 From: FTCHD <144691102+FTCHD@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:33:35 +0100 Subject: [PATCH] DOWNLOAD (ga) Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com> --- lib/rclone/api.ts | 2 +- package-lock.json | 162 +++++++++++++++++++++++++++++++++ package.json | 1 + src/pages/Download.tsx | 201 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 341 insertions(+), 25 deletions(-) diff --git a/lib/rclone/api.ts b/lib/rclone/api.ts index dcad566..9e0eb0e 100644 --- a/lib/rclone/api.ts +++ b/lib/rclone/api.ts @@ -715,7 +715,7 @@ export async function startDownload({ fs, remote, url, - autoFilename = true, + autoFilename = false, }: { fs: string // a remote name string e.g. "drive:" remote: string // a path within that remote e.g. "dir" diff --git a/package-lock.json b/package-lock.json index 086dfbc..4616f9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@formkit/auto-animate": "^0.9.0", "@heroui/react": "^2.7.11", + "@mux/mux-player-react": "^3.8.0", "@sentry/browser": "^10.8.0", "@tauri-apps/api": "~2.8.0", "@tauri-apps/plugin-autostart": "^2.5.0", @@ -2625,6 +2626,74 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mux/mux-data-google-ima": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@mux/mux-data-google-ima/-/mux-data-google-ima-0.2.8.tgz", + "integrity": "sha512-0ZEkHdcZ6bS8QtcjFcoJeZxJTpX7qRIledf4q1trMWPznugvtajCjCM2kieK/pzkZj1JM6liDRFs1PJSfVUs2A==", + "license": "MIT", + "dependencies": { + "mux-embed": "5.9.0" + } + }, + "node_modules/@mux/mux-player": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-3.8.0.tgz", + "integrity": "sha512-2KcJdW4BBX8JDcXpclFKaNBsqpebtaEfTzwm5lPP1Lf6y5OMILvf2tqVCOczurREVFyaEoVD71vL0I5Vvqb1dA==", + "license": "MIT", + "dependencies": { + "@mux/mux-video": "0.27.2", + "@mux/playback-core": "0.31.2", + "media-chrome": "~4.15.1", + "player.style": "^0.3.0" + } + }, + "node_modules/@mux/mux-player-react": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-3.8.0.tgz", + "integrity": "sha512-c9TKtK9nsSpXOuC1LVLmmHA+Zlpcx4mzgGaA7ZlukrGMfoXWvA90ROSVAAjXRA+UKSHdLIbvNofgG3P6rEE/4Q==", + "license": "MIT", + "dependencies": { + "@mux/mux-player": "3.8.0", + "@mux/playback-core": "0.31.2", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0", + "react": "^17.0.2 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0", + "react-dom": "^17.0.2 || ^17.0.2-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@mux/mux-video": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.27.2.tgz", + "integrity": "sha512-VAqSw/3kS/qBzjyFSX3wClIX5Kdk6eXXlhxIJRWlClYvUKGm9ruhd7HzkwZVOJguvUh5QbGoiGWBEW2xkNIXzw==", + "license": "MIT", + "dependencies": { + "@mux/mux-data-google-ima": "0.2.8", + "@mux/playback-core": "0.31.2", + "castable-video": "~1.1.11", + "custom-media-element": "~1.4.5", + "media-tracks": "~0.3.3" + } + }, + "node_modules/@mux/playback-core": { + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.31.2.tgz", + "integrity": "sha512-bhOVTGAuKCQuDzNOc3XvDq7vsgqy2DAacLP0WdJciUKjfZhs3oA11NbKG7qAN6akPnZVfgn0Jn/sJN8TRjE30A==", + "license": "MIT", + "dependencies": { + "hls.js": "~1.6.13", + "mux-embed": "^5.8.3" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5890,6 +5959,24 @@ ], "license": "CC-BY-4.0" }, + "node_modules/castable-video": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/castable-video/-/castable-video-1.1.11.tgz", + "integrity": "sha512-LCRTK6oe7SB1SiUQFzZCo6D6gcEzijqBTVIuj3smKpQdesXM18QTbCVqWgh9MfOeQgTx/i9ji5jGcdqNPeWg2g==", + "license": "MIT", + "dependencies": { + "custom-media-element": "~1.4.5" + } + }, + "node_modules/ce-la-react": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/ce-la-react/-/ce-la-react-0.3.1.tgz", + "integrity": "sha512-g0YwpZDPIwTwFumGTzNHcgJA6VhFfFCJkSNdUdC04br2UfU+56JDrJrJva3FZ7MToB4NDHAFBiPE/PZdNl1mQA==", + "license": "BSD-3-Clause", + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6050,6 +6137,12 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "devOptional": true }, + "node_modules/custom-media-element": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/custom-media-element/-/custom-media-element-1.4.5.tgz", + "integrity": "sha512-cjrsQufETwxjvwZbYbKBCJNvmQ2++G9AvT45zDi7NXL9k2PdVcs2h0jQz96J6G4TMKRCcEsoJ+QTgQD00Igtjw==", + "license": "MIT" + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -6353,6 +6446,12 @@ "node": ">= 0.4" } }, + "node_modules/hls.js": { + "version": "1.6.14", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.14.tgz", + "integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==", + "license": "Apache-2.0" + }, "node_modules/input-otp": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz", @@ -6565,6 +6664,21 @@ "node": ">=12" } }, + "node_modules/media-chrome": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.15.1.tgz", + "integrity": "sha512-Hxqr0qQ67ewmRaLJBqe5ayu53txFX+DODb9xBSHgTbw7j+gITGZ4llbPPEmqMlDnatw7IsF+AUh9rJYbpnn4ZQ==", + "license": "MIT", + "dependencies": { + "ce-la-react": "^0.3.0" + } + }, + "node_modules/media-tracks": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/media-tracks/-/media-tracks-0.3.3.tgz", + "integrity": "sha512-9P2FuUHnZZ3iji+2RQk7Zkh5AmZTnOG5fODACnjhCVveX1McY3jmCRHofIEI+yTBqplz7LXy48c7fQ3Uigp88w==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6629,6 +6743,12 @@ "dev": true, "license": "MIT" }, + "node_modules/mux-embed": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/mux-embed/-/mux-embed-5.9.0.tgz", + "integrity": "sha512-wmunL3uoPhma/tWy8PrDPZkvJpXvSFBwbD3KkC4PG8Ztjfb1X3hRJwGUAQyRz7z99b/ovLm2UTTitrkvStjH4w==", + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -6782,6 +6902,31 @@ "node": ">= 6" } }, + "node_modules/player.style": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/player.style/-/player.style-0.3.0.tgz", + "integrity": "sha512-ny1TbqA2ZsUd6jzN+F034+UMXVK7n5SrwepsrZ2gIqVz00Hn0ohCUbbUdst/2IOFCy0oiTbaOXkSFxRw1RmSlg==", + "license": "MIT", + "workspaces": [ + ".", + "site", + "examples/*", + "scripts/*", + "themes/*" + ], + "dependencies": { + "media-chrome": "~4.14.0" + } + }, + "node_modules/player.style/node_modules/media-chrome": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.14.0.tgz", + "integrity": "sha512-IEdFb4blyF15vLvQzLIn6USJBv7Kf2ne+TfLQKBYI5Z0f9VEBVZz5MKy4Uhi0iA9lStl2S9ENIujJRuJIa5OiA==", + "license": "MIT", + "dependencies": { + "ce-la-react": "^0.3.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -6919,6 +7064,17 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6971,6 +7127,12 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/package.json b/package.json index ebbc5e0..2ebbc4d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@formkit/auto-animate": "^0.9.0", "@heroui/react": "^2.7.11", + "@mux/mux-player-react": "^3.8.0", "@sentry/browser": "^10.8.0", "@tauri-apps/api": "~2.8.0", "@tauri-apps/plugin-autostart": "^2.5.0", diff --git a/src/pages/Download.tsx b/src/pages/Download.tsx index bbc8bc9..07107b0 100644 --- a/src/pages/Download.tsx +++ b/src/pages/Download.tsx @@ -1,23 +1,60 @@ -import { Button, Input } from '@heroui/react' +import { Button, Image, Input, Spinner } from '@heroui/react' +import MuxPlayer from '@mux/mux-player-react' import { getCurrentWindow } from '@tauri-apps/api/window' import { message } from '@tauri-apps/plugin-dialog' import { DownloadIcon, FoldersIcon, XIcon } from 'lucide-react' -import { useState } from 'react' -import { useSearchParams } from 'react-router-dom' +import { useEffect, useState } from 'react' import { startDownload } from '../../lib/rclone/api' import { PathField } from '../components/PathFinder' -export default function Download() { - const [searchParams] = useSearchParams() +function isValidUrl(url: string) { + try { + new URL(url) + return true + } catch { + return false + } +} +function isYoutubeUrl(url: string) { + return url.includes('youtube.com') || url.includes('youtu.be') +} + +function getDateFilename() { + return new Date() + .toLocaleString(undefined, { + dateStyle: 'short', + timeStyle: 'medium', + }) + .replace(',', ' at') + .replace(/[\/]/g, '-') + .replace(/[:]/g, '.') +} + +function getUrlDomain(url: string) { + const hostname = new URL(url).hostname + return hostname.split('.').slice(0, -1).join('.') || hostname.split('.')[0] +} + +export default function Download() { const [url, setUrl] = useState() - const [source, setSource] = useState( - searchParams.get('initialSource') ? searchParams.get('initialSource')! : undefined - ) + const [destination, setDestination] = useState() + const [filename, setFilename] = useState() const [isStarted, setIsStarted] = useState(false) const [isLoading, setIsLoading] = useState(false) + const [downloadData, setDownloadData] = useState< + | { + url: string + title: string + extension: string + type: 'video' | 'audio' | 'file' | 'image' + } + | undefined + >() + const [isFetchingDownloadData, setIsFetchingDownloadData] = useState(false) + async function handleStartDownload() { setIsLoading(true) @@ -30,7 +67,7 @@ export default function Download() { return } - if (!source) { + if (!destination) { await message('Please select a destination path', { title: 'Error', kind: 'error', @@ -39,10 +76,8 @@ export default function Download() { return } - const [fs, remote] = source.split(':') - - if (!fs || !remote) { - await message('Invalid destination path', { + if (!filename) { + await message('Please enter a filename', { title: 'Error', kind: 'error', }) @@ -50,13 +85,17 @@ export default function Download() { return } + const downloadUrl = downloadData?.url || url + try { await startDownload({ - fs, - remote, - url, + fs: destination, + remote: filename, + url: downloadUrl, }) + // await _startDownload() + setIsStarted(true) await message('Download job started', { @@ -76,26 +115,95 @@ export default function Download() { const buttonText = (() => { if (isLoading) return 'STARTING...' - if (!source) return 'Please select a destination path' + if (!url) return 'Please enter a URL' + if (!isValidUrl(url)) return 'Invalid URL' + if (!destination) return 'Please select a destination path' return 'DOWNLOAD' })() const buttonIcon = (() => { if (isLoading) return - if (!source) return + if (!url) return + if (!isValidUrl(url)) return + if (!destination) return return })() + useEffect(() => { + if (!url || !isValidUrl(url)) { + setFilename(undefined) + setDownloadData(undefined) + setIsFetchingDownloadData(false) + return + } + + console.log('[Download] Fetching download data for URL:', url) + + const abortController = new AbortController() + setIsFetchingDownloadData(true) + + let parsedFilename: typeof filename + let parsedDownloadData: typeof downloadData + + fetch(`https://rcloneui.com/api/download?url=${encodeURIComponent(url)}`, { + signal: abortController.signal, + }) + .then(async (response) => { + if (response.ok) { + const result = await response.json() + + if (result.data && Array.isArray(result.data) && result.data.length > 0) { + console.log('[Download] Successfully fetched download data') + const item = result.data[0] + parsedDownloadData = { + url: item.url, + title: item.title, + extension: item.extension, + type: item.type, + } + parsedFilename = `${item.title.substring(0, 42).trim()}.${item.extension || 'txt'}` + console.log(parsedDownloadData) + console.log(parsedFilename) + } + } + + if (parsedFilename && parsedDownloadData) { + console.log('[Download] Setting filename and download data') + setFilename(parsedFilename) + setDownloadData(parsedDownloadData) + } else { + const extractedExtension = url.split('.').pop()?.split('?')[0]?.toLowerCase() + + if (extractedExtension) { + parsedFilename = url.split('/').pop()?.split('?')[0] + } else { + parsedFilename = `${getUrlDomain(url)} ${getDateFilename()}.txt` + } + + setFilename(parsedFilename) + setDownloadData(undefined) + } + + setIsFetchingDownloadData(false) + }) + .catch() + + return () => { + abortController.abort() + } + }, [url]) + return ( -
+
{/* Main Content */} -
+
setUrl(e.target.value)} fullWidth={true} + description="Supports Youtube, TikTok, SoundCloud, Google Drive, etc." size="lg" data-focus-visible="false" autoComplete="off" @@ -105,6 +213,7 @@ export default function Download() { endContent={
+ +
+ {isFetchingDownloadData && } + + {!isFetchingDownloadData && + downloadData && + downloadData.type === 'video' && + !isYoutubeUrl(url || '') && ( + + )} + + {!isFetchingDownloadData && downloadData && downloadData.type === 'image' && ( + + )} + + {!isFetchingDownloadData && downloadData && downloadData.type === 'audio' && ( + + )}
@@ -137,7 +290,7 @@ export default function Download() { color="primary" size="lg" onPress={() => { - setSource(undefined) + setDestination(undefined) setIsStarted(false) }} data-focus-visible="false" @@ -164,7 +317,7 @@ export default function Download() { fullWidth={true} type="button" color="primary" - isDisabled={isLoading || !source} + isDisabled={isLoading || !destination || isFetchingDownloadData} isLoading={isLoading} endContent={buttonIcon} className="max-w-2xl gap-2"