feat: added aria2 support and some other improvements

This commit is contained in:
2025-08-30 23:35:14 +05:30
parent b73ab86066
commit ff18323494
18 changed files with 301 additions and 39 deletions

View File

@@ -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.
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**

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c3e5c362d2c0e66552cd39c303337641affcfbb2a1f08b3c02dd7d50452b97f
size 4915976

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:34d16e5eb78a6ea33256e2b87cdbdad37872f803479d2ddfff085792e1d907d3
size 5268704

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be2099c214f63a3cb4954b09a0becd6e2e34660b886d4c898d260febfe9d70c2
size 5649408

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:36f66dab69edcc44255d0dba90c93f5aa4a304ec60c7136d8c279dfc89c23e1d
size 9666624

View File

@@ -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
}
]
}

View File

@@ -105,5 +105,80 @@ pub fn get_migrations() -> Vec<Migration> {
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,
}]
}

View File

@@ -38,7 +38,8 @@
"externalBin": [
"binaries/yt-dlp",
"binaries/ffmpeg",
"binaries/ffprobe"
"binaries/ffprobe",
"binaries/aria2c"
],
"linux": {
"deb": {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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();

View File

@@ -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() {
/>
<Label htmlFor="max-retries" className="text-xs text-muted-foreground">(Current: {maxRetries}) (Default: 5, Maximum: 100)</Label>
</div>
<div className="aria2">
<h3 className="font-semibold">Aria2</h3>
<p className="text-xs text-muted-foreground mb-3">Use aria2c as external downloader (recommended for large files and unstable connections, resuming is not supported)</p>
<Switch
id="aria2"
checked={useAria2}
onCheckedChange={(checked) => saveSettingsKey('use_aria2', checked)}
/>
</div>
</TabsContent>
<TabsContent key="appearance" value="appearance" className="flex flex-col gap-4 min-h-[310px]">
<div className="app-theme">

View File

@@ -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
]
)
}

View File

@@ -151,6 +151,7 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((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<SettingsPageStatesStore>((set)
sponsorblock_mark: 'default',
sponsorblock_remove_categories: [],
sponsorblock_mark_categories: [],
use_aria2: false,
// extension settings
websocket_port: 53511
},

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -21,32 +21,130 @@ export function getRouteName(location: string, routes: Array<RoutesObj> = 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<DownloadProgress> = {
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;
}
}
});