Compare commits
51 Commits
18
.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
3
.gitattributes
vendored
@@ -1,2 +1 @@
|
||||
src-tauri/binaries/* filter=lfs diff=lfs merge=lfs -text
|
||||
src-tauri/resources/binaries/* filter=lfs diff=lfs merge=lfs -text
|
||||
* text=auto eol=lf
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: neosubhamoy
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**OS info (required):**
|
||||
- OS: [e.g. Windows 11]
|
||||
- OS Architecture: [e.g. x64]
|
||||
- OS Version: [e.g. 25H2 26200.6899]
|
||||
|
||||
**App info (required):**
|
||||
- NeoDLP Version: [e.g. 0.3.1]
|
||||
- YT-DLP Version: [e.g. 2025.10.18.232824]
|
||||
- NeoDLP Installation Mode: [e.g. msi, exe, winget]
|
||||
|
||||
**App logs (required):**
|
||||
Paste the full app session logs here
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE REQUEST]"
|
||||
labels: feature request
|
||||
assignees: neosubhamoy
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
31
.github/ISSUE_TEMPLATE/test_result.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Test result
|
||||
about: Share your test results
|
||||
title: "[TEST RESULT]"
|
||||
labels: test result
|
||||
assignees: neosubhamoy
|
||||
|
||||
---
|
||||
|
||||
**Describe the result**
|
||||
A clear and concise description of what's the final result of your testing.
|
||||
|
||||
**Did you noticed any problem?**
|
||||
Mention if you noticed any problems or inconsistencies during the testing
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain the results.
|
||||
|
||||
**What's working (required):**
|
||||
- [ ] Package installation
|
||||
- [ ] Downloads
|
||||
- [ ] Settings and configurations
|
||||
- [ ] Browser integration
|
||||
|
||||
**Test environment (required):**
|
||||
- OS: [e.g. Windows 11]
|
||||
- OS Architecture: [e.g. x64]
|
||||
- OS Version: [e.g. 25H2 26200.6899]
|
||||
- NeoDLP Version: [e.g. 0.3.1]
|
||||
- YT-DLP Version: [e.g. 2025.10.18.232824]
|
||||
- NeoDLP Installation Mode: [e.g. msi, exe, winget]
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
.github/images/completed-downloads.png
vendored
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
.github/images/downloader.png
vendored
Normal file
|
After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 541 KiB After Width: | Height: | Size: 541 KiB |
BIN
.github/images/ongoing-downloads.png
vendored
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
.github/images/settings.png
vendored
Normal file
|
After Width: | Height: | Size: 94 KiB |
32
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
|
||||
name: 🚀 Release on GitHub
|
||||
jobs:
|
||||
release:
|
||||
@@ -14,31 +14,27 @@ jobs:
|
||||
include:
|
||||
- platform: 'macos-latest'
|
||||
args: '--target aarch64-apple-darwin --config ./src-tauri/tauri.macos-aarch64.conf.json'
|
||||
arch: 'aarch64-apple-darwin'
|
||||
- platform: 'macos-latest'
|
||||
args: '--target x86_64-apple-darwin --config ./src-tauri/tauri.macos-x86_64.conf.json'
|
||||
arch: 'x86_64-apple-darwin'
|
||||
- platform: 'ubuntu-22.04'
|
||||
args: ''
|
||||
arch: ''
|
||||
args: '--target x86_64-unknown-linux-gnu --config ./src-tauri/tauri.linux-x86_64.conf.json'
|
||||
- platform: 'ubuntu-22.04-arm'
|
||||
args: '--target aarch64-unknown-linux-gnu --config ./src-tauri/tauri.linux-aarch64.conf.json'
|
||||
- platform: 'windows-latest'
|
||||
args: ''
|
||||
arch: ''
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: 🚚 Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: 🛠️ Install dependencies
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: 📦 Setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
@@ -56,6 +52,9 @@ jobs:
|
||||
- name: 🛠️ Install frontend dependencies
|
||||
run: npm install
|
||||
|
||||
- name: 📥 Download binaries
|
||||
run: npm run download
|
||||
|
||||
- name: 📄 Read and Process CHANGELOG (Unix)
|
||||
if: matrix.platform != 'windows-latest'
|
||||
id: changelog_unix
|
||||
@@ -64,12 +63,12 @@ jobs:
|
||||
if [ -f CHANGELOG.md ]; then
|
||||
# Extract version number from tag
|
||||
VERSION_NUM=$(echo "${{ github.ref_name }}" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$/\1/')
|
||||
|
||||
|
||||
# Read and replace placeholders
|
||||
CONTENT=$(cat CHANGELOG.md)
|
||||
CONTENT=${CONTENT//<release_tag>/${{ github.ref_name }}}
|
||||
CONTENT=${CONTENT//<version>/$VERSION_NUM}
|
||||
|
||||
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
@@ -85,12 +84,12 @@ jobs:
|
||||
if (Test-Path "CHANGELOG.md") {
|
||||
# Extract version number from tag
|
||||
$version_num = "${{ github.ref_name }}" -replace '^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$','$1'
|
||||
|
||||
|
||||
# Read and replace placeholders
|
||||
$content = Get-Content -Path CHANGELOG.md -Raw
|
||||
$content = $content -replace '<release_tag>', "${{ github.ref_name }}"
|
||||
$content = $content -replace '<version>', "$version_num"
|
||||
|
||||
|
||||
"content<<EOF" >> $env:GITHUB_OUTPUT
|
||||
$content >> $env:GITHUB_OUTPUT
|
||||
"EOF" >> $env:GITHUB_OUTPUT
|
||||
@@ -102,7 +101,6 @@ jobs:
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TARGET_ARCH: ${{ matrix.arch }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
with:
|
||||
@@ -113,4 +111,4 @@ jobs:
|
||||
prerelease: false
|
||||
includeUpdaterJson: true
|
||||
updaterJsonPreferNsis: true
|
||||
args: ${{ matrix.args }}
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
19
.gitignore
vendored
@@ -1,3 +1,14 @@
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.github/workflows/.secrets
|
||||
/target/
|
||||
src-tauri/binaries/*
|
||||
!src-tauri/binaries/.gitkeep
|
||||
src-tauri/resources/downloads/*
|
||||
!src-tauri/resources/downloads/.gitkeep
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@@ -7,12 +18,6 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.github/workflows/.secrets
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
@@ -22,4 +27,4 @@ dist-ssr
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
|
||||
42
CHANGELOG.md
@@ -1,28 +1,44 @@
|
||||
### ✨ Changelog
|
||||
|
||||
- DOWNLOADER: Added 'Cancel Search' button
|
||||
- FIXED: Downloaded files are not moving from temp to download folder in some linux distros
|
||||
- FIXED: 'Stop' all ongoing downloads button not working on linux
|
||||
- Improved search and download error handling
|
||||
- Removed subtitle (CC) embeding restriction from M4A files
|
||||
- Migrated to Zod v4 and improved form validations
|
||||
- Other minor fixes and improvements
|
||||
- HOTFIX: yt-dlp exiting with code 2 while resolving deno on macOS
|
||||
- Also, a small correction: Linux (deb, rpm) packages are not yet self-updateable
|
||||
|
||||
### 📝 Notes
|
||||
|
||||
> ⚠️ Linux Users: Make sure yt-dlp is not installed in your distro (otherwise you will get package installation conflict). Don't worry, You can still use yt-dlp cli as before (the only difference is that now it will be installed and auto-updated by neo-dlp, which You can also disable from neo-dlp Settings if you don't want to auto-update yt-dlp)
|
||||
> [!TIP]
|
||||
> This is a hotfix release for macOS only, You can skip this update if you are on other platforms
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a breaking update if you are coming from older version than `v0.3.0`. Users are adviced to complete/cancel all paused downloads before updating to this version, otherwise paused downloads may not resume properly or re-start from the begining.
|
||||
|
||||
> [!WARNING]
|
||||
> Linux users make sure `yt-dlp` and `deno` is not installed in your distro (otherwise you will get package installation conflict). Don't worry, You can still use yt-dlp cli as before (the only difference is that now it will be installed and auto-updated by neo-dlp, which You can also disable from neo-dlp Settings if you don't want to auto-update yt-dlp)
|
||||
|
||||
> This is an Un-Signed Build (Windows doesn't trust this Certificate so, it may flag this as malicious software, in that case, disable Windows SmartScreen and Defender, install it, and then re-enable them)
|
||||
|
||||
> This is an Un-Signed Build (MacOS doesn't trust this Certificate so, it may flag this as from 'unverified developer' and prevent it from opening, in that case, open Settings and allow it from 'Settings > Privacy and Security' section to get started)
|
||||
|
||||
### 📦 Shipped Binaries
|
||||
|
||||
| yt-dlp (updateable) | ffmpeg | ffprobe | aria2c | deno |
|
||||
| :---- | :---- | :---- | :---- | :---- |
|
||||
| v2025.11.05.232946 (nightly) | v7.1.1 | v7.1.1 | v1.37.0 | v2.5.6 |
|
||||
|
||||
> ‼️ Linux builds (deb, rpm) does not ships with `ffmpeg` and `ffprobe` (though it will be auto installed as a dependency by your package manager, if you are on fedora make sure to [enable rpmfusion free+nonfree repos](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup/#_enabling_the_rpm_fusion_repositories_using_command_line_utilities) before installing the rpm package)
|
||||
|
||||
> ‼️ MacOS builds (dmg, app) does not ships with `aria2c`, If you want to use [aria2](https://formulae.brew.sh/formula/aria2) install it via [homebrew](https://brew.sh)
|
||||
|
||||
### ⬇️ Download Section
|
||||
|
||||
| Arch\OS | Windows (msi) ⬆️ | Windows (exe) ⬆️ | Linux (deb) | Linux (rpm) | MacOS (dmg) ⬆️ | MacOS (app) ⬆️ |
|
||||
| :---- | :---- | :---- | :---- | :---- | :---- | :---- |
|
||||
| x86_64 | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64_en-US.msi) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64-setup.exe) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_amd64.deb) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP-<version>-1.x86_64.rpm) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64.dmg) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_x64.app.tar.gz) |
|
||||
| ARM64 | N/A | N/A | N/A | N/A | ⚠️ [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_aarch64.dmg) | ⚠️ [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_aarch64.app.tar.gz) |
|
||||
| Architecture | Windows (msi) ⬆️ | Windows (exe) ⬆️ | Linux (deb) | Linux (rpm) | Linux (AppImage) ⬆️ | MacOS (dmg) ⬆️ | MacOS (app) ⬆️ |
|
||||
| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- |
|
||||
| x86_64 | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64_en-US_windows.msi) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64-setup_windows.exe) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_amd64_linux.deb) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP-<version>-1.x86_64_linux.rpm) | 🚫 [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_amd64_linux.AppImage) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64_darwin.dmg) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_darwin_x64.app.tar.gz) |
|
||||
| ARM64 | N/A | 🪟 [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64-setup_windows.exe) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_arm64_linux.deb) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP-<version>-1.aarch64_linux.rpm) | N/A | ⚠️ [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_aarch64_darwin.dmg) | ⚠️ [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_darwin_aarch64.app.tar.gz) |
|
||||
|
||||
> ⬆️ icon indicates this packaging format supports in-built app-updater
|
||||
|
||||
> ⚠️ MacOS ARM64 binary downloads are experimental and may not open on Apple Silicon Macs if downloaded from browser (You will get 'Damaged File' error) it's because the binaries are not signed (signing MacOS binaries requires 99$/year Apple Developer Account subscription, which I can't afford RN!) and Apple Silicon Macs don't allow unsigned apps (downloaded from browser) to be installed on the system. If you want to use NeoDLP on your Apple Silicon Macs, you can simply use the command line [Curl-Bash Installer](https://neodlp.neosubhamoy.com/download) (Recommended) -OR- [compile it from source](https://github.com/neosubhamoy/neodlp?tab=readme-ov-file#%EF%B8%8F-contributing--building-from-source) in your Mac
|
||||
> 🪟 Windows x86_64 binary also works on ARM64 (Windows on ARM) devices with emulation (Not planning to release native Windows ARM64 build anytime soon as, x86_64 one works fine on ARM64 without noticeable performance impact)
|
||||
|
||||
> 🚫 Linux AppImage builds are experimental and does not support neodlp's browser intergration features due to it's sandboxed nature. Also, don't run the AppImage with portable (.home, .config) folders, it will break things (it is highly recommended to use native [deb, rpm, AUR] builds if possible for the full experiance, otherwise AppImages are good for trying out NeoDLP without installing)
|
||||
|
||||
> ⚠️ MacOS ARM64 binary downloads are experimental and may not open on Apple Silicon Macs if downloaded from browser (You will get 'Damaged File' error) it's because the binaries are not signed (signing MacOS binaries requires 99$/year Apple Developer Account subscription, which I can't afford RN!) and Apple Silicon Macs don't allow unsigned apps (downloaded from browser) to be installed on the system. If you want to use NeoDLP on your Apple Silicon Macs, you can simply use the command line [Curl-Bash Installer](https://neodlp.neosubhamoy.com/download) (Recommended) -OR- [compile it from source](https://github.com/neosubhamoy/neodlp?tab=readme-ov-file#%EF%B8%8F-contributing--building-from-source) in your Mac
|
||||
|
||||
144
README.md
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
# NeoDLP - (Neo Downloader Plus)
|
||||
|
||||
@@ -9,11 +9,12 @@ Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Int
|
||||
[](https://github.com/neosubhamoy/neodlp/releases)
|
||||
[](https://github.com/neosubhamoy/neodlp)
|
||||
|
||||
> [!TIP]
|
||||
> **🥰 Liked this project? Please consider giving it a Star (🌟) on github to show us your appreciation and help the algorythm recommend this project to even more awesome people like you!**
|
||||
|
||||
[](https://repology.org/project/neodlp/versions)
|
||||
|
||||
### ✨ Highlighted Features
|
||||
## ✨ Highlighted Features
|
||||
|
||||
- Download Video/Audio from popular sites (YT, FB, IG, X and other 2.5k+ [supported sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md))
|
||||
- Download Video/Audio in your preffered format (MP4, WEBM, MKV, MP3 etc.)
|
||||
@@ -21,9 +22,11 @@ Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Int
|
||||
- Supports Combining Video, Audio streams of your choice
|
||||
- Supports Multi-Language Subtitle/Caption (CC) embeding
|
||||
- Different Video/Audio metadata embeding options (info, chapters, thumbnail etc.)
|
||||
- SponsorBlock support (mark/remove video segments)
|
||||
- Network controls (proxy, rate limit etc.)
|
||||
- Highly customizable and many more...😉
|
||||
|
||||
### 🧩 Browser Integration
|
||||
## 🧩 Browser Integration
|
||||
|
||||
You can integrate NeoDLP with your favourite browser (any Chrome/Chromium/Firefox based browser) Just, install [NeoDLP Extension](https://github.com/neosubhamoy/neodlp-extension) to get started!
|
||||
|
||||
@@ -33,51 +36,98 @@ After installing the extension you can do the following directly from the browse
|
||||
|
||||
- Right Click Context Menu Action (Download with Neo Downloader Plus - Link, Selection, Media Source)
|
||||
|
||||
### 👀 Sneak Peek
|
||||
## 👀 Sneak Peek
|
||||
|
||||

|
||||

|
||||
|
||||
### 💻 Supported Platforms
|
||||
| Downloader | Completed Downloads | Ongoing Downloads | Settings |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
|  |  |  |  |
|
||||
|
||||
## 💻 Supported Platforms
|
||||
|
||||
- Windows (10 / 11)
|
||||
- Linux (Debian / Fedora / Arch Linux base)
|
||||
- MacOS (>10.3)
|
||||
- Linux (Debian / Fedora / RHEL / SUSE / Arch Linux base)
|
||||
- MacOS (>11)
|
||||
|
||||
> ⚠️ **NOTE:** Though most linux (debian/fedora/arch base) distros are supported but not all packages are tested on all these platforms, to save time (and some brain cells) and ship the software as fast as possible! (Currently only the debian package is tested on Ubuntu 24.04 - So, other linux packages may have issues, test it yourself and feel free to report issues if you found one)
|
||||
## 🤝 External Dependencies
|
||||
|
||||
### 🤝 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 & FFprobe](https://www.ffmpeg.org) - Used for video/audio post-processing
|
||||
- [Aria2](https://aria2.github.io) - Used as an external downloader for blazing fast downloads with yt-dlp (Not included with NeoDLP MacOS builds)
|
||||
- [Deno](https://deno.com) - Provides sandboxed javascript runtime environment for yt-dlp (Required for YT downloads, as per the new yt-dlp [announcement](https://github.com/yt-dlp/yt-dlp/issues/14404))
|
||||
|
||||
- [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
|
||||
## ℹ️ System Pre-Requirements
|
||||
|
||||
### ⬇️ Download and Installation
|
||||
- **Windows:** [Microsoft Visual C++ Redistributable 2015+](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) `winget install Microsoft.VCRedist.2015+.x64` (Will be auto-installed if you install NeoDLP via winget)
|
||||
- **MacOS:** XCode Command Line Tools `xcode-select --install` (Mostly, comes pre-installed on modern macos, still if you encounter any issue then try installing it manually)
|
||||
- **Linux:** Most linux packages comes with pre-defined system dependencies which will be auto installed by your package manager (if you are on `fedora` make sure to [enable rpmfusion free+nonfree repos](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup/#_enabling_the_rpm_fusion_repositories_using_command_line_utilities) before installing the rpm package. also, if you prefer to install dependencies manually [follow this](https://v2.tauri.app/start/prerequisites/#linux))
|
||||
|
||||
## ⬇️ Download and Installation
|
||||
|
||||
1. Download the latest [NeoDLP](https://github.com/neosubhamoy/neodlp/releases/latest) release based on your OS and CPU Architecture, then install it! -OR- Install it directly from an available distribution channel (listed below)
|
||||
|
||||
| Arch\OS | Windows | Linux | MacOS |
|
||||
| Architecture | Windows | Linux | MacOS |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| x86_64 | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) |
|
||||
| ARM64 | ❌ N/A | ❌ N/A | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) |
|
||||
| ARM64 | ✅ Emulation | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) |
|
||||
|
||||
> [!NOTE]
|
||||
> x86_64 Windows binary also works on ARM64 (Windows on ARM) devices with emulation (Not planning to release native Windows ARM64 build anytime soon as, x86_64 one works fine on ARM64 without noticeable performance impact)
|
||||
|
||||
| Platform (OS) | Distribution Channel | Installation Command / Instruction |
|
||||
| :---- | :---- | :---- |
|
||||
| Windows x86_64 | WinGet | `winget install neodlp` |
|
||||
| MacOS Universal | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/neodlp_macos_installer.sh \| bash` |
|
||||
| Linux x86_64 (Arch Linux) | AUR | `yay -S neodlp` |
|
||||
| Windows x86_64 / ARM64 | WinGet | `winget install neodlp` |
|
||||
| MacOS x86_64 / ARM64 | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/macos_installer.sh \| bash` |
|
||||
| Linux x86_64 / ARM64 | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/linux_installer.sh \| bash` |
|
||||
| Arch Linux x86_64 / ARM64 | AUR | `yay -S neodlp` |
|
||||
|
||||
### 💝 Support the Development
|
||||
## 🧪 Package Testing Status
|
||||
|
||||
NeoDLP is and will be always FREE to Use for Everyone and Open-Sourced. On the other hand the developent process of NeoDLP takes lots of time, effort and even sometimes money! So, if you appriciate my work and have the ability to donate, then please consider supporting the development by donating (even a very small donation matters and helps NeoDLP to be a better product!) Your support is the key to my motivation...🤗
|
||||
Though NeoDLP is supported on most platforms but not all packages are tested on all platforms, to save some time (and brain cells) and ship the software as fast as possible! Current test coverage is given below. So, untested packages may have issues, test it yourself and always feel free to report any issue on github.
|
||||
|
||||
> [!TIP]
|
||||
> If you have access to any of the untested systems listed below, you can test the packages there and send me the test results via creating an github issue! (that would be super helpful actualy 😊)
|
||||
|
||||
| Platform | Status | Platform | Status |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| Windows 10 (x64) | ✅ Tested | Windows 10 (ARM64) | ⚠️ Untested |
|
||||
| Windows 11 (x64) | ✅ Tested | Windows 11 (ARM64) | ✅ Tested |
|
||||
| MacOS 14 (x64) | ✅ Tested | MacOS 14 (ARM64) | ✅ Tested |
|
||||
| MacOS 15 (x64) | ⚠️ Untested | MacOS 15 (ARM64) | ✅ Tested |
|
||||
| MacOS 26 (x64) | ⚠️ Untested | MacOS 26 (ARM64) | ✅ Tested |
|
||||
| Ubuntu 24.04 LTS (x64) | ✅ Tested | Ubuntu 24.04 LTS (ARM64) | ⚠️ Untested |
|
||||
| Fedora 42 (x64) | ✅ Tested | Fedora 42 (ARM64) | ⚠️ Untested |
|
||||
| Arch Linux (x64) | ✅ Tested | Arch Linux (ARM64) | ✅ Tested |
|
||||
| openSUSE 16 (x64) | ⚠️ Untested | openSUSE 16 (ARM64) | ⚠️ Untested |
|
||||
| RHEL 10 (x64) | ⚠️ Untested | RHEL 10 (ARM64) | ⚠️ Untested |
|
||||
|
||||
## 💝 Support the Development
|
||||
|
||||
NeoDLP is and will be always FREE to Use and Open-Sourced for Everyone. On the other hand the developent process of NeoDLP takes lots of time, effort and even sometimes money! So, if you appriciate my work and have the ability to donate, then please consider supporting the development by donating (even a very small donation matters and helps NeoDLP to be a better product!) Your support is the key to my motivation...🤗
|
||||
|
||||
<a href="https://buymeacoffee.com/neosubhamoy" target="_blank" title="buymeacoffee">
|
||||
<img src="https://iili.io/JoQ0zN9.md.png" alt="buymeacoffee-orange-badge" style="width: 150px;">
|
||||
</a>
|
||||
|
||||
<br></br>
|
||||
|
||||
> 📌 **NOTE:** You can also donate via UPI by sending donations to this UPI ID directly: **[subhamoybiswas636-2@oksbi](upi://pay?pa=subhamoybiswas636-2@oksbi&pn=Subhamoy%20Biswas)**
|
||||
> [!NOTE]
|
||||
> You can also donate via UPI by sending donations to this UPI ID directly: **subhamoybiswas636-2@oksbi**
|
||||
|
||||
### ⚡ Technologies Used
|
||||
## 🪜 Roadmap
|
||||
|
||||
- [x] Add support for yt-dlp
|
||||
- [x] Add basic settings and customization
|
||||
- [x] Integrate with browsers
|
||||
- [x] Add aria2c support
|
||||
- [x] Add custom command support
|
||||
- [ ] Add more advanced settings and achive stability **(ongoing)**
|
||||
- [ ] Add media converter
|
||||
- [ ] Add multiple downloader engines
|
||||
- [ ] Add advanced web extractor
|
||||
- [ ] Add more cool stuffs 😉
|
||||
|
||||
## ⚡ Technologies Used
|
||||
|
||||

|
||||

|
||||
@@ -85,33 +135,53 @@ NeoDLP is and will be always FREE to Use for Everyone and Open-Sourced. On the o
|
||||

|
||||

|
||||
|
||||
### 🛠️ Contributing / Building from Source
|
||||
## 🛠️ Contributing / Building from Source
|
||||
|
||||
Want to be part of this? Feel free to contribute...!! Pull Requests are always welcome...!! (^_^) Follow these simple steps to start building:
|
||||
|
||||
* Make sure to install [Rust](https://www.rust-lang.org/tools/install), [Node.js](https://nodejs.org/en), [Git](https://git-scm.com/downloads) and [Git-LFS](https://git-lfs.com/) before proceeding.
|
||||
* Make sure to install [Rust](https://www.rust-lang.org/tools/install), [Node.js](https://nodejs.org/en), and [Git](https://git-scm.com/downloads) before proceeding.
|
||||
* Install [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS / platform
|
||||
1. Fork this repo in your github account.
|
||||
2. Git clone the forked repo in your local machine.
|
||||
3. Install Node.js dependencies: `npm install`
|
||||
4. Run development / build process
|
||||
> ⚠️ **IMPORTANT:** Make sure to run the build command once before running the dev command for the first time to avoid compile time errors
|
||||
3. Create a git branch (related to the feature you are working on) (Optional - Recommended)
|
||||
4. Install Node.js dependencies: `npm install`
|
||||
5. Download binaries (for current platform): `npm run download`
|
||||
6. Run development / build process
|
||||
> [!WARNING]
|
||||
> Make sure to run the `build` command once before running the `dev` command for the first time to avoid compile time errors
|
||||
```code
|
||||
# for windows and linux users
|
||||
npm run tauri dev # for development
|
||||
# for windows users
|
||||
npm run tauri dev # for development
|
||||
npm run tauri build # for production build
|
||||
|
||||
# for linux users (based on cpu architecture)
|
||||
npm run tauri dev -- --config "./src-tauri/tauri.linux-aarch64.conf.json" # for ARM64 devices, development
|
||||
npm run tauri build -- --config "./src-tauri/tauri.linux-aarch64.conf.json" # for ARM64 devices, production build
|
||||
|
||||
npm run tauri dev -- --config "./src-tauri/tauri.linux-x86_64.conf.json" # for x64 devices, development
|
||||
npm run tauri build -- --config "./src-tauri/tauri.linux-x86_64.conf.json" # for x64 devices, production build
|
||||
|
||||
# for macOS users (based on cpu architecture)
|
||||
npm run tauri dev -- --config "./src-tauri/tauri.macos-aarch64.conf.json" # for apple silicon macs, development
|
||||
npm run tauri dev -- --config "./src-tauri/tauri.macos-aarch64.conf.json" # for apple silicon macs, development
|
||||
npm run tauri build -- --config "./src-tauri/tauri.macos-aarch64.conf.json" # for apple silicon macs, production build
|
||||
|
||||
npm run tauri dev -- --config "./src-tauri/tauri.macos-x86_64.conf.json" # for intel x86 macs, development
|
||||
npm run tauri build -- --config "./src-tauri/tauri.macos-x86_64.conf.json" # for intel x86 macs, production build
|
||||
npm run tauri dev -- --config "./src-tauri/tauri.macos-x86_64.conf.json" # for intel x86 macs, development
|
||||
npm run tauri build -- --config "./src-tauri/tauri.macos-x86_64.conf.json" # for intel x86 macs, production build
|
||||
```
|
||||
5. Do the changes, Send a Pull Request with proper Description (NOTE: Pull Requests Without Proper Description will be Rejected)
|
||||
6. Do the changes, Send a Pull Request with proper Description (NOTE: Pull Requests Without Proper Description will be Rejected)
|
||||
|
||||
**⭕ Noticed any Bugs or Want to give us some suggetions? Always feel free to open a GitHub Issue. We would love to hear from you...!!**
|
||||
## ⭕ Bug Report
|
||||
|
||||
### 📝 License
|
||||
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...!!
|
||||
|
||||
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.
|
||||
## 💫 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 binaries are built by [@asdo92](https://github.com/asdo92/aria2-static-builds)
|
||||
|
||||
## 📝 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.
|
||||
|
||||
****
|
||||
An Open Sourced Project - Developed with ❤️ by **Subhamoy**
|
||||
|
||||
1975
package-lock.json
generated
128
package.json
@@ -1,53 +1,56 @@
|
||||
{
|
||||
"name": "neodlp",
|
||||
"private": true,
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"download": "node ./scripts/download-bins.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.15",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-devtools": "^5.83.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.1",
|
||||
"@tauri-apps/plugin-fs": "^2.4.1",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.0",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||
"@tauri-apps/plugin-sql": "^2.3.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tauri-apps/api": "^2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||
"@tauri-apps/plugin-sql": "^2.3.1",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -55,33 +58,34 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.525.0",
|
||||
"lucide-react": "^0.552.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-resizable-panels": "^3.0.3",
|
||||
"react-router-dom": "^7.7.0",
|
||||
"recharts": "^2.15.4",
|
||||
"sonner": "^2.0.6",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "^3.3.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"ulid": "^3.0.1",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.0.5",
|
||||
"zustand": "^5.0.6"
|
||||
"zod": "^4.1.12",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/node": "^24.0.15",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tauri-apps/cli": "^2.9.3",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.5"
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { execSync } from 'child_process';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
|
||||
// Define array of binary source directories
|
||||
const binSrcDirs = [
|
||||
path.join(__dirname, 'src-tauri', 'binaries'),
|
||||
path.join(__dirname, 'src-tauri', 'resources', 'binaries'),
|
||||
path.join(projectRoot, 'src-tauri', 'binaries'),
|
||||
];
|
||||
|
||||
function makeFilesExecutable() {
|
||||
@@ -35,7 +35,7 @@ function makeFilesExecutable() {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log(`Successfully made ${count} files executable in ${binSrc}`);
|
||||
totalCount += count;
|
||||
successDirs++;
|
||||
@@ -47,5 +47,5 @@ function makeFilesExecutable() {
|
||||
console.log(`\nSummary: Made ${totalCount} files executable across ${successDirs} directories`);
|
||||
}
|
||||
|
||||
console.log(`RUNNING: 🛠️ Build Script makeFilesExecutable.js`);
|
||||
makeFilesExecutable();
|
||||
console.log(`RUNNING: 🛠️ Build Script --> chmod.js`);
|
||||
makeFilesExecutable();
|
||||
442
scripts/download-bins.js
Normal file
@@ -0,0 +1,442 @@
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
|
||||
const downloadDir = path.join(projectRoot, 'src-tauri', 'resources', 'downloads');
|
||||
const binDir = path.join(projectRoot, 'src-tauri', 'binaries');
|
||||
|
||||
const platform = os.platform();
|
||||
const targetPlatform = process.argv[2];
|
||||
const targetBin = process.argv[3];
|
||||
|
||||
const versions = {
|
||||
'yt-dlp': 'latest',
|
||||
'ffmpeg-ffprobe': 'latest',
|
||||
'deno': 'latest',
|
||||
'aria2c': '1.37.0',
|
||||
};
|
||||
|
||||
const binaries = {
|
||||
'yt-dlp': [
|
||||
{
|
||||
name: 'yt-dlp-x86_64-pc-windows-msvc',
|
||||
platform: 'win32',
|
||||
url: `https://github.com/yt-dlp/yt-dlp-nightly-builds/releases${versions['yt-dlp'] === 'latest' ? '/latest' : ''}/download${versions['yt-dlp'] !== 'latest' ? '/'+versions['yt-dlp'] : ''}/yt-dlp.exe`,
|
||||
src: path.join(downloadDir, 'yt-dlp-x86_64-pc-windows-msvc.exe'),
|
||||
dest: [
|
||||
path.join(binDir, 'yt-dlp-x86_64-pc-windows-msvc.exe')
|
||||
],
|
||||
archive: null,
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'yt-dlp-x86_64-pc-windows-msvc.exe')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'yt-dlp-x86_64-unknown-linux-gnu',
|
||||
platform: 'linux',
|
||||
url: `https://github.com/yt-dlp/yt-dlp-nightly-builds/releases${versions['yt-dlp'] === 'latest' ? '/latest' : ''}/download${versions['yt-dlp'] !== 'latest' ? '/'+versions['yt-dlp'] : ''}/yt-dlp_linux`,
|
||||
src: path.join(downloadDir, 'yt-dlp-x86_64-unknown-linux-gnu'),
|
||||
dest: [
|
||||
path.join(binDir, 'yt-dlp-x86_64-unknown-linux-gnu')
|
||||
],
|
||||
archive: null,
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'yt-dlp-x86_64-unknown-linux-gnu')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'yt-dlp-aarch64-unknown-linux-gnu',
|
||||
platform: 'linux',
|
||||
url: `https://github.com/yt-dlp/yt-dlp-nightly-builds/releases${versions['yt-dlp'] === 'latest' ? '/latest' : ''}/download${versions['yt-dlp'] !== 'latest' ? '/'+versions['yt-dlp'] : ''}/yt-dlp_linux_aarch64`,
|
||||
src: path.join(downloadDir, 'yt-dlp-aarch64-unknown-linux-gnu'),
|
||||
dest: [
|
||||
path.join(binDir, 'yt-dlp-aarch64-unknown-linux-gnu')
|
||||
],
|
||||
archive: null,
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'yt-dlp-aarch64-unknown-linux-gnu')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'yt-dlp-universal-apple-darwin',
|
||||
platform: 'darwin',
|
||||
url: `https://github.com/yt-dlp/yt-dlp-nightly-builds/releases${versions['yt-dlp'] === 'latest' ? '/latest' : ''}/download${versions['yt-dlp'] !== 'latest' ? '/'+versions['yt-dlp'] : ''}/yt-dlp_macos`,
|
||||
src: path.join(downloadDir, 'yt-dlp-universal-apple-darwin'),
|
||||
dest: [
|
||||
path.join(binDir, 'yt-dlp-x86_64-apple-darwin'),
|
||||
path.join(binDir, 'yt-dlp-aarch64-apple-darwin')
|
||||
],
|
||||
archive: null,
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'yt-dlp-universal-apple-darwin')
|
||||
]
|
||||
},
|
||||
],
|
||||
'ffmpeg-ffprobe': [
|
||||
{
|
||||
name: 'ffmpeg-ffprobe-x86_64-pc-windows-msvc',
|
||||
platform: 'win32',
|
||||
url: `https://github.com/yt-dlp/FFmpeg-Builds/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffmpeg-master-latest-win64-gpl.zip`,
|
||||
src: path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl.zip'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl', 'bin', 'ffmpeg.exe'),
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl', 'bin', 'ffprobe.exe')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'ffmpeg-x86_64-pc-windows-msvc.exe'),
|
||||
path.join(binDir, 'ffprobe-x86_64-pc-windows-msvc.exe')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl.zip'),
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'ffmpeg-ffprobe-x86_64-unknown-linux-gnu',
|
||||
platform: 'linux',
|
||||
url: `https://github.com/yt-dlp/FFmpeg-Builds/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffmpeg-master-latest-linux64-gpl.tar.xz`,
|
||||
src: path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl.tar.xz'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'tar.xz',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl', 'bin', 'ffmpeg'),
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl', 'bin', 'ffprobe')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'ffmpeg-x86_64-unknown-linux-gnu'),
|
||||
path.join(binDir, 'ffprobe-x86_64-unknown-linux-gnu')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl.tar.xz'),
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'ffmpeg-ffprobe-aarch64-unknown-linux-gnu',
|
||||
platform: 'linux',
|
||||
url: `https://github.com/yt-dlp/FFmpeg-Builds/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffmpeg-master-latest-linuxarm64-gpl.tar.xz`,
|
||||
src: path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl.tar.xz'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'tar.xz',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl', 'bin', 'ffmpeg'),
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl', 'bin', 'ffprobe')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'ffmpeg-aarch64-unknown-linux-gnu'),
|
||||
path.join(binDir, 'ffprobe-aarch64-unknown-linux-gnu')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl.tar.xz'),
|
||||
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'ffmpeg-universal-apple-darwin',
|
||||
platform: 'darwin',
|
||||
url: `https://evermeet.cx/ffmpeg/get/zip`,
|
||||
src: path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'ffmpeg'),
|
||||
path.join(downloadDir, 'ffmpeg')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'ffmpeg-x86_64-apple-darwin'),
|
||||
path.join(binDir, 'ffmpeg-aarch64-apple-darwin')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
|
||||
path.join(downloadDir, 'ffmpeg')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'ffprobe-universal-apple-darwin',
|
||||
platform: 'darwin',
|
||||
url: `https://evermeet.cx/ffmpeg/get/ffprobe/zip`,
|
||||
src: path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'ffprobe'),
|
||||
path.join(downloadDir, 'ffprobe')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'ffprobe-x86_64-apple-darwin'),
|
||||
path.join(binDir, 'ffprobe-aarch64-apple-darwin')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
|
||||
path.join(downloadDir, 'ffprobe')
|
||||
]
|
||||
}
|
||||
],
|
||||
'deno': [
|
||||
{
|
||||
name: 'deno-x86_64-pc-windows-msvc',
|
||||
platform: 'win32',
|
||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-x86_64-pc-windows-msvc.zip`,
|
||||
src: path.join(downloadDir, 'deno-x86_64-pc-windows-msvc.zip'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'deno.exe')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'deno-x86_64-pc-windows-msvc.exe')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'deno-x86_64-pc-windows-msvc.zip'),
|
||||
path.join(downloadDir, 'deno.exe')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'deno-x86_64-unknown-linux-gnu',
|
||||
platform: 'linux',
|
||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-x86_64-unknown-linux-gnu.zip`,
|
||||
src: path.join(downloadDir, 'deno-x86_64-unknown-linux-gnu.zip'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'deno')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'deno-x86_64-unknown-linux-gnu')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'deno-x86_64-unknown-linux-gnu.zip'),
|
||||
path.join(downloadDir, 'deno')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'deno-aarch64-unknown-linux-gnu',
|
||||
platform: 'linux',
|
||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-aarch64-unknown-linux-gnu.zip`,
|
||||
src: path.join(downloadDir, 'deno-aarch64-unknown-linux-gnu.zip'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'deno')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'deno-aarch64-unknown-linux-gnu')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'deno-aarch64-unknown-linux-gnu.zip'),
|
||||
path.join(downloadDir, 'deno')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'deno-x86_64-apple-darwin',
|
||||
platform: 'darwin',
|
||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-x86_64-apple-darwin.zip`,
|
||||
src: path.join(downloadDir, 'deno-x86_64-apple-darwin.zip'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'deno')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'deno-x86_64-apple-darwin')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'deno-x86_64-apple-darwin.zip'),
|
||||
path.join(downloadDir, 'deno')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'deno-aarch64-apple-darwin',
|
||||
platform: 'darwin',
|
||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-aarch64-apple-darwin.zip`,
|
||||
src: path.join(downloadDir, 'deno-aarch64-apple-darwin.zip'),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, 'deno')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'deno-aarch64-apple-darwin')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, 'deno-aarch64-apple-darwin.zip'),
|
||||
path.join(downloadDir, 'deno')
|
||||
]
|
||||
}
|
||||
],
|
||||
'aria2c': [
|
||||
{
|
||||
name: 'aria2c-x86_64-pc-windows-msvc',
|
||||
platform: 'win32',
|
||||
url: `https://github.com/aria2/aria2/releases/download/release-${versions['aria2c']}/aria2-${versions['aria2c']}-win-64bit-build1.zip`,
|
||||
src: path.join(downloadDir, `aria2-${versions['aria2c']}-win-64bit-build1.zip`),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, `aria2-${versions['aria2c']}-win-64bit-build1`, 'aria2c.exe')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'aria2c-x86_64-pc-windows-msvc.exe')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, `aria2-${versions['aria2c']}-win-64bit-build1.zip`),
|
||||
path.join(downloadDir, `aria2-${versions['aria2c']}-win-64bit-build1`)
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'aria2c-x86_64-unknown-linux-gnu',
|
||||
platform: 'linux',
|
||||
url: `https://github.com/asdo92/aria2-static-builds/releases/download/v${versions['aria2c']}/aria2-${versions['aria2c']}-linux-gnu-64bit-build1.tar.bz2`,
|
||||
src: path.join(downloadDir, `aria2-${versions['aria2c']}-linux-gnu-64bit-build1.tar.bz2`),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'tar.bz2',
|
||||
binSrc: [
|
||||
path.join(downloadDir, `aria2-${versions['aria2c']}-linux-gnu-64bit-build1`, 'aria2c')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'aria2c-x86_64-unknown-linux-gnu')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, `aria2-${versions['aria2c']}-linux-gnu-64bit-build1.tar.bz2`),
|
||||
path.join(downloadDir, `aria2-${versions['aria2c']}-linux-gnu-64bit-build1`)
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'aria2c-aarch64-unknown-linux-gnu',
|
||||
platform: 'linux',
|
||||
url: `https://github.com/aria2/aria2/releases/download/release-${versions['aria2c']}/aria2-${versions['aria2c']}-aarch64-linux-android-build1.zip`,
|
||||
src: path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1.zip`),
|
||||
dest: null,
|
||||
archive: {
|
||||
type: 'zip',
|
||||
binSrc: [
|
||||
path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1`, 'aria2c')
|
||||
],
|
||||
binDest: [
|
||||
path.join(binDir, 'aria2c-aarch64-unknown-linux-gnu')
|
||||
]
|
||||
},
|
||||
cleanup: [
|
||||
path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1.zip`),
|
||||
path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1`)
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
function downloadAndProcess(bin) {
|
||||
console.log(`=> Processing: ${bin.name}`);
|
||||
console.log(`Downloading: ${bin.url}`);
|
||||
if (platform === 'win32') {
|
||||
execSync(`powershell -Command "Invoke-WebRequest -Uri '${bin.url}' -OutFile '${bin.src}'"`, { stdio: 'inherit' });
|
||||
} else {
|
||||
execSync(`curl -L "${bin.url}" -o "${bin.src}"`, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
if (bin.archive) {
|
||||
console.log(`Extracting: ${bin.src}`);
|
||||
if (platform === 'win32' && bin.archive.type === 'zip') {
|
||||
execSync(`powershell -Command "Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${bin.src}', '${downloadDir}')"`, { stdio: 'inherit' });
|
||||
} else if (bin.archive.type === 'tar.bz2') {
|
||||
execSync(`tar -xjf "${bin.src}" -C "${downloadDir}"`, { stdio: 'inherit' });
|
||||
} else if (bin.archive.type === 'zip') {
|
||||
execSync(`unzip -o "${bin.src}" -d "${downloadDir}"`, { stdio: 'inherit' });
|
||||
} else {
|
||||
execSync(`tar -xf "${bin.src}" -C "${downloadDir}"`, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
bin.archive.binSrc.forEach((src, index) => {
|
||||
const dest = bin.archive.binDest[index];
|
||||
console.log(`Moving: "${src}" to "${dest}"`);
|
||||
fs.copyFileSync(src, dest);
|
||||
if (platform !== 'win32') {
|
||||
fs.chmodSync(dest, 0o755);
|
||||
}
|
||||
});
|
||||
} else if (bin.dest) {
|
||||
bin.dest.forEach((dest) => {
|
||||
console.log(`Moving: "${bin.src}" to "${dest}"`);
|
||||
fs.copyFileSync(bin.src, dest);
|
||||
if (platform !== 'win32') {
|
||||
fs.chmodSync(dest, 0o755);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bin.cleanup.forEach((item) => {
|
||||
if (fs.existsSync(item)) {
|
||||
console.log(`Cleaning: "${item}"`);
|
||||
const stats = fs.statSync(item);
|
||||
if (stats.isDirectory()) {
|
||||
fs.rmSync(item, { recursive: true, force: true });
|
||||
} else {
|
||||
fs.unlinkSync(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (targetPlatform && !['win32', 'linux', 'darwin', 'all'].includes(targetPlatform)) {
|
||||
console.error(`ERROR: Invalid platform specified: '${targetPlatform}'. Use one of: win32, linux, darwin, or all`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (targetBin && !binaries.hasOwnProperty(targetBin) && targetBin !== 'all') {
|
||||
console.error(`ERROR: Invalid binary specified: '${targetBin}'. Use one of: ${Object.keys(binaries).join(', ')}, or all`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const effectivePlatform = targetPlatform || platform;
|
||||
const effectiveBin = targetBin || 'all';
|
||||
|
||||
console.log(`RUNNING: 📦 Binary Downloader (platform: ${effectivePlatform} | binary: ${effectiveBin})`);
|
||||
|
||||
Object.keys(binaries).forEach((binKey) => {
|
||||
if (effectiveBin !== 'all' && binKey !== effectiveBin) {
|
||||
return;
|
||||
}
|
||||
|
||||
binaries[binKey].forEach((bin) => {
|
||||
if (effectivePlatform !== 'all' && bin.platform !== effectivePlatform) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadAndProcess(bin);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('✅ Downloads Completed');
|
||||
@@ -5,8 +5,9 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
|
||||
console.log(`RUNNING: 🛠️ Build Script updateYtDlpBinary.js`);
|
||||
console.log(`RUNNING: 🛠️ Build Script --> update-yt-dlp.js`);
|
||||
|
||||
// Get the platform triple from command line arguments
|
||||
const platformTriple = process.argv[2];
|
||||
@@ -17,7 +18,7 @@ if (!platformTriple) {
|
||||
}
|
||||
|
||||
// Define the binaries directory
|
||||
const binariesDir = path.join(__dirname, 'src-tauri', 'binaries');
|
||||
const binariesDir = path.join(projectRoot, 'src-tauri', 'binaries');
|
||||
|
||||
// Construct the binary filename based on platform triple
|
||||
let binaryName = `yt-dlp-${platformTriple}`;
|
||||
@@ -50,7 +51,7 @@ execFile(binaryPath, ['--update-to', 'nightly'], (error, stdout, stderr) => {
|
||||
if (stderr) console.error(stderr);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
console.log(`Update successful for ${platformTriple}:`);
|
||||
console.log(stdout);
|
||||
});
|
||||
});
|
||||
2133
src-tauri/Cargo.lock
generated
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "neodlp"
|
||||
version = "0.2.1"
|
||||
version = "0.3.4"
|
||||
description = "NeoDLP"
|
||||
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
|
||||
edition = "2021"
|
||||
@@ -27,8 +27,9 @@ tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = "*"
|
||||
sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] }
|
||||
base64 = "0.22"
|
||||
directories = "5.0"
|
||||
directories = "6.0"
|
||||
futures-util = "0.3"
|
||||
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
@@ -36,6 +37,8 @@ tauri-plugin-os = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-clipboard-manager = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2"
|
||||
|
||||
0
src-tauri/binaries/.gitkeep
Normal file
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc5bd50ef656f1727d6f1c6c55688b21434e70cb5fb3e439701d1061ae094bf0
|
||||
size 34391824
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc5bd50ef656f1727d6f1c6c55688b21434e70cb5fb3e439701d1061ae094bf0
|
||||
size 34391824
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5fa4862eb50050941370fa78a8c56faa31516d5abaf1f7cf31bc56de166347d9
|
||||
size 18123562
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0aa0afe3d2b32c047b73083f3c8e56081d71bb33fe047357820d51d153d1d54f
|
||||
size 34608400
|
||||
@@ -22,6 +22,9 @@
|
||||
"fs:allow-app-write-recursive",
|
||||
"updater:default",
|
||||
"process:default",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"notification:default",
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [
|
||||
@@ -39,4 +42,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,45 @@
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/ffmpeg",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/ffprobe",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/aria2c",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/deno",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
"cmd": "ffmpeg",
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "aria2c",
|
||||
"cmd": "aria2c",
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "pkexec",
|
||||
"cmd": "pkexec",
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "powershell",
|
||||
"cmd": "powershell",
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -29,6 +64,36 @@
|
||||
"name": "binaries/yt-dlp",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/ffmpeg",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/ffprobe",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/aria2c",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/deno",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
"cmd": "ffmpeg",
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "aria2c",
|
||||
"cmd": "aria2c",
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -38,4 +103,4 @@
|
||||
"macOS",
|
||||
"linux"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
!macro NSIS_HOOK_POSTINSTALL
|
||||
; Add Registry Keys for Chrome Native Messaging Host
|
||||
WriteRegStr HKCU "Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp" "" "$INSTDIR\neodlp-msghost.json"
|
||||
WriteRegStr HKCU "Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp" "" "$INSTDIR\chrome.json"
|
||||
; Add Registry Keys for Firefox Native Messaging Host
|
||||
WriteRegStr HKCU "Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp" "" "$INSTDIR\neodlp-msghost-moz.json"
|
||||
WriteRegStr HKCU "Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp" "" "$INSTDIR\firefox.json"
|
||||
; Add entry for automatic startup with Windows
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}" "$\"$INSTDIR\neodlp.exe$\" --hidden"
|
||||
!macroend
|
||||
@@ -12,4 +12,4 @@
|
||||
DeleteRegKey HKCU "Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp"
|
||||
DeleteRegKey HKCU "Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp"
|
||||
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}"
|
||||
!macroend
|
||||
!macroend
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Component Id="NeoDlpRegEntriesFragment" Guid="*">
|
||||
<RegistryKey Root="HKLM" Key="Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]neodlp-msghost.json" KeyPath="no" />
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]chrome.json" KeyPath="no" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKLM" Key="Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]neodlp-msghost-moz.json" KeyPath="no" />
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]firefox.json" KeyPath="no" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Run">
|
||||
<RegistryValue Name="NeoDLP" Type="string" Value=""[INSTALLDIR]neodlp.exe" --hidden" KeyPath="no" />
|
||||
@@ -15,4 +15,4 @@
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
</Wix>
|
||||
|
||||
@@ -12,6 +12,6 @@ license = "MIT"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = "*"
|
||||
futures-util = "0.3"
|
||||
directories = "5.0"
|
||||
directories = "6.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e607b7f079c4eb0dc666ffca152f225020f8022c8c014dd94d91e6072f57228d
|
||||
size 79945800
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e607b7f079c4eb0dc666ffca152f225020f8022c8c014dd94d91e6072f57228d
|
||||
size 79945800
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c49b5913c9a107120c86b401af95df7965003f7fc6dbb4436f1f03c8ba391e8b
|
||||
size 127473664
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3ee15e5145c9eb4775c193ab824c592d4ff3744bb7f283f8db29bd3c3c961589
|
||||
size 79928672
|
||||
0
src-tauri/resources/downloads/.gitkeep
Normal file
@@ -174,6 +174,16 @@ fn get_config_file_path() -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_current_app_path() -> Result<String, String> {
|
||||
let exe_path = std::env::current_exe().map_err(|e| e.to_string())?;
|
||||
Ok(exe_path
|
||||
.parent()
|
||||
.ok_or("Failed to get parent directory")?
|
||||
.to_string_lossy()
|
||||
.into_owned())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_config(
|
||||
new_config: Config,
|
||||
@@ -452,6 +462,7 @@ async fn pause_ongoing_downloads(
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub async fn run() {
|
||||
let _ = fix_path_env::fix();
|
||||
let migrations = migrations::get_migrations();
|
||||
let config = load_config();
|
||||
let port = config.port;
|
||||
@@ -485,6 +496,8 @@ pub async fn run() {
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.manage(ImageCache(StdMutex::new(HashMap::new())))
|
||||
.manage(websocket_state.clone())
|
||||
.setup(move |app| {
|
||||
@@ -586,6 +599,7 @@ pub async fn run() {
|
||||
reset_config,
|
||||
get_config_file_path,
|
||||
restart_websocket_server,
|
||||
get_current_app_path,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -68,5 +68,85 @@ pub fn get_migrations() -> Vec<Migration> {
|
||||
);
|
||||
",
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 2,
|
||||
description: "add_columns_to_downloads",
|
||||
sql: "
|
||||
-- Create temporary table with all new columns
|
||||
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,
|
||||
custom_command TEXT,
|
||||
queue_config TEXT,
|
||||
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 with default values for new columns
|
||||
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,
|
||||
NULL, -- output_format
|
||||
0, -- embed_metadata
|
||||
0, -- embed_thumbnail
|
||||
NULL, -- sponsorblock_remove
|
||||
NULL, -- sponsorblock_mark
|
||||
0, -- use_aria2
|
||||
NULL, -- custom_command
|
||||
NULL, -- queue_config
|
||||
CURRENT_TIMESTAMP, -- created_at
|
||||
CURRENT_TIMESTAMP -- updated_at
|
||||
FROM downloads;
|
||||
|
||||
-- Drop the original table
|
||||
DROP TABLE downloads;
|
||||
|
||||
-- Rename temporary table to original name
|
||||
ALTER TABLE downloads_temp RENAME TO downloads;
|
||||
|
||||
-- Create trigger for updating updated_at timestamp
|
||||
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,
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "NeoDLP",
|
||||
"mainBinaryName": "neodlp",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.4",
|
||||
"identifier": "com.neosubhamoy.neodlp",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@@ -36,6 +36,9 @@
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"sql": {
|
||||
"preload": ["sqlite:database.db"]
|
||||
},
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDM0I4ODcyODdGOTM4MDIKUldRQ09QbUhjb2c3UENGY1lFUVdTVWhucmJ4QzdGeW9sU3VHVFlGNWY5anZab2s4SU1rMWFsekMK",
|
||||
"endpoints": [
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"identifier": "com.neosubhamoy.neodlp",
|
||||
"build": {
|
||||
"beforeDevCommand": "cargo build --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run dev",
|
||||
"beforeBuildCommand": "cargo build --release --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && node updateYtDlpBinary.js x86_64-unknown-linux-gnu && npm run build",
|
||||
"beforeDevCommand": "cargo build --target=aarch64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
|
||||
"beforeBuildCommand": "cargo build --release --target=aarch64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && npm run build",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
@@ -36,32 +36,33 @@
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/yt-dlp"
|
||||
"binaries/yt-dlp",
|
||||
"binaries/aria2c",
|
||||
"binaries/deno"
|
||||
],
|
||||
"resources": {
|
||||
"resources/binaries/ffmpeg-x86_64-unknown-linux-gnu": "binaries/ffmpeg-x86_64"
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["ffmpeg"],
|
||||
"files": {
|
||||
"/etc/opt/chrome/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
||||
"/etc/chromium/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
||||
"/usr/lib/mozilla/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/firefox.json",
|
||||
"/usr/bin/neodlp-msghost": "./target/release/neodlp-msghost",
|
||||
"/usr/bin/neodlp-msghost": "./target/aarch64-unknown-linux-gnu/release/neodlp-msghost",
|
||||
"/etc/xdg/autostart/neodlp-autostart.desktop": "./resources/autostart/linux/autostart.desktop"
|
||||
}
|
||||
},
|
||||
"rpm": {
|
||||
"epoch": 0,
|
||||
"release": "1",
|
||||
"depends": ["ffmpeg"],
|
||||
"files": {
|
||||
"/etc/opt/chrome/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
||||
"/etc/chromium/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
||||
"/usr/lib/mozilla/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/firefox.json",
|
||||
"/usr/bin/neodlp-msghost": "./target/release/neodlp-msghost",
|
||||
"/usr/bin/neodlp-msghost": "./target/aarch64-unknown-linux-gnu/release/neodlp-msghost",
|
||||
"/etc/xdg/autostart/neodlp-autostart.desktop": "./resources/autostart/linux/autostart.desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src-tauri/tauri.linux-x86_64.conf.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"identifier": "com.neosubhamoy.neodlp",
|
||||
"build": {
|
||||
"beforeDevCommand": "cargo build --target=x86_64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
|
||||
"beforeBuildCommand": "cargo build --release --target=x86_64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && npm run build",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "NeoDLP",
|
||||
"width": 1067,
|
||||
"height": 605,
|
||||
"visible": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null,
|
||||
"capabilities": [
|
||||
"default",
|
||||
"shell-scope"
|
||||
]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "rpm", "appimage"],
|
||||
"createUpdaterArtifacts": true,
|
||||
"licenseFile": "../LICENSE",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/yt-dlp",
|
||||
"binaries/aria2c",
|
||||
"binaries/deno"
|
||||
],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["ffmpeg"],
|
||||
"files": {
|
||||
"/etc/opt/chrome/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
||||
"/etc/chromium/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
||||
"/usr/lib/mozilla/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/firefox.json",
|
||||
"/usr/bin/neodlp-msghost": "./target/x86_64-unknown-linux-gnu/release/neodlp-msghost",
|
||||
"/etc/xdg/autostart/neodlp-autostart.desktop": "./resources/autostart/linux/autostart.desktop"
|
||||
}
|
||||
},
|
||||
"rpm": {
|
||||
"epoch": 0,
|
||||
"release": "1",
|
||||
"depends": ["ffmpeg"],
|
||||
"files": {
|
||||
"/etc/opt/chrome/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
||||
"/etc/chromium/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
||||
"/usr/lib/mozilla/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/firefox.json",
|
||||
"/usr/bin/neodlp-msghost": "./target/x86_64-unknown-linux-gnu/release/neodlp-msghost",
|
||||
"/etc/xdg/autostart/neodlp-autostart.desktop": "./resources/autostart/linux/autostart.desktop"
|
||||
}
|
||||
},
|
||||
"appimage": {
|
||||
"files": {
|
||||
"/usr/bin/ffmpeg": "./binaries/ffmpeg-x86_64-unknown-linux-gnu",
|
||||
"/usr/bin/ffprobe": "./binaries/ffprobe-x86_64-unknown-linux-gnu"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"identifier": "com.neosubhamoy.neodlp",
|
||||
"build": {
|
||||
"beforeDevCommand": "[[ -n \"$TARGET_ARCH\" ]] && ARCH=\"$TARGET_ARCH\" || ARCH=\"$(uname -m | sed 's/^arm64$/aarch64/')-apple-darwin\" && cargo build --target=$ARCH --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run dev",
|
||||
"beforeBuildCommand": "[[ -n \"$TARGET_ARCH\" ]] && ARCH=\"$TARGET_ARCH\" || ARCH=\"$(uname -m | sed 's/^arm64$/aarch64/')-apple-darwin\" && cargo build --release --target=$ARCH --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && node updateYtDlpBinary.js aarch64-apple-darwin && npm run build",
|
||||
"beforeDevCommand": "cargo build --target=aarch64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
|
||||
"beforeBuildCommand": "cargo build --release --target=aarch64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && npm run build",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
@@ -36,11 +36,13 @@
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/yt-dlp"
|
||||
"binaries/yt-dlp",
|
||||
"binaries/ffmpeg",
|
||||
"binaries/ffprobe",
|
||||
"binaries/deno"
|
||||
],
|
||||
"resources": {
|
||||
"target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
||||
"resources/binaries/ffmpeg-aarch64-apple-darwin": "binaries/ffmpeg-aarch64",
|
||||
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
|
||||
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
||||
@@ -49,4 +51,4 @@
|
||||
"providerShortName": "neosubhamoy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"identifier": "com.neosubhamoy.neodlp",
|
||||
"build": {
|
||||
"beforeDevCommand": "[[ -n \"$TARGET_ARCH\" ]] && ARCH=\"$TARGET_ARCH\" || ARCH=\"$(uname -m | sed 's/^arm64$/aarch64/')-apple-darwin\" && cargo build --target=$ARCH --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run dev",
|
||||
"beforeBuildCommand": "[[ -n \"$TARGET_ARCH\" ]] && ARCH=\"$TARGET_ARCH\" || ARCH=\"$(uname -m | sed 's/^arm64$/aarch64/')-apple-darwin\" && cargo build --release --target=$ARCH --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && node updateYtDlpBinary.js x86_64-apple-darwin && npm run build",
|
||||
"beforeDevCommand": "cargo build --target=x86_64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
|
||||
"beforeBuildCommand": "cargo build --release --target=x86_64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && npm run build",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
@@ -36,11 +36,13 @@
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/yt-dlp"
|
||||
"binaries/yt-dlp",
|
||||
"binaries/ffmpeg",
|
||||
"binaries/ffprobe",
|
||||
"binaries/deno"
|
||||
],
|
||||
"resources": {
|
||||
"target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
||||
"resources/binaries/ffmpeg-x86_64-apple-darwin": "binaries/ffmpeg-x86_64",
|
||||
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
|
||||
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
||||
@@ -49,4 +51,4 @@
|
||||
"providerShortName": "neosubhamoy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"identifier": "com.neosubhamoy.neodlp",
|
||||
"build": {
|
||||
"beforeDevCommand": "cargo build --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
|
||||
"beforeBuildCommand": "cargo build --release --manifest-path=./src-tauri/msghost/Cargo.toml && node updateYtDlpBinary.js x86_64-pc-windows-msvc && npm run build",
|
||||
"beforeBuildCommand": "cargo build --release --manifest-path=./src-tauri/msghost/Cargo.toml && npm run build",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
@@ -36,13 +36,16 @@
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/yt-dlp"
|
||||
"binaries/yt-dlp",
|
||||
"binaries/ffmpeg",
|
||||
"binaries/ffprobe",
|
||||
"binaries/aria2c",
|
||||
"binaries/deno"
|
||||
],
|
||||
"resources": {
|
||||
"resources/binaries/ffmpeg-x86_64-pc-windows-msvc.exe": "binaries/ffmpeg-x86_64.exe",
|
||||
"target/release/neodlp-msghost.exe": "neodlp-msghost.exe",
|
||||
"resources/msghost-manifest/windows/chrome.json": "neodlp-msghost.json",
|
||||
"resources/msghost-manifest/windows/firefox.json": "neodlp-msghost-moz.json"
|
||||
"resources/msghost-manifest/windows/chrome.json": "chrome.json",
|
||||
"resources/msghost-manifest/windows/firefox.json": "firefox.json"
|
||||
},
|
||||
"windows": {
|
||||
"wix": {
|
||||
@@ -54,4 +57,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
535
src/App.tsx
@@ -1,6 +1,5 @@
|
||||
import { ThemeProvider } from "@/providers/themeProvider";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { AppContext } from "@/providers/appContextProvider";
|
||||
import { DownloadState } from "@/types/download";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
@@ -8,7 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
||||
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
||||
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { determineFileType, generateDownloadId, generateSafeFilePath, generateVideoId, isObjEmpty, parseProgressLine, sanitizeFilename } from "@/utils";
|
||||
import { determineFileType, generateVideoId, isObjEmpty, parseProgressLine } from "@/utils";
|
||||
import { Command } from "@tauri-apps/plugin-shell";
|
||||
import { RawVideoInfo } from "@/types/video";
|
||||
import { useDeleteDownloadState, useSaveDownloadState, useSavePlaylistInfo, useSaveVideoInfo, useUpdateDownloadFilePath, useUpdateDownloadStatus } from "@/services/mutations";
|
||||
@@ -25,11 +24,14 @@ import { useNavigate } from "react-router-dom";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { useMacOsRegisterer } from "@/helpers/use-macos-registerer";
|
||||
import useAppUpdater from "@/helpers/use-app-updater";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||
import { toast } from "sonner";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
import { DownloadConfiguration } from "@/types/settings";
|
||||
import { ulid } from "ulid";
|
||||
import { sendNotification } from '@tauri-apps/plugin-notification';
|
||||
|
||||
export default function App({ children }: { children: React.ReactNode }) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
||||
const { data: settings, isSuccess: isSuccessFetchingSettings } = useFetchAllSettings();
|
||||
const { data: kvPairs, isSuccess: isSuccessFetchingKvPairs } = useFetchAllkVPairs();
|
||||
@@ -38,14 +40,13 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const globalDownloadStates = useDownloadStatesStore((state) => state.downloadStates);
|
||||
const setDownloadStates = useDownloadStatesStore((state) => state.setDownloadStates);
|
||||
const setPath = useBasePathsStore((state) => state.setPath);
|
||||
|
||||
|
||||
const ffmpegPath = useBasePathsStore((state) => state.ffmpegPath);
|
||||
const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath);
|
||||
const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath);
|
||||
|
||||
const setSearchPid = useCurrentVideoMetadataStore((state) => state.setSearchPid);
|
||||
|
||||
// const isUsingDefaultSettings = useSettingsPageStatesStore((state) => state.isUsingDefaultSettings);
|
||||
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
|
||||
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
|
||||
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
||||
@@ -71,7 +72,30 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const ALWAYS_REENCODE_VIDEO = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
||||
const EMBED_VIDEO_METADATA = useSettingsPageStatesStore(state => state.settings.embed_video_metadata);
|
||||
const EMBED_AUDIO_METADATA = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
|
||||
const EMBED_VIDEO_THUMBNAIL = useSettingsPageStatesStore(state => state.settings.embed_video_thumbnail);
|
||||
const EMBED_AUDIO_THUMBNAIL = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
|
||||
const USE_COOKIES = useSettingsPageStatesStore(state => state.settings.use_cookies);
|
||||
const IMPORT_COOKIES_FROM = useSettingsPageStatesStore(state => state.settings.import_cookies_from);
|
||||
const COOKIES_BROWSER = useSettingsPageStatesStore(state => state.settings.cookies_browser);
|
||||
const COOKIES_FILE = useSettingsPageStatesStore(state => state.settings.cookies_file);
|
||||
const USE_SPONSORBLOCK = useSettingsPageStatesStore(state => state.settings.use_sponsorblock);
|
||||
const SPONSORBLOCK_MODE = useSettingsPageStatesStore(state => state.settings.sponsorblock_mode);
|
||||
const SPONSORBLOCK_REMOVE = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove);
|
||||
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 USE_FORCE_INTERNET_PROTOCOL = useSettingsPageStatesStore(state => state.settings.use_force_internet_protocol);
|
||||
const FORCE_INTERNET_PROTOCOL = useSettingsPageStatesStore(state => state.settings.force_internet_protocol);
|
||||
const USE_CUSTOM_COMMANDS = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||
const CUSTOM_COMMANDS = useSettingsPageStatesStore(state => state.settings.custom_commands);
|
||||
const FILENAME_TEMPLATE = useSettingsPageStatesStore(state => state.settings.filename_template);
|
||||
const DEBUG_MODE = useSettingsPageStatesStore(state => state.settings.debug_mode);
|
||||
const LOG_VERBOSE = useSettingsPageStatesStore(state => state.settings.log_verbose);
|
||||
const LOG_WARNING = useSettingsPageStatesStore(state => state.settings.log_warning);
|
||||
const LOG_PROGRESS = useSettingsPageStatesStore(state => state.settings.log_progress);
|
||||
const ENABLE_NOTIFICATIONS = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
||||
const DOWNLOAD_COMPLETION_NOTIFICATION = useSettingsPageStatesStore(state => state.settings.download_completion_notification);
|
||||
|
||||
const isErrored = useDownloaderPageStatesStore((state) => state.isErrored);
|
||||
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
|
||||
@@ -82,13 +106,15 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
const navigate = useNavigate();
|
||||
const LOG = useLogger();
|
||||
const currentPlatform = platform();
|
||||
const { updateYtDlp } = useYtDlpUpdater();
|
||||
const { registerToMac } = useMacOsRegisterer();
|
||||
const { checkForAppUpdate } = useAppUpdater();
|
||||
const setKvPairsKey = useKvPairsStatesStore((state) => state.setKvPairsKey);
|
||||
const ytDlpUpdateLastCheck = useKvPairsStatesStore(state => state.kvPairs.ytdlp_update_last_check);
|
||||
const macOsRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.macos_registered_version);
|
||||
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const downloadStateSaver = useSaveDownloadState();
|
||||
const downloadStatusUpdater = useUpdateDownloadStatus();
|
||||
@@ -96,24 +122,65 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const videoInfoSaver = useSaveVideoInfo();
|
||||
const downloadStateDeleter = useDeleteDownloadState();
|
||||
const playlistInfoSaver = useSavePlaylistInfo();
|
||||
|
||||
|
||||
const ongoingDownloads = globalDownloadStates.filter(state => state.download_status === 'downloading' || state.download_status === 'starting');
|
||||
const queuedDownloads = globalDownloadStates.filter(state => state.download_status === 'queued').sort((a, b) => a.queue_index! - b.queue_index!);
|
||||
|
||||
const isProcessingQueueRef = useRef(false);
|
||||
const lastProcessedDownloadIdRef = useRef<string | null>(null);
|
||||
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
||||
const hasRunAppUpdateCheckRef = useRef(false);
|
||||
const isRegisteredToMacOsRef = useRef(false);
|
||||
|
||||
const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string): Promise<RawVideoInfo | null> => {
|
||||
|
||||
const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState, downloadConfig?: DownloadConfiguration): Promise<RawVideoInfo | null> => {
|
||||
try {
|
||||
const args = [url, '--dump-single-json', '--no-warnings'];
|
||||
if (formatId) args.push('-f', formatId);
|
||||
if (formatId) args.push('--format', formatId);
|
||||
if (selectedSubtitles) args.push('--embed-subs', '--sub-lang', selectedSubtitles);
|
||||
if (playlistIndex) args.push('--playlist-items', playlistIndex);
|
||||
if (PREFER_VIDEO_OVER_PLAYLIST) args.push('--no-playlist');
|
||||
if (PREFER_VIDEO_OVER_PLAYLIST && !playlistIndex) args.push('--no-playlist');
|
||||
if (STRICT_DOWNLOADABILITY_CHECK && !formatId) args.push('--check-all-formats');
|
||||
if (STRICT_DOWNLOADABILITY_CHECK && formatId) args.push('--check-formats');
|
||||
if (USE_PROXY && PROXY_URL) args.push('--proxy', PROXY_URL);
|
||||
|
||||
if ((USE_CUSTOM_COMMANDS && CUSTOM_COMMANDS && downloadConfig?.custom_command) || resumeState?.custom_command) {
|
||||
let customCommandArgs = null;
|
||||
if (resumeState?.custom_command) {
|
||||
customCommandArgs = resumeState.custom_command;
|
||||
} else if (CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig?.custom_command)) {
|
||||
let customCommand = CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig?.custom_command);
|
||||
customCommandArgs = customCommand ? customCommand.args : '';
|
||||
}
|
||||
if (customCommandArgs && customCommandArgs.trim() !== '') args.push(...customCommandArgs.split(' '));
|
||||
}
|
||||
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_PROXY && PROXY_URL) args.push('--proxy', PROXY_URL);
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_FORCE_INTERNET_PROTOCOL && FORCE_INTERNET_PROTOCOL) {
|
||||
if (FORCE_INTERNET_PROTOCOL === 'ipv4') {
|
||||
args.push('--force-ipv4');
|
||||
} else if (FORCE_INTERNET_PROTOCOL === 'ipv6') {
|
||||
args.push('--force-ipv6');
|
||||
}
|
||||
}
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_COOKIES) {
|
||||
if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) {
|
||||
args.push('--cookies-from-browser', COOKIES_BROWSER);
|
||||
} else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) {
|
||||
args.push('--cookies', COOKIES_FILE);
|
||||
}
|
||||
}
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (USE_SPONSORBLOCK || (resumeState?.sponsorblock_remove || resumeState?.sponsorblock_mark))) {
|
||||
if (SPONSORBLOCK_MODE === 'remove' || resumeState?.sponsorblock_remove) {
|
||||
let sponsorblockRemove = resumeState?.sponsorblock_remove || (SPONSORBLOCK_REMOVE === 'custom' ? (
|
||||
SPONSORBLOCK_REMOVE_CATEGORIES.length > 0 ? SPONSORBLOCK_REMOVE_CATEGORIES.join(',') : 'default'
|
||||
) : (SPONSORBLOCK_REMOVE));
|
||||
args.push('--sponsorblock-remove', sponsorblockRemove);
|
||||
} else if (SPONSORBLOCK_MODE === 'mark' || resumeState?.sponsorblock_mark) {
|
||||
let sponsorblockMark = resumeState?.sponsorblock_mark || (SPONSORBLOCK_MARK === 'custom' ? (
|
||||
SPONSORBLOCK_MARK_CATEGORIES.length > 0 ? SPONSORBLOCK_MARK_CATEGORIES.join(',') : 'default'
|
||||
) : (SPONSORBLOCK_MARK));
|
||||
args.push('--sponsorblock-mark', sponsorblockMark);
|
||||
}
|
||||
};
|
||||
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||
|
||||
let jsonOutput = '';
|
||||
@@ -126,14 +193,23 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
command.on('close', async (data) => {
|
||||
if (data.code !== 0) {
|
||||
console.error(`yt-dlp failed to fetch metadata with code ${data.code}`);
|
||||
LOG.error('NEODLP', `yt-dlp exited with code ${data.code} while fetching metadata for URL: ${url} (ignore if you manually cancelled)`);
|
||||
resolve(null);
|
||||
} else {
|
||||
try {
|
||||
const parsedData: RawVideoInfo = JSON.parse(jsonOutput);
|
||||
resolve(parsedData);
|
||||
const matchedJson = jsonOutput.match(/{.*}/);
|
||||
if (!matchedJson) {
|
||||
console.error(`Failed to match JSON: ${jsonOutput}`);
|
||||
LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url})`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const parsedJson: RawVideoInfo = JSON.parse(matchedJson[0]);
|
||||
resolve(parsedJson);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Failed to parse JSON: ${e}`);
|
||||
LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url}) with error: ${e}`);
|
||||
resolve(null);
|
||||
}
|
||||
}
|
||||
@@ -141,43 +217,46 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
|
||||
command.on('error', error => {
|
||||
console.error(`Error fetching metadata: ${error}`);
|
||||
LOG.error('NEODLP', `Error occurred while fetching metadata for URL: ${url} : ${error}`);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
LOG.info('NEODLP', `Fetching metadata for URL: ${url}, with args: ${args.join(' ')}`);
|
||||
command.spawn().then(child => {
|
||||
setSearchPid(child.pid);
|
||||
}).catch(e => {
|
||||
console.error(`Failed to spawn command: ${e}`);
|
||||
LOG.error('NEODLP', `Failed to spawn yt-dlp process for fetching metadata for URL: ${url} : ${e}`);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Failed to fetch metadata: ${e}`);
|
||||
LOG.error('NEODLP', `Failed to fetch metadata for URL: ${url} : ${e}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const startDownload = async (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => {
|
||||
|
||||
const startDownload = async (url: string, selectedFormat: string, downloadConfig: DownloadConfiguration, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => {
|
||||
LOG.info('NEODLP', `Initiating yt-dlp download for URL: ${url}`);
|
||||
// set error states to default
|
||||
setIsErrored(false);
|
||||
setIsErrorExpected(false);
|
||||
setErroredDownloadId(null);
|
||||
|
||||
console.log('Starting download:', { url, selectedFormat, selectedSubtitles, resumeState, playlistItems });
|
||||
console.log('Starting download:', { url, selectedFormat, downloadConfig, selectedSubtitles, resumeState, playlistItems });
|
||||
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
||||
console.error('FFmpeg or download paths not found');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const isPlaylist = (playlistItems && typeof playlistItems === 'string') || (resumeState?.playlist_id && resumeState?.playlist_index) ? true : false;
|
||||
const playlistIndex = isPlaylist ? (resumeState?.playlist_index?.toString() || playlistItems) : null;
|
||||
let videoMetadata = await fetchVideoMetadata(url, selectedFormat, isPlaylist && playlistIndex && typeof playlistIndex === 'string' ? playlistIndex : undefined);
|
||||
let videoMetadata = await fetchVideoMetadata(url, selectedFormat, isPlaylist && playlistIndex && typeof playlistIndex === 'string' ? playlistIndex : undefined, selectedSubtitles, resumeState);
|
||||
if (!videoMetadata) {
|
||||
console.error('Failed to fetch video metadata');
|
||||
toast({
|
||||
title: 'Download Failed',
|
||||
description: 'yt-dlp failed to fetch video metadata. Please try again later.',
|
||||
variant: 'destructive',
|
||||
toast.error("Download Failed", {
|
||||
description: "yt-dlp failed to fetch video metadata. Please try again later.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -192,30 +271,55 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
if (AUDIO_FORMAT !== 'auto' && fileType === 'audio') videoMetadata.ext = AUDIO_FORMAT;
|
||||
}
|
||||
|
||||
let configOutputFormat = null;
|
||||
if (downloadConfig.output_format && downloadConfig.output_format !== 'auto') {
|
||||
videoMetadata.ext = downloadConfig.output_format;
|
||||
configOutputFormat = downloadConfig.output_format;
|
||||
}
|
||||
if (resumeState && resumeState.output_format) videoMetadata.ext = resumeState.output_format;
|
||||
|
||||
const videoId = resumeState?.video_id || generateVideoId(videoMetadata.id, videoMetadata.webpage_url_domain);
|
||||
const playlistId = isPlaylist ? (resumeState?.playlist_id || generateVideoId(videoMetadata.playlist_id, videoMetadata.webpage_url_domain)) : null;
|
||||
const downloadId = resumeState?.download_id || generateDownloadId(videoMetadata.id, videoMetadata.webpage_url_domain);
|
||||
const tempDownloadPathForYtdlp = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.%(ext)s`);
|
||||
const tempDownloadPath = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.${videoMetadata.ext}`);
|
||||
let downloadFilePath = resumeState?.filepath || await join(downloadDirPath, sanitizeFilename(`${videoMetadata.title}_${videoMetadata.resolution || 'unknown'}[${videoMetadata.id}].${videoMetadata.ext}`));
|
||||
const downloadId = resumeState?.download_id || ulid() /*generateDownloadId(videoMetadata.id, videoMetadata.webpage_url_domain)*/;
|
||||
// const tempDownloadPathForYtdlp = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.%(ext)s`);
|
||||
// const tempDownloadPath = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.${videoMetadata.ext}`);
|
||||
// let downloadFilePath = resumeState?.filepath || await join(downloadDirPath, sanitizeFilename(`${videoMetadata.title}_${videoMetadata.resolution || 'unknown'}[${videoMetadata.id}].${videoMetadata.ext}`));
|
||||
let downloadFilePath: string | null = null;
|
||||
let processPid: number | null = null;
|
||||
const args = [
|
||||
url,
|
||||
'--newline',
|
||||
'--progress-template',
|
||||
'status:%(progress.status)s,progress:%(progress._percent_str)s,speed:%(progress.speed)f,downloaded:%(progress.downloaded_bytes)d,total:%(progress.total_bytes)d,eta:%(progress.eta)d',
|
||||
'--paths',
|
||||
`temp:${tempDownloadDirPath}`,
|
||||
'--paths',
|
||||
`home:${downloadDirPath}`,
|
||||
'--output',
|
||||
tempDownloadPathForYtdlp,
|
||||
'--ffmpeg-location',
|
||||
ffmpegPath,
|
||||
'-f',
|
||||
`${FILENAME_TEMPLATE}[${downloadId}].%(ext)s`,
|
||||
'--windows-filenames',
|
||||
'--restrict-filenames',
|
||||
'--exec',
|
||||
'after_move:echo Finalpath: {}',
|
||||
'--format',
|
||||
selectedFormat,
|
||||
'--no-mtime',
|
||||
'--no-warnings',
|
||||
'--retries',
|
||||
MAX_RETRIES.toString(),
|
||||
];
|
||||
|
||||
if (currentPlatform === 'macos') {
|
||||
args.push('--ffmpeg-location', '/Applications/NeoDLP.app/Contents/MacOS', '--js-runtimes', 'deno:/Applications/NeoDLP.app/Contents/MacOS/deno');
|
||||
}
|
||||
|
||||
if (!DEBUG_MODE || (DEBUG_MODE && !LOG_WARNING)) {
|
||||
args.push('--no-warnings');
|
||||
}
|
||||
|
||||
if (DEBUG_MODE && LOG_VERBOSE) {
|
||||
args.push('--verbose');
|
||||
}
|
||||
|
||||
if (selectedSubtitles) {
|
||||
args.push('--embed-subs', '--sub-lang', selectedSubtitles);
|
||||
}
|
||||
@@ -224,100 +328,163 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
args.push('--playlist-items', playlistIndex);
|
||||
}
|
||||
|
||||
if (fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) {
|
||||
if (VIDEO_FORMAT !== 'auto' && fileType === 'video+audio') {
|
||||
if (ALWAYS_REENCODE_VIDEO) {
|
||||
args.push('--recode-video', VIDEO_FORMAT);
|
||||
} else {
|
||||
args.push('--merge-output-format', VIDEO_FORMAT);
|
||||
let customCommandArgs = null;
|
||||
if ((USE_CUSTOM_COMMANDS && CUSTOM_COMMANDS && downloadConfig.custom_command) || resumeState?.custom_command) {
|
||||
if (resumeState?.custom_command) {
|
||||
customCommandArgs = resumeState.custom_command;
|
||||
} else if (CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig.custom_command)) {
|
||||
let customCommand = CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig.custom_command);
|
||||
customCommandArgs = customCommand ? customCommand.args : '';
|
||||
}
|
||||
}
|
||||
if (VIDEO_FORMAT !== 'auto' && fileType === 'video') {
|
||||
if (ALWAYS_REENCODE_VIDEO) {
|
||||
args.push('--recode-video', VIDEO_FORMAT);
|
||||
} else {
|
||||
args.push('--remux-video', VIDEO_FORMAT);
|
||||
|
||||
if (customCommandArgs && customCommandArgs.trim() !== '') args.push(...customCommandArgs.split(' '));
|
||||
}
|
||||
|
||||
let outputFormat = null;
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && ((fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) || configOutputFormat || resumeState?.output_format)) {
|
||||
const format = resumeState?.output_format || configOutputFormat;
|
||||
|
||||
if (format) {
|
||||
outputFormat = format;
|
||||
} else if (fileType === 'audio' && AUDIO_FORMAT !== 'auto') {
|
||||
outputFormat = AUDIO_FORMAT;
|
||||
} else if ((fileType === 'video' || fileType === 'video+audio') && VIDEO_FORMAT !== 'auto') {
|
||||
outputFormat = VIDEO_FORMAT;
|
||||
}
|
||||
|
||||
const recodeOrRemux = ALWAYS_REENCODE_VIDEO ? '--recode-video' : '--remux-video';
|
||||
const formatToUse = format || VIDEO_FORMAT;
|
||||
|
||||
// Handle video+audio
|
||||
if (fileType === 'video+audio' && (VIDEO_FORMAT !== 'auto' || format)) {
|
||||
args.push(ALWAYS_REENCODE_VIDEO ? '--recode-video' : '--merge-output-format', formatToUse);
|
||||
}
|
||||
// Handle video only
|
||||
else if (fileType === 'video' && (VIDEO_FORMAT !== 'auto' || format)) {
|
||||
args.push(recodeOrRemux, formatToUse);
|
||||
}
|
||||
// Handle audio only
|
||||
else if (fileType === 'audio' && (AUDIO_FORMAT !== 'auto' || format)) {
|
||||
args.push('--extract-audio', '--audio-format', format || AUDIO_FORMAT);
|
||||
}
|
||||
// Handle unknown filetype
|
||||
else if (fileType === 'unknown' && format) {
|
||||
if (['mkv', 'mp4', 'webm'].includes(format)) {
|
||||
args.push(recodeOrRemux, formatToUse);
|
||||
} else if (['mp3', 'm4a', 'opus'].includes(format)) {
|
||||
args.push('--extract-audio', '--audio-format', format);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (AUDIO_FORMAT !== 'auto' && fileType === 'audio') {
|
||||
args.push('--extract-audio', '--audio-format', AUDIO_FORMAT);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileType !== 'unknown' && (EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) {
|
||||
if (EMBED_VIDEO_METADATA && (fileType === 'video+audio' || fileType === 'video')) {
|
||||
args.push('--embed-metadata');
|
||||
}
|
||||
if (EMBED_AUDIO_METADATA && fileType === 'audio') {
|
||||
args.push('--embed-metadata');
|
||||
}
|
||||
let embedMetadata = 0;
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfig.embed_metadata || resumeState?.embed_metadata || EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) {
|
||||
const shouldEmbedMetaForVideo = (fileType === 'video+audio' || fileType === 'video') && (downloadConfig.embed_metadata || resumeState?.embed_metadata || (EMBED_VIDEO_METADATA && downloadConfig.embed_metadata === null));
|
||||
const shouldEmbedMetaForAudio = fileType === 'audio' && (downloadConfig.embed_metadata || resumeState?.embed_metadata || (EMBED_AUDIO_METADATA && downloadConfig.embed_metadata === null));
|
||||
const shouldEmbedMetaForUnknown = fileType === 'unknown' && (downloadConfig.embed_metadata || resumeState?.embed_metadata);
|
||||
|
||||
if (shouldEmbedMetaForUnknown || shouldEmbedMetaForVideo || shouldEmbedMetaForAudio) {
|
||||
embedMetadata = 1;
|
||||
args.push('--embed-metadata');
|
||||
}
|
||||
}
|
||||
|
||||
if (EMBED_AUDIO_THUMBNAIL && fileType === 'audio') {
|
||||
args.push('--embed-thumbnail');
|
||||
let embedThumbnail = 0;
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail || EMBED_VIDEO_THUMBNAIL || EMBED_AUDIO_THUMBNAIL)) {
|
||||
const shouldEmbedThumbForVideo = (fileType === 'video+audio' || fileType === 'video') && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail || (EMBED_VIDEO_THUMBNAIL && downloadConfig.embed_thumbnail === null));
|
||||
const shouldEmbedThumbForAudio = fileType === 'audio' && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail || (EMBED_AUDIO_THUMBNAIL && downloadConfig.embed_thumbnail === null));
|
||||
const shouldEmbedThumbForUnknown = fileType === 'unknown' && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail);
|
||||
|
||||
if (shouldEmbedThumbForUnknown || shouldEmbedThumbForVideo || shouldEmbedThumbForAudio) {
|
||||
embedThumbnail = 1;
|
||||
args.push('--embed-thumbnail');
|
||||
}
|
||||
}
|
||||
|
||||
if (resumeState) {
|
||||
args.push('--continue');
|
||||
} else {
|
||||
args.push('--no-continue');
|
||||
}
|
||||
|
||||
if (USE_PROXY && PROXY_URL) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_PROXY && PROXY_URL) {
|
||||
args.push('--proxy', PROXY_URL);
|
||||
}
|
||||
|
||||
if (USE_RATE_LIMIT && RATE_LIMIT) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_RATE_LIMIT && RATE_LIMIT) {
|
||||
args.push('--limit-rate', `${RATE_LIMIT}`);
|
||||
}
|
||||
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_FORCE_INTERNET_PROTOCOL && FORCE_INTERNET_PROTOCOL) {
|
||||
if (FORCE_INTERNET_PROTOCOL === 'ipv4') {
|
||||
args.push('--force-ipv4');
|
||||
} else if (FORCE_INTERNET_PROTOCOL === 'ipv6') {
|
||||
args.push('--force-ipv6');
|
||||
}
|
||||
}
|
||||
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_COOKIES) {
|
||||
if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) {
|
||||
args.push('--cookies-from-browser', COOKIES_BROWSER);
|
||||
} else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) {
|
||||
args.push('--cookies', COOKIES_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
let sponsorblockRemove = null;
|
||||
let sponsorblockMark = null;
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && ((downloadConfig.sponsorblock && downloadConfig.sponsorblock !== 'auto') || resumeState?.sponsorblock_remove || resumeState?.sponsorblock_mark || USE_SPONSORBLOCK)) {
|
||||
if (downloadConfig?.sponsorblock === 'remove' || resumeState?.sponsorblock_remove || (SPONSORBLOCK_MODE === 'remove' && !downloadConfig.sponsorblock)) {
|
||||
sponsorblockRemove = resumeState?.sponsorblock_remove || (SPONSORBLOCK_REMOVE === 'custom' ? (
|
||||
SPONSORBLOCK_REMOVE_CATEGORIES.length > 0 ? SPONSORBLOCK_REMOVE_CATEGORIES.join(',') : 'default'
|
||||
) : (SPONSORBLOCK_REMOVE));
|
||||
args.push('--sponsorblock-remove', sponsorblockRemove);
|
||||
} else if (downloadConfig?.sponsorblock === 'mark' || resumeState?.sponsorblock_mark || (SPONSORBLOCK_MODE === 'mark' && !downloadConfig.sponsorblock)) {
|
||||
sponsorblockMark = resumeState?.sponsorblock_mark || (SPONSORBLOCK_MARK === 'custom' ? (
|
||||
SPONSORBLOCK_MARK_CATEGORIES.length > 0 ? SPONSORBLOCK_MARK_CATEGORIES.join(',') : 'default'
|
||||
) : (SPONSORBLOCK_MARK));
|
||||
args.push('--sponsorblock-mark', sponsorblockMark);
|
||||
}
|
||||
}
|
||||
|
||||
let useAria2 = 0;
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (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'
|
||||
);
|
||||
LOG.warning('NEODLP', `Looks like you are using aria2 for this yt-dlp download: ${downloadId}. Make sure aria2 is installed on your system if you are on macOS for this to work. Also, pause/resume might not work as expected especially on windows (using aria2 is not recommended for most downloads).`);
|
||||
}
|
||||
|
||||
if (resumeState || (!USE_CUSTOM_COMMANDS && USE_ARIA2)) {
|
||||
args.push('--continue');
|
||||
} else {
|
||||
args.push('--no-continue');
|
||||
}
|
||||
|
||||
console.log('Starting download with args:', args);
|
||||
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||
|
||||
command.on('close', async (data) => {
|
||||
if (data.code !== 0) {
|
||||
console.error(`Download failed with code ${data.code}`);
|
||||
LOG.error(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code} (ignore if you manually paused or cancelled the download)`);
|
||||
if (!isErrorExpected) {
|
||||
setIsErrored(true);
|
||||
setErroredDownloadId(downloadId);
|
||||
}
|
||||
} else {
|
||||
if (await fs.exists(tempDownloadPath)) {
|
||||
downloadFilePath = await generateSafeFilePath(downloadFilePath);
|
||||
await fs.copyFile(tempDownloadPath, downloadFilePath);
|
||||
await fs.remove(tempDownloadPath);
|
||||
}
|
||||
|
||||
downloadFilePathUpdater.mutate({ download_id: downloadId, filepath: downloadFilePath }, {
|
||||
onSuccess: (data) => {
|
||||
console.log("Download filepath updated successfully:", data);
|
||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to update download filepath:", error);
|
||||
}
|
||||
})
|
||||
|
||||
downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, {
|
||||
onSuccess: (data) => {
|
||||
console.log("Download status updated successfully:", data);
|
||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to update download status:", error);
|
||||
}
|
||||
})
|
||||
LOG.info(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code}`);
|
||||
}
|
||||
});
|
||||
|
||||
command.on('error', error => {
|
||||
console.error(`Error: ${error}`);
|
||||
LOG.error(`YT-DLP Download ${downloadId}`, `Error occurred: ${error}`);
|
||||
setIsErrored(true);
|
||||
setErroredDownloadId(downloadId);
|
||||
});
|
||||
|
||||
command.stdout.on('data', line => {
|
||||
if (line.startsWith('status:')) {
|
||||
if (line.startsWith('status:') || line.startsWith('[#')) {
|
||||
// console.log(line);
|
||||
if (DEBUG_MODE && LOG_PROGRESS) LOG.progress(`YT-DLP Download ${downloadId}`, line);
|
||||
const currentProgress = parseProgressLine(line);
|
||||
const state: DownloadState = {
|
||||
download_id: downloadId,
|
||||
@@ -357,7 +524,15 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
eta: currentProgress.eta || null,
|
||||
filepath: downloadFilePath,
|
||||
filetype: determineFileType(videoMetadata.vcodec, videoMetadata.acodec) || null,
|
||||
filesize: videoMetadata.filesize_approx || null
|
||||
filesize: videoMetadata.filesize_approx || null,
|
||||
output_format: outputFormat,
|
||||
embed_metadata: embedMetadata,
|
||||
embed_thumbnail: embedThumbnail,
|
||||
sponsorblock_remove: sponsorblockRemove,
|
||||
sponsorblock_mark: sponsorblockMark,
|
||||
use_aria2: useAria2,
|
||||
custom_command: customCommandArgs,
|
||||
queue_config: null
|
||||
};
|
||||
downloadStateSaver.mutate(state, {
|
||||
onSuccess: (data) => {
|
||||
@@ -369,7 +544,49 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log(line);
|
||||
// console.log(line);
|
||||
if (line.trim() !== '') LOG.info(`YT-DLP Download ${downloadId}`, line);
|
||||
|
||||
if (line.startsWith('Finalpath: ')) {
|
||||
downloadFilePath = line.replace('Finalpath: ', '').trim().replace(/^"|"$/g, '');
|
||||
const downloadedFileExt = downloadFilePath.split('.').pop();
|
||||
|
||||
// Update completion status after a short delay to ensure database states are propagated correctly
|
||||
console.log(`Download completed with ID: ${downloadId}, updating filepath and status after 1.5s delay...`);
|
||||
setTimeout(async () => {
|
||||
LOG.info('NEODLP', `yt-dlp download completed with id: ${downloadId}`);
|
||||
downloadFilePathUpdater.mutate({ download_id: downloadId, filepath: downloadFilePath as string, ext: downloadedFileExt as string }, {
|
||||
onSuccess: (data) => {
|
||||
console.log("Download filepath updated successfully:", data);
|
||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to update download filepath:", error);
|
||||
}
|
||||
});
|
||||
|
||||
downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, {
|
||||
onSuccess: (data) => {
|
||||
console.log("Download status updated successfully:", data);
|
||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to update download status:", error);
|
||||
}
|
||||
});
|
||||
|
||||
toast.success("Download Completed", {
|
||||
description: `The download for "${videoMetadata.title}" has completed successfully.`,
|
||||
});
|
||||
|
||||
if (ENABLE_NOTIFICATIONS && DOWNLOAD_COMPLETION_NOTIFICATION) {
|
||||
sendNotification({
|
||||
title: "Download Completed",
|
||||
body: `The download for "${videoMetadata.title}" has completed successfully.`,
|
||||
});
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -442,7 +659,15 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
eta: resumeState?.eta || null,
|
||||
filepath: downloadFilePath,
|
||||
filetype: resumeState?.filetype || null,
|
||||
filesize: resumeState?.filesize || null
|
||||
filesize: resumeState?.filesize || null,
|
||||
output_format: resumeState?.output_format || null,
|
||||
embed_metadata: resumeState?.embed_metadata || 0,
|
||||
embed_thumbnail: resumeState?.embed_thumbnail || 0,
|
||||
sponsorblock_remove: resumeState?.sponsorblock_remove || null,
|
||||
sponsorblock_mark: resumeState?.sponsorblock_mark || null,
|
||||
use_aria2: resumeState?.use_aria2 || 0,
|
||||
custom_command: resumeState?.custom_command || null,
|
||||
queue_config: resumeState?.queue_config || ((!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) ? null : JSON.stringify(downloadConfig))
|
||||
}
|
||||
downloadStateSaver.mutate(state, {
|
||||
onSuccess: (data) => {
|
||||
@@ -460,21 +685,26 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
})
|
||||
|
||||
if (!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) {
|
||||
LOG.info('NEODLP', `Starting yt-dlp download with args: ${args.join(' ')}`);
|
||||
if(!DEBUG_MODE || (DEBUG_MODE && !LOG_PROGRESS)) LOG.warning('NEODLP', `Progress logs are hidden. Enable 'Debug Mode > Log Progress' in Settings to unhide.`);
|
||||
const child = await command.spawn();
|
||||
processPid = child.pid;
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
console.log("Download is queued, not starting immediately.");
|
||||
LOG.info('NEODLP', `Download queued with id: ${downloadId}`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to start download: ${e}`);
|
||||
LOG.error('NEODLP', `Failed to start download for URL: ${url} with error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const pauseDownload = async (downloadState: DownloadState) => {
|
||||
try {
|
||||
LOG.info('NEODLP', `Pausing yt-dlp download with id: ${downloadState.download_id} (as per user request)`);
|
||||
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
|
||||
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||
console.log("Killing process with PID:", downloadState.process_id);
|
||||
@@ -484,9 +714,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();
|
||||
@@ -499,28 +748,39 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
console.error(`Failed to pause download: ${e}`);
|
||||
LOG.error('NEODLP', `Failed to pause download with id: ${downloadState.download_id} with error: ${e}`);
|
||||
isProcessingQueueRef.current = false;
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const resumeDownload = async (downloadState: DownloadState) => {
|
||||
try {
|
||||
LOG.info('NEODLP', `Resuming yt-dlp download with id: ${downloadState.download_id} (as per user request)`);
|
||||
await startDownload(
|
||||
downloadState.playlist_id && downloadState.playlist_index ? downloadState.playlist_url : downloadState.url,
|
||||
downloadState.format_id,
|
||||
downloadState.queue_config ? JSON.parse(downloadState.queue_config) : {
|
||||
output_format: null,
|
||||
embed_metadata: null,
|
||||
embed_thumbnail: null,
|
||||
sponsorblock: null,
|
||||
custom_command: null
|
||||
},
|
||||
downloadState.subtitle_id,
|
||||
downloadState
|
||||
);
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
console.error(`Failed to resume download: ${e}`);
|
||||
LOG.error('NEODLP', `Failed to resume download with id: ${downloadState.download_id} with error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const cancelDownload = async (downloadState: DownloadState) => {
|
||||
try {
|
||||
LOG.info('NEODLP', `Cancelling yt-dlp download with id: ${downloadState.download_id} (as per user request)`);
|
||||
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
|
||||
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||
console.log("Killing process with PID:", downloadState.process_id);
|
||||
@@ -532,7 +792,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
// Reset processing flag and trigger queue processing
|
||||
isProcessingQueueRef.current = false;
|
||||
|
||||
|
||||
// Process the queue after a short delay
|
||||
setTimeout(() => {
|
||||
processQueuedDownloads();
|
||||
@@ -546,6 +806,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
console.error(`Failed to cancel download: ${e}`);
|
||||
LOG.error('NEODLP', `Failed to cancel download with id: ${downloadState.download_id} with error: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -565,35 +826,36 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
try {
|
||||
isProcessingQueueRef.current = true;
|
||||
console.log("Processing download queue...");
|
||||
|
||||
|
||||
// Get the first download in queue
|
||||
const downloadToStart = queuedDownloads[0];
|
||||
|
||||
|
||||
// Skip if we just processed this download to prevent loops
|
||||
if (lastProcessedDownloadIdRef.current === downloadToStart.download_id) {
|
||||
console.log("Skipping recently processed download:", downloadToStart.download_id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Double-check current state from global state
|
||||
const currentState = globalDownloadStates.find(
|
||||
state => state.download_id === downloadToStart.download_id
|
||||
);
|
||||
|
||||
|
||||
if (!currentState || currentState.download_status !== 'queued') {
|
||||
console.log("Download no longer in queued state:", downloadToStart.download_id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log("Starting queued download:", downloadToStart.download_id);
|
||||
LOG.info('NEODLP', `Starting queued download with id: ${downloadToStart.download_id}`);
|
||||
lastProcessedDownloadIdRef.current = downloadToStart.download_id;
|
||||
|
||||
|
||||
// Update status to 'starting' first
|
||||
await downloadStatusUpdater.mutateAsync({
|
||||
download_id: downloadToStart.download_id,
|
||||
download_status: 'starting'
|
||||
});
|
||||
|
||||
|
||||
// Fetch latest state after status update
|
||||
await queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
|
||||
@@ -601,12 +863,20 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
await startDownload(
|
||||
downloadToStart.url,
|
||||
downloadToStart.format_id,
|
||||
downloadToStart.queue_config ? JSON.parse(downloadToStart.queue_config) : {
|
||||
output_format: null,
|
||||
embed_metadata: null,
|
||||
embed_thumbnail: null,
|
||||
sponsorblock: null,
|
||||
custom_command: null
|
||||
},
|
||||
downloadToStart.subtitle_id,
|
||||
downloadToStart
|
||||
);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error processing download queue:", error);
|
||||
LOG.error('NEODLP', `Error processing download queue: ${error}`);
|
||||
} finally {
|
||||
// Important: reset the processing flag
|
||||
setTimeout(() => {
|
||||
@@ -623,13 +893,6 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for App updates
|
||||
useEffect(() => {
|
||||
checkForAppUpdate().catch((error) => {
|
||||
console.error("Error checking for app update:", error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Prevent app from closing
|
||||
useEffect(() => {
|
||||
const handleCloseRequested = (event: any) => {
|
||||
@@ -649,6 +912,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
appWindow.setFocus();
|
||||
navigate('/');
|
||||
if (event.payload.url) {
|
||||
LOG.info('NEODLP', `Received download request from neodlp browser extension for URL: ${event.payload.url}`);
|
||||
const { setRequestedUrl, setAutoSubmitSearch } = useCurrentVideoMetadataStore.getState();
|
||||
setRequestedUrl(event.payload.url);
|
||||
setAutoSubmitSearch(true);
|
||||
@@ -705,7 +969,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
setIsKvPairsStatePropagated(true);
|
||||
}
|
||||
}, [kvPairs, isSuccessFetchingKvPairs]);
|
||||
|
||||
|
||||
// Initiate/Resolve base app paths
|
||||
useEffect(() => {
|
||||
const initPaths = async () => {
|
||||
@@ -715,13 +979,13 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const downloadDirPath = await downloadDir();
|
||||
const tempDirPath = await tempDir();
|
||||
const resourceDirPath = await resourceDir();
|
||||
|
||||
|
||||
const ffmpegPath = await join(resourceDirPath, 'binaries', `ffmpeg-${currentArch}${currentExeExtension ? '.' + currentExeExtension : ''}`);
|
||||
const tempDownloadDirPath = await join(tempDirPath, config.appPkgName, 'downloads');
|
||||
const appDownloadDirPath = await join(downloadDirPath, config.appName);
|
||||
|
||||
if(!await fs.exists(tempDownloadDirPath)) fs.mkdir(tempDownloadDirPath, { recursive: true }).then(() => { console.log(`Created DIR: ${tempDownloadDirPath}`) });
|
||||
|
||||
|
||||
setPath('ffmpegPath', ffmpegPath);
|
||||
setPath('tempDownloadDirPath', tempDownloadDirPath);
|
||||
if (DOWNLOAD_DIR) {
|
||||
@@ -778,6 +1042,24 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
fetchYtDlpVersion();
|
||||
}, [ytDlpVersion, setYtDlpVersion]);
|
||||
|
||||
// Check for app update
|
||||
useEffect(() => {
|
||||
// Only run once when both settings and KV pairs are loaded
|
||||
if (!isSettingsStatePropagated || !isKvPairsStatePropagated) {
|
||||
console.log("Skipping app update check, waiting for configs to load...");
|
||||
return;
|
||||
}
|
||||
// Skip if we've already run the update check once
|
||||
if (hasRunAppUpdateCheckRef.current) {
|
||||
console.log("App update check already performed in this session, skipping");
|
||||
return;
|
||||
}
|
||||
hasRunAppUpdateCheckRef.current = true;
|
||||
checkForAppUpdate().catch((error) => {
|
||||
console.error("Error checking for app update:", error);
|
||||
});
|
||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||
|
||||
// Check for yt-dlp auto-update
|
||||
useEffect(() => {
|
||||
// Only run once when both settings and KV pairs are loaded
|
||||
@@ -823,21 +1105,24 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
appVersion: appVersion,
|
||||
registeredVersion: macOsRegisteredVersion
|
||||
});
|
||||
const currentPlatform = platform();
|
||||
if (currentPlatform === 'macos' && (!macOsRegisteredVersion || macOsRegisteredVersion !== appVersion)) {
|
||||
console.log("Running MacOS auto registration...");
|
||||
LOG.info('NEODLP', 'Running macOS registration');
|
||||
registerToMac().then((result: { success: boolean, message: string }) => {
|
||||
if (result.success) {
|
||||
console.log("MacOS registration successful:", result.message);
|
||||
LOG.info('NEODLP', 'macOS registration successful');
|
||||
} else {
|
||||
console.error("MacOS registration failed:", result.message);
|
||||
LOG.error('NEODLP', `macOS registration failed: ${result.message}`);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error("Error during macOS registration:", error);
|
||||
LOG.error('NEODLP', `Error during macOS registration: ${error}`);
|
||||
});
|
||||
}
|
||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccessFetchingDownloadStates && downloadStates) {
|
||||
console.log("Download States fetched successfully:", downloadStates);
|
||||
@@ -851,7 +1136,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
processQueuedDownloads();
|
||||
}, 500);
|
||||
|
||||
|
||||
// Cleanup timeout if component unmounts or dependencies change
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [processQueuedDownloads, ongoingDownloads, queuedDownloads]);
|
||||
@@ -859,10 +1144,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
// show a toast and pause the download when yt-dlp exits unexpectedly
|
||||
useEffect(() => {
|
||||
if (isErrored && !isErrorExpected) {
|
||||
toast({
|
||||
title: "Download Failed",
|
||||
toast.error("Download Failed", {
|
||||
description: "yt-dlp exited unexpectedly. Please try again later",
|
||||
variant: "destructive",
|
||||
});
|
||||
if (erroredDownloadId) {
|
||||
downloadStatusUpdater.mutate({ download_id: erroredDownloadId, download_status: 'paused' }, {
|
||||
@@ -898,9 +1181,9 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
<ThemeProvider defaultTheme={APP_THEME || "system"} storageKey="vite-ui-theme">
|
||||
<TooltipProvider delayDuration={1000}>
|
||||
{children}
|
||||
<Toaster />
|
||||
<Sonner closeButton />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,55 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { getRouteName } from "@/utils";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
// import { Terminal } from "lucide-react";
|
||||
// import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
|
||||
export default function Navbar() {
|
||||
const location = useLocation();
|
||||
|
||||
const logs = useLogger().getLogs();
|
||||
|
||||
return (
|
||||
<nav className="flex justify-between items-center py-3 px-4 sticky top-0 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b z-50">
|
||||
<nav className="flex justify-between items-center py-3 px-4 sticky top-0 backdrop-blur supports-backdrop-filter:bg-background/60 border-b z-50">
|
||||
<div className="flex justify-center">
|
||||
<SidebarTrigger />
|
||||
<h1 className="text-lg text-primary font-semibold ml-4">{getRouteName(location.pathname)}</h1>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
{/* <Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Terminal />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Logs</p>
|
||||
</TooltipContent>
|
||||
</Tooltip> */}
|
||||
<Dialog>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Terminal />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Logs</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Log Viewer</DialogTitle>
|
||||
<DialogDescription>Monitor real-time app session logs (latest on top)</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2 p-2 max-h-[300px] overflow-y-scroll overflow-x-hidden bg-muted">
|
||||
{logs.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">NO LOGS TO SHOW!</p>
|
||||
) : (
|
||||
logs.slice().reverse().map((log, index) => (
|
||||
<div key={index} className={`flex flex-col ${log.level === 'error' ? 'text-red-500' : log.level === 'warning' ? 'text-amber-500' : log.level === 'debug' ? 'text-sky-500' : log.level === 'progress' ? 'text-emerald-500' : 'text-foreground'}`}>
|
||||
<p className="text-xs"><strong>{new Date(log.timestamp).toLocaleTimeString()}</strong> [{log.level.toUpperCase()}] <em>{log.context}</em> :</p>
|
||||
<p className="text-xs font-mono break-all">{log.message}</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
|
||||
@@ -14,10 +14,10 @@ import { Button } from "@/components/ui/button";
|
||||
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import useAppUpdater from "@/helpers/use-app-updater";
|
||||
|
||||
|
||||
export function AppSidebar() {
|
||||
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||
const ongoingDownloads = downloadStates.filter(state =>
|
||||
const ongoingDownloads = downloadStates.filter(state =>
|
||||
['starting', 'downloading', 'queued'].includes(state.download_status)
|
||||
);
|
||||
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
||||
@@ -63,12 +63,12 @@ export function AppSidebar() {
|
||||
setShowBadge(false);
|
||||
setShowUpdateCard(false);
|
||||
}
|
||||
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
@@ -99,7 +99,7 @@ export function AppSidebar() {
|
||||
{!open ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
<SidebarMenuButton
|
||||
isActive={isActiveSidebarItem(item.url, location.pathname, item.starts_with ? item.starts_with : false)}
|
||||
className="relative"
|
||||
asChild
|
||||
@@ -118,7 +118,7 @@ export function AppSidebar() {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<SidebarMenuButton
|
||||
<SidebarMenuButton
|
||||
isActive={isActiveSidebarItem(item.url, location.pathname, item.starts_with ? item.starts_with : false)}
|
||||
className="relative"
|
||||
asChild
|
||||
@@ -154,11 +154,11 @@ export function AppSidebar() {
|
||||
{appUpdate && open && showUpdateCard && (
|
||||
<Card className="gap-4 py-0">
|
||||
<CardHeader className="p-4 pb-0">
|
||||
<CardTitle className="text-sm">Update Available (v{appUpdate.version})</CardTitle>
|
||||
<CardTitle className="text-sm">Update Available (v{appUpdate?.version || '0.0.0'})</CardTitle>
|
||||
<CardDescription>
|
||||
A newer version of {config.appName} is available. Please update to the latest version for the best experience.
|
||||
</CardDescription>
|
||||
<a className="text-xs font-semibold cursor-pointer mt-1" href={`https://github.com/neosubhamoy/neodlp/releases/tag/v${appUpdate.version}`} target="_blank">✨ Read Changelog</a>
|
||||
<a className="text-xs font-semibold cursor-pointer mt-1" href={`https://github.com/neosubhamoy/neodlp/releases/tag/v${appUpdate?.version || '0.0.0'}`} target="_blank">✨ Read Changelog</a>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-2.5 p-4">
|
||||
<AlertDialog>
|
||||
@@ -169,13 +169,14 @@ export function AppSidebar() {
|
||||
disabled={ongoingDownloads.length > 0 || isUpdatingApp}
|
||||
onClick={() => downloadAndInstallAppUpdate(appUpdate)}
|
||||
>
|
||||
Download and Install
|
||||
Update Now
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader className="flex flex-col items-center text-center gap-2">
|
||||
<CircleArrowUp className="size-7 stroke-muted-foreground" />
|
||||
<AlertDialogTitle>Updating {config.appName}</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-center">Updating {config.appName} to v{appUpdate.version}, Please be patience! Do not quit the app untill the update finishes. The app will auto re-launch to complete the update, Please allow all system prompts from {config.appName} if asked.</AlertDialogDescription>
|
||||
<AlertDialogDescription className="text-center text-xs mb-2">Updating {config.appName} to v{appUpdate?.version || '0.0.0'}, Please be patience! Do not quit the app untill the update finishes. The app will auto re-launch to complete the update, Please allow all system prompts from {config.appName} if asked.</AlertDialogDescription>
|
||||
<Progress value={appUpdateDownloadProgress} className="w-full" />
|
||||
<AlertDialogDescription className="text-center">Downloading update... {appUpdateDownloadProgress}%</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
@@ -221,4 +222,4 @@ export function AppSidebar() {
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
import type { LegendPayload } from "recharts/types/component/DefaultLegendContent"
|
||||
import {
|
||||
NameType,
|
||||
Payload,
|
||||
ValueType,
|
||||
} from "recharts/types/component/DefaultTooltipContent"
|
||||
import type { Props as LegendProps } from "recharts/types/component/Legend"
|
||||
import { TooltipContentProps } from "recharts/types/component/Tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -20,6 +28,36 @@ type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
export type CustomTooltipProps = TooltipContentProps<ValueType, NameType> & {
|
||||
className?: string
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
labelFormatter?: (
|
||||
label: TooltipContentProps<number, string>["label"],
|
||||
payload: TooltipContentProps<number, string>["payload"]
|
||||
) => React.ReactNode
|
||||
formatter?: (
|
||||
value: number | string,
|
||||
name: string,
|
||||
item: Payload<number | string, string>,
|
||||
index: number,
|
||||
payload: ReadonlyArray<Payload<number | string, string>>
|
||||
) => React.ReactNode
|
||||
labelClassName?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export type ChartLegendContentProps = {
|
||||
className?: string
|
||||
hideIcon?: boolean
|
||||
verticalAlign?: LegendProps["verticalAlign"]
|
||||
payload?: LegendPayload[]
|
||||
nameKey?: string
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
@@ -82,17 +120,17 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
@@ -105,25 +143,18 @@ const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
labelClassName,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}) {
|
||||
}: CustomTooltipProps) {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
@@ -134,10 +165,14 @@ function ChartTooltipContent({
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
const value = (() => {
|
||||
const v =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label ?? label
|
||||
: itemConfig?.label
|
||||
|
||||
return typeof v === "string" || typeof v === "number" ? v : undefined
|
||||
})()
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
@@ -254,11 +289,7 @@ function ChartLegendContent({
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}) {
|
||||
}: ChartLegendContentProps) {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
@@ -348,4 +379,4 @@ export {
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,12 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: "group",
|
||||
icon: "group-data-[type=error]:!text-red-500 group-data-[type=success]:!text-green-500 group-data-[type=warning]:!text-amber-500 group-data-[type=info]:!text-sky-500",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { config } from "@/config";
|
||||
import { check as checkAppUpdate, Update } from "@tauri-apps/plugin-updater";
|
||||
import { relaunch as relaunchApp } from "@tauri-apps/plugin-process";
|
||||
import { useSettingsPageStatesStore } from "@/services/store";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
import { sendNotification } from '@tauri-apps/plugin-notification';
|
||||
|
||||
export default function useAppUpdater() {
|
||||
const setIsCheckingAppUpdate = useSettingsPageStatesStore(state => state.setIsCheckingAppUpdate);
|
||||
const setAppUpdate = useSettingsPageStatesStore(state => state.setAppUpdate);
|
||||
const setIsUpdating = useSettingsPageStatesStore(state => state.setIsUpdatingApp);
|
||||
const setDownloadProgress = useSettingsPageStatesStore(state => state.setAppUpdateDownloadProgress);
|
||||
const enableNotifications = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
||||
const updateNotification = useSettingsPageStatesStore(state => state.settings.update_notification);
|
||||
const LOG = useLogger();
|
||||
|
||||
const checkForAppUpdate = async () => {
|
||||
setIsCheckingAppUpdate(true);
|
||||
@@ -15,6 +21,13 @@ export default function useAppUpdater() {
|
||||
if (update) {
|
||||
setAppUpdate(update);
|
||||
console.log(`app update available v${update.version}`);
|
||||
LOG.info('NEODLP', `App update available v${update.version}`);
|
||||
if (enableNotifications && updateNotification) {
|
||||
sendNotification({
|
||||
title: `Update Available (v${update.version})`,
|
||||
body: `A newer version of ${config.appName} is available. Please update to the latest version for the best experience`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -25,6 +38,7 @@ export default function useAppUpdater() {
|
||||
|
||||
const downloadAndInstallAppUpdate = async (update: Update) => {
|
||||
setIsUpdating(true);
|
||||
LOG.info('NEODLP', `Downloading and installing app update v${update.version}`);
|
||||
let downloaded = 0;
|
||||
let contentLength: number | undefined = 0;
|
||||
await update.downloadAndInstall((event) => {
|
||||
@@ -52,4 +66,4 @@ export default function useAppUpdater() {
|
||||
checkForAppUpdate,
|
||||
downloadAndInstallAppUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
src/helpers/use-logger.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useLogsStore } from "@/services/store";
|
||||
|
||||
export function useLogger() {
|
||||
const logs = useLogsStore((state) => state.logs);
|
||||
const addLog = useLogsStore((state) => state.addLog);
|
||||
const clearLogs = useLogsStore((state) => state.clearLogs);
|
||||
|
||||
const logger = {
|
||||
info: (context: string, message: string) => {
|
||||
addLog({ timestamp: Date.now(), level: 'info', context, message });
|
||||
},
|
||||
warning: (context: string, message: string) => {
|
||||
addLog({ timestamp: Date.now(), level: 'warning', context, message });
|
||||
},
|
||||
error: (context: string, message: string) => {
|
||||
addLog({ timestamp: Date.now(), level: 'error', context, message });
|
||||
},
|
||||
debug: (context: string, message: string) => {
|
||||
addLog({ timestamp: Date.now(), level: 'debug', context, message });
|
||||
},
|
||||
progress: (context: string, message: string) => {
|
||||
addLog({ timestamp: Date.now(), level: 'progress', context, message });
|
||||
},
|
||||
getLogs: () => logs,
|
||||
clearLogs,
|
||||
};
|
||||
|
||||
return logger;
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
import { useResetSettings, useSaveSettingsKey } from "@/services/mutations";
|
||||
import { useSettingsPageStatesStore } from "@/services/store";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export function useSettings() {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const setSettingsKey = useSettingsPageStatesStore(state => state.setSettingsKey);
|
||||
const resetSettingsState = useSettingsPageStatesStore(state => state.resetSettings);
|
||||
@@ -22,10 +21,8 @@ export function useSettings() {
|
||||
onError: (error) => {
|
||||
console.error("Error saving settings key:", error);
|
||||
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
toast({
|
||||
title: "Failed to update settings",
|
||||
toast.error("Failed to update settings", {
|
||||
description: `Failed to update ${key}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -39,26 +36,21 @@ export function useSettings() {
|
||||
resetSettingsState();
|
||||
console.log("Settings reset successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
toast({
|
||||
title: "Settings reset successfully",
|
||||
toast.success("Settings reset successfully", {
|
||||
description: "All settings have been reset to default.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error resetting settings:", error);
|
||||
toast({
|
||||
title: "Failed to reset settings",
|
||||
toast.error("Failed to reset settings", {
|
||||
description: "Failed to reset settings to default.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error resetting settings:", error);
|
||||
toast({
|
||||
title: "Failed to reset settings",
|
||||
toast.error("Failed to reset settings", {
|
||||
description: "Failed to reset settings to default.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useSettingsPageStatesStore } from "@/services/store";
|
||||
import { useKvPairs } from "@/helpers/use-kvpairs";
|
||||
import { Command } from "@tauri-apps/plugin-shell";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { join } from "@tauri-apps/api/path";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function useYtDlpUpdater() {
|
||||
const { saveKvPair } = useKvPairs();
|
||||
@@ -9,26 +13,53 @@ export function useYtDlpUpdater() {
|
||||
const setIsUpdatingYtDlp = useSettingsPageStatesStore((state) => state.setIsUpdatingYtDlp);
|
||||
const setYtDlpVersion = useSettingsPageStatesStore((state) => state.setYtDlpVersion);
|
||||
const currentPlatform = platform();
|
||||
const LOG = useLogger();
|
||||
|
||||
const updateYtDlp = async () => {
|
||||
const CURRENT_TIMESTAMP = Date.now();
|
||||
setIsUpdatingYtDlp(true);
|
||||
LOG.info('NEODLP', 'Updating yt-dlp to latest version');
|
||||
try {
|
||||
const command = currentPlatform === 'linux' ? Command.create('pkexec', ['yt-dlp', '--update-to', ytDlpUpdateChannel]) : Command.sidecar('binaries/yt-dlp', ['--update-to', ytDlpUpdateChannel]);
|
||||
const output = await command.execute();
|
||||
if (output.code === 0) {
|
||||
console.log("yt-dlp updated successfully:", output.stdout);
|
||||
LOG.info('NEODLP', "yt-dlp updated successfully");
|
||||
saveKvPair('ytdlp_update_last_check', CURRENT_TIMESTAMP);
|
||||
setYtDlpVersion(null);
|
||||
toast.success("Update successful", { description: "yt-dlp has been updated successfully." });
|
||||
} else {
|
||||
if (currentPlatform === 'windows') {
|
||||
LOG.warning('NEODLP', "yt-dlp update failed! Now, attempting with elevated privileges.");
|
||||
const appPath = await invoke<string>('get_current_app_path');
|
||||
const ytdlpPath = await join(appPath, 'yt-dlp.exe');
|
||||
const elevateCommand = Command.create('powershell', ['Start-Process', `"${ytdlpPath}"`, '-ArgumentList', `"--update-to ${ytDlpUpdateChannel}"`, '-Verb', 'RunAs', '-Wait', '-WindowStyle', 'Hidden']);
|
||||
const elevateOutput = await elevateCommand.execute();
|
||||
if (elevateOutput.code === 0) {
|
||||
console.log("yt-dlp updated successfully with elevation:", elevateOutput.stdout);
|
||||
LOG.info('NEODLP', "yt-dlp updated successfully with elevation");
|
||||
saveKvPair('ytdlp_update_last_check', CURRENT_TIMESTAMP);
|
||||
setYtDlpVersion(null);
|
||||
toast.success("Update successful", { description: "yt-dlp has been updated successfully." });
|
||||
} else {
|
||||
console.error("Failed to update yt-dlp with elevation:", elevateOutput.stderr);
|
||||
LOG.error('NEODLP', `Failed to update yt-dlp with elevation: ${elevateOutput.stderr}`);
|
||||
toast.error("Update failed", { description: "Failed to update yt-dlp." });
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.error("Failed to update yt-dlp:", output.stderr);
|
||||
LOG.error('NEODLP', `Failed to update yt-dlp: ${output.stderr}`);
|
||||
toast.error("Update failed", { description: "Failed to update yt-dlp." });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to update yt-dlp:', e);
|
||||
LOG.error('NEODLP', `Exception while updating yt-dlp: ${e}`);
|
||||
toast.error("Update failed", { description: "An error occurred while updating yt-dlp." });
|
||||
} finally {
|
||||
setIsUpdatingYtDlp(false);
|
||||
}
|
||||
}
|
||||
|
||||
return { updateYtDlp };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
import { useAppContext } from "@/providers/appContextProvider";
|
||||
import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { determineFileType, fileFormatFilter, formatBitrate, formatDurationString, formatFileSize, formatReleaseDate, formatYtStyleCount, isObjEmpty, sortByBitrate } from "@/utils";
|
||||
import { Calendar, Clock, DownloadCloud, Eye, Info, Loader2, Music, ThumbsUp, Video, File, ListVideo, PackageSearch, AlertCircleIcon, X } from "lucide-react";
|
||||
import { Calendar, Clock, DownloadCloud, Eye, Info, Loader2, Music, ThumbsUp, Video, File, ListVideo, PackageSearch, AlertCircleIcon, X, Settings2, Clipboard } from "lucide-react";
|
||||
import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
|
||||
@@ -24,6 +24,12 @@ import { config } from "@/config";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
|
||||
const searchFormSchema = z.object({
|
||||
url: z.url({
|
||||
@@ -35,8 +41,7 @@ const searchFormSchema = z.object({
|
||||
|
||||
export default function DownloaderPage() {
|
||||
const { fetchVideoMetadata, startDownload } = useAppContext();
|
||||
const { toast } = useToast();
|
||||
|
||||
|
||||
const videoUrl = useCurrentVideoMetadataStore((state) => state.videoUrl);
|
||||
const videoMetadata = useCurrentVideoMetadataStore((state) => state.videoMetadata);
|
||||
const isMetadataLoading = useCurrentVideoMetadataStore((state) => state.isMetadataLoading);
|
||||
@@ -52,23 +57,34 @@ export default function DownloaderPage() {
|
||||
const setShowSearchError = useCurrentVideoMetadataStore((state) => state.setShowSearchError);
|
||||
|
||||
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
|
||||
const activeDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.activeDownloadConfigurationTab);
|
||||
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
|
||||
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
|
||||
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
|
||||
const selectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormat);
|
||||
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
|
||||
const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex);
|
||||
const downloadConfiguration = useDownloaderPageStatesStore((state) => state.downloadConfiguration);
|
||||
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
|
||||
const setActiveDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadConfigurationTab);
|
||||
const setIsStartingDownload = useDownloaderPageStatesStore((state) => state.setIsStartingDownload);
|
||||
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
|
||||
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
|
||||
const setSelectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormat);
|
||||
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
|
||||
const setSelectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.setSelectedPlaylistVideoIndex);
|
||||
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
|
||||
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
|
||||
|
||||
const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||
const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||
|
||||
const embedVideoMetadata = useSettingsPageStatesStore(state => state.settings.embed_video_metadata);
|
||||
const embedAudioMetadata = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
|
||||
const embedVideoThumbnail = useSettingsPageStatesStore(state => state.settings.embed_video_thumbnail);
|
||||
const embedAudioThumbnail = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
|
||||
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||
const customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands);
|
||||
|
||||
const audioOnlyFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('audio'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('audio'))) : [];
|
||||
const videoOnlyFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('video'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('video'))) : [];
|
||||
const combinedFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('video+audio'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('video+audio'))) : [];
|
||||
@@ -147,7 +163,10 @@ export default function DownloaderPage() {
|
||||
|
||||
let selectedFormatExtensionMsg = 'Auto - unknown';
|
||||
if (activeDownloadModeTab === 'combine') {
|
||||
if (videoFormat !== 'auto') {
|
||||
if (downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Combined - ${downloadConfiguration.output_format.toUpperCase()}`;
|
||||
}
|
||||
else if (videoFormat !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Combined - ${videoFormat.toUpperCase()}`;
|
||||
}
|
||||
else if (selectedAudioFormat?.ext && selectedVideoFormat?.ext) {
|
||||
@@ -156,10 +175,12 @@ export default function DownloaderPage() {
|
||||
selectedFormatExtensionMsg = `Combined - unknown`;
|
||||
}
|
||||
} else if (selectedFormat?.ext) {
|
||||
if ((selectedFormatFileType === 'video+audio' || selectedFormatFileType === 'video') && videoFormat !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Forced - ${videoFormat.toUpperCase()}`;
|
||||
} else if (selectedFormatFileType === 'audio' && audioFormat !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Forced - ${audioFormat.toUpperCase()}`;
|
||||
if ((selectedFormatFileType === 'video+audio' || selectedFormatFileType === 'video') && ((downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') || videoFormat !== 'auto')) {
|
||||
selectedFormatExtensionMsg = `Forced - ${(downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') ? downloadConfiguration.output_format.toUpperCase() : videoFormat.toUpperCase()}`;
|
||||
} else if (selectedFormatFileType === 'audio' && ((downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') || audioFormat !== 'auto')) {
|
||||
selectedFormatExtensionMsg = `Forced - ${(downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') ? downloadConfiguration.output_format.toUpperCase() : audioFormat.toUpperCase()}`;
|
||||
} else if (selectedFormatFileType === 'unknown' && downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Forced - ${downloadConfiguration.output_format.toUpperCase()}`;
|
||||
} else {
|
||||
selectedFormatExtensionMsg = `Auto - ${selectedFormat.ext.toUpperCase()}`;
|
||||
}
|
||||
@@ -221,15 +242,14 @@ export default function DownloaderPage() {
|
||||
setSelectedCombinableAudioFormat('');
|
||||
setSelectedSubtitles([]);
|
||||
setSelectedPlaylistVideoIndex('1');
|
||||
resetDownloadConfiguration();
|
||||
|
||||
fetchVideoMetadata(values.url).then((metadata) => {
|
||||
if (!metadata || (metadata._type !== 'video' && metadata._type !== 'playlist') || (metadata && metadata._type === 'video' && metadata.formats.length <= 0) || (metadata && metadata._type === 'playlist' && metadata.entries.length <= 0)) {
|
||||
const showSearchError = useCurrentVideoMetadataStore.getState().showSearchError;
|
||||
if (showSearchError) {
|
||||
toast({
|
||||
title: 'Oops! No results found',
|
||||
description: 'The provided URL does not contain any downloadable content or you are not connected to the internet. Please check the URL, your network connection and try again.',
|
||||
variant: "destructive"
|
||||
toast.error("Oops! No results found", {
|
||||
description: "The provided URL does not contain any downloadable content or you are not connected to the internet. Please check the URL, your network connection and try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -273,6 +293,10 @@ export default function DownloaderPage() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useCustomCommands ? setActiveDownloadConfigurationTab('commands') : setActiveDownloadConfigurationTab('options');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (watchedUrl !== videoUrl) {
|
||||
setVideoUrl(watchedUrl);
|
||||
@@ -286,18 +310,18 @@ export default function DownloaderPage() {
|
||||
searchForm.setValue("url", requestedUrl);
|
||||
setVideoUrl(requestedUrl);
|
||||
}
|
||||
|
||||
|
||||
// Auto-submit the form if the flag is set
|
||||
if (autoSubmitSearch && requestedUrl) {
|
||||
if (!isMetadataLoading) {
|
||||
// trigger a validation check on the URL field first then get the result
|
||||
await searchForm.trigger("url");
|
||||
const isValidUrl = !searchForm.getFieldState("url").invalid;
|
||||
|
||||
|
||||
if (isValidUrl) {
|
||||
// Reset the flag first to prevent loops
|
||||
setAutoSubmitSearch(false);
|
||||
|
||||
|
||||
// Submit the form with a small delay to ensure UI is ready
|
||||
setTimeout(() => {
|
||||
handleSearchSubmit({ url: requestedUrl });
|
||||
@@ -307,20 +331,16 @@ export default function DownloaderPage() {
|
||||
// If URL is invalid, just reset the flag
|
||||
setAutoSubmitSearch(false);
|
||||
setRequestedUrl('');
|
||||
toast({
|
||||
title: 'Invalid URL',
|
||||
description: 'The provided URL is not valid.',
|
||||
variant: "destructive"
|
||||
toast.error("Invalid URL", {
|
||||
description: "The provided URL is not valid.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// If metadata is loading, just reset the flag
|
||||
setAutoSubmitSearch(false);
|
||||
setRequestedUrl('');
|
||||
toast({
|
||||
title: 'Search in progress',
|
||||
description: 'Search in progress, try again later.',
|
||||
variant: "destructive"
|
||||
toast.info("Search in progress", {
|
||||
description: "There's a search in progress, Please try again later.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -373,6 +393,36 @@ export default function DownloaderPage() {
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
{!isMetadataLoading && !videoUrl && (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
disabled={isMetadataLoading}
|
||||
onClick={async () => {
|
||||
const text = await readText();
|
||||
if (text) {
|
||||
searchForm.setValue("url", text);
|
||||
setVideoUrl(text);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clipboard className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
{!isMetadataLoading && videoUrl && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={isMetadataLoading}
|
||||
onClick={() => {
|
||||
searchForm.setValue("url", '');
|
||||
setVideoUrl('');
|
||||
}}
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!videoUrl || Object.keys(searchFormErrors).length > 0 || isMetadataLoading}
|
||||
@@ -442,7 +492,13 @@ export default function DownloaderPage() {
|
||||
<Tabs
|
||||
className=""
|
||||
value={activeDownloadModeTab}
|
||||
onValueChange={(tab) => setActiveDownloadModeTab(tab)}
|
||||
onValueChange={(tab) => {
|
||||
setActiveDownloadModeTab(tab)
|
||||
setDownloadConfigurationKey('output_format', null);
|
||||
setDownloadConfigurationKey('embed_metadata', null);
|
||||
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||
setDownloadConfigurationKey('sponsorblock', null);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm flex items-center gap-2">
|
||||
@@ -488,6 +544,10 @@ export default function DownloaderPage() {
|
||||
// if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
|
||||
// setSelectedSubtitles([]);
|
||||
// }
|
||||
setDownloadConfigurationKey('output_format', null);
|
||||
setDownloadConfigurationKey('embed_metadata', null);
|
||||
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||
setDownloadConfigurationKey('sponsorblock', null);
|
||||
}}
|
||||
>
|
||||
<p className="text-xs">Suggested</p>
|
||||
@@ -588,6 +648,10 @@ export default function DownloaderPage() {
|
||||
value={selectedCombinableAudioFormat}
|
||||
onValueChange={(value) => {
|
||||
setSelectedCombinableAudioFormat(value);
|
||||
setDownloadConfigurationKey('output_format', null);
|
||||
setDownloadConfigurationKey('embed_metadata', null);
|
||||
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||
setDownloadConfigurationKey('sponsorblock', null);
|
||||
}}
|
||||
>
|
||||
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
|
||||
@@ -609,6 +673,10 @@ export default function DownloaderPage() {
|
||||
value={selectedCombinableVideoFormat}
|
||||
onValueChange={(value) => {
|
||||
setSelectedCombinableVideoFormat(value);
|
||||
setDownloadConfigurationKey('output_format', null);
|
||||
setDownloadConfigurationKey('embed_metadata', null);
|
||||
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||
setDownloadConfigurationKey('sponsorblock', null);
|
||||
}}
|
||||
>
|
||||
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
|
||||
@@ -660,7 +728,7 @@ export default function DownloaderPage() {
|
||||
>
|
||||
{videoMetadata.entries.map((entry) => entry ? (
|
||||
<PlaylistToggleGroupItem
|
||||
key={entry.playlist_index}
|
||||
key={entry.playlist_index}
|
||||
value={entry.playlist_index.toString()}
|
||||
video={entry}
|
||||
/>
|
||||
@@ -675,6 +743,7 @@ export default function DownloaderPage() {
|
||||
setSelectedSubtitles([]);
|
||||
setSelectedCombinableVideoFormat('');
|
||||
setSelectedCombinableAudioFormat('');
|
||||
resetDownloadConfiguration();
|
||||
}}
|
||||
>
|
||||
{videoMetadata.entries.map((entry) => entry ? (
|
||||
@@ -912,57 +981,273 @@ export default function DownloaderPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm text-nowrap max-w-[30rem] xl:max-w-[50rem] overflow-hidden text-ellipsis">{videoMetadata._type === 'video' ? videoMetadata.title : videoMetadata._type === 'playlist' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].title : 'Unknown' }</span>
|
||||
<span className="text-sm text-nowrap max-w-120 xl:max-w-200 overflow-hidden text-ellipsis">{videoMetadata._type === 'video' ? videoMetadata.title : videoMetadata._type === 'playlist' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].title : 'Unknown' }</span>
|
||||
<span className="text-xs text-muted-foreground">{selectedFormatFinalMsg}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setIsStartingDownload(true);
|
||||
try {
|
||||
if (videoMetadata._type === 'playlist') {
|
||||
await startDownload(
|
||||
videoMetadata.original_url,
|
||||
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selectedDownloadFormat,
|
||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
|
||||
undefined,
|
||||
selectedPlaylistVideoIndex
|
||||
);
|
||||
} else if (videoMetadata._type === 'video') {
|
||||
await startDownload(
|
||||
videoMetadata.webpage_url,
|
||||
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selectedDownloadFormat,
|
||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null
|
||||
);
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={!selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat))}
|
||||
>
|
||||
<Settings2 className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Configurations</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DialogContent className="sm:max-w-[450px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Configurations</DialogTitle>
|
||||
<DialogDescription>Tweak this download's configurations</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2 max-h-[300px] overflow-y-scroll overflow-x-hidden no-scrollbar">
|
||||
<Tabs
|
||||
className=""
|
||||
value={activeDownloadConfigurationTab}
|
||||
onValueChange={(tab) => setActiveDownloadConfigurationTab(tab)}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="options">Options</TabsTrigger>
|
||||
<TabsTrigger value="commands">Commands</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="options">
|
||||
{useCustomCommands ? (
|
||||
<Alert className="mt-2 mb-3">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle className="text-sm">Options Unavailable!</AlertTitle>
|
||||
<AlertDescription className="text-xs">
|
||||
You cannot use these options when custom commands are enabled. To use these options, disable custom commands from Settings.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className="video-format">
|
||||
<Label className="text-xs mb-3 mt-2">Output Format ({(selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? 'Video' : selectedFormatFileType && selectedFormatFileType === 'audio' ? 'Audio' : 'Unknown'})</Label>
|
||||
{(selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? (
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4 flex-wrap"
|
||||
value={downloadConfiguration.output_format ?? 'auto'}
|
||||
onValueChange={(value) => setDownloadConfigurationKey('output_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="v-auto" />
|
||||
<Label htmlFor="v-auto">Follow Settings</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mp4" id="v-mp4" />
|
||||
<Label htmlFor="v-mp4">MP4</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="webm" id="v-webm" />
|
||||
<Label htmlFor="v-webm">WEBM</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mkv" id="v-mkv" />
|
||||
<Label htmlFor="v-mkv">MKV</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
) : selectedFormatFileType && selectedFormatFileType === 'audio' ? (
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4 flex-wrap"
|
||||
value={downloadConfiguration.output_format ?? 'auto'}
|
||||
onValueChange={(value) => setDownloadConfigurationKey('output_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="a-auto" />
|
||||
<Label htmlFor="a-auto">Follow Settings</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="m4a" id="a-m4a" />
|
||||
<Label htmlFor="a-m4a">M4A</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="opus" id="a-opus" />
|
||||
<Label htmlFor="a-opus">OPUS</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mp3" id="a-mp3" />
|
||||
<Label htmlFor="a-mp3">MP3</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
) : (
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4 flex-wrap"
|
||||
value={downloadConfiguration.output_format ?? 'auto'}
|
||||
onValueChange={(value) => setDownloadConfigurationKey('output_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="u-auto" />
|
||||
<Label htmlFor="u-auto">Follow Settings</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mp4" id="u-mp4" />
|
||||
<Label htmlFor="u-mp4">MP4</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="webm" id="u-webm" />
|
||||
<Label htmlFor="u-webm">WEBM</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mkv" id="u-mkv" />
|
||||
<Label htmlFor="u-mkv">MKV</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="m4a" id="u-m4a" />
|
||||
<Label htmlFor="u-m4a">M4A</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="opus" id="u-opus" />
|
||||
<Label htmlFor="u-opus">OPUS</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mp3" id="u-mp3" />
|
||||
<Label htmlFor="u-mp3">MP3</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</div>
|
||||
<div className="sponsorblock">
|
||||
<Label className="text-xs my-3">Sponsorblock Mode</Label>
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4 flex-wrap"
|
||||
value={downloadConfiguration.sponsorblock ?? 'auto'}
|
||||
onValueChange={(value) => setDownloadConfigurationKey('sponsorblock', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="sb-auto" />
|
||||
<Label htmlFor="sb-auto">Follow Settings</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="remove" id="sb-remove" />
|
||||
<Label htmlFor="sb-remove">Remove</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mark" id="sb-mark" />
|
||||
<Label htmlFor="sb-mark">Mark</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="embeding-options">
|
||||
<Label className="text-xs my-3">Embeding Options</Label>
|
||||
<div className="flex items-center space-x-2 mt-3">
|
||||
<Switch
|
||||
id="embed-metadata"
|
||||
checked={downloadConfiguration.embed_metadata !== null ? downloadConfiguration.embed_metadata : (selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? embedVideoMetadata : selectedFormatFileType && selectedFormatFileType === 'audio' ? embedAudioMetadata : false}
|
||||
onCheckedChange={(checked) => setDownloadConfigurationKey('embed_metadata', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-metadata">Embed Metadata</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-3">
|
||||
<Switch
|
||||
id="embed-thumbnail"
|
||||
checked={downloadConfiguration.embed_thumbnail !== null ? downloadConfiguration.embed_thumbnail : (selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? embedVideoThumbnail : selectedFormatFileType && selectedFormatFileType === 'audio' ? embedAudioThumbnail : false}
|
||||
onCheckedChange={(checked) => setDownloadConfigurationKey('embed_thumbnail', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-thumbnail">Embed Thumbnail</Label>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="commands">
|
||||
{!useCustomCommands ? (
|
||||
<Alert className="mt-2 mb-3">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle className="text-sm">Enable Custom Commands!</AlertTitle>
|
||||
<AlertDescription className="text-xs">
|
||||
To run custom commands for downloads, please enable it from the Settings.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className="custom-commands">
|
||||
<Label className="text-xs mb-3 mt-2">Run Custom Command</Label>
|
||||
{customCommands.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">NO CUSTOM COMMAND TEMPLATE ADDED YET!</p>
|
||||
) : (
|
||||
<RadioGroup
|
||||
orientation="vertical"
|
||||
className="flex flex-col gap-2"
|
||||
disabled={!useCustomCommands}
|
||||
value={downloadConfiguration.custom_command}
|
||||
onValueChange={(value) => setDownloadConfigurationKey('custom_command', value)}
|
||||
>
|
||||
{customCommands.map((command) => (
|
||||
<div className="flex items-center gap-3" key={command.id}>
|
||||
<RadioGroupItem value={command.id} id={`cmd-${command.id}`} />
|
||||
<Label htmlFor={`cmd-${command.id}`}>{command.label}</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setIsStartingDownload(true);
|
||||
try {
|
||||
if (videoMetadata._type === 'playlist') {
|
||||
await startDownload(
|
||||
videoMetadata.original_url,
|
||||
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selectedDownloadFormat,
|
||||
downloadConfiguration,
|
||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
|
||||
undefined,
|
||||
selectedPlaylistVideoIndex
|
||||
);
|
||||
} else if (videoMetadata._type === 'video') {
|
||||
await startDownload(
|
||||
videoMetadata.webpage_url,
|
||||
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selectedDownloadFormat,
|
||||
downloadConfiguration,
|
||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null
|
||||
);
|
||||
}
|
||||
// toast({
|
||||
// title: 'Download Initiated',
|
||||
// description: 'Download initiated, it will start shortly.',
|
||||
// });
|
||||
} catch (error) {
|
||||
console.error('Download failed to start:', error);
|
||||
toast.error("Failed to Start Download", {
|
||||
description: "There was an error initiating the download."
|
||||
});
|
||||
} finally {
|
||||
setIsStartingDownload(false);
|
||||
}
|
||||
// toast({
|
||||
// title: 'Download Initiated',
|
||||
// description: 'Download initiated, it will start shortly.',
|
||||
// });
|
||||
} catch (error) {
|
||||
console.error('Download failed to start:', error);
|
||||
toast({
|
||||
title: 'Failed to Start Download',
|
||||
description: 'There was an error initiating the download.',
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsStartingDownload(false);
|
||||
}
|
||||
}}
|
||||
disabled={isStartingDownload || !selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat))}
|
||||
>
|
||||
{isStartingDownload ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Starting Download
|
||||
</>
|
||||
) : (
|
||||
'Start Download'
|
||||
)}
|
||||
</Button>
|
||||
}}
|
||||
disabled={isStartingDownload || !selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat)) || (useCustomCommands && !downloadConfiguration.custom_command)}
|
||||
>
|
||||
{isStartingDownload ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Starting Download
|
||||
</>
|
||||
) : (
|
||||
'Start Download'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
import { useAppContext } from "@/providers/appContextProvider";
|
||||
import { useDownloadActionStatesStore, useDownloadStatesStore, useLibraryPageStatesStore } from "@/services/store";
|
||||
import { useCurrentVideoMetadataStore, useDownloadActionStatesStore, useDownloadStatesStore, useLibraryPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils";
|
||||
import { AudioLines, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, Pause, Play, Square, Trash2, Video, X } from "lucide-react";
|
||||
import { AudioLines, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, Pause, Play, Search, Square, Trash2, Video, X } from "lucide-react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import * as fs from "@tauri-apps/plugin-fs";
|
||||
import { DownloadState } from "@/types/download";
|
||||
@@ -20,12 +20,13 @@ import { Label } from "@/components/ui/label";
|
||||
import Heading from "@/components/heading";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
|
||||
export default function LibraryPage() {
|
||||
const activeTab = useLibraryPageStatesStore(state => state.activeTab);
|
||||
const setActiveTab = useLibraryPageStatesStore(state => state.setActiveTab);
|
||||
|
||||
|
||||
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||
const downloadActions = useDownloadActionStatesStore(state => state.downloadActions);
|
||||
const setIsResumingDownload = useDownloadActionStatesStore(state => state.setIsResumingDownload);
|
||||
@@ -33,15 +34,24 @@ export default function LibraryPage() {
|
||||
const setIsCancelingDownload = useDownloadActionStatesStore(state => state.setIsCancelingDownload);
|
||||
const setIsDeleteFileChecked = useDownloadActionStatesStore(state => state.setIsDeleteFileChecked);
|
||||
|
||||
const debugMode = useSettingsPageStatesStore(state => state.settings.debug_mode);
|
||||
|
||||
const { pauseDownload, resumeDownload, cancelDownload } = useAppContext()
|
||||
const { toast } = useToast();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const downloadStateDeleter = useDeleteDownloadState();
|
||||
const navigate = useNavigate();
|
||||
const LOG = useLogger();
|
||||
|
||||
const incompleteDownloads = downloadStates.filter(state => state.download_status !== 'completed');
|
||||
const completedDownloads = downloadStates.filter(state => state.download_status === 'completed');
|
||||
const ongoingDownloads = downloadStates.filter(state =>
|
||||
const completedDownloads = downloadStates.filter(state => state.download_status === 'completed')
|
||||
.sort((a, b) => {
|
||||
// Latest updated first
|
||||
const dateA = a.updated_at ? new Date(a.updated_at).getTime() : 0;
|
||||
const dateB = b.updated_at ? new Date(b.updated_at).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
const ongoingDownloads = downloadStates.filter(state =>
|
||||
['starting', 'downloading', 'queued'].includes(state.download_status)
|
||||
);
|
||||
|
||||
@@ -49,24 +59,19 @@ export default function LibraryPage() {
|
||||
if (filePath && await fs.exists(filePath)) {
|
||||
try {
|
||||
await invoke('open_file_with_app', { filePath: filePath, appName: app }).then(() => {
|
||||
toast({
|
||||
title: 'Opening file',
|
||||
description: `Opening the file with ${app ? app : 'default app'}.`,
|
||||
toast.info(`${app === 'explorer' ? 'Revealing' : 'Opening'} file`, {
|
||||
description: `${app === 'explorer' ? 'Revealing' : 'Opening'} the file ${app === 'explorer' ? 'in' : 'with'} ${app ? app : 'default app'}.`,
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: 'Failed to open file',
|
||||
description: 'An error occurred while trying to open the file.',
|
||||
variant: "destructive"
|
||||
toast.error(`Failed to ${app === 'explorer' ? 'reveal' : 'open'} file`, {
|
||||
description: `An error occurred while trying to ${app === 'explorer' ? 'reveal' : 'open'} the file.`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
toast({
|
||||
title: 'File unavailable',
|
||||
description: 'The file you are trying to open does not exist.',
|
||||
variant: "destructive"
|
||||
toast.info("File unavailable", {
|
||||
description: `The file you are trying to ${app === 'explorer' ? 'reveal' : 'open'} does not exist.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -88,18 +93,15 @@ export default function LibraryPage() {
|
||||
onSuccess: (data) => {
|
||||
console.log("Download State deleted successfully:", data);
|
||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
toast({
|
||||
title: 'Removed from downloads',
|
||||
description: 'The download has been removed successfully.',
|
||||
})
|
||||
toast.success("Removed from downloads", {
|
||||
description: "The download has been removed successfully.",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to delete download state:", error);
|
||||
toast({
|
||||
title: 'Failed to remove download',
|
||||
description: 'An error occurred while trying to remove the download.',
|
||||
variant: "destructive"
|
||||
})
|
||||
toast.error("Failed to remove download", {
|
||||
description: "An error occurred while trying to remove the download.",
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -112,26 +114,39 @@ export default function LibraryPage() {
|
||||
await pauseDownload(state);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: 'Failed to stop download',
|
||||
toast.error("Failed to stop download", {
|
||||
description: `An error occurred while trying to stop the download for ${state.title}.`,
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsPausingDownload(state.download_id, false);
|
||||
}
|
||||
}
|
||||
if (ongoingDownloads.length === 0) {
|
||||
toast({
|
||||
title: 'Stopped ongoing downloads',
|
||||
description: 'All ongoing downloads have been stopped successfully.',
|
||||
toast.success("Stopped ongoing downloads", {
|
||||
description: "All ongoing downloads have been stopped successfully.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
toast({
|
||||
title: 'No ongoing downloads',
|
||||
description: 'There are no ongoing downloads to stop.',
|
||||
variant: "destructive"
|
||||
toast.info("No ongoing downloads", {
|
||||
description: "There are no ongoing downloads to stop.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = async (url: string, isPlaylist: boolean) => {
|
||||
try {
|
||||
LOG.info('NEODLP', `Received search request from library for URL: ${url}`);
|
||||
navigate('/');
|
||||
const { setRequestedUrl, setAutoSubmitSearch } = useCurrentVideoMetadataStore.getState();
|
||||
setRequestedUrl(url);
|
||||
setAutoSubmitSearch(true);
|
||||
toast.info(`Initiating ${isPlaylist ? 'Playlist' : 'Video'} Search`, {
|
||||
description: `Initiating search for the selected ${isPlaylist ? 'playlist' : 'video'}.`,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Failed to initiate search", {
|
||||
description: "An error occurred while trying to initiate the search.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -269,7 +284,11 @@ export default function LibraryPage() {
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => openFile(state.filepath, 'explorer')}>
|
||||
<FolderInput className="w-4 h-4" />
|
||||
Open in Explorer
|
||||
Reveal
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleSearch(state.url, state.playlist_id ? true : false)}>
|
||||
<Search className="w-4 h-4" />
|
||||
Search
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
@@ -362,7 +381,7 @@ export default function LibraryPage() {
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground">{ state.download_status && (
|
||||
`${state.download_status === 'downloading' && state.status === 'finished' ? 'Processing' : state.download_status.charAt(0).toUpperCase() + state.download_status.slice(1)} ${state.download_status === 'downloading' && state.status !== 'finished' && state.speed ? `• Speed: ${formatSpeed(state.speed)}` : ""} ${state.download_status === 'downloading' && state.eta ? `• ETA: ${formatSecToTimeString(state.eta)}` : ""}`
|
||||
`${state.download_status === 'downloading' && state.status === 'finished' ? 'Processing' : state.download_status.charAt(0).toUpperCase() + state.download_status.slice(1)} ${debugMode && state.download_id ? `• ID: ${state.download_id.toUpperCase()}` : ""} ${state.download_status === 'downloading' && state.status !== 'finished' && state.speed ? `• Speed: ${formatSpeed(state.speed)}` : ""} ${state.download_status === 'downloading' && state.eta ? `• ETA: ${formatSecToTimeString(state.eta)}` : ""}`
|
||||
)}</div>
|
||||
</div>
|
||||
<div className="w-full flex items-center gap-2 mt-2">
|
||||
@@ -374,16 +393,13 @@ export default function LibraryPage() {
|
||||
setIsResumingDownload(state.download_id, true);
|
||||
try {
|
||||
await resumeDownload(state)
|
||||
// toast({
|
||||
// title: 'Resumed Download',
|
||||
// description: 'Download resumed, it will re-start shortly.',
|
||||
// toast.success("Resumed Download", {
|
||||
// description: "Download resumed, it will re-start shortly.",
|
||||
// })
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: 'Failed to Resume Download',
|
||||
description: 'An error occurred while trying to resume the download.',
|
||||
variant: "destructive"
|
||||
toast.error("Failed to Resume Download", {
|
||||
description: "An error occurred while trying to resume the download.",
|
||||
})
|
||||
} finally {
|
||||
setIsResumingDownload(state.download_id, false);
|
||||
@@ -411,16 +427,13 @@ export default function LibraryPage() {
|
||||
setIsPausingDownload(state.download_id, true);
|
||||
try {
|
||||
await pauseDownload(state)
|
||||
// toast({
|
||||
// title: 'Paused Download',
|
||||
// description: 'Download paused successfully.',
|
||||
// toast.success("Paused Download", {
|
||||
// description: "Download paused successfully.",
|
||||
// })
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: 'Failed to Pause Download',
|
||||
description: 'An error occurred while trying to pause the download.',
|
||||
variant: "destructive"
|
||||
toast.error("Failed to Pause Download", {
|
||||
description: "An error occurred while trying to pause the download."
|
||||
})
|
||||
} finally {
|
||||
setIsPausingDownload(state.download_id, false);
|
||||
@@ -448,16 +461,13 @@ export default function LibraryPage() {
|
||||
setIsCancelingDownload(state.download_id, true);
|
||||
try {
|
||||
await cancelDownload(state)
|
||||
toast({
|
||||
title: 'Canceled Download',
|
||||
description: 'Download canceled successfully.',
|
||||
toast.success("Canceled Download", {
|
||||
description: "Download canceled successfully.",
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: 'Failed to Cancel Download',
|
||||
description: 'An error occurred while trying to cancel the download.',
|
||||
variant: "destructive"
|
||||
toast.error("Failed to Cancel Download", {
|
||||
description: "An error occurred while trying to cancel the download.",
|
||||
})
|
||||
} finally {
|
||||
setIsCancelingDownload(state.download_id, false);
|
||||
@@ -494,4 +504,4 @@ export default function LibraryPage() {
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Heading from "@/components/heading";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useBasePathsStore, useDownloadStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { useBasePathsStore, useDownloaderPageStatesStore, useDownloadStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { ArrowDownToLine, ArrowRight, BrushCleaning, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, Sun, Terminal, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { ArrowDownToLine, ArrowRight, BellRing, BrushCleaning, Bug, Cookie, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, ShieldMinus, SquareTerminal, Sun, Terminal, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "@/providers/themeProvider";
|
||||
@@ -26,7 +26,11 @@ import { SlidingButton } from "@/components/custom/slidingButton";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import * as fs from "@tauri-apps/plugin-fs";
|
||||
import { join } from "@tauri-apps/api/path";
|
||||
import { formatSpeed } from "@/utils";
|
||||
import { formatSpeed, generateID } from "@/utils";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
|
||||
|
||||
const websocketPortSchema = z.object({
|
||||
port: z.coerce.number<number>({
|
||||
@@ -64,8 +68,16 @@ const rateLimitSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const addCustomCommandSchema = z.object({
|
||||
label: z.string().min(1, { message: "Label is required" }),
|
||||
args: z.string().min(1, { message: "Arguments are required" }),
|
||||
});
|
||||
|
||||
const filenameTemplateShcema = z.object({
|
||||
template: z.string().min(1, { message: "Filename Template is required" }),
|
||||
});
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { toast } = useToast();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const activeTab = useSettingsPageStatesStore(state => state.activeTab);
|
||||
@@ -95,15 +107,43 @@ export default function SettingsPage() {
|
||||
const alwaysReencodeVideo = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
||||
const embedVideoMetadata = useSettingsPageStatesStore(state => state.settings.embed_video_metadata);
|
||||
const embedAudioMetadata = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
|
||||
const embedVideoThumbnail = useSettingsPageStatesStore(state => state.settings.embed_video_thumbnail);
|
||||
const embedAudioThumbnail = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
|
||||
const useCookies = useSettingsPageStatesStore(state => state.settings.use_cookies);
|
||||
const importCookiesFrom = useSettingsPageStatesStore(state => state.settings.import_cookies_from);
|
||||
const cookiesBrowser = useSettingsPageStatesStore(state => state.settings.cookies_browser);
|
||||
const cookiesFile = useSettingsPageStatesStore(state => state.settings.cookies_file);
|
||||
const useSponsorblock = useSettingsPageStatesStore(state => state.settings.use_sponsorblock);
|
||||
const sponsorblockMode = useSettingsPageStatesStore(state => state.settings.sponsorblock_mode);
|
||||
const sponsorblockRemove = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove);
|
||||
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 useForceInternetProtocol = useSettingsPageStatesStore(state => state.settings.use_force_internet_protocol);
|
||||
const forceInternetProtocol = useSettingsPageStatesStore(state => state.settings.force_internet_protocol);
|
||||
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||
const customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands);
|
||||
const filenameTemplate = useSettingsPageStatesStore(state => state.settings.filename_template);
|
||||
const debugMode = useSettingsPageStatesStore(state => state.settings.debug_mode);
|
||||
const logVerbose = useSettingsPageStatesStore(state => state.settings.log_verbose);
|
||||
const logWarning = useSettingsPageStatesStore(state => state.settings.log_warning);
|
||||
const logProgress = useSettingsPageStatesStore(state => state.settings.log_progress);
|
||||
const enableNotifications = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
||||
const updateNotification = useSettingsPageStatesStore(state => state.settings.update_notification);
|
||||
const downloadCompletionNotification = useSettingsPageStatesStore(state => state.settings.download_completion_notification);
|
||||
|
||||
const websocketPort = useSettingsPageStatesStore(state => state.settings.websocket_port);
|
||||
const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort);
|
||||
const setIsChangingWebSocketPort = useSettingsPageStatesStore(state => state.setIsChangingWebSocketPort);
|
||||
const isRestartingWebSocketServer = useSettingsPageStatesStore(state => state.isRestartingWebSocketServer);
|
||||
const setIsRestartingWebSocketServer = useSettingsPageStatesStore(state => state.setIsRestartingWebSocketServer);
|
||||
|
||||
|
||||
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
|
||||
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
|
||||
|
||||
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||
const ongoingDownloads = downloadStates.filter(state =>
|
||||
const ongoingDownloads = downloadStates.filter(state =>
|
||||
['starting', 'downloading', 'queued'].includes(state.download_status)
|
||||
);
|
||||
|
||||
@@ -120,20 +160,30 @@ export default function SettingsPage() {
|
||||
{ value: 'system', icon: Monitor, label: 'System' },
|
||||
];
|
||||
|
||||
const sponsorblockCategories = [
|
||||
{ code: 'sponsor', label: 'Sponsorship' },
|
||||
{ code: 'intro', label: 'Intro' },
|
||||
{ code: 'outro', label: 'Outro' },
|
||||
{ code: 'interaction', label: 'Interaction' },
|
||||
{ code: 'selfpromo', label: 'Self Promotion' },
|
||||
{ code: 'music_offtopic', label: 'Music Offtopic' },
|
||||
{ code: 'preview', label: 'Preview' },
|
||||
{ code: 'filler', label: 'Filler' },
|
||||
{ code: 'poi_highlight', label: 'Point of Interest' },
|
||||
{ code: 'chapter', label: 'Chapter' },
|
||||
];
|
||||
|
||||
const openLink = async (url: string, app: string | null) => {
|
||||
try {
|
||||
await invoke('open_file_with_app', { filePath: url, appName: app }).then(() => {
|
||||
toast({
|
||||
title: 'Opening Link',
|
||||
toast.info("Opening link", {
|
||||
description: `Opening link with ${app ? app : 'default app'}.`,
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: 'Failed to open link',
|
||||
description: 'An error occurred while trying to open the link.',
|
||||
variant: "destructive"
|
||||
toast.error("Failed to open link", {
|
||||
description: "An error occurred while trying to open the link.",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -148,20 +198,16 @@ export default function SettingsPage() {
|
||||
await fs.remove(filePath);
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: "Temporary Downloads Cleaned",
|
||||
toast.success("Temporary Downloads Cleaned", {
|
||||
description: "All temporary downloads have been successfully cleaned up.",
|
||||
});
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "Temporary Downloads Cleanup Failed",
|
||||
toast.error("Temporary Downloads Cleanup Failed", {
|
||||
description: "An error occurred while trying to clean up temporary downloads. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
toast({
|
||||
title: "No Temporary Downloads",
|
||||
toast.info("No Temporary Downloads", {
|
||||
description: "There are no temporary downloads to clean up.",
|
||||
});
|
||||
}
|
||||
@@ -180,16 +226,13 @@ export default function SettingsPage() {
|
||||
function handleProxyUrlSubmit(values: z.infer<typeof proxyUrlSchema>) {
|
||||
try {
|
||||
saveSettingsKey('proxy_url', values.url);
|
||||
toast({
|
||||
title: "Proxy URL updated",
|
||||
toast.success("Proxy URL updated", {
|
||||
description: `Proxy URL changed to ${values.url}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing proxy URL:", error);
|
||||
toast({
|
||||
title: "Failed to change proxy URL",
|
||||
description: "Please try again.",
|
||||
variant: "destructive",
|
||||
toast.error("Failed to change proxy URL", {
|
||||
description: "An error occurred while trying to change the proxy URL. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -207,16 +250,87 @@ export default function SettingsPage() {
|
||||
function handleRateLimitSubmit(values: z.infer<typeof rateLimitSchema>) {
|
||||
try {
|
||||
saveSettingsKey('rate_limit', values.rate_limit);
|
||||
toast({
|
||||
title: "Rate Limit updated",
|
||||
toast.success("Rate Limit updated", {
|
||||
description: `Rate Limit changed to ${values.rate_limit} bytes/s`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing rate limit:", error);
|
||||
toast({
|
||||
title: "Failed to change rate limit",
|
||||
description: "Please try again.",
|
||||
variant: "destructive",
|
||||
toast.error("Failed to change rate limit", {
|
||||
description: "An error occurred while trying to change the rate limit. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const addCustomCommandForm = useForm<z.infer<typeof addCustomCommandSchema>>({
|
||||
resolver: zodResolver(addCustomCommandSchema),
|
||||
defaultValues: {
|
||||
label: '',
|
||||
args: '',
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
const watchedLabel = addCustomCommandForm.watch("label");
|
||||
const watchedArgs = addCustomCommandForm.watch("args");
|
||||
const { errors: addCustomCommandFormErrors } = addCustomCommandForm.formState;
|
||||
|
||||
function handleAddCustomCommandSubmit(values: z.infer<typeof addCustomCommandSchema>) {
|
||||
try {
|
||||
const newCommand = {
|
||||
id: generateID(),
|
||||
label: values.label,
|
||||
args: values.args,
|
||||
};
|
||||
const updatedCommands = [...customCommands, newCommand];
|
||||
saveSettingsKey('custom_commands', updatedCommands);
|
||||
toast.success("Custom Command added", {
|
||||
description: `Custom Command "${values.label}" added.`,
|
||||
});
|
||||
addCustomCommandForm.reset();
|
||||
} catch (error) {
|
||||
console.error("Error adding custom command:", error);
|
||||
toast.error("Failed to add custom command", {
|
||||
description: "An error occurred while trying to add the custom command. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveCustomCommandSubmit(commandId: string) {
|
||||
try {
|
||||
const removedCommand = customCommands.find(command => command.id === commandId);
|
||||
const updatedCommands = customCommands.filter(command => command.id !== commandId);
|
||||
saveSettingsKey('custom_commands', updatedCommands);
|
||||
setDownloadConfigurationKey('custom_command', null);
|
||||
toast.success("Custom Command removed", {
|
||||
description: `Custom Command "${removedCommand?.label}" removed.`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error removing custom command:", error);
|
||||
toast.error("Failed to remove custom command", {
|
||||
description: "An error occurred while trying to remove the custom command. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const filenameTemplateForm = useForm<z.infer<typeof filenameTemplateShcema>>({
|
||||
resolver: zodResolver(filenameTemplateShcema),
|
||||
defaultValues: {
|
||||
template: filenameTemplate,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
const watchedFilenameTemplate = filenameTemplateForm.watch("template");
|
||||
const { errors: filenameTemplateFormErrors } = filenameTemplateForm.formState;
|
||||
|
||||
function handleFilenameTemplateSubmit(values: z.infer<typeof filenameTemplateShcema>) {
|
||||
try {
|
||||
saveSettingsKey('filename_template', values.template);
|
||||
toast.success("Filename Template updated", {
|
||||
description: `Filename Template changed to ${values.template}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing filename template:", error);
|
||||
toast.error("Failed to change filename template", {
|
||||
description: "An error occurred while trying to change the filename template. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -245,16 +359,13 @@ export default function SettingsPage() {
|
||||
}
|
||||
});
|
||||
saveSettingsKey('websocket_port', updatedConfig.port);
|
||||
toast({
|
||||
title: "Websocket port updated",
|
||||
toast.success("Websocket port updated", {
|
||||
description: `Websocket port changed to ${values.port}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing websocket port:", error);
|
||||
toast({
|
||||
title: "Failed to change websocket port",
|
||||
description: "Please try again.",
|
||||
variant: "destructive",
|
||||
toast.error("Failed to change websocket port", {
|
||||
description: "An error occurred while trying to change the websocket port. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsChangingWebSocketPort(false);
|
||||
@@ -299,7 +410,14 @@ export default function SettingsPage() {
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={
|
||||
() => resetSettings()
|
||||
() => {
|
||||
resetSettings()
|
||||
proxyUrlForm.reset();
|
||||
rateLimitForm.reset();
|
||||
addCustomCommandForm.reset();
|
||||
filenameTemplateForm.reset();
|
||||
websocketPortForm.reset();
|
||||
}
|
||||
}>Reset</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -401,9 +519,34 @@ export default function SettingsPage() {
|
||||
value="network"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><Wifi className="size-4" /> Network</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key="cookies"
|
||||
value="cookies"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><Cookie className="size-4" /> Cookies</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key="sponsorblock"
|
||||
value="sponsorblock"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><ShieldMinus className="size-4" /> Sponsorblock</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key="notifications"
|
||||
value="notifications"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><BellRing className="size-4" /> Notifications</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key="commands"
|
||||
value="commands"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><SquareTerminal className="size-4" /> Commands</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key="debug"
|
||||
value="debug"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><Bug className="size-4" /> Debug</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="min-h-full flex flex-col max-w-[55%] w-full border-l border-border pl-4">
|
||||
<TabsContent key="general" value="general" className="flex flex-col gap-4 min-h-[235px]">
|
||||
<TabsContent key="general" value="general" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="max-parallel-downloads">
|
||||
<h3 className="font-semibold">Max Parallel Downloads</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Set maximum number of allowed parallel downloads</p>
|
||||
@@ -448,8 +591,18 @@ 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 only if you are experiancing too slow download speeds with native downloader, you need to install aria2 via homebrew if you are on macos to use this feature)</p>
|
||||
<Switch
|
||||
id="aria2"
|
||||
checked={useAria2}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_aria2', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="appearance" value="appearance" className="flex flex-col gap-4 min-h-[235px]">
|
||||
<TabsContent key="appearance" value="appearance" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="app-theme">
|
||||
<h3 className="font-semibold">Theme</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Choose app interface theme</p>
|
||||
@@ -472,7 +625,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="folders" value="folders" className="flex flex-col gap-4 min-h-[235px]">
|
||||
<TabsContent key="folders" value="folders" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="download-dir">
|
||||
<h3 className="font-semibold">Download Folder</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Set default download folder (directory)</p>
|
||||
@@ -492,10 +645,8 @@ export default function SettingsPage() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting folder:", error);
|
||||
toast({
|
||||
title: "Failed to select folder",
|
||||
description: "Please try again.",
|
||||
variant: "destructive",
|
||||
toast.error("Failed to select folder", {
|
||||
description: "An error occurred while trying to select the download folder. Please try again.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -533,8 +684,38 @@ export default function SettingsPage() {
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
<div className="filename-template">
|
||||
<h3 className="font-semibold">Filename Template</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Set the template for naming downloaded files (download id and file extension will be auto-appended at the end, changing template may cause paused downloads to re-start from begining)</p>
|
||||
<Form {...filenameTemplateForm}>
|
||||
<form onSubmit={filenameTemplateForm.handleSubmit(handleFilenameTemplateSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||
<FormField
|
||||
control={filenameTemplateForm.control}
|
||||
name="template"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter filename template"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!watchedFilenameTemplate || watchedFilenameTemplate === filenameTemplate || Object.keys(filenameTemplateFormErrors).length > 0}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="formats" value="formats" className="flex flex-col gap-4 min-h-[235px]">
|
||||
<TabsContent key="formats" value="formats" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="video-format">
|
||||
<h3 className="font-semibold">Video Format</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Choose in which format the final video file will be saved</p>
|
||||
@@ -543,6 +724,7 @@ export default function SettingsPage() {
|
||||
className="flex items-center gap-4"
|
||||
value={videoFormat}
|
||||
onValueChange={(value) => saveSettingsKey('video_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="v-auto" />
|
||||
@@ -570,6 +752,7 @@ export default function SettingsPage() {
|
||||
className="flex items-center gap-4"
|
||||
value={audioFormat}
|
||||
onValueChange={(value) => saveSettingsKey('audio_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="a-auto" />
|
||||
@@ -596,11 +779,12 @@ export default function SettingsPage() {
|
||||
id="always-reencode-video"
|
||||
checked={alwaysReencodeVideo}
|
||||
onCheckedChange={(checked) => saveSettingsKey('always_reencode_video', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="metadata" value="metadata" className="flex flex-col gap-4 min-h-[235px]">
|
||||
<div className="embed-video-metadata">
|
||||
<TabsContent key="metadata" value="metadata" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="embed-metadata">
|
||||
<h3 className="font-semibold">Embed Metadata</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Wheather to embed metadata in video/audio files (info, chapters)</p>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
@@ -608,6 +792,7 @@ export default function SettingsPage() {
|
||||
id="embed-video-metadata"
|
||||
checked={embedVideoMetadata}
|
||||
onCheckedChange={(checked) => saveSettingsKey('embed_video_metadata', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-video-metadata">Video</Label>
|
||||
</div>
|
||||
@@ -616,21 +801,35 @@ export default function SettingsPage() {
|
||||
id="embed-audio-metadata"
|
||||
checked={embedAudioMetadata}
|
||||
onCheckedChange={(checked) => saveSettingsKey('embed_audio_metadata', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-audio-metadata">Audio</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="embed-audio-thumbnail">
|
||||
<h3 className="font-semibold">Embed Thumbnail in Audio</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Wheather to embed thumbnail in audio files (as cover art)</p>
|
||||
<Switch
|
||||
id="embed-audio-thumbnail"
|
||||
checked={embedAudioThumbnail}
|
||||
onCheckedChange={(checked) => saveSettingsKey('embed_audio_thumbnail', checked)}
|
||||
/>
|
||||
<div className="embed-thumbnail">
|
||||
<h3 className="font-semibold">Embed Thumbnail</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Wheather to embed thumbnail in video/audio files (as cover art)</p>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Switch
|
||||
id="embed-video-thumbnail"
|
||||
checked={embedVideoThumbnail}
|
||||
onCheckedChange={(checked) => saveSettingsKey('embed_video_thumbnail', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-video-thumbnail">Video</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="embed-audio-thumbnail"
|
||||
checked={embedAudioThumbnail}
|
||||
onCheckedChange={(checked) => saveSettingsKey('embed_audio_thumbnail', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-audio-thumbnail">Audio</Label>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="network" value="network" className="flex flex-col gap-4 min-h-[235px]">
|
||||
<TabsContent key="network" value="network" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="proxy">
|
||||
<h3 className="font-semibold">Proxy</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use proxy for downloads, Unblocks blocked sites in your region (download speed may affect, some sites may not work)</p>
|
||||
@@ -639,6 +838,7 @@ export default function SettingsPage() {
|
||||
id="use-proxy"
|
||||
checked={useProxy}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_proxy', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-proxy">Use Proxy</Label>
|
||||
</div>
|
||||
@@ -654,10 +854,11 @@ export default function SettingsPage() {
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter proxy URL"
|
||||
readOnly={useCustomCommands}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Label htmlFor="url" className="text-xs text-muted-foreground">(Configured: {proxyUrl ? 'Yes' : 'No'}, Status: {useProxy ? 'Enabled' : 'Disabled'})</Label>
|
||||
<Label htmlFor="url" className="text-xs text-muted-foreground">(Configured: {proxyUrl ? 'Yes' : 'No'}, Status: {useProxy && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -679,6 +880,7 @@ export default function SettingsPage() {
|
||||
id="use-rate-limit"
|
||||
checked={useRateLimit}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_rate_limit', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-rate-limit">Use Rate Limit</Label>
|
||||
</div>
|
||||
@@ -694,10 +896,11 @@ export default function SettingsPage() {
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter rate limit in bytes/s"
|
||||
readOnly={useCustomCommands}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Label htmlFor="rate_limit" className="text-xs text-muted-foreground">(Configured: {rateLimit ? `${rateLimit} = ${formatSpeed(rateLimit)}` : 'No'}, Status: {useRateLimit ? 'Enabled' : 'Disabled'}) (Default: 1048576, Range: 1024-104857600)</Label>
|
||||
<Label htmlFor="rate_limit" className="text-xs text-muted-foreground">(Configured: {rateLimit ? `${rateLimit} = ${formatSpeed(rateLimit)}` : 'No'}, Status: {useRateLimit && !useCustomCommands ? 'Enabled' : 'Disabled'}) (Default: 1048576, Range: 1024-104857600)</Label>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -711,6 +914,444 @@ export default function SettingsPage() {
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="force-internet-protocol">
|
||||
<h3 className="font-semibold">Force Internet Protocol</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Force use a specific internet protocol (ipv4/ipv6) for all downloads, useful if your network supports only one (some sites may not work)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="use-force-internet-protocol"
|
||||
checked={useForceInternetProtocol}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_force_internet_protocol', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-force-internet-protocol">Force IPV</Label>
|
||||
</div>
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4 mb-2"
|
||||
value={forceInternetProtocol}
|
||||
onValueChange={(value) => saveSettingsKey('force_internet_protocol', value)}
|
||||
disabled={!useForceInternetProtocol || useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="ipv4" id="force-ipv4" />
|
||||
<Label htmlFor="force-ipv4">Use IPv4 Only</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="ipv6" id="force-ipv6" />
|
||||
<Label htmlFor="force-ipv6">Use IPv6 Only</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<Label className="text-xs text-muted-foreground">(Forced: {forceInternetProtocol === "ipv4" ? 'IPv4' : 'IPv6'}, Status: {useForceInternetProtocol && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="cookies" value="cookies" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="cookies">
|
||||
<h3 className="font-semibold">Cookies</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use cookies to access exclusive/private (login-protected) contents from sites (use wisely, over-use can even block/ban your account)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="use-cookies"
|
||||
checked={useCookies}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_cookies', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-cookies">Use Cookies</Label>
|
||||
</div>
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4"
|
||||
value={importCookiesFrom}
|
||||
onValueChange={(value) => saveSettingsKey('import_cookies_from', value)}
|
||||
disabled={!useCookies || useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="browser" id="cookies-browser" />
|
||||
<Label htmlFor="cookies-browser">Import from Browser</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="file" id="cookies-file" />
|
||||
<Label htmlFor="cookies-file">Import from Text File</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex flex-col gap-2 mt-5 mb-2">
|
||||
<Label className="text-xs">Import Cookies from Browser</Label>
|
||||
<Select
|
||||
value={cookiesBrowser}
|
||||
onValueChange={(value) => saveSettingsKey('cookies_browser', value)}
|
||||
disabled={importCookiesFrom !== "browser" || !useCookies || useCustomCommands}
|
||||
>
|
||||
<SelectTrigger className="w-[230px] ring-0 focus:ring-0">
|
||||
<SelectValue placeholder="Select browser to import cookies" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Browsers</SelectLabel>
|
||||
<SelectItem value="firefox">Firefox (Recommended)</SelectItem>
|
||||
<SelectItem value="chrome">Chrome</SelectItem>
|
||||
<SelectItem value="chromium">Chromium</SelectItem>
|
||||
<SelectItem value="safari">Safari</SelectItem>
|
||||
<SelectItem value="brave">Brave</SelectItem>
|
||||
<SelectItem value="edge">Edge</SelectItem>
|
||||
<SelectItem value="opera">Opera</SelectItem>
|
||||
<SelectItem value="vivaldi">Vivaldi</SelectItem>
|
||||
<SelectItem value="whale">Whale</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-3 mb-2">
|
||||
<Label className="text-xs">Import Cookies from Text File (Netscape format)</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Input className="focus-visible:ring-0" type="text" placeholder="Select cookies text file" value={cookiesFile ?? ''} disabled={importCookiesFrom !== "file" || !useCookies} readOnly/>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={importCookiesFrom !== "file" || !useCookies || useCustomCommands}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const file = await open({
|
||||
multiple: false,
|
||||
directory: false,
|
||||
filters: [
|
||||
{ name: 'Text', extensions: ['txt'] },
|
||||
],
|
||||
});
|
||||
if (file && typeof file === 'string') {
|
||||
saveSettingsKey('cookies_file', file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting file:", error);
|
||||
toast.error("Failed to select file", {
|
||||
description: "An error occurred while trying to select the cookies text file. Please try again.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FolderOpen className="w-4 h-4" /> Browse
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Label className="text-xs text-muted-foreground">(Configured: {importCookiesFrom === "browser" ? 'Yes' : cookiesFile ? 'Yes' : 'No'}, From: {importCookiesFrom === "browser" ? 'Browser' : 'Text'}, Status: {useCookies && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="sponsorblock" value="sponsorblock" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="sponsorblock">
|
||||
<h3 className="font-semibold">Sponsor Block</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="use-sponsorblock"
|
||||
checked={useSponsorblock}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_sponsorblock', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-sponsorblock">Use Sponsorblock</Label>
|
||||
</div>
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4"
|
||||
value={sponsorblockMode}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_mode', value)}
|
||||
disabled={!useSponsorblock || useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="remove" id="sponsorblock-remove" />
|
||||
<Label htmlFor="sponsorblock-remove">Remove Segments</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mark" id="sponsorblock-mark" />
|
||||
<Label htmlFor="sponsorblock-mark">Mark Segments</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex flex-col gap-2 mt-5">
|
||||
<Label className="text-xs mb-1">Sponsorblock Remove Categories</Label>
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4"
|
||||
value={sponsorblockRemove}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_remove', value)}
|
||||
disabled={/*!useSponsorblock || sponsorblockMode !== "remove" ||*/ useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="default" id="sponsorblock-remove-default" />
|
||||
<Label htmlFor="sponsorblock-remove-default">Default</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="all" id="sponsorblock-remove-all" />
|
||||
<Label htmlFor="sponsorblock-remove-all">All</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="custom" id="sponsorblock-remove-custom" />
|
||||
<Label htmlFor="sponsorblock-remove-custom">Custom</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<ToggleGroup
|
||||
type="multiple"
|
||||
variant="outline"
|
||||
className="flex flex-col items-start gap-2 mt-1"
|
||||
value={sponsorblockRemove === "custom" ? sponsorblockRemoveCategories : sponsorblockRemove === "default" ? sponsorblockCategories.filter((cat) => cat.code !== 'poi_highlight' && cat.code !== 'filler').map((cat) => cat.code) : sponsorblockRemove === "all" ? sponsorblockCategories.filter((cat) => cat.code !== 'poi_highlight').map((cat) => cat.code) : []}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_remove_categories', value)}
|
||||
disabled={/*!useSponsorblock || sponsorblockMode !== "remove" ||*/ sponsorblockRemove !== "custom" || useCustomCommands}
|
||||
>
|
||||
<div className="flex gap-2 flex-wrap items-center">
|
||||
{sponsorblockCategories.map((category) => (
|
||||
category.code !== "poi_highlight" && (
|
||||
<ToggleGroupItem
|
||||
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
|
||||
value={category.code}
|
||||
size="sm"
|
||||
aria-label={category.label}
|
||||
key={category.code}
|
||||
>
|
||||
{category.label}
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<Label className="text-xs mb-1">Sponsorblock Mark Categories</Label>
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4"
|
||||
value={sponsorblockMark}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_mark', value)}
|
||||
disabled={/*!useSponsorblock || sponsorblockMode !== "mark" ||*/ useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="default" id="sponsorblock-mark-default" />
|
||||
<Label htmlFor="sponsorblock-mark-default">Default</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="all" id="sponsorblock-mark-all" />
|
||||
<Label htmlFor="sponsorblock-mark-all">All</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="custom" id="sponsorblock-mark-custom" />
|
||||
<Label htmlFor="sponsorblock-mark-custom">Custom</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<ToggleGroup
|
||||
type="multiple"
|
||||
variant="outline"
|
||||
className="flex flex-col items-start gap-2 mt-1 mb-2"
|
||||
value={sponsorblockMark === "custom" ? sponsorblockMarkCategories : sponsorblockMark === "default" ? sponsorblockCategories.map((cat) => cat.code) : sponsorblockMark === "all" ? sponsorblockCategories.map((cat) => cat.code) : []}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_mark_categories', value)}
|
||||
disabled={/*!useSponsorblock || sponsorblockMode !== "mark" ||*/ sponsorblockMark !== "custom" || useCustomCommands}
|
||||
>
|
||||
<div className="flex gap-2 flex-wrap items-center">
|
||||
{sponsorblockCategories.map((category) => (
|
||||
<ToggleGroupItem
|
||||
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
|
||||
value={category.code}
|
||||
size="sm"
|
||||
aria-label={category.label}
|
||||
key={category.code}
|
||||
>
|
||||
{category.label}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</div>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<Label className="text-xs text-muted-foreground">(Configured: {sponsorblockMode === "remove" && sponsorblockRemove === "custom" && sponsorblockRemoveCategories.length <= 0 ? 'No' : sponsorblockMode === "mark" && sponsorblockMark === "custom" && sponsorblockMarkCategories.length <= 0 ? 'No' : 'Yes'}, Mode: {sponsorblockMode === "remove" ? 'Remove' : 'Mark'}, Status: {useSponsorblock && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="notifications" value="notifications" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="notifications">
|
||||
<h3 className="font-semibold">Desktop Notifications</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Enable desktop notifications for app events (updates, download completions, etc.)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="enable-notifications"
|
||||
checked={enableNotifications}
|
||||
onCheckedChange={async (checked) => {
|
||||
if (checked) {
|
||||
const granted = await isPermissionGranted();
|
||||
if (!granted) {
|
||||
const permission = await requestPermission();
|
||||
if (permission !== 'granted') {
|
||||
toast.error("Notification Permission Denied", {
|
||||
description: "You have denied the notification permission. Please enable it from your system settings to receive notifications.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
saveSettingsKey('enable_notifications', checked)
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="enable-notifications">Enable Notifications</Label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-5">
|
||||
<Label className="text-xs mb-1">Notification Categories</Label>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Switch
|
||||
id="update-notification"
|
||||
checked={updateNotification}
|
||||
onCheckedChange={(checked) => saveSettingsKey('update_notification', checked)}
|
||||
disabled={!enableNotifications}
|
||||
/>
|
||||
<Label htmlFor="update-notification">App Updates</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Switch
|
||||
id="download-completion-notification"
|
||||
checked={downloadCompletionNotification}
|
||||
onCheckedChange={(checked) => saveSettingsKey('download_completion_notification', checked)}
|
||||
disabled={!enableNotifications}
|
||||
/>
|
||||
<Label htmlFor="download-completion-notification">Download Completion</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="commands" value="commands" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="custom-commands">
|
||||
<h3 className="font-semibold">Custom Commands</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3"> Run custom yt-dlp commands for your downloads</p>
|
||||
<Alert className="mb-3">
|
||||
<TriangleAlert />
|
||||
<AlertTitle className="text-sm">Most Settings will be Disabled!</AlertTitle>
|
||||
<AlertDescription className="text-xs">
|
||||
This feature is intended for advanced users only. Turning it on will disable most other settings in the app. Make sure you know what you are doing before using this feature, otherwise things could break easily.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="use-custom-commands"
|
||||
checked={useCustomCommands}
|
||||
onCheckedChange={(checked) => {
|
||||
saveSettingsKey('use_custom_commands', checked)
|
||||
resetDownloadConfiguration();
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="use-custom-commands">Use Custom Commands</Label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Form {...addCustomCommandForm}>
|
||||
<form onSubmit={addCustomCommandForm.handleSubmit(handleAddCustomCommandSubmit)} className="flex flex-col gap-3" autoComplete="off">
|
||||
<FormField
|
||||
control={addCustomCommandForm.control}
|
||||
name="args"
|
||||
disabled={!useCustomCommands}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter yt-dlp command line arguments (no need to start with 'yt-dlp', already passed args: url, output paths, selected formats, selected subtitles, playlist item. also, bulk downloading is not supported)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-4 w-full">
|
||||
<FormField
|
||||
control={addCustomCommandForm.control}
|
||||
name="label"
|
||||
disabled={!useCustomCommands}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter template label"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
{/* <Label htmlFor="label" className="text-xs text-muted-foreground">(Configured: {rateLimit ? `${rateLimit} = ${formatSpeed(rateLimit)}` : 'No'}, Status: {useRateLimit ? 'Enabled' : 'Disabled'}) (Default: 1048576, Range: 1024-104857600)</Label> */}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!watchedLabel || !watchedArgs || Object.keys(addCustomCommandFormErrors).length > 0 || !useCustomCommands}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex-flex-col gap-2 mt-4">
|
||||
<Label className="text-xs mb-3">Custom Command Templates</Label>
|
||||
{customCommands.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">NO CUSTOM COMMAND TEMPLATE ADDED YET!</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
{customCommands.map((command) => (
|
||||
<div key={command.id} className="p-2 flex justify-between gap-2 border border-border rounded-md">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-sm mb-1">{command.label}</h5>
|
||||
<p className="text-xs font-mono text-muted-foreground">{command.args}</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
disabled={!useCustomCommands}
|
||||
onClick={() => {
|
||||
handleRemoveCustomCommandSubmit(command.id);
|
||||
}}
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="debug" value="debug" className="flex flex-col gap-4 min-h-[425px]">
|
||||
<div className="debug-mode">
|
||||
<h3 className="font-semibold">Debug Mode</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Enable debug mode for troubleshooting issues (get debug logs, download ids, and more)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="debug-mode"
|
||||
checked={debugMode}
|
||||
onCheckedChange={(checked) => saveSettingsKey('debug_mode', checked)}
|
||||
/>
|
||||
<Label htmlFor="debug-mode">Enable Debug Mode</Label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-5">
|
||||
<Label className="text-xs mb-1">Logging Options</Label>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Switch
|
||||
id="log-verbose"
|
||||
checked={logVerbose}
|
||||
onCheckedChange={(checked) => saveSettingsKey('log_verbose', checked)}
|
||||
disabled={!debugMode}
|
||||
/>
|
||||
<Label htmlFor="log-verbose">Verbose Logging</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Switch
|
||||
id="log-warning"
|
||||
checked={logWarning}
|
||||
onCheckedChange={(checked) => saveSettingsKey('log_warning', checked)}
|
||||
disabled={!debugMode}
|
||||
/>
|
||||
<Label htmlFor="log-warning">Log Warnings</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Switch
|
||||
id="log-progress"
|
||||
checked={logProgress}
|
||||
onCheckedChange={(checked) => saveSettingsKey('log_progress', checked)}
|
||||
disabled={!debugMode}
|
||||
/>
|
||||
<Label htmlFor="log-progress">Log Progress</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
@@ -739,16 +1380,13 @@ export default function SettingsPage() {
|
||||
setIsRestartingWebSocketServer(true);
|
||||
try {
|
||||
await invoke("restart_websocket_server");
|
||||
toast({
|
||||
title: "Websocket server restarted",
|
||||
toast.success("Websocket server restarted", {
|
||||
description: "Websocket server restarted successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error restarting websocket server:", error);
|
||||
toast({
|
||||
title: "Failed to restart websocket server",
|
||||
description: "Please try again.",
|
||||
variant: "destructive",
|
||||
toast.error("Failed to restart websocket server", {
|
||||
description: "An error occurred while trying to restart the websocket server. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsRestartingWebSocketServer(false);
|
||||
@@ -834,7 +1472,7 @@ export default function SettingsPage() {
|
||||
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'msedge')}>Edge</Button>
|
||||
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'opera')}>Opera</Button>
|
||||
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'brave')}>Brave</Button>
|
||||
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'arc')}>Arc</Button>
|
||||
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'vivaldi')}>Vivaldi</Button>
|
||||
<Button variant="outline" onClick={() => openLink('https://addons.mozilla.org/en-US/firefox/addon/neo-downloader-plus', 'zen')}>Zen</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-2">* These links opens with coresponding browsers only. Make sure the browser is installed before clicking the link</p>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { DownloadState } from '@/types/download';
|
||||
import { DownloadConfiguration } from '@/types/settings';
|
||||
import { RawVideoInfo } from '@/types/video';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
interface AppContextType {
|
||||
fetchVideoMetadata: (url: string, formatId?: string) => Promise<RawVideoInfo | null>;
|
||||
startDownload: (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => Promise<void>;
|
||||
fetchVideoMetadata: (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState, downloadConfig?: DownloadConfiguration) => Promise<RawVideoInfo | null>;
|
||||
startDownload: (url: string, selectedFormat: string, downloadConfig: DownloadConfiguration, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => Promise<void>;
|
||||
pauseDownload: (state: DownloadState) => Promise<void>;
|
||||
resumeDownload: (state: DownloadState) => Promise<void>;
|
||||
cancelDownload: (state: DownloadState) => Promise<void>;
|
||||
@@ -18,4 +19,4 @@ export const AppContext = createContext<AppContextType>({
|
||||
cancelDownload: async () => {}
|
||||
});
|
||||
|
||||
export const useAppContext = () => useContext(AppContext);
|
||||
export const useAppContext = () => useContext(AppContext);
|
||||
|
||||
@@ -196,7 +196,15 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
||||
eta = $22,
|
||||
filepath = $23,
|
||||
filetype = $24,
|
||||
filesize = $25
|
||||
filesize = $25,
|
||||
output_format = $26,
|
||||
embed_metadata = $27,
|
||||
embed_thumbnail = $28,
|
||||
sponsorblock_remove = $29,
|
||||
sponsorblock_mark = $30,
|
||||
use_aria2 = $31,
|
||||
custom_command = $32,
|
||||
queue_config = $33
|
||||
WHERE download_id = $1`,
|
||||
[
|
||||
downloadState.download_id,
|
||||
@@ -223,7 +231,15 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
||||
downloadState.eta,
|
||||
downloadState.filepath,
|
||||
downloadState.filetype,
|
||||
downloadState.filesize
|
||||
downloadState.filesize,
|
||||
downloadState.output_format,
|
||||
downloadState.embed_metadata,
|
||||
downloadState.embed_thumbnail,
|
||||
downloadState.sponsorblock_remove,
|
||||
downloadState.sponsorblock_mark,
|
||||
downloadState.use_aria2,
|
||||
downloadState.custom_command,
|
||||
downloadState.queue_config
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -252,8 +268,16 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
||||
eta,
|
||||
filepath,
|
||||
filetype,
|
||||
filesize
|
||||
) 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)`,
|
||||
filesize,
|
||||
output_format,
|
||||
embed_metadata,
|
||||
embed_thumbnail,
|
||||
sponsorblock_remove,
|
||||
sponsorblock_mark,
|
||||
use_aria2,
|
||||
custom_command,
|
||||
queue_config
|
||||
) 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, $32, $33)`,
|
||||
[
|
||||
downloadState.download_id,
|
||||
downloadState.download_status,
|
||||
@@ -279,7 +303,15 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
||||
downloadState.eta,
|
||||
downloadState.filepath,
|
||||
downloadState.filetype,
|
||||
downloadState.filesize
|
||||
downloadState.filesize,
|
||||
downloadState.output_format,
|
||||
downloadState.embed_metadata,
|
||||
downloadState.embed_thumbnail,
|
||||
downloadState.sponsorblock_remove,
|
||||
downloadState.sponsorblock_mark,
|
||||
downloadState.use_aria2,
|
||||
downloadState.custom_command,
|
||||
downloadState.queue_config
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -292,11 +324,11 @@ export const updateDownloadStatus = async (download_id: string, download_status:
|
||||
)
|
||||
}
|
||||
|
||||
export const updateDownloadFilePath = async (download_id: string, filepath: string) => {
|
||||
export const updateDownloadFilePath = async (download_id: string, filepath: string, ext: string) => {
|
||||
const db = await Database.load('sqlite:database.db')
|
||||
return await db.execute(
|
||||
'UPDATE downloads SET filepath = $2 WHERE download_id = $1',
|
||||
[download_id, filepath]
|
||||
'UPDATE downloads SET filepath = $2, ext = $3 WHERE download_id = $1',
|
||||
[download_id, filepath, ext]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -423,4 +455,4 @@ export const deleteKvPair = async (key: string) => {
|
||||
'DELETE FROM kv_store WHERE key = $1',
|
||||
[key]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ export function useUpdateDownloadStatus() {
|
||||
|
||||
export function useUpdateDownloadFilePath() {
|
||||
return useMutation({
|
||||
mutationFn: (data: { download_id: string; filepath: string }) =>
|
||||
updateDownloadFilePath(data.download_id, data.filepath)
|
||||
mutationFn: (data: { download_id: string; filepath: string, ext: string }) =>
|
||||
updateDownloadFilePath(data.download_id, data.filepath, data.ext)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,4 +64,4 @@ export function useDeleteKvPair() {
|
||||
return useMutation({
|
||||
mutationFn: (key: string) => deleteKvPair(key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, LibraryPageStatesStore, SettingsPageStatesStore } from '@/types/store';
|
||||
import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, LibraryPageStatesStore, LogsStore, SettingsPageStatesStore } from '@/types/store';
|
||||
import { create } from 'zustand';
|
||||
|
||||
export const useBasePathsStore = create<BasePathsStore>((set) => ({
|
||||
@@ -15,7 +15,7 @@ export const useDownloadStatesStore = create<DownloadStatesStore>((set) => ({
|
||||
const existingIndex = prev.downloadStates.findIndex(
|
||||
item => item.download_id === state.download_id
|
||||
);
|
||||
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Update existing state
|
||||
const updatedStates = [...prev.downloadStates];
|
||||
@@ -47,22 +47,47 @@ export const useCurrentVideoMetadataStore = create<CurrentVideoMetadataStore>((s
|
||||
|
||||
export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((set) => ({
|
||||
activeDownloadModeTab: 'selective',
|
||||
activeDownloadConfigurationTab: 'options',
|
||||
isStartingDownload: false,
|
||||
selectedDownloadFormat: 'best',
|
||||
selectedCombinableVideoFormat: '',
|
||||
selectedCombinableAudioFormat: '',
|
||||
selectedSubtitles: [],
|
||||
selectedPlaylistVideoIndex: '1',
|
||||
downloadConfiguration: {
|
||||
output_format: null,
|
||||
embed_metadata: null,
|
||||
embed_thumbnail: null,
|
||||
sponsorblock: null,
|
||||
custom_command: null
|
||||
},
|
||||
isErrored: false,
|
||||
isErrorExpected: false,
|
||||
erroredDownloadId: null,
|
||||
setActiveDownloadModeTab: (tab) => set(() => ({ activeDownloadModeTab: tab })),
|
||||
setActiveDownloadConfigurationTab: (tab) => set(() => ({ activeDownloadConfigurationTab: tab })),
|
||||
setIsStartingDownload: (isStarting) => set(() => ({ isStartingDownload: isStarting })),
|
||||
setSelectedDownloadFormat: (format) => set(() => ({ selectedDownloadFormat: format })),
|
||||
setSelectedCombinableVideoFormat: (format) => set(() => ({ selectedCombinableVideoFormat: format })),
|
||||
setSelectedCombinableAudioFormat: (format) => set(() => ({ selectedCombinableAudioFormat: format })),
|
||||
setSelectedSubtitles: (subtitles) => set(() => ({ selectedSubtitles: subtitles })),
|
||||
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index })),
|
||||
setDownloadConfigurationKey: (key, value) => set((state) => ({
|
||||
downloadConfiguration: {
|
||||
...state.downloadConfiguration,
|
||||
[key]: value
|
||||
}
|
||||
})),
|
||||
setDownloadConfiguration: (config) => set(() => ({ downloadConfiguration: config })),
|
||||
resetDownloadConfiguration: () => set(() => ({
|
||||
downloadConfiguration: {
|
||||
output_format: null,
|
||||
embed_metadata: null,
|
||||
embed_thumbnail: null,
|
||||
sponsorblock: null,
|
||||
custom_command: null
|
||||
}
|
||||
})),
|
||||
setIsErrored: (isErrored) => set(() => ({ isErrored: isErrored })),
|
||||
setIsErrorExpected: (isErrorExpected) => set(() => ({ isErrorExpected: isErrorExpected })),
|
||||
setErroredDownloadId: (downloadId) => set(() => ({ erroredDownloadId: downloadId })),
|
||||
@@ -140,7 +165,32 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
always_reencode_video: false,
|
||||
embed_video_metadata: false,
|
||||
embed_audio_metadata: true,
|
||||
embed_video_thumbnail: false,
|
||||
embed_audio_thumbnail: true,
|
||||
use_cookies: false,
|
||||
import_cookies_from: 'browser',
|
||||
cookies_browser: 'firefox',
|
||||
cookies_file: '',
|
||||
use_sponsorblock: false,
|
||||
sponsorblock_mode: 'remove',
|
||||
sponsorblock_remove: 'default',
|
||||
sponsorblock_mark: 'default',
|
||||
sponsorblock_remove_categories: [],
|
||||
sponsorblock_mark_categories: [],
|
||||
use_aria2: false,
|
||||
use_force_internet_protocol: false,
|
||||
force_internet_protocol: 'ipv4',
|
||||
use_custom_commands: false,
|
||||
custom_commands: [],
|
||||
filename_template: '%(title)s_%(resolution|unknown)s',
|
||||
debug_mode: false,
|
||||
log_verbose: true,
|
||||
log_warning: true,
|
||||
log_progress: false,
|
||||
enable_notifications: false,
|
||||
update_notification: true,
|
||||
download_completion_notification: false,
|
||||
// extension settings
|
||||
websocket_port: 53511
|
||||
},
|
||||
isUsingDefaultSettings: true,
|
||||
@@ -184,7 +234,32 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
always_reencode_video: false,
|
||||
embed_video_metadata: false,
|
||||
embed_audio_metadata: true,
|
||||
embed_video_thumbnail: false,
|
||||
embed_audio_thumbnail: true,
|
||||
use_cookies: false,
|
||||
import_cookies_from: 'browser',
|
||||
cookies_browser: 'firefox',
|
||||
cookies_file: '',
|
||||
use_sponsorblock: false,
|
||||
sponsorblock_mode: 'remove',
|
||||
sponsorblock_remove: 'default',
|
||||
sponsorblock_mark: 'default',
|
||||
sponsorblock_remove_categories: [],
|
||||
sponsorblock_mark_categories: [],
|
||||
use_aria2: false,
|
||||
use_force_internet_protocol: false,
|
||||
force_internet_protocol: 'ipv4',
|
||||
use_custom_commands: false,
|
||||
custom_commands: [],
|
||||
filename_template: '%(title)s_%(resolution|unknown)s',
|
||||
debug_mode: false,
|
||||
log_verbose: true,
|
||||
log_warning: true,
|
||||
log_progress: false,
|
||||
enable_notifications: false,
|
||||
update_notification: true,
|
||||
download_completion_notification: false,
|
||||
// extension settings
|
||||
websocket_port: 53511
|
||||
},
|
||||
isUsingDefaultSettings: true
|
||||
@@ -210,4 +285,11 @@ export const useKvPairsStatesStore = create<KvPairsStatesStore>((set) => ({
|
||||
}
|
||||
})),
|
||||
setKvPairs: (kvPairs) => set(() => ({ kvPairs }))
|
||||
}));
|
||||
}));
|
||||
|
||||
export const useLogsStore = create<LogsStore>((set) => ({
|
||||
logs: [],
|
||||
setLogs: (logs) => set(() => ({ logs })),
|
||||
addLog: (log) => set((state) => ({ logs: [...state.logs, log] })),
|
||||
clearLogs: () => set(() => ({ logs: [] }))
|
||||
}));
|
||||
|
||||
@@ -37,6 +37,16 @@ export interface DownloadState {
|
||||
filepath: string | null;
|
||||
filetype: string | null;
|
||||
filesize: number | null;
|
||||
output_format: string | null;
|
||||
embed_metadata: number;
|
||||
embed_thumbnail: number;
|
||||
sponsorblock_remove: string | null;
|
||||
sponsorblock_mark: string | null;
|
||||
use_aria2: number;
|
||||
custom_command: string | null;
|
||||
queue_config: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface Download {
|
||||
@@ -65,6 +75,16 @@ export interface Download {
|
||||
filepath: string | null;
|
||||
filetype: string | null;
|
||||
filesize: number | null;
|
||||
output_format: string | null;
|
||||
embed_metadata: number;
|
||||
embed_thumbnail: number;
|
||||
sponsorblock_remove: string | null;
|
||||
sponsorblock_mark: string | null;
|
||||
use_aria2: number;
|
||||
custom_command: string | null;
|
||||
queue_config: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface DownloadProgress {
|
||||
@@ -74,4 +94,4 @@ export interface DownloadProgress {
|
||||
downloaded: number | null;
|
||||
total: number | null;
|
||||
eta: number | null;
|
||||
}
|
||||
}
|
||||
|
||||
6
src/types/logs.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Log {
|
||||
timestamp: number;
|
||||
level: 'info' | 'warning' | 'error' | 'debug' | 'progress';
|
||||
context: string;
|
||||
message: string;
|
||||
}
|
||||
@@ -3,6 +3,12 @@ export interface SettingsTable {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CustomCommand {
|
||||
id: string;
|
||||
label: string;
|
||||
args: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
ytdlp_update_channel: string;
|
||||
ytdlp_auto_update: boolean;
|
||||
@@ -21,7 +27,39 @@ export interface Settings {
|
||||
always_reencode_video: boolean;
|
||||
embed_video_metadata: boolean;
|
||||
embed_audio_metadata: boolean;
|
||||
embed_video_thumbnail: boolean;
|
||||
embed_audio_thumbnail: boolean;
|
||||
use_cookies: boolean;
|
||||
import_cookies_from: string;
|
||||
cookies_browser: string;
|
||||
cookies_file: string;
|
||||
use_sponsorblock: boolean;
|
||||
sponsorblock_mode: string;
|
||||
sponsorblock_remove: string;
|
||||
sponsorblock_mark: string;
|
||||
sponsorblock_remove_categories: string[];
|
||||
sponsorblock_mark_categories: string[];
|
||||
use_aria2: boolean;
|
||||
use_force_internet_protocol: boolean;
|
||||
force_internet_protocol: string;
|
||||
use_custom_commands: boolean;
|
||||
custom_commands: CustomCommand[];
|
||||
filename_template: string;
|
||||
debug_mode: boolean;
|
||||
log_verbose: boolean;
|
||||
log_warning: boolean;
|
||||
log_progress: boolean;
|
||||
enable_notifications: boolean;
|
||||
update_notification: boolean;
|
||||
download_completion_notification: boolean;
|
||||
// extension settings
|
||||
websocket_port: number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DownloadConfiguration {
|
||||
output_format: string | null;
|
||||
embed_metadata: boolean | null;
|
||||
embed_thumbnail: boolean | null;
|
||||
sponsorblock: string | null;
|
||||
custom_command: string | null;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { DownloadState } from "@/types/download";
|
||||
import { RawVideoInfo } from "@/types/video";
|
||||
import { Settings } from "@/types/settings";
|
||||
import { DownloadConfiguration, Settings } from "@/types/settings";
|
||||
import { KvStore } from "@/types/kvStore";
|
||||
import { Update } from "@tauri-apps/plugin-updater";
|
||||
import { Log } from "@/types/logs";
|
||||
|
||||
export interface BasePathsStore {
|
||||
ffmpegPath: string | null;
|
||||
@@ -36,22 +37,28 @@ export interface CurrentVideoMetadataStore {
|
||||
|
||||
export interface DownloaderPageStatesStore {
|
||||
activeDownloadModeTab: string;
|
||||
activeDownloadConfigurationTab: string;
|
||||
isStartingDownload: boolean;
|
||||
selectedDownloadFormat: string;
|
||||
selectedCombinableVideoFormat: string;
|
||||
selectedCombinableAudioFormat: string;
|
||||
selectedSubtitles: string[];
|
||||
selectedPlaylistVideoIndex: string;
|
||||
downloadConfiguration: DownloadConfiguration;
|
||||
isErrored: boolean;
|
||||
isErrorExpected: boolean;
|
||||
erroredDownloadId: string | null;
|
||||
setActiveDownloadModeTab: (tab: string) => void;
|
||||
setActiveDownloadConfigurationTab: (tab: string) => void;
|
||||
setIsStartingDownload: (isStarting: boolean) => void;
|
||||
setSelectedDownloadFormat: (format: string) => void;
|
||||
setSelectedCombinableVideoFormat: (format: string) => void;
|
||||
setSelectedCombinableAudioFormat: (format: string) => void;
|
||||
setSelectedSubtitles: (subtitles: string[]) => void;
|
||||
setSelectedPlaylistVideoIndex: (index: string) => void;
|
||||
setDownloadConfigurationKey: (key: string, value: unknown) => void;
|
||||
setDownloadConfiguration: (config: DownloadConfiguration) => void;
|
||||
resetDownloadConfiguration: () => void;
|
||||
setIsErrored: (isErrored: boolean) => void;
|
||||
setIsErrorExpected: (isErrorExpected: boolean) => void;
|
||||
setErroredDownloadId: (downloadId: string | null) => void;
|
||||
@@ -118,4 +125,11 @@ export interface KvPairsStatesStore {
|
||||
kvPairs: KvStore
|
||||
setKvPairsKey: (key: string, value: unknown) => void;
|
||||
setKvPairs: (kvPairs: KvStore) => void;
|
||||
}
|
||||
|
||||
export interface LogsStore {
|
||||
logs: Log[];
|
||||
setLogs: (logs: Log[]) => void;
|
||||
addLog: (log: Log) => void;
|
||||
clearLogs: () => void;
|
||||
}
|
||||
140
src/utils.ts
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,6 +209,10 @@ export const formatCodec = (codec: string) => {
|
||||
return codec.toUpperCase();
|
||||
}
|
||||
|
||||
export const generateID = () => {
|
||||
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
export const generateDownloadId = (videoId: string, host: string) => {
|
||||
host = host.trim().split('.')[0];
|
||||
return `${host}_${videoId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Paths */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||