From ff18323494dee0617856c6e814453af160f7587c Mon Sep 17 00:00:00 2001 From: Subhamoy Biswas Date: Sat, 30 Aug 2025 23:35:14 +0530 Subject: [PATCH] feat: added aria2 support and some other improvements --- README.md | 21 ++- .../binaries/aria2c-aarch64-apple-darwin | 3 + src-tauri/binaries/aria2c-x86_64-apple-darwin | 3 + .../aria2c-x86_64-pc-windows-msvc.exe | 3 + .../binaries/aria2c-x86_64-unknown-linux-gnu | 3 + src-tauri/capabilities/shell.json | 10 ++ src-tauri/src/migrations.rs | 75 ++++++++++ src-tauri/tauri.linux.conf.json | 3 +- src-tauri/tauri.macos-aarch64.conf.json | 3 +- src-tauri/tauri.macos-x86_64.conf.json | 3 +- src-tauri/tauri.windows.conf.json | 3 +- src/App.tsx | 45 +++++- src/pages/settings.tsx | 10 ++ src/services/database.ts | 14 +- src/services/store.ts | 2 + src/types/download.ts | 2 + src/types/settings.ts | 1 + src/utils.ts | 136 +++++++++++++++--- 18 files changed, 301 insertions(+), 39 deletions(-) create mode 100644 src-tauri/binaries/aria2c-aarch64-apple-darwin create mode 100644 src-tauri/binaries/aria2c-x86_64-apple-darwin create mode 100644 src-tauri/binaries/aria2c-x86_64-pc-windows-msvc.exe create mode 100644 src-tauri/binaries/aria2c-x86_64-unknown-linux-gnu diff --git a/README.md b/README.md index 2a7b6a7..fc8afcb 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,15 @@ After installing the extension you can do the following directly from the browse - Windows (10 / 11) - Linux (Debian / Fedora / Arch Linux base) -- MacOS (>10.3) +- MacOS (>10.5) > ⚠️ **NOTE:** Though most linux (debian/fedora/arch base) distros are supported but not all packages are tested on all these platforms, to save some time (and brain cells) and ship the software as fast as possible! (Currently only the debian package is tested on Ubuntu 24.04 LTS - So, other linux packages may have issues, test it yourself and feel free to report issues if you found one) ### 🤝 External Dependencies -- [YT-DLP](https://github.com/yt-dlp/yt-dlp) - The core CLI tool used to download Video/Audio from the Web (Hero of the show 😎) -- [FFmpeg](https://www.ffmpeg.org) - Used for Video/Audio post-processing +- [YT-DLP](https://github.com/yt-dlp/yt-dlp) (Latest Nightly) - The core CLI tool used to download video/audio from the web (Hero of the show 😎) +- [FFmpeg & FFprobe](https://www.ffmpeg.org) (v7.1.1) - Used for video/audio post-processing +- [Aria2](https://aria2.github.io) (v1.37.0) - Used as an external downloader for blazing fast downloads with yt-dlp ### ⬇️ Download and Installation @@ -83,6 +84,7 @@ NeoDLP is and will be always FREE to Use and Open-Sourced for Everyone. On the o - [x] Add support for yt-dlp - [x] Add basic settings and customization - [x] Integrate with browsers +- [x] Add aria2c support - [ ] Add more advanced settings and achive stability **(ongoing)** - [ ] Add media converter - [ ] Add multiple downloader engines @@ -125,8 +127,17 @@ npm run tauri build -- --config "./src-tauri/tauri.macos-x86_64.conf.json" # ### ⭕ Bug Report -Noticed any Bug? or Want to give me some suggetions? Always feel free to open a [GitHub Issue](https://github.com/neosubhamoy/neodlp/issues). I would love to hear from you...!! +Noticed any Bug? or Want to give me some suggetion? Always feel free to open a [GitHub Issue](https://github.com/neosubhamoy/neodlp/issues). I would love to hear from you...!! + +### 💫 Credits + +- NeoDLP's 'Format Selection' options are inspired from the [Seal](https://github.com/JunkFood02/Seal) app by [@JunkFood02](https://github.com/JunkFood02) +- Aria2 Linux x86_64 static build is provided by [@q3aql](https://github.com/q3aql/aria2-static-builds) +- Aria2 MacOS x86_64 and ARM64 static builds are provided by [@tofuliang](https://github.com/tofuliang/aria2) ### 📝 License -NeoDLP is Licensed under the [MIT license](https://github.com/neosubhamoy/neodlp/blob/main/LICENSE). Anyone can view, modify, use (personal and commercial) or distribute it's sources without any attribution and extra permissions. \ No newline at end of file +NeoDLP is Licensed under the [MIT license](https://github.com/neosubhamoy/neodlp/blob/main/LICENSE). Anyone can view, modify, use (personal and commercial) or distribute it's sources without any attribution and extra permissions. + +**** +An Open Sourced Project - Developed with ❤️ by **Subhamoy** \ No newline at end of file diff --git a/src-tauri/binaries/aria2c-aarch64-apple-darwin b/src-tauri/binaries/aria2c-aarch64-apple-darwin new file mode 100644 index 0000000..88cf4bd --- /dev/null +++ b/src-tauri/binaries/aria2c-aarch64-apple-darwin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c3e5c362d2c0e66552cd39c303337641affcfbb2a1f08b3c02dd7d50452b97f +size 4915976 diff --git a/src-tauri/binaries/aria2c-x86_64-apple-darwin b/src-tauri/binaries/aria2c-x86_64-apple-darwin new file mode 100644 index 0000000..3e0f442 --- /dev/null +++ b/src-tauri/binaries/aria2c-x86_64-apple-darwin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34d16e5eb78a6ea33256e2b87cdbdad37872f803479d2ddfff085792e1d907d3 +size 5268704 diff --git a/src-tauri/binaries/aria2c-x86_64-pc-windows-msvc.exe b/src-tauri/binaries/aria2c-x86_64-pc-windows-msvc.exe new file mode 100644 index 0000000..924ae3c --- /dev/null +++ b/src-tauri/binaries/aria2c-x86_64-pc-windows-msvc.exe @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be2099c214f63a3cb4954b09a0becd6e2e34660b886d4c898d260febfe9d70c2 +size 5649408 diff --git a/src-tauri/binaries/aria2c-x86_64-unknown-linux-gnu b/src-tauri/binaries/aria2c-x86_64-unknown-linux-gnu new file mode 100644 index 0000000..f5d3bf9 --- /dev/null +++ b/src-tauri/binaries/aria2c-x86_64-unknown-linux-gnu @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36f66dab69edcc44255d0dba90c93f5aa4a304ec60c7136d8c279dfc89c23e1d +size 9666624 diff --git a/src-tauri/capabilities/shell.json b/src-tauri/capabilities/shell.json index 7de3a26..8bce441 100644 --- a/src-tauri/capabilities/shell.json +++ b/src-tauri/capabilities/shell.json @@ -25,6 +25,11 @@ "args": true, "sidecar": true }, + { + "name": "binaries/aria2c", + "args": true, + "sidecar": true + }, { "name": "pkexec", "cmd": "pkexec", @@ -49,6 +54,11 @@ "name": "binaries/ffprobe", "args": true, "sidecar": true + }, + { + "name": "binaries/aria2c", + "args": true, + "sidecar": true } ] } diff --git a/src-tauri/src/migrations.rs b/src-tauri/src/migrations.rs index dea00a3..015bb2a 100644 --- a/src-tauri/src/migrations.rs +++ b/src-tauri/src/migrations.rs @@ -105,5 +105,80 @@ pub fn get_migrations() -> Vec { END; ", kind: MigrationKind::Up, + }, + Migration { + version: 3, + description: "add_use_aria2_column_to_downloads_with_proper_position", + sql: " + -- Create temporary table with the new column in the correct position + CREATE TABLE downloads_temp ( + id INTEGER PRIMARY KEY NOT NULL, + download_id TEXT UNIQUE NOT NULL, + download_status TEXT NOT NULL, + video_id TEXT NOT NULL, + format_id TEXT NOT NULL, + subtitle_id TEXT, + queue_index INTEGER, + playlist_id TEXT, + playlist_index INTEGER, + resolution TEXT, + ext TEXT, + abr REAL, + vbr REAL, + acodec TEXT, + vcodec TEXT, + dynamic_range TEXT, + process_id INTEGER, + status TEXT, + progress REAL, + total INTEGER, + downloaded INTEGER, + speed REAL, + eta INTEGER, + filepath TEXT, + filetype TEXT, + filesize INTEGER, + output_format TEXT, + embed_metadata INTEGER NOT NULL DEFAULT 0, + embed_thumbnail INTEGER NOT NULL DEFAULT 0, + sponsorblock_remove TEXT, + sponsorblock_mark TEXT, + use_aria2 INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (video_id) REFERENCES video_info (video_id), + FOREIGN KEY (playlist_id) REFERENCES playlist_info (playlist_id) + ); + + -- Copy all data from original table to temporary table + INSERT INTO downloads_temp SELECT + id, download_id, download_status, video_id, format_id, subtitle_id, + queue_index, playlist_id, playlist_index, resolution, ext, abr, vbr, + acodec, vcodec, dynamic_range, process_id, status, progress, total, + downloaded, speed, eta, filepath, filetype, filesize, output_format, + embed_metadata, embed_thumbnail, sponsorblock_remove, sponsorblock_mark, + 0, -- use_aria2 default value + created_at, updated_at + FROM downloads; + + -- Drop existing triggers for the original table + DROP TRIGGER IF EXISTS update_downloads_updated_at; + DROP TRIGGER IF EXISTS set_downloads_timestamps; + + -- Drop the original table + DROP TABLE downloads; + + -- Rename temporary table to original name + ALTER TABLE downloads_temp RENAME TO downloads; + + -- Create only the update trigger (as insert trigger not needed anymore) + CREATE TRIGGER IF NOT EXISTS update_downloads_updated_at + AFTER UPDATE ON downloads + FOR EACH ROW + BEGIN + UPDATE downloads SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; + ", + kind: MigrationKind::Up, }] } diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 38060ad..8a41dd4 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -38,7 +38,8 @@ "externalBin": [ "binaries/yt-dlp", "binaries/ffmpeg", - "binaries/ffprobe" + "binaries/ffprobe", + "binaries/aria2c" ], "linux": { "deb": { diff --git a/src-tauri/tauri.macos-aarch64.conf.json b/src-tauri/tauri.macos-aarch64.conf.json index 7cc0b41..a979ff3 100644 --- a/src-tauri/tauri.macos-aarch64.conf.json +++ b/src-tauri/tauri.macos-aarch64.conf.json @@ -38,7 +38,8 @@ "externalBin": [ "binaries/yt-dlp", "binaries/ffmpeg", - "binaries/ffprobe" + "binaries/ffprobe", + "binaries/aria2c" ], "resources": { "target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost", diff --git a/src-tauri/tauri.macos-x86_64.conf.json b/src-tauri/tauri.macos-x86_64.conf.json index fc7819a..97cdc39 100644 --- a/src-tauri/tauri.macos-x86_64.conf.json +++ b/src-tauri/tauri.macos-x86_64.conf.json @@ -38,7 +38,8 @@ "externalBin": [ "binaries/yt-dlp", "binaries/ffmpeg", - "binaries/ffprobe" + "binaries/ffprobe", + "binaries/aria2c" ], "resources": { "target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost", diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 932039a..b193578 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -38,7 +38,8 @@ "externalBin": [ "binaries/yt-dlp", "binaries/ffmpeg", - "binaries/ffprobe" + "binaries/ffprobe", + "binaries/aria2c" ], "resources": { "target/release/neodlp-msghost.exe": "neodlp-msghost.exe", diff --git a/src/App.tsx b/src/App.tsx index b204398..29e5b3b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -80,6 +80,7 @@ export default function App({ children }: { children: React.ReactNode }) { const SPONSORBLOCK_MARK = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark); const SPONSORBLOCK_REMOVE_CATEGORIES = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove_categories); const SPONSORBLOCK_MARK_CATEGORIES = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark_categories); + const USE_ARIA2 = useSettingsPageStatesStore(state => state.settings.use_aria2); const isErrored = useDownloaderPageStatesStore((state) => state.isErrored); const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected); @@ -247,7 +248,7 @@ export default function App({ children }: { children: React.ReactNode }) { let outputFormat = null; if (fileType !== 'unknown' && ((VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto') || resumeState?.output_format)) { - outputFormat = resumeState?.output_format || (fileType === 'video+audio' ? VIDEO_FORMAT : (fileType === 'video' ? VIDEO_FORMAT : (fileType === 'audio' ? AUDIO_FORMAT : null))); + outputFormat = resumeState?.output_format || (fileType === 'video+audio' && VIDEO_FORMAT !== 'auto' ? VIDEO_FORMAT : (fileType === 'video' && VIDEO_FORMAT !== 'auto' ? VIDEO_FORMAT : (fileType === 'audio' && AUDIO_FORMAT !== 'auto' ? AUDIO_FORMAT : null))); if ((VIDEO_FORMAT !== 'auto' && fileType === 'video+audio') || (resumeState?.output_format && fileType === 'video+audio')) { if (ALWAYS_REENCODE_VIDEO) { args.push('--recode-video', resumeState?.output_format || VIDEO_FORMAT); @@ -316,8 +317,18 @@ export default function App({ children }: { children: React.ReactNode }) { args.push('--sponsorblock-mark', sponsorblockMark); } } + + let useAria2 = 0; + if (USE_ARIA2 || resumeState?.use_aria2) { + useAria2 = 1; + args.push( + '--downloader', 'aria2c', + '--downloader', 'dash,m3u8:native', + '--downloader-args', 'aria2c:-c -j 16 -x 16 -s 16 -k 1M --check-certificate=false' + ); + } - if (resumeState) { + if (resumeState || USE_ARIA2) { args.push('--continue'); } else { args.push('--no-continue'); @@ -369,7 +380,8 @@ export default function App({ children }: { children: React.ReactNode }) { }); command.stdout.on('data', line => { - if (line.startsWith('status:')) { + if (line.startsWith('status:') || line.startsWith('[#')) { + console.log(line); const currentProgress = parseProgressLine(line); const state: DownloadState = { download_id: downloadId, @@ -414,7 +426,8 @@ export default function App({ children }: { children: React.ReactNode }) { embed_metadata: embedMetadata, embed_thumbnail: embedThumbnail, sponsorblock_remove: sponsorblockRemove, - sponsorblock_mark: sponsorblockMark + sponsorblock_mark: sponsorblockMark, + use_aria2: useAria2 }; downloadStateSaver.mutate(state, { onSuccess: (data) => { @@ -504,7 +517,8 @@ export default function App({ children }: { children: React.ReactNode }) { embed_metadata: resumeState?.embed_metadata || 0, embed_thumbnail: resumeState?.embed_thumbnail || 0, sponsorblock_remove: resumeState?.sponsorblock_remove || null, - sponsorblock_mark: resumeState?.sponsorblock_mark || null + sponsorblock_mark: resumeState?.sponsorblock_mark || null, + use_aria2: resumeState?.use_aria2 || 0 } downloadStateSaver.mutate(state, { onSuccess: (data) => { @@ -546,9 +560,28 @@ export default function App({ children }: { children: React.ReactNode }) { onSuccess: (data) => { console.log("Download status updated successfully:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); + + /* re-check if the download is properly paused (if not try again after a small delay) + as the pause opertion happens within high throughput of operations and have a high chgance of failure. + */ + if (isSuccessFetchingDownloadStates && downloadStates.find(state => state.download_id === downloadState.download_id)?.download_status !== 'paused') { + console.log("Download status not updated to paused yet, retrying..."); + setTimeout(() => { + downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, { + onSuccess: (data) => { + console.log("Download status updated successfully on retry:", data); + queryClient.invalidateQueries({ queryKey: ['download-states'] }); + }, + onError: (error) => { + console.error("Failed to update download status:", error); + } + }); + }, 200); + } + // Reset the processing flag to ensure queue can be processed isProcessingQueueRef.current = false; - + // Process the queue after a short delay to ensure state is updated setTimeout(() => { processQueuedDownloads(); diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index a5f84cb..0ec29d5 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -106,6 +106,7 @@ export default function SettingsPage() { const sponsorblockMark = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark); const sponsorblockRemoveCategories = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove_categories); const sponsorblockMarkCategories = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark_categories); + const useAria2 = useSettingsPageStatesStore(state => state.settings.use_aria2); const websocketPort = useSettingsPageStatesStore(state => state.settings.websocket_port); const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort); @@ -466,6 +467,15 @@ export default function SettingsPage() { /> +
+

Aria2

+

Use aria2c as external downloader (recommended for large files and unstable connections, resuming is not supported)

+ saveSettingsKey('use_aria2', checked)} + /> +
diff --git a/src/services/database.ts b/src/services/database.ts index 89b2edc..e27eb4b 100644 --- a/src/services/database.ts +++ b/src/services/database.ts @@ -201,7 +201,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => { embed_metadata = $27, embed_thumbnail = $28, sponsorblock_remove = $29, - sponsorblock_mark = $30 + sponsorblock_mark = $30, + use_aria2 = $31 WHERE download_id = $1`, [ downloadState.download_id, @@ -233,7 +234,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => { downloadState.embed_metadata, downloadState.embed_thumbnail, downloadState.sponsorblock_remove, - downloadState.sponsorblock_mark + downloadState.sponsorblock_mark, + downloadState.use_aria2 ] ) } @@ -267,8 +269,9 @@ export const saveDownloadState = async (downloadState: DownloadState) => { embed_metadata, embed_thumbnail, sponsorblock_remove, - sponsorblock_mark - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30)`, + sponsorblock_mark, + use_aria2 + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31)`, [ downloadState.download_id, downloadState.download_status, @@ -299,7 +302,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => { downloadState.embed_metadata, downloadState.embed_thumbnail, downloadState.sponsorblock_remove, - downloadState.sponsorblock_mark + downloadState.sponsorblock_mark, + downloadState.use_aria2 ] ) } diff --git a/src/services/store.ts b/src/services/store.ts index 1bd12b2..91113d5 100644 --- a/src/services/store.ts +++ b/src/services/store.ts @@ -151,6 +151,7 @@ export const useSettingsPageStatesStore = create((set) sponsorblock_mark: 'default', sponsorblock_remove_categories: [], sponsorblock_mark_categories: [], + use_aria2: false, // extension settings websocket_port: 53511 }, @@ -206,6 +207,7 @@ export const useSettingsPageStatesStore = create((set) sponsorblock_mark: 'default', sponsorblock_remove_categories: [], sponsorblock_mark_categories: [], + use_aria2: false, // extension settings websocket_port: 53511 }, diff --git a/src/types/download.ts b/src/types/download.ts index 70f7faf..61c0001 100644 --- a/src/types/download.ts +++ b/src/types/download.ts @@ -42,6 +42,7 @@ export interface DownloadState { embed_thumbnail: number; sponsorblock_remove: string | null; sponsorblock_mark: string | null; + use_aria2: number; created_at?: string; updated_at?: string; } @@ -77,6 +78,7 @@ export interface Download { embed_thumbnail: number; sponsorblock_remove: string | null; sponsorblock_mark: string | null; + use_aria2: number; created_at: string; updated_at: string; } diff --git a/src/types/settings.ts b/src/types/settings.ts index 6a904d5..2c9572f 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -32,6 +32,7 @@ export interface Settings { sponsorblock_mark: string; sponsorblock_remove_categories: string[]; sponsorblock_mark_categories: string[]; + use_aria2: boolean; // extension settings websocket_port: number; } \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 3441080..edb8428 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,32 +21,130 @@ export function getRouteName(location: string, routes: Array = AllRou return lastPart ? lastPart.toUpperCase() : 'Dashboard'; } +const convertToBytes = (value: number, unit: string): number => { + switch (unit) { + case 'B': + return value; + case 'KiB': + return value * 1024; + case 'MiB': + return value * 1024 * 1024; + case 'GiB': + return value * 1024 * 1024 * 1024; + default: + return value; + } +}; + export const parseProgressLine = (line: string): DownloadProgress => { const progress: Partial = { status: 'downloading' }; + // Check if line contains both aria2c and yt-dlp format (combined format) + if (line.includes(']status:')) { + // Extract the status part after the closing bracket + const statusIndex = line.indexOf(']status:'); + if (statusIndex !== -1) { + const statusPart = line.substring(statusIndex + 1); // +1 to skip the ']' + // Parse the yt-dlp format part + statusPart.split(',').forEach(pair => { + const [key, value] = pair.split(':'); + if (key && value) { + switch (key.trim()) { + case 'status': + progress.status = value.trim(); + break; + case 'progress': + progress.progress = parseFloat(value.replace('%', '').trim()); + break; + case 'speed': + progress.speed = parseFloat(value); + break; + case 'downloaded': + progress.downloaded = parseInt(value, 10); + break; + case 'total': + progress.total = parseInt(value, 10); + break; + case 'eta': + if (value.trim() !== 'NA') { + progress.eta = parseInt(value, 10); + } + break; + } + } + }); + } + return progress as DownloadProgress; + } + + // Check if line is aria2c format only + if (line.startsWith('[#') && line.includes('MiB') && line.includes('%')) { + // Parse aria2c format: [#99f72b 2.5MiB/3.4MiB(75%) CN:1 DL:503KiB ETA:1s] + + // Extract progress percentage + const progressMatch = line.match(/\((\d+(?:\.\d+)?)%\)/); + if (progressMatch) { + progress.progress = parseFloat(progressMatch[1]); + } + + // Extract downloaded/total sizes + const sizeMatch = line.match(/(\d+(?:\.\d+)?)(MiB|KiB|GiB|B)\/(\d+(?:\.\d+)?)(MiB|KiB|GiB|B)/); + if (sizeMatch) { + const downloaded = parseFloat(sizeMatch[1]); + const downloadedUnit = sizeMatch[2]; + const total = parseFloat(sizeMatch[3]); + const totalUnit = sizeMatch[4]; + + // Convert to bytes + progress.downloaded = convertToBytes(downloaded, downloadedUnit); + progress.total = convertToBytes(total, totalUnit); + } + + // Extract download speed + const speedMatch = line.match(/DL:(\d+(?:\.\d+)?)(KiB|MiB|GiB|B)/); + if (speedMatch) { + const speed = parseFloat(speedMatch[1]); + const speedUnit = speedMatch[2]; + progress.speed = convertToBytes(speed, speedUnit); + } + + // Extract ETA + const etaMatch = line.match(/ETA:(\d+)s/); + if (etaMatch) { + progress.eta = parseInt(etaMatch[1], 10); + } + + return progress as DownloadProgress; + } + + // Original yt-dlp format: status:downloading,progress: 75.1%,speed:1022692.427018,downloaded:30289474,total:40331784,eta:9 line.split(',').forEach(pair => { const [key, value] = pair.split(':'); - switch (key) { - case 'status': - progress.status = value.trim(); - break; - case 'progress': - progress.progress = parseFloat(value.replace('%', '').trim()); - break; - case 'speed': - progress.speed = parseFloat(value); - break; - case 'downloaded': - progress.downloaded = parseInt(value, 10); - break; - case 'total': - progress.total = parseInt(value, 10); - break; - case 'eta': - progress.eta = parseInt(value, 10); - break; + if (key && value) { + switch (key.trim()) { + case 'status': + progress.status = value.trim(); + break; + case 'progress': + progress.progress = parseFloat(value.replace('%', '').trim()); + break; + case 'speed': + progress.speed = parseFloat(value); + break; + case 'downloaded': + progress.downloaded = parseInt(value, 10); + break; + case 'total': + progress.total = parseInt(value, 10); + break; + case 'eta': + if (value.trim() !== 'NA') { + progress.eta = parseInt(value, 10); + } + break; + } } });