DOWNLOAD (ga)

Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com>
This commit is contained in:
FTCHD
2025-11-01 20:33:35 +01:00
parent 1908209941
commit 746aaacea4
4 changed files with 341 additions and 25 deletions
+1 -1
View File
@@ -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"
+162
View File
@@ -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",
+1
View File
@@ -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",
+177 -24
View File
@@ -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<string | undefined>()
const [source, setSource] = useState<string | undefined>(
searchParams.get('initialSource') ? searchParams.get('initialSource')! : undefined
)
const [destination, setDestination] = useState<string | undefined>()
const [filename, setFilename] = useState<string | undefined>()
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 <FoldersIcon className="w-5 h-5" />
if (!url) return <XIcon className="w-5 h-5" />
if (!isValidUrl(url)) return <XIcon className="w-5 h-5" />
if (!destination) return <FoldersIcon className="w-5 h-5" />
return <DownloadIcon className="w-5 h-5 fill-current" />
})()
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 (
<div className="flex flex-col h-screen gap-10 pt-10">
<div className="flex flex-col h-screen gap-2 pt-10">
{/* Main Content */}
<div className="flex flex-col flex-1 w-full max-w-3xl gap-6 mx-auto">
<div className="flex flex-col flex-1 w-full max-w-3xl gap-4 mx-auto">
<Input
label="URL"
placeholder="Enter a URL"
value={url}
onChange={(e) => 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={
<Button
variant="faded"
color="primary"
onPress={() => {
setTimeout(() => {
navigator.clipboard.readText().then((text) => {
@@ -120,13 +229,57 @@ export default function Download() {
{/* Path Display */}
<PathField
path={source || ''}
setPath={setSource}
path={destination || ''}
setPath={setDestination}
label="Destination"
description="Select the destination folder or manually enter a folder path"
placeholder="Enter a remote:/path as destination"
showPicker={true}
showFiles={false}
/>
<Input
label="Filename"
placeholder="Enter a filename"
description="Make sure to include an extension"
value={filename}
onChange={(e) => setFilename(e.target.value)}
fullWidth={true}
isDisabled={!url || !destination}
size="lg"
data-focus-visible="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
/>
</div>
<div className="flex items-center justify-center flex-1 overflow-hidden">
{isFetchingDownloadData && <Spinner size="lg" />}
{!isFetchingDownloadData &&
downloadData &&
downloadData.type === 'video' &&
!isYoutubeUrl(url || '') && (
<MuxPlayer
autoPlay={true}
muted={true}
// controls={true}
src={downloadData.url}
className="object-contain w-full h-64"
/>
)}
{!isFetchingDownloadData && downloadData && downloadData.type === 'image' && (
<Image src={downloadData.url} className="object-contain w-full h-64" />
)}
{!isFetchingDownloadData && downloadData && downloadData.type === 'audio' && (
<audio src={downloadData.url} controls={true}>
<track kind="captions" src="" srcLang="en" />
</audio>
)}
</div>
<div className="sticky bottom-0 z-50 flex items-center justify-center flex-none gap-2 p-4 border-t border-neutral-500/20 bg-neutral-900/50 backdrop-blur-lg">
@@ -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"