mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2026-02-05 01:52:22 +05:30
Compare commits
67 Commits
18
.editorconfig
Normal file
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
|
||||||
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -1,2 +1,3 @@
|
|||||||
src-tauri/binaries/* filter=lfs diff=lfs merge=lfs -text
|
* text=auto eol=lf
|
||||||
src-tauri/resources/binaries/* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
|
src-tauri/binaries/* filter=lfs diff=lfs merge=lfs -text
|
||||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
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
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
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]
|
||||||
16
.github/images/banner.svg
vendored
Normal file
16
.github/images/banner.svg
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<svg width="600" height="130" viewBox="0 0 600 130" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_3_2)">
|
||||||
|
<path d="M86.9062 11H21.0938C9.44399 11 0 20.444 0 32.0938V97.9062C0 109.556 9.44399 119 21.0938 119H86.9062C98.556 119 108 109.556 108 97.9062V32.0938C108 20.444 98.556 11 86.9062 11Z" fill="url(#paint0_linear_3_2)"/>
|
||||||
|
<path d="M55.8196 96.5455C54.7881 97.5856 53.1065 97.5856 52.075 96.5455L27.028 71.2863C25.3778 69.6221 26.5566 66.793 28.9002 66.793H78.9943C81.3379 66.793 82.5168 69.6221 80.8666 71.2863L55.8196 96.5455Z" fill="#FAFAFA"/>
|
||||||
|
<path d="M67.8164 34.4141H40.0781C38.6219 34.4141 37.4414 35.5946 37.4414 37.0508V68.2695C37.4414 69.7257 38.6219 70.9062 40.0781 70.9062H67.8164C69.2726 70.9062 70.4531 69.7257 70.4531 68.2695V37.0508C70.4531 35.5946 69.2726 34.4141 67.8164 34.4141Z" fill="#FAFAFA"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_3_2" x1="13.6582" y1="26.6621" x2="97.1367" y2="102.02" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#4444FF"/>
|
||||||
|
<stop offset="1" stop-color="#FF43D0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_3_2">
|
||||||
|
<rect width="108" height="108" fill="white" transform="translate(0 11)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
.github/images/completed-downloads.png
vendored
Normal file
BIN
.github/images/completed-downloads.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
BIN
.github/images/downloader.png
vendored
Normal file
BIN
.github/images/downloader.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
138
.github/images/mockup.svg
vendored
Normal file
138
.github/images/mockup.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 541 KiB |
BIN
.github/images/ongoing-downloads.png
vendored
Normal file
BIN
.github/images/ongoing-downloads.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
.github/images/settings.png
vendored
Normal file
BIN
.github/images/settings.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- 'v*.*.*'
|
||||||
|
|
||||||
name: 🚀 Release on GitHub
|
name: 🚀 Release on GitHub
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -14,31 +14,39 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- platform: 'macos-latest'
|
- platform: 'macos-latest'
|
||||||
args: '--target aarch64-apple-darwin --config ./src-tauri/tauri.macos-aarch64.conf.json'
|
args: '--target aarch64-apple-darwin --config ./src-tauri/tauri.macos-aarch64.conf.json'
|
||||||
arch: 'aarch64-apple-darwin'
|
|
||||||
- platform: 'macos-latest'
|
- platform: 'macos-latest'
|
||||||
args: '--target x86_64-apple-darwin --config ./src-tauri/tauri.macos-x86_64.conf.json'
|
args: '--target x86_64-apple-darwin --config ./src-tauri/tauri.macos-x86_64.conf.json'
|
||||||
arch: 'x86_64-apple-darwin'
|
|
||||||
- platform: 'ubuntu-22.04'
|
- platform: 'ubuntu-22.04'
|
||||||
args: ''
|
args: '--target x86_64-unknown-linux-gnu --config ./src-tauri/tauri.linux-x86_64.conf.json'
|
||||||
arch: ''
|
- platform: 'ubuntu-22.04-arm'
|
||||||
|
args: '--target aarch64-unknown-linux-gnu --config ./src-tauri/tauri.linux-aarch64.conf.json'
|
||||||
- platform: 'windows-latest'
|
- platform: 'windows-latest'
|
||||||
args: ''
|
args: ''
|
||||||
arch: ''
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: 🚚 Checkout repository
|
- name: 🚚 Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
|
||||||
lfs: true
|
- name: 🔐 Configure Git LFS
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
git config --global credential.helper store
|
||||||
|
echo "https://${{ secrets.LFS_USERNAME }}:${{ secrets.LFS_PASSWORD }}@lfs.neosubhamoy.com" > ~/.git-credentials
|
||||||
|
|
||||||
|
- name: 📥 Pull LFS objects
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
git lfs install
|
||||||
|
git lfs pull
|
||||||
|
|
||||||
- name: 🛠️ Install dependencies
|
- name: 🛠️ Install dependencies
|
||||||
if: matrix.platform == 'ubuntu-22.04'
|
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
- name: 📦 Setup node
|
- name: 📦 Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version: 'lts/*'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -64,12 +72,12 @@ jobs:
|
|||||||
if [ -f CHANGELOG.md ]; then
|
if [ -f CHANGELOG.md ]; then
|
||||||
# Extract version number from tag
|
# Extract version number from tag
|
||||||
VERSION_NUM=$(echo "${{ github.ref_name }}" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$/\1/')
|
VERSION_NUM=$(echo "${{ github.ref_name }}" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$/\1/')
|
||||||
|
|
||||||
# Read and replace placeholders
|
# Read and replace placeholders
|
||||||
CONTENT=$(cat CHANGELOG.md)
|
CONTENT=$(cat CHANGELOG.md)
|
||||||
CONTENT=${CONTENT//<release_tag>/${{ github.ref_name }}}
|
CONTENT=${CONTENT//<release_tag>/${{ github.ref_name }}}
|
||||||
CONTENT=${CONTENT//<version>/$VERSION_NUM}
|
CONTENT=${CONTENT//<version>/$VERSION_NUM}
|
||||||
|
|
||||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||||
echo "$CONTENT" >> $GITHUB_OUTPUT
|
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
@@ -85,12 +93,12 @@ jobs:
|
|||||||
if (Test-Path "CHANGELOG.md") {
|
if (Test-Path "CHANGELOG.md") {
|
||||||
# Extract version number from tag
|
# Extract version number from tag
|
||||||
$version_num = "${{ github.ref_name }}" -replace '^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$','$1'
|
$version_num = "${{ github.ref_name }}" -replace '^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$','$1'
|
||||||
|
|
||||||
# Read and replace placeholders
|
# Read and replace placeholders
|
||||||
$content = Get-Content -Path CHANGELOG.md -Raw
|
$content = Get-Content -Path CHANGELOG.md -Raw
|
||||||
$content = $content -replace '<release_tag>', "${{ github.ref_name }}"
|
$content = $content -replace '<release_tag>', "${{ github.ref_name }}"
|
||||||
$content = $content -replace '<version>', "$version_num"
|
$content = $content -replace '<version>', "$version_num"
|
||||||
|
|
||||||
"content<<EOF" >> $env:GITHUB_OUTPUT
|
"content<<EOF" >> $env:GITHUB_OUTPUT
|
||||||
$content >> $env:GITHUB_OUTPUT
|
$content >> $env:GITHUB_OUTPUT
|
||||||
"EOF" >> $env:GITHUB_OUTPUT
|
"EOF" >> $env:GITHUB_OUTPUT
|
||||||
@@ -102,7 +110,6 @@ jobs:
|
|||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TARGET_ARCH: ${{ matrix.arch }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
with:
|
with:
|
||||||
@@ -113,4 +120,4 @@ jobs:
|
|||||||
prerelease: false
|
prerelease: false
|
||||||
includeUpdaterJson: true
|
includeUpdaterJson: true
|
||||||
updaterJsonPreferNsis: true
|
updaterJsonPreferNsis: true
|
||||||
args: ${{ matrix.args }}
|
args: ${{ matrix.args }}
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,6 +11,8 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
.github/workflows/.secrets
|
||||||
|
/target/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
2
.lfsconfig
Normal file
2
.lfsconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[lfs]
|
||||||
|
url = https://lfs.neosubhamoy.com
|
||||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,26 +1,45 @@
|
|||||||
### ✨ Changelog
|
### ✨ Changelog
|
||||||
|
|
||||||
- Migrated to React 19, TailwindCSS 4 and ShadcnUI 2.6
|
- Added Debug Mode (with log customization)
|
||||||
- Added new 'Extension' tab
|
- Added quick paste and clear buttons on downloader
|
||||||
- Fixed: Default download directory not updating
|
- Fixed browser integration not working on Windows MSI install
|
||||||
- Fixed: MacOS large dock icon (#1)
|
- Fixed the occasional freezing issue on macOS while downloading large files
|
||||||
|
- Now Linux (deb, rpm) packages supports in-built app-updater
|
||||||
- Other minor fixes and improvements
|
- Other minor fixes and improvements
|
||||||
|
|
||||||
### 📝 Notes
|
### 📝 Notes
|
||||||
|
|
||||||
> ⚠️ Linux Users: Make sure yt-dlp is not installed in your distro (otherwise you will get package installation conflict)
|
> [!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 (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)
|
> 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.10.25.232842 (nightly) | v7.1.1 | v7.1.1 | v1.37.0 | v2.5.4 |
|
||||||
|
|
||||||
|
> ‼️ 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
|
### ⬇️ Download Section
|
||||||
|
|
||||||
| Arch\OS | Windows (msi) ⬆️ | Windows (exe) ⬆️ | Linux (deb) | Linux (rpm) | MacOS (dmg) ⬆️ | MacOS (app) ⬆️ |
|
| Arch\OS | 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.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) |
|
| 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 | 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) |
|
| 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
|
> ⬆️ Now, all packages 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
|
||||||
|
|||||||
167
README.md
167
README.md
@@ -1,43 +1,133 @@
|
|||||||
|

|
||||||
|
|
||||||
# NeoDLP - (Neo Downloader Plus)
|
# NeoDLP - (Neo Downloader Plus)
|
||||||
|
|
||||||
Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration
|
Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration
|
||||||
|
|
||||||
[](https://github.com/neosubhamoy/neodlp)
|
[](https://github.com/neosubhamoy/neodlp)
|
||||||
[](https://github.com/neosubhamoy/neodlp)
|
[](https://github.com/neosubhamoy/neodlp)
|
||||||
|
[](https://github.com/neosubhamoy/neodlp/releases)
|
||||||
[](https://github.com/neosubhamoy/neodlp)
|
[](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!**
|
> **🥰 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!**
|
||||||
|
|
||||||
### 💻 Supported Platforms
|
[](https://repology.org/project/neodlp/versions)
|
||||||
|
|
||||||
|
## ✨ 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.)
|
||||||
|
- Supports both Video and Playlist download
|
||||||
|
- 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
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
After installing the extension you can do the following directly from the browser:
|
||||||
|
|
||||||
|
- Quick Search (search current browser address with NeoDLP) (via pressing keyboard shortcut `ALT`+`SHIFT`+`Q`, You can also change this shortcut key combo from browser settings)
|
||||||
|
|
||||||
|
- Right Click Context Menu Action (Download with Neo Downloader Plus - Link, Selection, Media Source)
|
||||||
|
|
||||||
|
## 👀 Sneak Peek
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| Downloader | Completed Downloads | Ongoing Downloads | Settings |
|
||||||
|
| :---- | :---- | :---- | :---- |
|
||||||
|
|  |  |  |  |
|
||||||
|
|
||||||
|
## 💻 Supported Platforms
|
||||||
|
|
||||||
- Windows (10 / 11)
|
- Windows (10 / 11)
|
||||||
- Linux (Debian / Fedora / Arch Linux base)
|
- Linux (Debian / Fedora / RHEL / SUSE / Arch Linux base)
|
||||||
- MacOS (>10.3)
|
- MacOS (>11)
|
||||||
|
|
||||||
### 🌐 Supported Sites
|
## 🤝 External Dependencies
|
||||||
|
|
||||||
- All [Supported Sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) by [yt-dlp](https://github.com/yt-dlp/yt-dlp) **(2.5K+)**
|
- [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))
|
||||||
|
|
||||||
### 🧩 External Dependencies
|
## ℹ️ System Pre-Requirements
|
||||||
|
|
||||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) - The core CLI Tool used to download Video/Audio from the Web
|
- **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)
|
||||||
- [FFmpeg](https://www.ffmpeg.org) - Used for Video/Audio Post-processing
|
- **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
|
## ⬇️ 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
|
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 |
|
| Arch\OS | 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) |
|
| 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 |
|
| Platform (OS) | Distribution Channel | Installation Command / Instruction |
|
||||||
| :---- | :---- | :---- |
|
| :---- | :---- | :---- |
|
||||||
| Windows x86_64 | WinGet | `winget install neodlp` |
|
| Windows x86_64 / ARM64 | WinGet | `winget install neodlp` |
|
||||||
| MacOS Universal | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/neodlp_macos_installer.sh \| bash` |
|
| MacOS x86_64 / ARM64 | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/macos_installer.sh \| bash` |
|
||||||
| Linux x86_64 (Arch Linux) | AUR | `yay -S neodlp` |
|
| 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` |
|
||||||
|
|
||||||
### ⚡ Technologies Used
|
## 🧪 Package Testing Status
|
||||||
|
|
||||||
|
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**
|
||||||
|
|
||||||
|
## 🪜 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
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -45,33 +135,52 @@ Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Int
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
### 🛠️ 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:
|
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), [Git](https://git-scm.com/downloads) and [Git-LFS](https://git-lfs.com/) before proceeding.
|
||||||
* Install [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS / platform
|
* Install [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS / platform
|
||||||
1. Fork this repo in your github account.
|
1. Fork this repo in your github account.
|
||||||
2. Git clone the forked repo in your local machine.
|
2. Git clone the forked repo in your local machine. (**NOTE:** I've recently switched from GitHub LFS Server to my own self-hosted LFS Server! Cause GitHub LFS Storage is too expencive for me and NeoDLP requires a lots of LFS bandwidth. So, If you currently clone the repo it will clone the codebase but not the LFS Objects, If you want to clone the LFS Objects unfortunately you have to ask me for auth credentials - which will be only provided to you in certain conditions)
|
||||||
3. Install Node.js dependencies: `npm install`
|
3. Create a git branch (related to the feature you are working on) (Optional - Recommended)
|
||||||
4. Run development / build process
|
4. Install Node.js dependencies: `npm install`
|
||||||
> ⚠️ Make sure to run the build command once before running the dev command for the first time to avoid build time errors
|
5. 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
|
```code
|
||||||
# for windows and linux users
|
# for windows users
|
||||||
npm run tauri dev # for development
|
npm run tauri dev # for development
|
||||||
npm run tauri build # for production build
|
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)
|
# 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 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 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 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 Windows x86_64 and 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**
|
||||||
|
|||||||
2217
package-lock.json
generated
2217
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
116
package.json
116
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neodlp",
|
"name": "neodlp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.1",
|
"version": "0.3.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -10,78 +10,80 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-context-menu": "^2.2.15",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-hover-card": "^1.1.14",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-menubar": "^1.1.15",
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slider": "^1.3.5",
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.12",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-toast": "^1.2.14",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tanstack/react-query": "^5.80.7",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"@tanstack/react-query-devtools": "^5.80.7",
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2.9.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.7",
|
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||||
"@tauri-apps/plugin-process": "^2.2.1",
|
"@tauri-apps/plugin-os": "^2.3.2",
|
||||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-sql": "^2.2.0",
|
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-sql": "^2.3.1",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.515.0",
|
"lucide-react": "^0.548.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.2.0",
|
||||||
"react-day-picker": "^9.7.0",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.65.0",
|
||||||
"react-resizable-panels": "^3.0.2",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-router-dom": "^7.6.2",
|
"react-router-dom": "^7.9.4",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^3.3.0",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"ulid": "^3.0.1",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.25.64",
|
"zod": "^4.1.12",
|
||||||
"zustand": "^5.0.5"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.16",
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2.9.1",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.9.1",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@vitejs/plugin-react": "^4.5.2",
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
"postcss": "^8.5.5",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.16",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^7.1.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { execSync } from 'child_process';
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
// Define array of binary source directories
|
// Define array of binary source directories
|
||||||
const binSrcDirs = [
|
const binSrcDirs = [
|
||||||
path.join(__dirname, 'src-tauri', 'binaries'),
|
path.join(projectRoot, 'src-tauri', 'binaries'),
|
||||||
path.join(__dirname, 'src-tauri', 'resources', 'binaries'),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function makeFilesExecutable() {
|
function makeFilesExecutable() {
|
||||||
@@ -35,7 +35,7 @@ function makeFilesExecutable() {
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Successfully made ${count} files executable in ${binSrc}`);
|
console.log(`Successfully made ${count} files executable in ${binSrc}`);
|
||||||
totalCount += count;
|
totalCount += count;
|
||||||
successDirs++;
|
successDirs++;
|
||||||
@@ -47,5 +47,5 @@ function makeFilesExecutable() {
|
|||||||
console.log(`\nSummary: Made ${totalCount} files executable across ${successDirs} directories`);
|
console.log(`\nSummary: Made ${totalCount} files executable across ${successDirs} directories`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`RUNNING: 🛠️ Build Script makeFilesExecutable.js`);
|
console.log(`RUNNING: 🛠️ Build Script --> chmod.js`);
|
||||||
makeFilesExecutable();
|
makeFilesExecutable();
|
||||||
@@ -5,8 +5,9 @@ import { fileURLToPath } from 'url';
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
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
|
// Get the platform triple from command line arguments
|
||||||
const platformTriple = process.argv[2];
|
const platformTriple = process.argv[2];
|
||||||
@@ -17,7 +18,7 @@ if (!platformTriple) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define the binaries directory
|
// 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
|
// Construct the binary filename based on platform triple
|
||||||
let binaryName = `yt-dlp-${platformTriple}`;
|
let binaryName = `yt-dlp-${platformTriple}`;
|
||||||
@@ -50,7 +51,7 @@ execFile(binaryPath, ['--update-to', 'nightly'], (error, stdout, stderr) => {
|
|||||||
if (stderr) console.error(stderr);
|
if (stderr) console.error(stderr);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Update successful for ${platformTriple}:`);
|
console.log(`Update successful for ${platformTriple}:`);
|
||||||
console.log(stdout);
|
console.log(stdout);
|
||||||
});
|
});
|
||||||
2308
src-tauri/Cargo.lock
generated
2308
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "neodlp"
|
name = "neodlp"
|
||||||
version = "0.1.1"
|
version = "0.3.2"
|
||||||
description = "NeoDLP"
|
description = "NeoDLP"
|
||||||
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
|
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -27,8 +27,9 @@ tokio = { version = "1", features = ["full"] }
|
|||||||
tokio-tungstenite = "*"
|
tokio-tungstenite = "*"
|
||||||
sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] }
|
sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
directories = "5.0"
|
directories = "6.0"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
@@ -36,6 +37,7 @@ tauri-plugin-os = "2"
|
|||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
|
tauri-plugin-clipboard-manager = "2"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
|
|||||||
3
src-tauri/binaries/aria2c-aarch64-unknown-linux-gnu
Normal file
3
src-tauri/binaries/aria2c-aarch64-unknown-linux-gnu
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9397aac0de54c8c15b8166486eb80bfe27937bd6d6b6af4bb8383b155213bec1
|
||||||
|
size 6100888
|
||||||
3
src-tauri/binaries/aria2c-x86_64-pc-windows-msvc.exe
Normal file
3
src-tauri/binaries/aria2c-x86_64-pc-windows-msvc.exe
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cca868da48a85c13a56ccac4dfa8c098f7ed799786a9eaf88248221dbb785bb9
|
||||||
|
size 8089088
|
||||||
3
src-tauri/binaries/aria2c-x86_64-unknown-linux-gnu
Normal file
3
src-tauri/binaries/aria2c-x86_64-unknown-linux-gnu
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:36f66dab69edcc44255d0dba90c93f5aa4a304ec60c7136d8c279dfc89c23e1d
|
||||||
|
size 9666624
|
||||||
3
src-tauri/binaries/deno-aarch64-apple-darwin
Normal file
3
src-tauri/binaries/deno-aarch64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b2243469ad3e5d874a2ccf87d3375ea6566c65b9aeae7154de7ad4dd403ef23d
|
||||||
|
size 91664944
|
||||||
3
src-tauri/binaries/deno-aarch64-unknown-linux-gnu
Normal file
3
src-tauri/binaries/deno-aarch64-unknown-linux-gnu
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f13fc741f238849e8c2d48587ae4eced59abec6864b05b618feb5dc28168baff
|
||||||
|
size 103329904
|
||||||
3
src-tauri/binaries/deno-x86_64-apple-darwin
Normal file
3
src-tauri/binaries/deno-x86_64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:30c6df2176d096fafbdc0f049a68d4a4466360fd8f8daf698d3fc406b0f7a5c7
|
||||||
|
size 102795504
|
||||||
3
src-tauri/binaries/deno-x86_64-pc-windows-msvc.exe
Normal file
3
src-tauri/binaries/deno-x86_64-pc-windows-msvc.exe
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9037f4f141020246aac5f65336cda8127808d644a391df2502f76ef7ea3bdefb
|
||||||
|
size 117761496
|
||||||
3
src-tauri/binaries/deno-x86_64-unknown-linux-gnu
Normal file
3
src-tauri/binaries/deno-x86_64-unknown-linux-gnu
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d96ceee08834553afb6d6cc6bc76cc3120ce765fe309ce1813b0dd1428c0bce9
|
||||||
|
size 113570336
|
||||||
3
src-tauri/binaries/ffmpeg-aarch64-apple-darwin
Normal file
3
src-tauri/binaries/ffmpeg-aarch64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7717da6f1d21ec928aba5421f3ca83eddba63f6602e80a14901a2935982bf2f0
|
||||||
|
size 80129848
|
||||||
3
src-tauri/binaries/ffmpeg-aarch64-unknown-linux-gnu
Normal file
3
src-tauri/binaries/ffmpeg-aarch64-unknown-linux-gnu
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8f0364b1e3db45af96ca1149bc76b6593713446dff28458681302861214a2ca5
|
||||||
|
size 152316736
|
||||||
3
src-tauri/binaries/ffmpeg-x86_64-apple-darwin
Normal file
3
src-tauri/binaries/ffmpeg-x86_64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7717da6f1d21ec928aba5421f3ca83eddba63f6602e80a14901a2935982bf2f0
|
||||||
|
size 80129848
|
||||||
3
src-tauri/binaries/ffmpeg-x86_64-pc-windows-msvc.exe
Normal file
3
src-tauri/binaries/ffmpeg-x86_64-pc-windows-msvc.exe
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1df43d5ee4c7ef9379f29fdddd9f7c538f6362de478ed4883ac566ad0dd65166
|
||||||
|
size 190388224
|
||||||
3
src-tauri/binaries/ffmpeg-x86_64-unknown-linux-gnu
Normal file
3
src-tauri/binaries/ffmpeg-x86_64-unknown-linux-gnu
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3e3b84c425dd3c9f3b88712df0a3cf3ea844fb5968f15ed05eae63a0c2068fc7
|
||||||
|
size 191335144
|
||||||
3
src-tauri/binaries/ffprobe-aarch64-apple-darwin
Normal file
3
src-tauri/binaries/ffprobe-aarch64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:90cae105568f54d029fbaacf25a4879f44b25aaa32fa2ba369696dfeac00d6c9
|
||||||
|
size 79947928
|
||||||
3
src-tauri/binaries/ffprobe-aarch64-unknown-linux-gnu
Normal file
3
src-tauri/binaries/ffprobe-aarch64-unknown-linux-gnu
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:265c8a8a5b386970facc84e54cb3bfb72579d6729ce0d9b9a64d1ae4864c1274
|
||||||
|
size 152125760
|
||||||
3
src-tauri/binaries/ffprobe-x86_64-apple-darwin
Normal file
3
src-tauri/binaries/ffprobe-x86_64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:90cae105568f54d029fbaacf25a4879f44b25aaa32fa2ba369696dfeac00d6c9
|
||||||
|
size 79947928
|
||||||
3
src-tauri/binaries/ffprobe-x86_64-pc-windows-msvc.exe
Normal file
3
src-tauri/binaries/ffprobe-x86_64-pc-windows-msvc.exe
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:19208c2236b754cd359f6397ea1202521d961bba586c2434a0c99d056281aa87
|
||||||
|
size 190191104
|
||||||
3
src-tauri/binaries/ffprobe-x86_64-unknown-linux-gnu
Normal file
3
src-tauri/binaries/ffprobe-x86_64-unknown-linux-gnu
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f8d7c94d5f600f8acd3555ebb64ce87a410dca9a84bb4ec586ba2c76cb594fdd
|
||||||
|
size 191123432
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fc5bd50ef656f1727d6f1c6c55688b21434e70cb5fb3e439701d1061ae094bf0
|
oid sha256:4b8c840bf89c4428ada0c79bbae63e300d889c15efaf09321d42f502689bc5ed
|
||||||
size 34391824
|
size 35764384
|
||||||
|
|||||||
3
src-tauri/binaries/yt-dlp-aarch64-unknown-linux-gnu
Normal file
3
src-tauri/binaries/yt-dlp-aarch64-unknown-linux-gnu
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:59de22585cc971159674d797a2e094fb0c05a9238bbb8d3f7120f4867dfc699f
|
||||||
|
size 37285184
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fc5bd50ef656f1727d6f1c6c55688b21434e70cb5fb3e439701d1061ae094bf0
|
oid sha256:4b8c840bf89c4428ada0c79bbae63e300d889c15efaf09321d42f502689bc5ed
|
||||||
size 34391824
|
size 35764384
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c20996d097127884243f4780d929b3769d55418c0efa9bd7a98999f387b5fbed
|
oid sha256:2f7446c110e4d2ccad95338871cd82d60354a85b82cfe1bc776b67b0deb8db8a
|
||||||
size 18113133
|
size 18344563
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0aa0afe3d2b32c047b73083f3c8e56081d71bb33fe047357820d51d153d1d54f
|
oid sha256:d960d82b390bf63f79863d0a7b51df638ffc5d31e93aae42ba8bcf35927d94a0
|
||||||
size 34608400
|
size 37600736
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
"fs:allow-app-write-recursive",
|
"fs:allow-app-write-recursive",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"process:default",
|
"process:default",
|
||||||
|
"clipboard-manager:allow-read-text",
|
||||||
|
"clipboard-manager:allow-write-text",
|
||||||
{
|
{
|
||||||
"identifier": "opener:allow-open-path",
|
"identifier": "opener:allow-open-path",
|
||||||
"allow": [
|
"allow": [
|
||||||
@@ -39,4 +41,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,36 @@
|
|||||||
"args": true,
|
"args": true,
|
||||||
"sidecar": 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",
|
"name": "pkexec",
|
||||||
"cmd": "pkexec",
|
"cmd": "pkexec",
|
||||||
@@ -29,6 +59,36 @@
|
|||||||
"name": "binaries/yt-dlp",
|
"name": "binaries/yt-dlp",
|
||||||
"args": true,
|
"args": true,
|
||||||
"sidecar": 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 +98,4 @@
|
|||||||
"macOS",
|
"macOS",
|
||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
!macro NSIS_HOOK_POSTINSTALL
|
!macro NSIS_HOOK_POSTINSTALL
|
||||||
; Add Registry Keys for Chrome Native Messaging Host
|
; 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
|
; 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
|
; Add entry for automatic startup with Windows
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}" "$\"$INSTDIR\neodlp.exe$\" --hidden"
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}" "$\"$INSTDIR\neodlp.exe$\" --hidden"
|
||||||
!macroend
|
!macroend
|
||||||
@@ -12,4 +12,4 @@
|
|||||||
DeleteRegKey HKCU "Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp"
|
DeleteRegKey HKCU "Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp"
|
||||||
DeleteRegKey HKCU "Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp"
|
DeleteRegKey HKCU "Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp"
|
||||||
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}"
|
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}"
|
||||||
!macroend
|
!macroend
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<DirectoryRef Id="TARGETDIR">
|
<DirectoryRef Id="TARGETDIR">
|
||||||
<Component Id="NeoDlpRegEntriesFragment" Guid="*">
|
<Component Id="NeoDlpRegEntriesFragment" Guid="*">
|
||||||
<RegistryKey Root="HKLM" Key="Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp" Action="createAndRemoveOnUninstall">
|
<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>
|
||||||
<RegistryKey Root="HKLM" Key="Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp" Action="createAndRemoveOnUninstall">
|
<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>
|
||||||
<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Run">
|
<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Run">
|
||||||
<RegistryValue Name="NeoDLP" Type="string" Value=""[INSTALLDIR]neodlp.exe" --hidden" KeyPath="no" />
|
<RegistryValue Name="NeoDLP" Type="string" Value=""[INSTALLDIR]neodlp.exe" --hidden" KeyPath="no" />
|
||||||
@@ -15,4 +15,4 @@
|
|||||||
</Component>
|
</Component>
|
||||||
</DirectoryRef>
|
</DirectoryRef>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</Wix>
|
</Wix>
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ license = "MIT"
|
|||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-tungstenite = "*"
|
tokio-tungstenite = "*"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
directories = "5.0"
|
directories = "6.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<string>com.neosubhamoy.neodlp</string>
|
<string>com.neosubhamoy.neodlp</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>/Applications/neodlp.app/Contents/MacOS/neodlp</string>
|
<string>/Applications/NeoDLP.app/Contents/MacOS/neodlp</string>
|
||||||
<string>--hidden</string>
|
<string>--hidden</string>
|
||||||
</array>
|
</array>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
"description": "NeoDLP MsgHost",
|
"description": "NeoDLP MsgHost",
|
||||||
"path": "/usr/bin/neodlp-msghost",
|
"path": "/usr/bin/neodlp-msghost",
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/", "chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
|
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/"]
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
"description": "NeoDLP MsgHost",
|
"description": "NeoDLP MsgHost",
|
||||||
"path": "/Applications/NeoDLP.app/Contents/Resources/neodlp-msghost",
|
"path": "/Applications/NeoDLP.app/Contents/Resources/neodlp-msghost",
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/", "chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
|
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/"]
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
"description": "NeoDLP MsgHost",
|
"description": "NeoDLP MsgHost",
|
||||||
"path": "neodlp-msghost.exe",
|
"path": "neodlp-msghost.exe",
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/", "chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
|
"allowed_origins": ["chrome-extension://mehopeailfjmiloiiohgicphlcgpompf/"]
|
||||||
}
|
}
|
||||||
@@ -452,6 +452,7 @@ async fn pause_ongoing_downloads(
|
|||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub async fn run() {
|
pub async fn run() {
|
||||||
|
let _ = fix_path_env::fix();
|
||||||
let migrations = migrations::get_migrations();
|
let migrations = migrations::get_migrations();
|
||||||
let config = load_config();
|
let config = load_config();
|
||||||
let port = config.port;
|
let port = config.port;
|
||||||
@@ -485,6 +486,7 @@ pub async fn run() {
|
|||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.manage(ImageCache(StdMutex::new(HashMap::new())))
|
.manage(ImageCache(StdMutex::new(HashMap::new())))
|
||||||
.manage(websocket_state.clone())
|
.manage(websocket_state.clone())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
|
|||||||
@@ -68,5 +68,85 @@ pub fn get_migrations() -> Vec<Migration> {
|
|||||||
);
|
);
|
||||||
",
|
",
|
||||||
kind: MigrationKind::Up,
|
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",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "NeoDLP",
|
"productName": "NeoDLP",
|
||||||
"mainBinaryName": "neodlp",
|
"mainBinaryName": "neodlp",
|
||||||
"version": "0.1.1",
|
"version": "0.3.2",
|
||||||
"identifier": "com.neosubhamoy.neodlp",
|
"identifier": "com.neosubhamoy.neodlp",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
@@ -36,6 +36,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
"sql": {
|
||||||
|
"preload": ["sqlite:database.db"]
|
||||||
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDM0I4ODcyODdGOTM4MDIKUldRQ09QbUhjb2c3UENGY1lFUVdTVWhucmJ4QzdGeW9sU3VHVFlGNWY5anZab2s4SU1rMWFsekMK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDM0I4ODcyODdGOTM4MDIKUldRQ09QbUhjb2c3UENGY1lFUVdTVWhucmJ4QzdGeW9sU3VHVFlGNWY5anZab2s4SU1rMWFsekMK",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"identifier": "com.neosubhamoy.neodlp",
|
"identifier": "com.neosubhamoy.neodlp",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "cargo build --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run dev",
|
"beforeDevCommand": "cargo build --target=aarch64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && node scripts/chmod.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",
|
"beforeBuildCommand": "cargo build --release --target=aarch64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && node scripts/chmod.js && npm run build",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
@@ -36,32 +36,33 @@
|
|||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"externalBin": [
|
"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": {
|
"linux": {
|
||||||
"deb": {
|
"deb": {
|
||||||
|
"depends": ["ffmpeg"],
|
||||||
"files": {
|
"files": {
|
||||||
"/etc/opt/chrome/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
"/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",
|
"/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/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"
|
"/etc/xdg/autostart/neodlp-autostart.desktop": "./resources/autostart/linux/autostart.desktop"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rpm": {
|
"rpm": {
|
||||||
"epoch": 0,
|
"epoch": 0,
|
||||||
"release": "1",
|
"release": "1",
|
||||||
|
"depends": ["ffmpeg"],
|
||||||
"files": {
|
"files": {
|
||||||
"/etc/opt/chrome/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
|
"/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",
|
"/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/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"
|
"/etc/xdg/autostart/neodlp-autostart.desktop": "./resources/autostart/linux/autostart.desktop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
74
src-tauri/tauri.linux-x86_64.conf.json
Normal file
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 && node scripts/chmod.js && npm run dev",
|
||||||
|
"beforeBuildCommand": "cargo build --release --target=x86_64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && node scripts/chmod.js && 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",
|
"identifier": "com.neosubhamoy.neodlp",
|
||||||
"build": {
|
"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",
|
"beforeDevCommand": "cargo build --target=aarch64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && node scripts/chmod.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",
|
"beforeBuildCommand": "cargo build --release --target=aarch64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && node scripts/chmod.js && npm run build",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
@@ -36,11 +36,13 @@
|
|||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"externalBin": [
|
"externalBin": [
|
||||||
"binaries/yt-dlp"
|
"binaries/yt-dlp",
|
||||||
|
"binaries/ffmpeg",
|
||||||
|
"binaries/ffprobe",
|
||||||
|
"binaries/deno"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
"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/chrome.json": "neodlp-msghost.json",
|
||||||
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
||||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
||||||
@@ -49,4 +51,4 @@
|
|||||||
"providerShortName": "neosubhamoy"
|
"providerShortName": "neosubhamoy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"identifier": "com.neosubhamoy.neodlp",
|
"identifier": "com.neosubhamoy.neodlp",
|
||||||
"build": {
|
"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",
|
"beforeDevCommand": "cargo build --target=x86_64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && node scripts/chmod.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",
|
"beforeBuildCommand": "cargo build --release --target=x86_64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && node scripts/chmod.js && npm run build",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
@@ -36,11 +36,13 @@
|
|||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"externalBin": [
|
"externalBin": [
|
||||||
"binaries/yt-dlp"
|
"binaries/yt-dlp",
|
||||||
|
"binaries/ffmpeg",
|
||||||
|
"binaries/ffprobe",
|
||||||
|
"binaries/deno"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
"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/chrome.json": "neodlp-msghost.json",
|
||||||
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
||||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
||||||
@@ -49,4 +51,4 @@
|
|||||||
"providerShortName": "neosubhamoy"
|
"providerShortName": "neosubhamoy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"identifier": "com.neosubhamoy.neodlp",
|
"identifier": "com.neosubhamoy.neodlp",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "cargo build --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
|
"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",
|
"devUrl": "http://localhost:1420",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
@@ -36,13 +36,16 @@
|
|||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"externalBin": [
|
"externalBin": [
|
||||||
"binaries/yt-dlp"
|
"binaries/yt-dlp",
|
||||||
|
"binaries/ffmpeg",
|
||||||
|
"binaries/ffprobe",
|
||||||
|
"binaries/aria2c",
|
||||||
|
"binaries/deno"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"resources/binaries/ffmpeg-x86_64-pc-windows-msvc.exe": "binaries/ffmpeg-x86_64.exe",
|
|
||||||
"target/release/neodlp-msghost.exe": "neodlp-msghost.exe",
|
"target/release/neodlp-msghost.exe": "neodlp-msghost.exe",
|
||||||
"resources/msghost-manifest/windows/chrome.json": "neodlp-msghost.json",
|
"resources/msghost-manifest/windows/chrome.json": "chrome.json",
|
||||||
"resources/msghost-manifest/windows/firefox.json": "neodlp-msghost-moz.json"
|
"resources/msghost-manifest/windows/firefox.json": "firefox.json"
|
||||||
},
|
},
|
||||||
"windows": {
|
"windows": {
|
||||||
"wix": {
|
"wix": {
|
||||||
@@ -54,4 +57,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
567
src/App.tsx
567
src/App.tsx
@@ -1,14 +1,13 @@
|
|||||||
import { ThemeProvider } from "@/providers/themeProvider";
|
import { ThemeProvider } from "@/providers/themeProvider";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
|
||||||
import { AppContext } from "@/providers/appContextProvider";
|
import { AppContext } from "@/providers/appContextProvider";
|
||||||
import { DownloadState } from "@/types/download";
|
import { DownloadState } from "@/types/download";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
||||||
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
||||||
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
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 { Command } from "@tauri-apps/plugin-shell";
|
||||||
import { RawVideoInfo } from "@/types/video";
|
import { RawVideoInfo } from "@/types/video";
|
||||||
import { useDeleteDownloadState, useSaveDownloadState, useSavePlaylistInfo, useSaveVideoInfo, useUpdateDownloadFilePath, useUpdateDownloadStatus } from "@/services/mutations";
|
import { useDeleteDownloadState, useSaveDownloadState, useSavePlaylistInfo, useSaveVideoInfo, useUpdateDownloadFilePath, useUpdateDownloadStatus } from "@/services/mutations";
|
||||||
@@ -25,6 +24,11 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import { useMacOsRegisterer } from "@/helpers/use-macos-registerer";
|
import { useMacOsRegisterer } from "@/helpers/use-macos-registerer";
|
||||||
import useAppUpdater from "@/helpers/use-app-updater";
|
import useAppUpdater from "@/helpers/use-app-updater";
|
||||||
|
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";
|
||||||
|
|
||||||
export default function App({ children }: { children: React.ReactNode }) {
|
export default function App({ children }: { children: React.ReactNode }) {
|
||||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
||||||
@@ -35,12 +39,13 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const globalDownloadStates = useDownloadStatesStore((state) => state.downloadStates);
|
const globalDownloadStates = useDownloadStatesStore((state) => state.downloadStates);
|
||||||
const setDownloadStates = useDownloadStatesStore((state) => state.setDownloadStates);
|
const setDownloadStates = useDownloadStatesStore((state) => state.setDownloadStates);
|
||||||
const setPath = useBasePathsStore((state) => state.setPath);
|
const setPath = useBasePathsStore((state) => state.setPath);
|
||||||
|
|
||||||
const ffmpegPath = useBasePathsStore((state) => state.ffmpegPath);
|
const ffmpegPath = useBasePathsStore((state) => state.ffmpegPath);
|
||||||
const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath);
|
const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath);
|
||||||
const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath);
|
const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath);
|
||||||
|
|
||||||
// const isUsingDefaultSettings = useSettingsPageStatesStore((state) => state.isUsingDefaultSettings);
|
const setSearchPid = useCurrentVideoMetadataStore((state) => state.setSearchPid);
|
||||||
|
|
||||||
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
|
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
|
||||||
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
|
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
|
||||||
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
||||||
@@ -53,20 +58,59 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const YTDLP_UPDATE_CHANNEL = useSettingsPageStatesStore(state => state.settings.ytdlp_update_channel);
|
const YTDLP_UPDATE_CHANNEL = useSettingsPageStatesStore(state => state.settings.ytdlp_update_channel);
|
||||||
const APP_THEME = useSettingsPageStatesStore(state => state.settings.theme);
|
const APP_THEME = useSettingsPageStatesStore(state => state.settings.theme);
|
||||||
const MAX_PARALLEL_DOWNLOADS = useSettingsPageStatesStore(state => state.settings.max_parallel_downloads);
|
const MAX_PARALLEL_DOWNLOADS = useSettingsPageStatesStore(state => state.settings.max_parallel_downloads);
|
||||||
|
const MAX_RETRIES = useSettingsPageStatesStore(state => state.settings.max_retries);
|
||||||
const DOWNLOAD_DIR = useSettingsPageStatesStore(state => state.settings.download_dir);
|
const DOWNLOAD_DIR = useSettingsPageStatesStore(state => state.settings.download_dir);
|
||||||
const PREFER_VIDEO_OVER_PLAYLIST = useSettingsPageStatesStore(state => state.settings.prefer_video_over_playlist);
|
const PREFER_VIDEO_OVER_PLAYLIST = useSettingsPageStatesStore(state => state.settings.prefer_video_over_playlist);
|
||||||
|
const STRICT_DOWNLOADABILITY_CHECK = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check);
|
||||||
const USE_PROXY = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
const USE_PROXY = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
||||||
const PROXY_URL = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
const PROXY_URL = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
||||||
|
const USE_RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.use_rate_limit);
|
||||||
|
const RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.rate_limit);
|
||||||
|
const VIDEO_FORMAT = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||||
|
const AUDIO_FORMAT = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||||
|
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_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 isErrored = useDownloaderPageStatesStore((state) => state.isErrored);
|
||||||
|
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
|
||||||
|
const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId);
|
||||||
|
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored);
|
||||||
|
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
|
||||||
|
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId);
|
||||||
|
|
||||||
const appWindow = getCurrentWebviewWindow()
|
const appWindow = getCurrentWebviewWindow()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const LOG = useLogger();
|
||||||
|
const currentPlatform = platform();
|
||||||
const { updateYtDlp } = useYtDlpUpdater();
|
const { updateYtDlp } = useYtDlpUpdater();
|
||||||
const { registerToMac } = useMacOsRegisterer();
|
const { registerToMac } = useMacOsRegisterer();
|
||||||
const { checkForAppUpdate } = useAppUpdater();
|
const { checkForAppUpdate } = useAppUpdater();
|
||||||
const setKvPairsKey = useKvPairsStatesStore((state) => state.setKvPairsKey);
|
const setKvPairsKey = useKvPairsStatesStore((state) => state.setKvPairsKey);
|
||||||
const ytDlpUpdateLastCheck = useKvPairsStatesStore(state => state.kvPairs.ytdlp_update_last_check);
|
const ytDlpUpdateLastCheck = useKvPairsStatesStore(state => state.kvPairs.ytdlp_update_last_check);
|
||||||
const macOsRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.macos_registered_version);
|
const macOsRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.macos_registered_version);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const downloadStateSaver = useSaveDownloadState();
|
const downloadStateSaver = useSaveDownloadState();
|
||||||
const downloadStatusUpdater = useUpdateDownloadStatus();
|
const downloadStatusUpdater = useUpdateDownloadStatus();
|
||||||
@@ -74,7 +118,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const videoInfoSaver = useSaveVideoInfo();
|
const videoInfoSaver = useSaveVideoInfo();
|
||||||
const downloadStateDeleter = useDeleteDownloadState();
|
const downloadStateDeleter = useDeleteDownloadState();
|
||||||
const playlistInfoSaver = useSavePlaylistInfo();
|
const playlistInfoSaver = useSavePlaylistInfo();
|
||||||
|
|
||||||
const ongoingDownloads = globalDownloadStates.filter(state => state.download_status === 'downloading' || state.download_status === 'starting');
|
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 queuedDownloads = globalDownloadStates.filter(state => state.download_status === 'queued').sort((a, b) => a.queue_index! - b.queue_index!);
|
||||||
|
|
||||||
@@ -82,14 +126,56 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const lastProcessedDownloadIdRef = useRef<string | null>(null);
|
const lastProcessedDownloadIdRef = useRef<string | null>(null);
|
||||||
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
||||||
const isRegisteredToMacOsRef = 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 {
|
try {
|
||||||
const args = [url, '--dump-single-json'];
|
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 (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 (USE_PROXY && PROXY_URL) args.push('--proxy', PROXY_URL);
|
if (STRICT_DOWNLOADABILITY_CHECK && !formatId) args.push('--check-all-formats');
|
||||||
|
if (STRICT_DOWNLOADABILITY_CHECK && formatId) args.push('--check-formats');
|
||||||
|
|
||||||
|
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);
|
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||||
|
|
||||||
let jsonOutput = '';
|
let jsonOutput = '';
|
||||||
@@ -99,72 +185,136 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
jsonOutput += line;
|
jsonOutput += line;
|
||||||
});
|
});
|
||||||
|
|
||||||
command.on('close', async () => {
|
command.on('close', async (data) => {
|
||||||
try {
|
if (data.code !== 0) {
|
||||||
const data: RawVideoInfo = JSON.parse(jsonOutput);
|
console.error(`yt-dlp failed to fetch metadata with code ${data.code}`);
|
||||||
resolve(data);
|
LOG.error('NEODLP', `yt-dlp exited with code ${data.code} while fetching metadata for URL: ${url} (ignore if you manually cancelled)`);
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(`Failed to parse JSON: ${e}`);
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
command.on('error', error => {
|
command.on('error', error => {
|
||||||
console.error(`Error fetching metadata: ${error}`);
|
console.error(`Error fetching metadata: ${error}`);
|
||||||
|
LOG.error('NEODLP', `Error occurred while fetching metadata for URL: ${url} : ${error}`);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
command.spawn().catch(e => {
|
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}`);
|
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);
|
resolve(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to fetch metadata: ${e}`);
|
console.error(`Failed to fetch metadata: ${e}`);
|
||||||
|
LOG.error('NEODLP', `Failed to fetch metadata for URL: ${url} : ${e}`);
|
||||||
return null;
|
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) => {
|
||||||
console.log('Starting download:', { url, selectedFormat, selectedSubtitles, resumeState, playlistItems });
|
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, downloadConfig, selectedSubtitles, resumeState, playlistItems });
|
||||||
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
||||||
console.error('FFmpeg or download paths not found');
|
console.error('FFmpeg or download paths not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPlaylist = (playlistItems && typeof playlistItems === 'string') || (resumeState?.playlist_id && resumeState?.playlist_index) ? true : false;
|
const isPlaylist = (playlistItems && typeof playlistItems === 'string') || (resumeState?.playlist_id && resumeState?.playlist_index) ? true : false;
|
||||||
const playlistIndex = isPlaylist ? (resumeState?.playlist_index?.toString() || playlistItems) : null;
|
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) {
|
if (!videoMetadata) {
|
||||||
console.error('Failed to fetch video metadata');
|
console.error('Failed to fetch video metadata');
|
||||||
|
toast.error("Download Failed", {
|
||||||
|
description: "yt-dlp failed to fetch video metadata. Please try again later.",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Video Metadata:', videoMetadata);
|
console.log('Video Metadata:', videoMetadata);
|
||||||
videoMetadata = isPlaylist ? videoMetadata.entries[0] : videoMetadata;
|
videoMetadata = isPlaylist ? videoMetadata.entries[0] : videoMetadata;
|
||||||
|
|
||||||
|
const fileType = determineFileType(videoMetadata.vcodec, videoMetadata.acodec);
|
||||||
|
|
||||||
|
if (fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) {
|
||||||
|
if (VIDEO_FORMAT !== 'auto' && (fileType === 'video+audio' || fileType === 'video')) videoMetadata.ext = VIDEO_FORMAT;
|
||||||
|
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 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 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 downloadId = resumeState?.download_id || ulid() /*generateDownloadId(videoMetadata.id, videoMetadata.webpage_url_domain)*/;
|
||||||
const tempDownloadPathForYtdlp = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.%(ext)s`);
|
// const tempDownloadPathForYtdlp = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.%(ext)s`);
|
||||||
const tempDownloadPath = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.${videoMetadata.ext}`);
|
// 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 = 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;
|
let processPid: number | null = null;
|
||||||
const args = [
|
const args = [
|
||||||
url,
|
url,
|
||||||
'--newline',
|
'--newline',
|
||||||
'--progress-template',
|
'--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',
|
'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',
|
'--output',
|
||||||
tempDownloadPathForYtdlp,
|
`${FILENAME_TEMPLATE}[${downloadId}].%(ext)s`,
|
||||||
'--ffmpeg-location',
|
'--windows-filenames',
|
||||||
ffmpegPath,
|
'--restrict-filenames',
|
||||||
'-f',
|
'--exec',
|
||||||
|
'after_move:echo Finalpath: {}',
|
||||||
|
'--format',
|
||||||
selectedFormat,
|
selectedFormat,
|
||||||
'--no-mtime',
|
'--no-mtime',
|
||||||
|
'--retries',
|
||||||
|
MAX_RETRIES.toString(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (currentPlatform === 'macos') {
|
||||||
|
args.push('--ffmpeg-location', '/Applications/NeoDLP.app/Contents/MacOS');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DEBUG_MODE || (DEBUG_MODE && !LOG_WARNING)) {
|
||||||
|
args.push('--no-warnings');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG_MODE && LOG_VERBOSE) {
|
||||||
|
args.push('--verbose');
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedSubtitles) {
|
if (selectedSubtitles) {
|
||||||
args.push('--embed-subs', '--sub-lang', selectedSubtitles);
|
args.push('--embed-subs', '--sub-lang', selectedSubtitles);
|
||||||
}
|
}
|
||||||
@@ -173,56 +323,157 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
args.push('--playlist-items', playlistIndex);
|
args.push('--playlist-items', playlistIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resumeState) {
|
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 (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let embedMetadata = 0;
|
||||||
|
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfig.embed_metadata || resumeState?.embed_metadata || EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) {
|
||||||
|
const shouldEmbedForVideo = (fileType === 'video+audio' || fileType === 'video') && (downloadConfig.embed_metadata || resumeState?.embed_metadata || (EMBED_VIDEO_METADATA && downloadConfig.embed_metadata === null));
|
||||||
|
const shouldEmbedForAudio = fileType === 'audio' && (downloadConfig.embed_metadata || resumeState?.embed_metadata || (EMBED_AUDIO_METADATA && downloadConfig.embed_metadata === null));
|
||||||
|
const shouldEmbedForUnknown = fileType === 'unknown' && (downloadConfig.embed_metadata || resumeState?.embed_metadata);
|
||||||
|
|
||||||
|
if (shouldEmbedForUnknown || shouldEmbedForVideo || shouldEmbedForAudio) {
|
||||||
|
embedMetadata = 1;
|
||||||
|
args.push('--embed-metadata');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let embedThumbnail = 0;
|
||||||
|
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail || (fileType === 'audio' && EMBED_AUDIO_THUMBNAIL && downloadConfig.embed_thumbnail === null))) {
|
||||||
|
embedThumbnail = 1;
|
||||||
|
args.push('--embed-thumbnail');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_PROXY && PROXY_URL) {
|
||||||
|
args.push('--proxy', PROXY_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) && (USE_SPONSORBLOCK || (resumeState?.sponsorblock_remove || resumeState?.sponsorblock_mark))) {
|
||||||
|
if (SPONSORBLOCK_MODE === 'remove' || resumeState?.sponsorblock_remove) {
|
||||||
|
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) {
|
||||||
|
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');
|
args.push('--continue');
|
||||||
} else {
|
} else {
|
||||||
args.push('--no-continue');
|
args.push('--no-continue');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (USE_PROXY && PROXY_URL) {
|
|
||||||
args.push('--proxy', PROXY_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Starting download with args:', args);
|
console.log('Starting download with args:', args);
|
||||||
const command = Command.sidecar('binaries/yt-dlp', args);
|
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||||
|
|
||||||
command.on('close', async data => {
|
command.on('close', async (data) => {
|
||||||
if (data.code !== 0) {
|
if (data.code !== 0) {
|
||||||
console.error(`Download failed with code ${data.code}`);
|
console.error(`Download failed with code ${data.code}`);
|
||||||
} else {
|
LOG.error(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code} (ignore if you manually paused or cancelled the download)`);
|
||||||
downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, {
|
if (!isErrorExpected) {
|
||||||
onSuccess: (data) => {
|
setIsErrored(true);
|
||||||
console.log("Download status updated successfully:", data);
|
setErroredDownloadId(downloadId);
|
||||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error("Failed to update download status:", error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (await fs.exists(tempDownloadPath)) {
|
|
||||||
downloadFilePath = await generateSafeFilePath(downloadFilePath);
|
|
||||||
await fs.rename(tempDownloadPath, downloadFilePath);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
downloadFilePathUpdater.mutate({ download_id: downloadId, filepath: downloadFilePath }, {
|
LOG.info(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code}`);
|
||||||
onSuccess: (data) => {
|
|
||||||
console.log("Download filepath updated successfully:", data);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error("Failed to update download filepath:", error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
command.on('error', error => {
|
command.on('error', error => {
|
||||||
console.error(`Error: ${error}`);
|
console.error(`Error: ${error}`);
|
||||||
|
LOG.error(`YT-DLP Download ${downloadId}`, `Error occurred: ${error}`);
|
||||||
|
setIsErrored(true);
|
||||||
|
setErroredDownloadId(downloadId);
|
||||||
});
|
});
|
||||||
|
|
||||||
command.stdout.on('data', line => {
|
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 currentProgress = parseProgressLine(line);
|
||||||
const state: DownloadState = {
|
const state: DownloadState = {
|
||||||
download_id: downloadId,
|
download_id: downloadId,
|
||||||
@@ -262,7 +513,15 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
eta: currentProgress.eta || null,
|
eta: currentProgress.eta || null,
|
||||||
filepath: downloadFilePath,
|
filepath: downloadFilePath,
|
||||||
filetype: determineFileType(videoMetadata.vcodec, videoMetadata.acodec) || null,
|
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, {
|
downloadStateSaver.mutate(state, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -274,7 +533,38 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} 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 2s delay...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -347,7 +637,15 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
eta: resumeState?.eta || null,
|
eta: resumeState?.eta || null,
|
||||||
filepath: downloadFilePath,
|
filepath: downloadFilePath,
|
||||||
filetype: resumeState?.filetype || null,
|
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, {
|
downloadStateSaver.mutate(state, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -365,30 +663,57 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) {
|
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();
|
const child = await command.spawn();
|
||||||
processPid = child.pid;
|
processPid = child.pid;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
console.log("Download is queued, not starting immediately.");
|
console.log("Download is queued, not starting immediately.");
|
||||||
|
LOG.info('NEODLP', `Download queued with id: ${downloadId}`);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to start download: ${e}`);
|
console.error(`Failed to start download: ${e}`);
|
||||||
|
LOG.error('NEODLP', `Failed to start download for URL: ${url} with error: ${e}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pauseDownload = async (downloadState: DownloadState) => {
|
const pauseDownload = async (downloadState: DownloadState) => {
|
||||||
try {
|
try {
|
||||||
console.log("Killing process with PID:", downloadState.process_id);
|
LOG.info('NEODLP', `Pausing yt-dlp download with id: ${downloadState.download_id} (as per user request)`);
|
||||||
await invoke('kill_all_process', { pid: downloadState.process_id });
|
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);
|
||||||
|
await invoke('kill_all_process', { pid: downloadState.process_id });
|
||||||
|
}
|
||||||
downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, {
|
downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
console.log("Download status updated successfully:", data);
|
console.log("Download status updated successfully:", data);
|
||||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
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
|
// Reset the processing flag to ensure queue can be processed
|
||||||
isProcessingQueueRef.current = false;
|
isProcessingQueueRef.current = false;
|
||||||
|
|
||||||
// Process the queue after a short delay to ensure state is updated
|
// Process the queue after a short delay to ensure state is updated
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
processQueuedDownloads();
|
processQueuedDownloads();
|
||||||
@@ -401,29 +726,40 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to pause download: ${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;
|
isProcessingQueueRef.current = false;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resumeDownload = async (downloadState: DownloadState) => {
|
const resumeDownload = async (downloadState: DownloadState) => {
|
||||||
try {
|
try {
|
||||||
|
LOG.info('NEODLP', `Resuming yt-dlp download with id: ${downloadState.download_id} (as per user request)`);
|
||||||
await startDownload(
|
await startDownload(
|
||||||
downloadState.playlist_id && downloadState.playlist_index ? downloadState.playlist_url : downloadState.url,
|
downloadState.playlist_id && downloadState.playlist_index ? downloadState.playlist_url : downloadState.url,
|
||||||
downloadState.format_id,
|
downloadState.format_id,
|
||||||
|
downloadState.queue_config ? JSON.parse(downloadState.queue_config) : {
|
||||||
|
output_format: null,
|
||||||
|
embed_metadata: null,
|
||||||
|
embed_thumbnail: null,
|
||||||
|
custom_command: null
|
||||||
|
},
|
||||||
downloadState.subtitle_id,
|
downloadState.subtitle_id,
|
||||||
downloadState
|
downloadState
|
||||||
);
|
);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to resume download: ${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;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelDownload = async (downloadState: DownloadState) => {
|
const cancelDownload = async (downloadState: DownloadState) => {
|
||||||
try {
|
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)) {
|
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);
|
console.log("Killing process with PID:", downloadState.process_id);
|
||||||
await invoke('kill_all_process', { pid: downloadState.process_id });
|
await invoke('kill_all_process', { pid: downloadState.process_id });
|
||||||
}
|
}
|
||||||
@@ -433,7 +769,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||||
// Reset processing flag and trigger queue processing
|
// Reset processing flag and trigger queue processing
|
||||||
isProcessingQueueRef.current = false;
|
isProcessingQueueRef.current = false;
|
||||||
|
|
||||||
// Process the queue after a short delay
|
// Process the queue after a short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
processQueuedDownloads();
|
processQueuedDownloads();
|
||||||
@@ -447,6 +783,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to cancel download: ${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;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,35 +803,36 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
isProcessingQueueRef.current = true;
|
isProcessingQueueRef.current = true;
|
||||||
console.log("Processing download queue...");
|
console.log("Processing download queue...");
|
||||||
|
|
||||||
// Get the first download in queue
|
// Get the first download in queue
|
||||||
const downloadToStart = queuedDownloads[0];
|
const downloadToStart = queuedDownloads[0];
|
||||||
|
|
||||||
// Skip if we just processed this download to prevent loops
|
// Skip if we just processed this download to prevent loops
|
||||||
if (lastProcessedDownloadIdRef.current === downloadToStart.download_id) {
|
if (lastProcessedDownloadIdRef.current === downloadToStart.download_id) {
|
||||||
console.log("Skipping recently processed download:", downloadToStart.download_id);
|
console.log("Skipping recently processed download:", downloadToStart.download_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check current state from global state
|
// Double-check current state from global state
|
||||||
const currentState = globalDownloadStates.find(
|
const currentState = globalDownloadStates.find(
|
||||||
state => state.download_id === downloadToStart.download_id
|
state => state.download_id === downloadToStart.download_id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentState || currentState.download_status !== 'queued') {
|
if (!currentState || currentState.download_status !== 'queued') {
|
||||||
console.log("Download no longer in queued state:", downloadToStart.download_id);
|
console.log("Download no longer in queued state:", downloadToStart.download_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Starting queued download:", downloadToStart.download_id);
|
console.log("Starting queued download:", downloadToStart.download_id);
|
||||||
|
LOG.info('NEODLP', `Starting queued download with id: ${downloadToStart.download_id}`);
|
||||||
lastProcessedDownloadIdRef.current = downloadToStart.download_id;
|
lastProcessedDownloadIdRef.current = downloadToStart.download_id;
|
||||||
|
|
||||||
// Update status to 'starting' first
|
// Update status to 'starting' first
|
||||||
await downloadStatusUpdater.mutateAsync({
|
await downloadStatusUpdater.mutateAsync({
|
||||||
download_id: downloadToStart.download_id,
|
download_id: downloadToStart.download_id,
|
||||||
download_status: 'starting'
|
download_status: 'starting'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch latest state after status update
|
// Fetch latest state after status update
|
||||||
await queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
await queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||||
|
|
||||||
@@ -502,12 +840,19 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
await startDownload(
|
await startDownload(
|
||||||
downloadToStart.url,
|
downloadToStart.url,
|
||||||
downloadToStart.format_id,
|
downloadToStart.format_id,
|
||||||
|
downloadToStart.queue_config ? JSON.parse(downloadToStart.queue_config) : {
|
||||||
|
output_format: null,
|
||||||
|
embed_metadata: null,
|
||||||
|
embed_thumbnail: null,
|
||||||
|
custom_command: null
|
||||||
|
},
|
||||||
downloadToStart.subtitle_id,
|
downloadToStart.subtitle_id,
|
||||||
downloadToStart
|
downloadToStart
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing download queue:", error);
|
console.error("Error processing download queue:", error);
|
||||||
|
LOG.error('NEODLP', `Error processing download queue: ${error}`);
|
||||||
} finally {
|
} finally {
|
||||||
// Important: reset the processing flag
|
// Important: reset the processing flag
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -530,7 +875,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
console.error("Error checking for app update:", error);
|
console.error("Error checking for app update:", error);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Prevent app from closing
|
// Prevent app from closing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleCloseRequested = (event: any) => {
|
const handleCloseRequested = (event: any) => {
|
||||||
@@ -550,6 +895,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
appWindow.setFocus();
|
appWindow.setFocus();
|
||||||
navigate('/');
|
navigate('/');
|
||||||
if (event.payload.url) {
|
if (event.payload.url) {
|
||||||
|
LOG.info('NEODLP', `Received download request from neodlp browser extension for URL: ${event.payload.url}`);
|
||||||
const { setRequestedUrl, setAutoSubmitSearch } = useCurrentVideoMetadataStore.getState();
|
const { setRequestedUrl, setAutoSubmitSearch } = useCurrentVideoMetadataStore.getState();
|
||||||
setRequestedUrl(event.payload.url);
|
setRequestedUrl(event.payload.url);
|
||||||
setAutoSubmitSearch(true);
|
setAutoSubmitSearch(true);
|
||||||
@@ -606,7 +952,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
setIsKvPairsStatePropagated(true);
|
setIsKvPairsStatePropagated(true);
|
||||||
}
|
}
|
||||||
}, [kvPairs, isSuccessFetchingKvPairs]);
|
}, [kvPairs, isSuccessFetchingKvPairs]);
|
||||||
|
|
||||||
// Initiate/Resolve base app paths
|
// Initiate/Resolve base app paths
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initPaths = async () => {
|
const initPaths = async () => {
|
||||||
@@ -616,13 +962,13 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const downloadDirPath = await downloadDir();
|
const downloadDirPath = await downloadDir();
|
||||||
const tempDirPath = await tempDir();
|
const tempDirPath = await tempDir();
|
||||||
const resourceDirPath = await resourceDir();
|
const resourceDirPath = await resourceDir();
|
||||||
|
|
||||||
const ffmpegPath = await join(resourceDirPath, 'binaries', `ffmpeg-${currentArch}${currentExeExtension ? '.' + currentExeExtension : ''}`);
|
const ffmpegPath = await join(resourceDirPath, 'binaries', `ffmpeg-${currentArch}${currentExeExtension ? '.' + currentExeExtension : ''}`);
|
||||||
const tempDownloadDirPath = await join(tempDirPath, config.appPkgName, 'downloads');
|
const tempDownloadDirPath = await join(tempDirPath, config.appPkgName, 'downloads');
|
||||||
const appDownloadDirPath = await join(downloadDirPath, config.appName);
|
const appDownloadDirPath = await join(downloadDirPath, config.appName);
|
||||||
|
|
||||||
if(!await fs.exists(tempDownloadDirPath)) fs.mkdir(tempDownloadDirPath, { recursive: true }).then(() => { console.log(`Created DIR: ${tempDownloadDirPath}`) });
|
if(!await fs.exists(tempDownloadDirPath)) fs.mkdir(tempDownloadDirPath, { recursive: true }).then(() => { console.log(`Created DIR: ${tempDownloadDirPath}`) });
|
||||||
|
|
||||||
setPath('ffmpegPath', ffmpegPath);
|
setPath('ffmpegPath', ffmpegPath);
|
||||||
setPath('tempDownloadDirPath', tempDownloadDirPath);
|
setPath('tempDownloadDirPath', tempDownloadDirPath);
|
||||||
if (DOWNLOAD_DIR) {
|
if (DOWNLOAD_DIR) {
|
||||||
@@ -701,6 +1047,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const YTDLP_UPDATE_INTERVAL = 86400000 // 24H;
|
const YTDLP_UPDATE_INTERVAL = 86400000 // 24H;
|
||||||
if (YTDLP_AUTO_UPDATE && (ytDlpUpdateLastCheck === null || currentTimestamp - ytDlpUpdateLastCheck > YTDLP_UPDATE_INTERVAL)) {
|
if (YTDLP_AUTO_UPDATE && (ytDlpUpdateLastCheck === null || currentTimestamp - ytDlpUpdateLastCheck > YTDLP_UPDATE_INTERVAL)) {
|
||||||
console.log("Running auto-update for yt-dlp...");
|
console.log("Running auto-update for yt-dlp...");
|
||||||
|
LOG.info('NEODLP', 'Updating yt-dlp to latest version (triggered because auto-update is enabled)');
|
||||||
updateYtDlp();
|
updateYtDlp();
|
||||||
} else {
|
} else {
|
||||||
console.log("Skipping yt-dlp auto-update, either disabled or recently updated.");
|
console.log("Skipping yt-dlp auto-update, either disabled or recently updated.");
|
||||||
@@ -724,21 +1071,24 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
appVersion: appVersion,
|
appVersion: appVersion,
|
||||||
registeredVersion: macOsRegisteredVersion
|
registeredVersion: macOsRegisteredVersion
|
||||||
});
|
});
|
||||||
const currentPlatform = platform();
|
|
||||||
if (currentPlatform === 'macos' && (!macOsRegisteredVersion || macOsRegisteredVersion !== appVersion)) {
|
if (currentPlatform === 'macos' && (!macOsRegisteredVersion || macOsRegisteredVersion !== appVersion)) {
|
||||||
console.log("Running MacOS auto registration...");
|
console.log("Running MacOS auto registration...");
|
||||||
|
LOG.info('NEODLP', 'Running macOS registration');
|
||||||
registerToMac().then((result: { success: boolean, message: string }) => {
|
registerToMac().then((result: { success: boolean, message: string }) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log("MacOS registration successful:", result.message);
|
console.log("MacOS registration successful:", result.message);
|
||||||
|
LOG.info('NEODLP', 'macOS registration successful');
|
||||||
} else {
|
} else {
|
||||||
console.error("MacOS registration failed:", result.message);
|
console.error("MacOS registration failed:", result.message);
|
||||||
|
LOG.error('NEODLP', `macOS registration failed: ${result.message}`);
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error("Error during macOS registration:", error);
|
console.error("Error during macOS registration:", error);
|
||||||
|
LOG.error('NEODLP', `Error during macOS registration: ${error}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSuccessFetchingDownloadStates && downloadStates) {
|
if (isSuccessFetchingDownloadStates && downloadStates) {
|
||||||
console.log("Download States fetched successfully:", downloadStates);
|
console.log("Download States fetched successfully:", downloadStates);
|
||||||
@@ -752,19 +1102,54 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
processQueuedDownloads();
|
processQueuedDownloads();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// Cleanup timeout if component unmounts or dependencies change
|
// Cleanup timeout if component unmounts or dependencies change
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}, [processQueuedDownloads, ongoingDownloads, queuedDownloads]);
|
}, [processQueuedDownloads, ongoingDownloads, queuedDownloads]);
|
||||||
|
|
||||||
|
// show a toast and pause the download when yt-dlp exits unexpectedly
|
||||||
|
useEffect(() => {
|
||||||
|
if (isErrored && !isErrorExpected) {
|
||||||
|
toast.error("Download Failed", {
|
||||||
|
description: "yt-dlp exited unexpectedly. Please try again later",
|
||||||
|
});
|
||||||
|
if (erroredDownloadId) {
|
||||||
|
downloadStatusUpdater.mutate({ download_id: erroredDownloadId, download_status: 'paused' }, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
console.log("Download status updated successfully:", data);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to update download status:", error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setErroredDownloadId(null);
|
||||||
|
}
|
||||||
|
setIsErrored(false);
|
||||||
|
setIsErrorExpected(false);
|
||||||
|
}
|
||||||
|
}, [isErrored, isErrorExpected, erroredDownloadId, setIsErrored, setIsErrorExpected, setErroredDownloadId]);
|
||||||
|
|
||||||
|
// auto reset error states after 3 seconds of expecting an error
|
||||||
|
useEffect(() => {
|
||||||
|
if (isErrorExpected) {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setIsErrored(false);
|
||||||
|
setIsErrorExpected(false);
|
||||||
|
setErroredDownloadId(null);
|
||||||
|
}, 3000);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}, [isErrorExpected, setIsErrorExpected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}>
|
<AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}>
|
||||||
<ThemeProvider defaultTheme={APP_THEME || "system"} storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme={APP_THEME || "system"} storageKey="vite-ui-theme">
|
||||||
<TooltipProvider delayDuration={1000}>
|
<TooltipProvider delayDuration={1000}>
|
||||||
{children}
|
{children}
|
||||||
<Toaster />
|
<Sonner closeButton />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,55 @@
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { getRouteName } from "@/utils";
|
import { getRouteName } from "@/utils";
|
||||||
// import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
// import { Terminal } from "lucide-react";
|
import { Terminal } from "lucide-react";
|
||||||
// import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
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() {
|
export default function Navbar() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const logs = useLogger().getLogs();
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex justify-center">
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<h1 className="text-lg text-primary font-semibold ml-4">{getRouteName(location.pathname)}</h1>
|
<h1 className="text-lg text-primary font-semibold ml-4">{getRouteName(location.pathname)}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-center items-center">
|
||||||
{/* <Tooltip>
|
<Dialog>
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button variant="outline" size="icon">
|
<TooltipTrigger asChild>
|
||||||
<Terminal />
|
<DialogTrigger asChild>
|
||||||
</Button>
|
<Button variant="outline" size="icon">
|
||||||
</TooltipTrigger>
|
<Terminal />
|
||||||
<TooltipContent>
|
</Button>
|
||||||
<p>Logs</p>
|
</DialogTrigger>
|
||||||
</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip> */}
|
<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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { config } from "@/config";
|
import { config } from "@/config";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/components/ui/sidebar";
|
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/components/ui/sidebar";
|
||||||
import { CircleArrowUp, Download, Puzzle, Settings, SquarePlay, } from "lucide-react";
|
import { CircleArrowUp, Download, Settings, SquarePlay, } from "lucide-react";
|
||||||
import { isActive as isActiveSidebarItem } from "@/utils";
|
import { isActive as isActiveSidebarItem } from "@/utils";
|
||||||
import { RoutesObj } from "@/types/route";
|
import { RoutesObj } from "@/types/route";
|
||||||
import { useDownloadStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
import { useDownloadStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||||
@@ -14,10 +14,10 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import useAppUpdater from "@/helpers/use-app-updater";
|
import useAppUpdater from "@/helpers/use-app-updater";
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||||
const ongoingDownloads = downloadStates.filter(state =>
|
const ongoingDownloads = downloadStates.filter(state =>
|
||||||
['starting', 'downloading', 'queued'].includes(state.download_status)
|
['starting', 'downloading', 'queued'].includes(state.download_status)
|
||||||
);
|
);
|
||||||
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
||||||
@@ -29,6 +29,7 @@ export function AppSidebar() {
|
|||||||
const { open } = useSidebar();
|
const { open } = useSidebar();
|
||||||
const { downloadAndInstallAppUpdate } = useAppUpdater();
|
const { downloadAndInstallAppUpdate } = useAppUpdater();
|
||||||
const [showBadge, setShowBadge] = useState(false);
|
const [showBadge, setShowBadge] = useState(false);
|
||||||
|
const [showUpdateCard, setShowUpdateCard] = useState(false);
|
||||||
|
|
||||||
const topItems: Array<RoutesObj> = [
|
const topItems: Array<RoutesObj> = [
|
||||||
{
|
{
|
||||||
@@ -40,11 +41,6 @@ export function AppSidebar() {
|
|||||||
title: "Library",
|
title: "Library",
|
||||||
url: "/library",
|
url: "/library",
|
||||||
icon: SquarePlay,
|
icon: SquarePlay,
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Extension",
|
|
||||||
url: "/extension",
|
|
||||||
icon: Puzzle,
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -61,16 +57,18 @@ export function AppSidebar() {
|
|||||||
if (open) {
|
if (open) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
setShowBadge(true);
|
setShowBadge(true);
|
||||||
|
setShowUpdateCard(true);
|
||||||
}, 300);
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
setShowBadge(false);
|
setShowBadge(false);
|
||||||
|
setShowUpdateCard(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
};
|
};
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
@@ -101,7 +99,7 @@ export function AppSidebar() {
|
|||||||
{!open ? (
|
{!open ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
isActive={isActiveSidebarItem(item.url, location.pathname, item.starts_with ? item.starts_with : false)}
|
isActive={isActiveSidebarItem(item.url, location.pathname, item.starts_with ? item.starts_with : false)}
|
||||||
className="relative"
|
className="relative"
|
||||||
asChild
|
asChild
|
||||||
@@ -120,7 +118,7 @@ export function AppSidebar() {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
isActive={isActiveSidebarItem(item.url, location.pathname, item.starts_with ? item.starts_with : false)}
|
isActive={isActiveSidebarItem(item.url, location.pathname, item.starts_with ? item.starts_with : false)}
|
||||||
className="relative"
|
className="relative"
|
||||||
asChild
|
asChild
|
||||||
@@ -153,13 +151,14 @@ export function AppSidebar() {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{appUpdate && open && (
|
{appUpdate && open && showUpdateCard && (
|
||||||
<Card>
|
<Card className="gap-4 py-0">
|
||||||
<CardHeader className="p-4 pb-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>
|
<CardDescription>
|
||||||
A new version of {config.appName} is available. Please update to the latest version for the best experience.
|
A newer version of {config.appName} is available. Please update to the latest version for the best experience.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
<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>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-2.5 p-4">
|
<CardContent className="grid gap-2.5 p-4">
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
@@ -170,13 +169,14 @@ export function AppSidebar() {
|
|||||||
disabled={ongoingDownloads.length > 0 || isUpdatingApp}
|
disabled={ongoingDownloads.length > 0 || isUpdatingApp}
|
||||||
onClick={() => downloadAndInstallAppUpdate(appUpdate)}
|
onClick={() => downloadAndInstallAppUpdate(appUpdate)}
|
||||||
>
|
>
|
||||||
Download and Install
|
Update Now
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader className="flex flex-col items-center text-center gap-2">
|
<AlertDialogHeader className="flex flex-col items-center text-center gap-2">
|
||||||
|
<CircleArrowUp className="size-7 stroke-muted-foreground" />
|
||||||
<AlertDialogTitle>Updating {config.appName}</AlertDialogTitle>
|
<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" />
|
<Progress value={appUpdateDownloadProgress} className="w-full" />
|
||||||
<AlertDialogDescription className="text-center">Downloading update... {appUpdateDownloadProgress}%</AlertDialogDescription>
|
<AlertDialogDescription className="text-center">Downloading update... {appUpdateDownloadProgress}%</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
@@ -222,4 +222,4 @@ export function AppSidebar() {
|
|||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RechartsPrimitive from "recharts"
|
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"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -20,6 +28,36 @@ type ChartContextProps = {
|
|||||||
config: ChartConfig
|
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)
|
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||||
|
|
||||||
function useChart() {
|
function useChart() {
|
||||||
@@ -82,17 +120,17 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|||||||
__html: Object.entries(THEMES)
|
__html: Object.entries(THEMES)
|
||||||
.map(
|
.map(
|
||||||
([theme, prefix]) => `
|
([theme, prefix]) => `
|
||||||
${prefix} [data-chart=${id}] {
|
${prefix} [data-chart=${id}] {
|
||||||
${colorConfig
|
${colorConfig
|
||||||
.map(([key, itemConfig]) => {
|
.map(([key, itemConfig]) => {
|
||||||
const color =
|
const color =
|
||||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||||
itemConfig.color
|
itemConfig.color
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
return color ? ` --color-${key}: ${color};` : null
|
||||||
})
|
})
|
||||||
.join("\n")}
|
.join("\n")}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
}}
|
}}
|
||||||
@@ -105,25 +143,18 @@ const ChartTooltip = RechartsPrimitive.Tooltip
|
|||||||
function ChartTooltipContent({
|
function ChartTooltipContent({
|
||||||
active,
|
active,
|
||||||
payload,
|
payload,
|
||||||
|
label,
|
||||||
className,
|
className,
|
||||||
indicator = "dot",
|
indicator = "dot",
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
hideIndicator = false,
|
hideIndicator = false,
|
||||||
label,
|
|
||||||
labelFormatter,
|
labelFormatter,
|
||||||
labelClassName,
|
|
||||||
formatter,
|
formatter,
|
||||||
|
labelClassName,
|
||||||
color,
|
color,
|
||||||
nameKey,
|
nameKey,
|
||||||
labelKey,
|
labelKey,
|
||||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
}: CustomTooltipProps) {
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
hideLabel?: boolean
|
|
||||||
hideIndicator?: boolean
|
|
||||||
indicator?: "line" | "dot" | "dashed"
|
|
||||||
nameKey?: string
|
|
||||||
labelKey?: string
|
|
||||||
}) {
|
|
||||||
const { config } = useChart()
|
const { config } = useChart()
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
@@ -134,10 +165,14 @@ function ChartTooltipContent({
|
|||||||
const [item] = payload
|
const [item] = payload
|
||||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
const value =
|
const value = (() => {
|
||||||
!labelKey && typeof label === "string"
|
const v =
|
||||||
? config[label as keyof typeof config]?.label || label
|
!labelKey && typeof label === "string"
|
||||||
: itemConfig?.label
|
? config[label as keyof typeof config]?.label ?? label
|
||||||
|
: itemConfig?.label
|
||||||
|
|
||||||
|
return typeof v === "string" || typeof v === "number" ? v : undefined
|
||||||
|
})()
|
||||||
|
|
||||||
if (labelFormatter) {
|
if (labelFormatter) {
|
||||||
return (
|
return (
|
||||||
@@ -254,11 +289,7 @@ function ChartLegendContent({
|
|||||||
payload,
|
payload,
|
||||||
verticalAlign = "bottom",
|
verticalAlign = "bottom",
|
||||||
nameKey,
|
nameKey,
|
||||||
}: React.ComponentProps<"div"> &
|
}: ChartLegendContentProps) {
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
|
||||||
hideIcon?: boolean
|
|
||||||
nameKey?: string
|
|
||||||
}) {
|
|
||||||
const { config } = useChart()
|
const { config } = useChart()
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
@@ -348,4 +379,4 @@ export {
|
|||||||
ChartLegend,
|
ChartLegend,
|
||||||
ChartLegendContent,
|
ChartLegendContent,
|
||||||
ChartStyle,
|
ChartStyle,
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,12 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
"--normal-border": "var(--border)",
|
"--normal-border": "var(--border)",
|
||||||
} as React.CSSProperties
|
} 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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,15 +31,16 @@ export default function useAppUpdater() {
|
|||||||
switch (event.event) {
|
switch (event.event) {
|
||||||
case 'Started':
|
case 'Started':
|
||||||
contentLength = event.data.contentLength;
|
contentLength = event.data.contentLength;
|
||||||
console.log(`started downloading ${event.data.contentLength} bytes`);
|
console.log(`started downloading app update of ${event.data.contentLength} bytes`);
|
||||||
break;
|
break;
|
||||||
case 'Progress':
|
case 'Progress':
|
||||||
downloaded += event.data.chunkLength;
|
downloaded += event.data.chunkLength;
|
||||||
setDownloadProgress(downloaded / (contentLength || 0));
|
const progress = (downloaded / (contentLength || 1)) * 100;
|
||||||
console.log(`downloaded ${downloaded} from ${contentLength}`);
|
setDownloadProgress(Math.round(progress * 10) / 10);
|
||||||
|
console.log(`downloaded ${downloaded} bytes from ${contentLength} bytes of app update`);
|
||||||
break;
|
break;
|
||||||
case 'Finished':
|
case 'Finished':
|
||||||
console.log('download finished');
|
console.log('app update download finished');
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/helpers/use-logger.ts
Normal file
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 { useResetSettings, useSaveSettingsKey } from "@/services/mutations";
|
||||||
import { useSettingsPageStatesStore } from "@/services/store";
|
import { useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
export function useSettings() {
|
export function useSettings() {
|
||||||
const { toast } = useToast();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const setSettingsKey = useSettingsPageStatesStore(state => state.setSettingsKey);
|
const setSettingsKey = useSettingsPageStatesStore(state => state.setSettingsKey);
|
||||||
const resetSettingsState = useSettingsPageStatesStore(state => state.resetSettings);
|
const resetSettingsState = useSettingsPageStatesStore(state => state.resetSettings);
|
||||||
@@ -22,10 +21,8 @@ export function useSettings() {
|
|||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("Error saving settings key:", error);
|
console.error("Error saving settings key:", error);
|
||||||
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||||
toast({
|
toast.error("Failed to update settings", {
|
||||||
title: "Failed to update settings",
|
|
||||||
description: `Failed to update ${key}`,
|
description: `Failed to update ${key}`,
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -39,26 +36,21 @@ export function useSettings() {
|
|||||||
resetSettingsState();
|
resetSettingsState();
|
||||||
console.log("Settings reset successfully");
|
console.log("Settings reset successfully");
|
||||||
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||||
toast({
|
toast.success("Settings reset successfully", {
|
||||||
title: "Settings reset successfully",
|
|
||||||
description: "All settings have been reset to default.",
|
description: "All settings have been reset to default.",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error resetting settings:", error);
|
console.error("Error resetting settings:", error);
|
||||||
toast({
|
toast.error("Failed to reset settings", {
|
||||||
title: "Failed to reset settings",
|
|
||||||
description: "Failed to reset settings to default.",
|
description: "Failed to reset settings to default.",
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("Error resetting settings:", error);
|
console.error("Error resetting settings:", error);
|
||||||
toast({
|
toast.error("Failed to reset settings", {
|
||||||
title: "Failed to reset settings",
|
|
||||||
description: "Failed to reset settings to default.",
|
description: "Failed to reset settings to default.",
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import RootLayout from "@/pages/layout/root";
|
|||||||
import DownloaderPage from "@/pages/downloader";
|
import DownloaderPage from "@/pages/downloader";
|
||||||
import LibraryPage from "@/pages/library";
|
import LibraryPage from "@/pages/library";
|
||||||
import SettingsPage from "@/pages/settings";
|
import SettingsPage from "@/pages/settings";
|
||||||
import ExtensionPage from "@/pages/extension";
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
@@ -19,7 +18,6 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|||||||
<Route path="/" element={<RootLayout />}>
|
<Route path="/" element={<RootLayout />}>
|
||||||
<Route index element={<DownloaderPage />} />
|
<Route index element={<DownloaderPage />} />
|
||||||
<Route path="/library" element={<LibraryPage />} />
|
<Route path="/library" element={<LibraryPage />} />
|
||||||
<Route path="/extension" element={<ExtensionPage />} />
|
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,77 +0,0 @@
|
|||||||
import { SlidingButton } from "@/components/custom/slidingButton";
|
|
||||||
import Heading from "@/components/heading";
|
|
||||||
import { ArrowRight } from "lucide-react";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
export default function ExtensionPage() {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const openLink = async (url: string, app: string | null) => {
|
|
||||||
try {
|
|
||||||
await invoke('open_file_with_app', { filePath: url, appName: app }).then(() => {
|
|
||||||
toast({
|
|
||||||
title: '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"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto p-4 space-y-4">
|
|
||||||
<Heading title="Extension" description="Integrate NeoDLP with your favourite browser" />
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<SlidingButton
|
|
||||||
slidingContent={
|
|
||||||
<div className="flex items-center justify-center gap-2 text-white dark:text-black">
|
|
||||||
<ArrowRight className="size-4" />
|
|
||||||
<span>Get Now</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'chrome')}
|
|
||||||
>
|
|
||||||
<span className="font-semibold flex items-center gap-2">
|
|
||||||
<svg className="size-4 fill-white dark:fill-black" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<path d="M0 256C0 209.4 12.5 165.6 34.3 127.1L144.1 318.3C166 357.5 207.9 384 256 384C270.3 384 283.1 381.7 296.8 377.4L220.5 509.6C95.9 492.3 0 385.3 0 256zM365.1 321.6C377.4 302.4 384 279.1 384 256C384 217.8 367.2 183.5 340.7 160H493.4C505.4 189.6 512 222.1 512 256C512 397.4 397.4 511.1 256 512L365.1 321.6zM477.8 128H256C193.1 128 142.3 172.1 130.5 230.7L54.2 98.5C101 38.5 174 0 256 0C350.8 0 433.5 51.5 477.8 128V128zM168 256C168 207.4 207.4 168 256 168C304.6 168 344 207.4 344 256C344 304.6 304.6 344 256 344C207.4 344 168 304.6 168 256z"/>
|
|
||||||
</svg>
|
|
||||||
Get Chrome Extension
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">from Chrome Web Store</span>
|
|
||||||
</SlidingButton>
|
|
||||||
<SlidingButton
|
|
||||||
slidingContent={
|
|
||||||
<div className="flex items-center justify-center gap-2 text-white dark:text-black">
|
|
||||||
<ArrowRight className="size-4" />
|
|
||||||
<span>Get Now</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
onClick={() => openLink('https://addons.mozilla.org/en-US/firefox/addon/neo-downloader-plus', 'firefox')}
|
|
||||||
>
|
|
||||||
<span className="font-semibold flex items-center gap-2">
|
|
||||||
<svg className="size-4 fill-white dark:fill-black" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<path d="M130.2 127.5C130.4 127.6 130.3 127.6 130.2 127.5V127.5zM481.6 172.9C471 147.4 449.6 119.9 432.7 111.2C446.4 138.1 454.4 165 457.4 185.2C457.4 185.3 457.4 185.4 457.5 185.6C429.9 116.8 383.1 89.1 344.9 28.7C329.9 5.1 334 3.5 331.8 4.1L331.7 4.2C285 30.1 256.4 82.5 249.1 126.9C232.5 127.8 216.2 131.9 201.2 139C199.8 139.6 198.7 140.7 198.1 142C197.4 143.4 197.2 144.9 197.5 146.3C197.7 147.2 198.1 148 198.6 148.6C199.1 149.3 199.8 149.9 200.5 150.3C201.3 150.7 202.1 151 203 151.1C203.8 151.1 204.7 151 205.5 150.8L206 150.6C221.5 143.3 238.4 139.4 255.5 139.2C318.4 138.7 352.7 183.3 363.2 201.5C350.2 192.4 326.8 183.3 304.3 187.2C392.1 231.1 368.5 381.8 247 376.4C187.5 373.8 149.9 325.5 146.4 285.6C146.4 285.6 157.7 243.7 227 243.7C234.5 243.7 256 222.8 256.4 216.7C256.3 214.7 213.8 197.8 197.3 181.5C188.4 172.8 184.2 168.6 180.5 165.5C178.5 163.8 176.4 162.2 174.2 160.7C168.6 141.2 168.4 120.6 173.5 101.1C148.5 112.5 129 130.5 114.8 146.4H114.7C105 134.2 105.7 93.8 106.3 85.3C106.1 84.8 99 89 98.1 89.7C89.5 95.7 81.6 102.6 74.3 110.1C58 126.7 30.1 160.2 18.8 211.3C14.2 231.7 12 255.7 12 263.6C12 398.3 121.2 507.5 255.9 507.5C376.6 507.5 478.9 420.3 496.4 304.9C507.9 228.2 481.6 173.8 481.6 172.9z"/>
|
|
||||||
</svg>
|
|
||||||
Get Firefox Extension
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">from Mozilla Addons Store</span>
|
|
||||||
</SlidingButton>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<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://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 befor clicking the link</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,11 @@ import { AspectRatio } from "@/components/ui/aspect-ratio";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { toast } from "sonner";
|
||||||
import { useAppContext } from "@/providers/appContextProvider";
|
import { useAppContext } from "@/providers/appContextProvider";
|
||||||
import { useDownloadActionStatesStore, useDownloadStatesStore } from "@/services/store";
|
import { useCurrentVideoMetadataStore, useDownloadActionStatesStore, useDownloadStatesStore, useLibraryPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils";
|
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils";
|
||||||
import { AudioLines, Clock, CloudDownload, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, PackageCheck, Pause, Play, 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 { invoke } from "@tauri-apps/api/core";
|
||||||
import * as fs from "@tauri-apps/plugin-fs";
|
import * as fs from "@tauri-apps/plugin-fs";
|
||||||
import { DownloadState } from "@/types/download";
|
import { DownloadState } from "@/types/download";
|
||||||
@@ -18,9 +18,15 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import Heading from "@/components/heading";
|
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() {
|
export default function LibraryPage() {
|
||||||
|
const activeTab = useLibraryPageStatesStore(state => state.activeTab);
|
||||||
|
const setActiveTab = useLibraryPageStatesStore(state => state.setActiveTab);
|
||||||
|
|
||||||
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||||
const downloadActions = useDownloadActionStatesStore(state => state.downloadActions);
|
const downloadActions = useDownloadActionStatesStore(state => state.downloadActions);
|
||||||
const setIsResumingDownload = useDownloadActionStatesStore(state => state.setIsResumingDownload);
|
const setIsResumingDownload = useDownloadActionStatesStore(state => state.setIsResumingDownload);
|
||||||
@@ -28,37 +34,44 @@ export default function LibraryPage() {
|
|||||||
const setIsCancelingDownload = useDownloadActionStatesStore(state => state.setIsCancelingDownload);
|
const setIsCancelingDownload = useDownloadActionStatesStore(state => state.setIsCancelingDownload);
|
||||||
const setIsDeleteFileChecked = useDownloadActionStatesStore(state => state.setIsDeleteFileChecked);
|
const setIsDeleteFileChecked = useDownloadActionStatesStore(state => state.setIsDeleteFileChecked);
|
||||||
|
|
||||||
|
const debugMode = useSettingsPageStatesStore(state => state.settings.debug_mode);
|
||||||
|
|
||||||
const { pauseDownload, resumeDownload, cancelDownload } = useAppContext()
|
const { pauseDownload, resumeDownload, cancelDownload } = useAppContext()
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const downloadStateDeleter = useDeleteDownloadState();
|
const downloadStateDeleter = useDeleteDownloadState();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const LOG = useLogger();
|
||||||
|
|
||||||
const incompleteDownloads = downloadStates.filter(state => state.download_status !== 'completed');
|
const incompleteDownloads = downloadStates.filter(state => state.download_status !== 'completed');
|
||||||
const completedDownloads = downloadStates.filter(state => state.download_status === 'completed');
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
const openFile = async (filePath: string | null, app: string | null) => {
|
const openFile = async (filePath: string | null, app: string | null) => {
|
||||||
if (filePath && await fs.exists(filePath)) {
|
if (filePath && await fs.exists(filePath)) {
|
||||||
try {
|
try {
|
||||||
await invoke('open_file_with_app', { filePath: filePath, appName: app }).then(() => {
|
await invoke('open_file_with_app', { filePath: filePath, appName: app }).then(() => {
|
||||||
toast({
|
toast.info(`${app === 'explorer' ? 'Revealing' : 'Opening'} file`, {
|
||||||
title: 'Opening file',
|
description: `${app === 'explorer' ? 'Revealing' : 'Opening'} the file ${app === 'explorer' ? 'in' : 'with'} ${app ? app : 'default app'}.`,
|
||||||
description: `Opening the file with ${app ? app : 'default app'}.`,
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast({
|
toast.error(`Failed to ${app === 'explorer' ? 'reveal' : 'open'} file`, {
|
||||||
title: 'Failed to open file',
|
description: `An error occurred while trying to ${app === 'explorer' ? 'reveal' : 'open'} the file.`,
|
||||||
description: 'An error occurred while trying to open the file.',
|
|
||||||
variant: "destructive"
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast.info("File unavailable", {
|
||||||
title: 'File unavailable',
|
description: `The file you are trying to ${app === 'explorer' ? 'reveal' : 'open'} does not exist.`,
|
||||||
description: 'The file you are trying to open does not exist.',
|
|
||||||
variant: "destructive"
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,345 +93,415 @@ export default function LibraryPage() {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
console.log("Download State deleted successfully:", data);
|
console.log("Download State deleted successfully:", data);
|
||||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||||
toast({
|
toast.success("Removed from downloads", {
|
||||||
title: 'Removed from downloads',
|
description: "The download has been removed successfully.",
|
||||||
description: 'The download has been removed successfully.',
|
});
|
||||||
})
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("Failed to delete download state:", error);
|
console.error("Failed to delete download state:", error);
|
||||||
toast({
|
toast.error("Failed to remove download", {
|
||||||
title: 'Failed to remove download',
|
description: "An error occurred while trying to remove the download.",
|
||||||
description: 'An error occurred while trying to remove the download.',
|
});
|
||||||
variant: "destructive"
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stopOngoingDownloads = async () => {
|
||||||
|
if (ongoingDownloads.length > 0) {
|
||||||
|
for (const state of ongoingDownloads) {
|
||||||
|
setIsPausingDownload(state.download_id, true);
|
||||||
|
try {
|
||||||
|
await pauseDownload(state);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast.error("Failed to stop download", {
|
||||||
|
description: `An error occurred while trying to stop the download for ${state.title}.`,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsPausingDownload(state.download_id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ongoingDownloads.length === 0) {
|
||||||
|
toast.success("Stopped ongoing downloads", {
|
||||||
|
description: "All ongoing downloads have been stopped successfully.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4 space-y-4">
|
<div className="container mx-auto p-4 space-y-4">
|
||||||
<Heading title="Library" description="Manage all your downloads in one place" />
|
<Heading title="Library" description="Manage all your downloads in one place" />
|
||||||
<div className="w-full fle flex-col">
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<div className="flex w-full items-center gap-3 mb-2">
|
<div className="w-full flex items-center justify-between mb-4">
|
||||||
<CloudDownload className="size-5" />
|
<TabsList>
|
||||||
<h3 className="text-nowrap font-semibold">Incomplete Downloads</h3>
|
<TabsTrigger value="completed">Completed {completedDownloads.length > 0 && (`(${completedDownloads.length})`)}</TabsTrigger>
|
||||||
|
<TabsTrigger value="incomplete">Incomplete {(incompleteDownloads.length > 0 && ongoingDownloads.length <= 0) && (`(${incompleteDownloads.length})`)} {ongoingDownloads.length > 0 && (<Badge className="h-4 min-w-4 rounded-full px-1 font-mono tabular-nums ml-1">{ongoingDownloads.length}</Badge>)}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-fit"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={ongoingDownloads.length <= 0}
|
||||||
|
>
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Stop all ongoing downloads?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to stop all ongoing downloads? This will pause all downloads including the download queue.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => stopOngoingDownloads()}
|
||||||
|
>Stop</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="horizontal" className="" />
|
<TabsContent value="completed">
|
||||||
</div>
|
<div className="w-full flex flex-col gap-2">
|
||||||
<div className="w-full flex flex-col gap-2">
|
{completedDownloads.length > 0 ? (
|
||||||
{incompleteDownloads.length > 0 ? (
|
completedDownloads.map((state) => {
|
||||||
incompleteDownloads.map((state) => {
|
const itemActionStates = downloadActions[state.download_id] || {
|
||||||
const itemActionStates = downloadActions[state.download_id] || {
|
isResuming: false,
|
||||||
isResuming: false,
|
isPausing: false,
|
||||||
isPausing: false,
|
isCanceling: false,
|
||||||
isCanceling: false,
|
isDeleteFileChecked: false,
|
||||||
isDeleteFileChecked: false,
|
};
|
||||||
};
|
return (
|
||||||
return (
|
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
||||||
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
<div className="w-[30%] flex flex-col justify-between gap-2">
|
||||||
<div className="w-[30%] flex flex-col justify-between gap-2">
|
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border mb-2">
|
||||||
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border">
|
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
||||||
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
</AspectRatio>
|
||||||
</AspectRatio>
|
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
||||||
{state.ext && (
|
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
||||||
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
<Video className="w-4 h-4 mr-2" />
|
||||||
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
)}
|
||||||
<Video className="w-4 h-4 mr-2" />
|
{state.filetype && state.filetype === 'audio' && (
|
||||||
)}
|
<Music className="w-4 h-4 mr-2" />
|
||||||
{state.filetype && state.filetype === 'audio' && (
|
)}
|
||||||
<Music className="w-4 h-4 mr-2" />
|
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
||||||
)}
|
<File className="w-4 h-4 mr-2" />
|
||||||
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
)}
|
||||||
<File className="w-4 h-4 mr-2" />
|
{state.ext?.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
||||||
)}
|
</span>
|
||||||
{state.ext.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
</div>
|
||||||
</span>
|
<div className="w-full flex flex-col justify-between gap-2">
|
||||||
)}
|
<div className="flex flex-col gap-1">
|
||||||
</div>
|
<h4 className="">{state.title}</h4>
|
||||||
<div className="w-full flex flex-col justify-between">
|
<p className="text-xs text-muted-foreground">{state.channel ? state.channel : 'unknown'} {state.host ? `• ${state.host}` : 'unknown'}</p>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex items-center mt-1">
|
||||||
<h4>{state.title}</h4>
|
<span className="text-xs text-muted-foreground flex items-center pr-3"><Clock className="w-4 h-4 mr-2"/> {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'}</span>
|
||||||
{((state.download_status === 'starting') || (state.download_status === 'downloading' && state.status === 'finished')) && (
|
<Separator orientation="vertical" />
|
||||||
<IndeterminateProgress indeterminate={true} className="w-full" />
|
<span className="text-xs text-muted-foreground flex items-center px-3">
|
||||||
)}
|
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
||||||
{(state.download_status === 'downloading' || state.download_status === 'paused') && state.progress && state.status !== 'finished' && (
|
<FileVideo2 className="w-4 h-4 mr-2"/>
|
||||||
<div className="w-full flex items-center gap-2">
|
)}
|
||||||
<span className="text-sm text-nowrap">{state.progress}%</span>
|
{state.filetype && state.filetype === 'audio' && (
|
||||||
<Progress value={state.progress} />
|
<FileAudio2 className="w-4 h-4 mr-2" />
|
||||||
<span className="text-sm text-nowrap">{
|
)}
|
||||||
state.downloaded && state.total
|
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
||||||
? `(${formatFileSize(state.downloaded)} / ${formatFileSize(state.total)})`
|
<FileQuestion className="w-4 h-4 mr-2" />
|
||||||
: null
|
)}
|
||||||
}</span>
|
{state.filesize ? formatFileSize(state.filesize) : 'unknown'}
|
||||||
|
</span>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<span className="text-xs text-muted-foreground flex items-center pl-3"><AudioLines className="w-4 h-4 mr-2"/>
|
||||||
|
{state.vbr && state.abr ? (
|
||||||
|
formatBitrate(state.vbr + state.abr)
|
||||||
|
) : state.vbr ? (
|
||||||
|
formatBitrate(state.vbr)
|
||||||
|
) : state.abr ? (
|
||||||
|
formatBitrate(state.abr)
|
||||||
|
) : (
|
||||||
|
'unknown'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="hidden xl:flex items-center mt-1 gap-2 flex-wrap text-xs">
|
||||||
|
{state.playlist_id && state.playlist_index && (
|
||||||
|
<span
|
||||||
|
className="border border-border py-1 px-2 rounded flex items-center cursor-pointer"
|
||||||
|
title={`${state.playlist_title ?? 'UNKNOWN PLAYLIST'}` + ' by ' + `${state.playlist_channel ?? 'UNKNOWN CHANNEL'}`}
|
||||||
|
>
|
||||||
|
<ListVideo className="w-4 h-4 mr-2" /> Playlist ({state.playlist_index} of {state.playlist_n_entries})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{state.vcodec && (
|
||||||
|
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.vcodec)}</span>
|
||||||
|
)}
|
||||||
|
{state.acodec && (
|
||||||
|
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.acodec)}</span>
|
||||||
|
)}
|
||||||
|
{state.dynamic_range && state.dynamic_range !== 'SDR' && (
|
||||||
|
<span className="border border-border py-1 px-2 rounded">{state.dynamic_range}</span>
|
||||||
|
)}
|
||||||
|
{state.subtitle_id && (
|
||||||
|
<span
|
||||||
|
className="border border-border py-1 px-2 rounded cursor-pointer"
|
||||||
|
title={`EMBEDED SUBTITLE (${state.subtitle_id})`}
|
||||||
|
>
|
||||||
|
ESUB
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="w-full flex items-center gap-2">
|
||||||
<div className="text-xs text-muted-foreground">{ state.download_status && (
|
<Button size="sm" onClick={() => openFile(state.filepath, null)}>
|
||||||
`${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)}` : ""}`
|
<Play className="w-4 h-4" />
|
||||||
)}</div>
|
Open
|
||||||
</div>
|
|
||||||
<div className="w-full flex items-center gap-2 mt-2">
|
|
||||||
{state.download_status === 'paused' ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="w-fill"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsResumingDownload(state.download_id, true);
|
|
||||||
try {
|
|
||||||
await resumeDownload(state)
|
|
||||||
// toast({
|
|
||||||
// title: '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"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsResumingDownload(state.download_id, false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={itemActionStates.isResuming || itemActionStates.isCanceling}
|
|
||||||
>
|
|
||||||
{itemActionStates.isResuming ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Resuming
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Play className="w-4 h-4" />
|
|
||||||
Resume
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="w-fill"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsPausingDownload(state.download_id, true);
|
|
||||||
try {
|
|
||||||
await pauseDownload(state)
|
|
||||||
// toast({
|
|
||||||
// title: '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"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsPausingDownload(state.download_id, false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={itemActionStates.isPausing || itemActionStates.isCanceling || state.download_status !== 'downloading' || (state.download_status === 'downloading' && state.status === 'finished')}
|
|
||||||
>
|
|
||||||
{itemActionStates.isPausing ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Pausing
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Pause className="w-4 h-4" />
|
|
||||||
Pause
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsCancelingDownload(state.download_id, true);
|
|
||||||
try {
|
|
||||||
await cancelDownload(state)
|
|
||||||
toast({
|
|
||||||
title: '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"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsCancelingDownload(state.download_id, false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={itemActionStates.isCanceling || itemActionStates.isResuming || itemActionStates.isPausing || state.download_status === 'starting' || (state.download_status === 'downloading' && state.status === 'finished')}
|
|
||||||
>
|
|
||||||
{itemActionStates.isCanceling ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Canceling
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
Cancel
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div className="w-full flex items-center justify-center text-muted-foreground text-sm">No Incomplete downloads!</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="w-full fle flex-col">
|
|
||||||
<div className="flex w-full items-center gap-3 mb-2">
|
|
||||||
<PackageCheck className="size-5" />
|
|
||||||
<h3 className="text-nowrap font-semibold">Completed Downloads</h3>
|
|
||||||
</div>
|
|
||||||
<Separator orientation="horizontal" className="" />
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col gap-2">
|
|
||||||
{completedDownloads.length > 0 ? (
|
|
||||||
completedDownloads.map((state) => {
|
|
||||||
const itemActionStates = downloadActions[state.download_id] || {
|
|
||||||
isResuming: false,
|
|
||||||
isPausing: false,
|
|
||||||
isCanceling: false,
|
|
||||||
isDeleteFileChecked: false,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
|
||||||
<div className="w-[30%] flex flex-col justify-between gap-2">
|
|
||||||
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border mb-2">
|
|
||||||
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
|
||||||
</AspectRatio>
|
|
||||||
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
|
||||||
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
|
||||||
<Video className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{state.filetype && state.filetype === 'audio' && (
|
|
||||||
<Music className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
|
||||||
<File className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{state.ext?.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col justify-between gap-2">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h4 className="">{state.title}</h4>
|
|
||||||
<p className="text-xs text-muted-foreground">{state.channel ? state.channel : 'unknown'} {state.host ? `• ${state.host}` : 'unknown'}</p>
|
|
||||||
<div className="flex items-center mt-1">
|
|
||||||
<span className="text-xs text-muted-foreground flex items-center pr-3"><Clock className="w-4 h-4 mr-2"/> {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'}</span>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span className="text-xs text-muted-foreground flex items-center px-3">
|
|
||||||
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
|
||||||
<FileVideo2 className="w-4 h-4 mr-2"/>
|
|
||||||
)}
|
|
||||||
{state.filetype && state.filetype === 'audio' && (
|
|
||||||
<FileAudio2 className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
|
||||||
<FileQuestion className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{state.filesize ? formatFileSize(state.filesize) : 'unknown'}
|
|
||||||
</span>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span className="text-xs text-muted-foreground flex items-center pl-3"><AudioLines className="w-4 h-4 mr-2"/>
|
|
||||||
{state.vbr && state.abr ? (
|
|
||||||
formatBitrate(state.vbr + state.abr)
|
|
||||||
) : state.vbr ? (
|
|
||||||
formatBitrate(state.vbr)
|
|
||||||
) : state.abr ? (
|
|
||||||
formatBitrate(state.abr)
|
|
||||||
) : (
|
|
||||||
'unknown'
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="hidden xl:flex items-center mt-1 gap-2 flex-wrap text-xs">
|
|
||||||
{state.playlist_id && state.playlist_index && (
|
|
||||||
<span
|
|
||||||
className="border border-border py-1 px-2 rounded flex items-center cursor-pointer"
|
|
||||||
title={`${state.playlist_title ?? 'UNKNOWN PLAYLIST'}` + ' by ' + `${state.playlist_channel ?? 'UNKNOWN CHANNEL'}`}
|
|
||||||
>
|
|
||||||
<ListVideo className="w-4 h-4 mr-2" /> Playlist ({state.playlist_index} of {state.playlist_n_entries})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{state.vcodec && (
|
|
||||||
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.vcodec)}</span>
|
|
||||||
)}
|
|
||||||
{state.acodec && (
|
|
||||||
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.acodec)}</span>
|
|
||||||
)}
|
|
||||||
{state.dynamic_range && state.dynamic_range !== 'SDR' && (
|
|
||||||
<span className="border border-border py-1 px-2 rounded">{state.dynamic_range}</span>
|
|
||||||
)}
|
|
||||||
{state.subtitle_id && (
|
|
||||||
<span
|
|
||||||
className="border border-border py-1 px-2 rounded cursor-pointer"
|
|
||||||
title={`EMBEDED SUBTITLE (${state.subtitle_id})`}
|
|
||||||
>
|
|
||||||
ESUB
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex items-center gap-2">
|
|
||||||
<Button size="sm" onClick={() => openFile(state.filepath, null)}>
|
|
||||||
<Play className="w-4 h-4" />
|
|
||||||
Open
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => openFile(state.filepath, 'explorer')}>
|
|
||||||
<FolderInput className="w-4 h-4" />
|
|
||||||
Open in Explorer
|
|
||||||
</Button>
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button size="sm" variant="destructive">
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
Remove
|
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
<Button size="sm" variant="outline" onClick={() => openFile(state.filepath, 'explorer')}>
|
||||||
<AlertDialogContent>
|
<FolderInput className="w-4 h-4" />
|
||||||
<AlertDialogHeader>
|
Reveal
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
</Button>
|
||||||
<AlertDialogDescription>
|
<Button size="sm" variant="outline" onClick={() => handleSearch(state.url, state.playlist_id ? true : false)}>
|
||||||
This action cannot be undone! it will permanently remove this from downloads.
|
<Search className="w-4 h-4" />
|
||||||
</AlertDialogDescription>
|
Search
|
||||||
<div className="flex items-center space-x-2">
|
</Button>
|
||||||
<Checkbox id="delete-file" checked={itemActionStates.isDeleteFileChecked} onCheckedChange={() => {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} />
|
<AlertDialog>
|
||||||
<Label htmlFor="delete-file">Delete the downloaded file</Label>
|
<AlertDialogTrigger asChild>
|
||||||
</div>
|
<Button size="sm" variant="destructive">
|
||||||
</AlertDialogHeader>
|
<Trash2 className="w-4 h-4" />
|
||||||
<AlertDialogFooter>
|
Remove
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
</Button>
|
||||||
<AlertDialogAction onClick={
|
</AlertDialogTrigger>
|
||||||
() => removeFromDownloads(state, itemActionStates.isDeleteFileChecked).then(() => {
|
<AlertDialogContent>
|
||||||
setIsDeleteFileChecked(state.download_id, false);
|
<AlertDialogHeader>
|
||||||
})
|
<AlertDialogTitle>Remove from library?</AlertDialogTitle>
|
||||||
}>Remove</AlertDialogAction>
|
<AlertDialogDescription>
|
||||||
</AlertDialogFooter>
|
Are you sure you want to remove this download from the library? You can also delete the downloaded file by cheking the box below. This action cannot be undone.
|
||||||
</AlertDialogContent>
|
</AlertDialogDescription>
|
||||||
</AlertDialog>
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox id="delete-file" checked={itemActionStates.isDeleteFileChecked} onCheckedChange={() => {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} />
|
||||||
|
<Label htmlFor="delete-file">Delete the downloaded file</Label>
|
||||||
|
</div>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={
|
||||||
|
() => removeFromDownloads(state, itemActionStates.isDeleteFileChecked).then(() => {
|
||||||
|
setIsDeleteFileChecked(state.download_id, false);
|
||||||
|
})
|
||||||
|
}>Remove</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-col items-center gap-2 justify-center mt-27">
|
||||||
|
<h4 className="text-4xl font-bold text-muted-foreground/80 dark:text-muted">Nothing!</h4>
|
||||||
|
<p className="text-lg font-semibold text-muted-foreground/50">No Completed Downloads</p>
|
||||||
|
<p className="max-w-[50%] text-center text-xs text-muted-foreground/70">You have not completed any downloads yet. Complete downloading something to see here :)</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
})
|
</div>
|
||||||
) : (
|
</TabsContent>
|
||||||
<div className="w-full flex items-center justify-center text-muted-foreground text-sm">No Completed downloads!</div>
|
<TabsContent value="incomplete">
|
||||||
)}
|
<div className="w-full flex flex-col gap-2">
|
||||||
</div>
|
{incompleteDownloads.length > 0 ? (
|
||||||
|
incompleteDownloads.map((state) => {
|
||||||
|
const itemActionStates = downloadActions[state.download_id] || {
|
||||||
|
isResuming: false,
|
||||||
|
isPausing: false,
|
||||||
|
isCanceling: false,
|
||||||
|
isDeleteFileChecked: false,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
||||||
|
<div className="w-[30%] flex flex-col justify-between gap-2">
|
||||||
|
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border">
|
||||||
|
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
||||||
|
</AspectRatio>
|
||||||
|
{state.ext && (
|
||||||
|
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
||||||
|
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
||||||
|
<Video className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{state.filetype && state.filetype === 'audio' && (
|
||||||
|
<Music className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
||||||
|
<File className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{state.ext.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col justify-between">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4>{state.title}</h4>
|
||||||
|
{((state.download_status === 'starting') || (state.download_status === 'downloading' && state.status === 'finished')) && (
|
||||||
|
<IndeterminateProgress indeterminate={true} className="w-full" />
|
||||||
|
)}
|
||||||
|
{(state.download_status === 'downloading' || state.download_status === 'paused') && state.progress && state.status !== 'finished' && (
|
||||||
|
<div className="w-full flex items-center gap-2">
|
||||||
|
<span className="text-sm text-nowrap">{state.progress}%</span>
|
||||||
|
<Progress value={state.progress} />
|
||||||
|
<span className="text-sm text-nowrap">{
|
||||||
|
state.downloaded && state.total
|
||||||
|
? `(${formatFileSize(state.downloaded)} / ${formatFileSize(state.total)})`
|
||||||
|
: null
|
||||||
|
}</span>
|
||||||
|
</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)} ${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">
|
||||||
|
{state.download_status === 'paused' ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-fill"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsResumingDownload(state.download_id, true);
|
||||||
|
try {
|
||||||
|
await resumeDownload(state)
|
||||||
|
// toast.success("Resumed Download", {
|
||||||
|
// description: "Download resumed, it will re-start shortly.",
|
||||||
|
// })
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast.error("Failed to Resume Download", {
|
||||||
|
description: "An error occurred while trying to resume the download.",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsResumingDownload(state.download_id, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={itemActionStates.isResuming || itemActionStates.isCanceling}
|
||||||
|
>
|
||||||
|
{itemActionStates.isResuming ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Resuming
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
Resume
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-fill"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsPausingDownload(state.download_id, true);
|
||||||
|
try {
|
||||||
|
await pauseDownload(state)
|
||||||
|
// toast.success("Paused Download", {
|
||||||
|
// description: "Download paused successfully.",
|
||||||
|
// })
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast.error("Failed to Pause Download", {
|
||||||
|
description: "An error occurred while trying to pause the download."
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsPausingDownload(state.download_id, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={itemActionStates.isPausing || itemActionStates.isCanceling || state.download_status !== 'downloading' || (state.download_status === 'downloading' && state.status === 'finished')}
|
||||||
|
>
|
||||||
|
{itemActionStates.isPausing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Pausing
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Pause className="w-4 h-4" />
|
||||||
|
Pause
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsCancelingDownload(state.download_id, true);
|
||||||
|
try {
|
||||||
|
await cancelDownload(state)
|
||||||
|
toast.success("Canceled Download", {
|
||||||
|
description: "Download canceled successfully.",
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast.error("Failed to Cancel Download", {
|
||||||
|
description: "An error occurred while trying to cancel the download.",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsCancelingDownload(state.download_id, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={itemActionStates.isCanceling || itemActionStates.isResuming || itemActionStates.isPausing || state.download_status === 'starting' || (state.download_status === 'downloading' && state.status === 'finished')}
|
||||||
|
>
|
||||||
|
{itemActionStates.isCanceling ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Canceling
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
Cancel
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-col items-center gap-2 justify-center mt-27">
|
||||||
|
<h4 className="text-4xl font-bold text-muted-foreground/80 dark:text-muted">Nothing!</h4>
|
||||||
|
<p className="text-lg font-semibold text-muted-foreground/50">No Incomplete Downloads</p>
|
||||||
|
<p className="max-w-[50%] text-center text-xs text-muted-foreground/70">You have all caught up! Sit back and relax or just spin up a new download to see here :)</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
|||||||
import { DownloadState } from '@/types/download';
|
import { DownloadState } from '@/types/download';
|
||||||
|
import { DownloadConfiguration } from '@/types/settings';
|
||||||
import { RawVideoInfo } from '@/types/video';
|
import { RawVideoInfo } from '@/types/video';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
interface AppContextType {
|
interface AppContextType {
|
||||||
fetchVideoMetadata: (url: string, formatId?: string) => Promise<RawVideoInfo | null>;
|
fetchVideoMetadata: (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState, downloadConfig?: DownloadConfiguration) => Promise<RawVideoInfo | null>;
|
||||||
startDownload: (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => Promise<void>;
|
startDownload: (url: string, selectedFormat: string, downloadConfig: DownloadConfiguration, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => Promise<void>;
|
||||||
pauseDownload: (state: DownloadState) => Promise<void>;
|
pauseDownload: (state: DownloadState) => Promise<void>;
|
||||||
resumeDownload: (state: DownloadState) => Promise<void>;
|
resumeDownload: (state: DownloadState) => Promise<void>;
|
||||||
cancelDownload: (state: DownloadState) => Promise<void>;
|
cancelDownload: (state: DownloadState) => Promise<void>;
|
||||||
@@ -18,4 +19,4 @@ export const AppContext = createContext<AppContextType>({
|
|||||||
cancelDownload: async () => {}
|
cancelDownload: async () => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAppContext = () => useContext(AppContext);
|
export const useAppContext = () => useContext(AppContext);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Download, Puzzle, Settings, SquarePlay } from "lucide-react";
|
import { Download, Settings, SquarePlay } from "lucide-react";
|
||||||
import { RoutesObj } from "@/types/route";
|
import { RoutesObj } from "@/types/route";
|
||||||
|
|
||||||
export const AllRoutes: Array<RoutesObj> = [
|
export const AllRoutes: Array<RoutesObj> = [
|
||||||
@@ -12,11 +12,6 @@ export const AllRoutes: Array<RoutesObj> = [
|
|||||||
url: "/library",
|
url: "/library",
|
||||||
icon: SquarePlay,
|
icon: SquarePlay,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Extension",
|
|
||||||
url: "/extension",
|
|
||||||
icon: Puzzle,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
url: "/settings",
|
url: "/settings",
|
||||||
|
|||||||
@@ -196,7 +196,15 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
|||||||
eta = $22,
|
eta = $22,
|
||||||
filepath = $23,
|
filepath = $23,
|
||||||
filetype = $24,
|
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`,
|
WHERE download_id = $1`,
|
||||||
[
|
[
|
||||||
downloadState.download_id,
|
downloadState.download_id,
|
||||||
@@ -223,7 +231,15 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
|||||||
downloadState.eta,
|
downloadState.eta,
|
||||||
downloadState.filepath,
|
downloadState.filepath,
|
||||||
downloadState.filetype,
|
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,
|
eta,
|
||||||
filepath,
|
filepath,
|
||||||
filetype,
|
filetype,
|
||||||
filesize
|
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)`,
|
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_id,
|
||||||
downloadState.download_status,
|
downloadState.download_status,
|
||||||
@@ -279,7 +303,15 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
|||||||
downloadState.eta,
|
downloadState.eta,
|
||||||
downloadState.filepath,
|
downloadState.filepath,
|
||||||
downloadState.filetype,
|
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')
|
const db = await Database.load('sqlite:database.db')
|
||||||
return await db.execute(
|
return await db.execute(
|
||||||
'UPDATE downloads SET filepath = $2 WHERE download_id = $1',
|
'UPDATE downloads SET filepath = $2, ext = $3 WHERE download_id = $1',
|
||||||
[download_id, filepath]
|
[download_id, filepath, ext]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,4 +455,4 @@ export const deleteKvPair = async (key: string) => {
|
|||||||
'DELETE FROM kv_store WHERE key = $1',
|
'DELETE FROM kv_store WHERE key = $1',
|
||||||
[key]
|
[key]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ export function useUpdateDownloadStatus() {
|
|||||||
|
|
||||||
export function useUpdateDownloadFilePath() {
|
export function useUpdateDownloadFilePath() {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (data: { download_id: string; filepath: string }) =>
|
mutationFn: (data: { download_id: string; filepath: string, ext: string }) =>
|
||||||
updateDownloadFilePath(data.download_id, data.filepath)
|
updateDownloadFilePath(data.download_id, data.filepath, data.ext)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,4 +64,4 @@ export function useDeleteKvPair() {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (key: string) => deleteKvPair(key)
|
mutationFn: (key: string) => deleteKvPair(key)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, SettingsPageStatesStore } from '@/types/store';
|
import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, LibraryPageStatesStore, LogsStore, SettingsPageStatesStore } from '@/types/store';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
export const useBasePathsStore = create<BasePathsStore>((set) => ({
|
export const useBasePathsStore = create<BasePathsStore>((set) => ({
|
||||||
@@ -15,7 +15,7 @@ export const useDownloadStatesStore = create<DownloadStatesStore>((set) => ({
|
|||||||
const existingIndex = prev.downloadStates.findIndex(
|
const existingIndex = prev.downloadStates.findIndex(
|
||||||
item => item.download_id === state.download_id
|
item => item.download_id === state.download_id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
// Update existing state
|
// Update existing state
|
||||||
const updatedStates = [...prev.downloadStates];
|
const updatedStates = [...prev.downloadStates];
|
||||||
@@ -34,22 +34,66 @@ export const useCurrentVideoMetadataStore = create<CurrentVideoMetadataStore>((s
|
|||||||
isMetadataLoading: false,
|
isMetadataLoading: false,
|
||||||
requestedUrl: '',
|
requestedUrl: '',
|
||||||
autoSubmitSearch: false,
|
autoSubmitSearch: false,
|
||||||
|
searchPid: null,
|
||||||
|
showSearchError: true,
|
||||||
setVideoUrl: (url) => set(() => ({ videoUrl: url })),
|
setVideoUrl: (url) => set(() => ({ videoUrl: url })),
|
||||||
setVideoMetadata: (metadata) => set(() => ({ videoMetadata: metadata })),
|
setVideoMetadata: (metadata) => set(() => ({ videoMetadata: metadata })),
|
||||||
setIsMetadataLoading: (isLoading) => set(() => ({ isMetadataLoading: isLoading })),
|
setIsMetadataLoading: (isLoading) => set(() => ({ isMetadataLoading: isLoading })),
|
||||||
setRequestedUrl: (url) => set(() => ({ requestedUrl: url })),
|
setRequestedUrl: (url) => set(() => ({ requestedUrl: url })),
|
||||||
setAutoSubmitSearch: (autoSubmit) => set(() => ({ autoSubmitSearch: autoSubmit })),
|
setAutoSubmitSearch: (autoSubmit) => set(() => ({ autoSubmitSearch: autoSubmit })),
|
||||||
|
setSearchPid: (pid) => set(() => ({ searchPid: pid })),
|
||||||
|
setShowSearchError: (showError) => set(() => ({ showSearchError: showError }))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((set) => ({
|
export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((set) => ({
|
||||||
|
activeDownloadModeTab: 'selective',
|
||||||
|
activeDownloadConfigurationTab: 'options',
|
||||||
isStartingDownload: false,
|
isStartingDownload: false,
|
||||||
selctedDownloadFormat: 'best',
|
selectedDownloadFormat: 'best',
|
||||||
|
selectedCombinableVideoFormat: '',
|
||||||
|
selectedCombinableAudioFormat: '',
|
||||||
selectedSubtitles: [],
|
selectedSubtitles: [],
|
||||||
selectedPlaylistVideoIndex: '1',
|
selectedPlaylistVideoIndex: '1',
|
||||||
|
downloadConfiguration: {
|
||||||
|
output_format: null,
|
||||||
|
embed_metadata: null,
|
||||||
|
embed_thumbnail: 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 })),
|
setIsStartingDownload: (isStarting) => set(() => ({ isStartingDownload: isStarting })),
|
||||||
setSelctedDownloadFormat: (format) => set(() => ({ selctedDownloadFormat: format })),
|
setSelectedDownloadFormat: (format) => set(() => ({ selectedDownloadFormat: format })),
|
||||||
|
setSelectedCombinableVideoFormat: (format) => set(() => ({ selectedCombinableVideoFormat: format })),
|
||||||
|
setSelectedCombinableAudioFormat: (format) => set(() => ({ selectedCombinableAudioFormat: format })),
|
||||||
setSelectedSubtitles: (subtitles) => set(() => ({ selectedSubtitles: subtitles })),
|
setSelectedSubtitles: (subtitles) => set(() => ({ selectedSubtitles: subtitles })),
|
||||||
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index }))
|
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,
|
||||||
|
custom_command: null
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
setIsErrored: (isErrored) => set(() => ({ isErrored: isErrored })),
|
||||||
|
setIsErrorExpected: (isErrorExpected) => set(() => ({ isErrorExpected: isErrorExpected })),
|
||||||
|
setErroredDownloadId: (downloadId) => set(() => ({ erroredDownloadId: downloadId })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useLibraryPageStatesStore = create<LibraryPageStatesStore>((set) => ({
|
||||||
|
activeTab: 'completed',
|
||||||
|
setActiveTab: (tab) => set(() => ({ activeTab: tab }))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((set) => ({
|
export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((set) => ({
|
||||||
@@ -93,7 +137,9 @@ export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((s
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set) => ({
|
export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set) => ({
|
||||||
activeTab: 'general',
|
activeTab: 'app',
|
||||||
|
activeSubAppTab: 'general',
|
||||||
|
activeSubExtTab: 'install',
|
||||||
appVersion: null,
|
appVersion: null,
|
||||||
isFetchingAppVersion: false,
|
isFetchingAppVersion: false,
|
||||||
ytDlpVersion: null,
|
ytDlpVersion: null,
|
||||||
@@ -105,9 +151,40 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
theme: 'system',
|
theme: 'system',
|
||||||
download_dir: '',
|
download_dir: '',
|
||||||
prefer_video_over_playlist: true,
|
prefer_video_over_playlist: true,
|
||||||
|
strict_downloadablity_check: false,
|
||||||
max_parallel_downloads: 2,
|
max_parallel_downloads: 2,
|
||||||
|
max_retries: 5,
|
||||||
use_proxy: false,
|
use_proxy: false,
|
||||||
proxy_url: '',
|
proxy_url: '',
|
||||||
|
use_rate_limit: false,
|
||||||
|
rate_limit: 1048576, // 1 MB/s
|
||||||
|
video_format: 'auto',
|
||||||
|
audio_format: 'auto',
|
||||||
|
always_reencode_video: false,
|
||||||
|
embed_video_metadata: false,
|
||||||
|
embed_audio_metadata: true,
|
||||||
|
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,
|
||||||
|
// extension settings
|
||||||
websocket_port: 53511
|
websocket_port: 53511
|
||||||
},
|
},
|
||||||
isUsingDefaultSettings: true,
|
isUsingDefaultSettings: true,
|
||||||
@@ -118,6 +195,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
isUpdatingApp: false,
|
isUpdatingApp: false,
|
||||||
appUpdateDownloadProgress: 0,
|
appUpdateDownloadProgress: 0,
|
||||||
setActiveTab: (tab) => set(() => ({ activeTab: tab })),
|
setActiveTab: (tab) => set(() => ({ activeTab: tab })),
|
||||||
|
setActiveSubAppTab: (tab) => set(() => ({ activeSubAppTab: tab })),
|
||||||
|
setActiveSubExtTab: (tab) => set(() => ({ activeSubExtTab: tab })),
|
||||||
setAppVersion: (version) => set(() => ({ appVersion: version })),
|
setAppVersion: (version) => set(() => ({ appVersion: version })),
|
||||||
setIsFetchingAppVersion: (isFetching) => set(() => ({ isFetchingAppVersion: isFetching })),
|
setIsFetchingAppVersion: (isFetching) => set(() => ({ isFetchingAppVersion: isFetching })),
|
||||||
setYtDlpVersion: (version) => set(() => ({ ytDlpVersion: version })),
|
setYtDlpVersion: (version) => set(() => ({ ytDlpVersion: version })),
|
||||||
@@ -137,9 +216,40 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
theme: 'system',
|
theme: 'system',
|
||||||
download_dir: '',
|
download_dir: '',
|
||||||
prefer_video_over_playlist: true,
|
prefer_video_over_playlist: true,
|
||||||
|
strict_downloadablity_check: false,
|
||||||
max_parallel_downloads: 2,
|
max_parallel_downloads: 2,
|
||||||
|
max_retries: 5,
|
||||||
use_proxy: false,
|
use_proxy: false,
|
||||||
proxy_url: '',
|
proxy_url: '',
|
||||||
|
use_rate_limit: false,
|
||||||
|
rate_limit: 1048576, // 1 MB/s
|
||||||
|
video_format: 'auto',
|
||||||
|
audio_format: 'auto',
|
||||||
|
always_reencode_video: false,
|
||||||
|
embed_video_metadata: false,
|
||||||
|
embed_audio_metadata: true,
|
||||||
|
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,
|
||||||
|
// extension settings
|
||||||
websocket_port: 53511
|
websocket_port: 53511
|
||||||
},
|
},
|
||||||
isUsingDefaultSettings: true
|
isUsingDefaultSettings: true
|
||||||
@@ -165,4 +275,11 @@ export const useKvPairsStatesStore = create<KvPairsStatesStore>((set) => ({
|
|||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
setKvPairs: (kvPairs) => set(() => ({ kvPairs }))
|
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;
|
filepath: string | null;
|
||||||
filetype: string | null;
|
filetype: string | null;
|
||||||
filesize: number | 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 {
|
export interface Download {
|
||||||
@@ -65,6 +75,16 @@ export interface Download {
|
|||||||
filepath: string | null;
|
filepath: string | null;
|
||||||
filetype: string | null;
|
filetype: string | null;
|
||||||
filesize: number | 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 {
|
export interface DownloadProgress {
|
||||||
@@ -74,4 +94,4 @@ export interface DownloadProgress {
|
|||||||
downloaded: number | null;
|
downloaded: number | null;
|
||||||
total: number | null;
|
total: number | null;
|
||||||
eta: number | null;
|
eta: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/types/logs.ts
Normal file
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,14 +3,58 @@ export interface SettingsTable {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CustomCommand {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
args: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
ytdlp_update_channel: string;
|
ytdlp_update_channel: string;
|
||||||
ytdlp_auto_update: boolean;
|
ytdlp_auto_update: boolean;
|
||||||
theme: 'dark' | 'light' | 'system';
|
theme: 'dark' | 'light' | 'system';
|
||||||
download_dir: string;
|
download_dir: string;
|
||||||
max_parallel_downloads: number;
|
max_parallel_downloads: number;
|
||||||
|
max_retries: number;
|
||||||
prefer_video_over_playlist: boolean;
|
prefer_video_over_playlist: boolean;
|
||||||
|
strict_downloadablity_check: boolean;
|
||||||
use_proxy: boolean;
|
use_proxy: boolean;
|
||||||
proxy_url: string;
|
proxy_url: string;
|
||||||
|
use_rate_limit: boolean;
|
||||||
|
rate_limit: number;
|
||||||
|
video_format: string;
|
||||||
|
audio_format: string;
|
||||||
|
always_reencode_video: boolean;
|
||||||
|
embed_video_metadata: boolean;
|
||||||
|
embed_audio_metadata: 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;
|
||||||
|
// extension settings
|
||||||
websocket_port: number;
|
websocket_port: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DownloadConfiguration {
|
||||||
|
output_format: string | null;
|
||||||
|
embed_metadata: boolean | null;
|
||||||
|
embed_thumbnail: boolean | null;
|
||||||
|
custom_command: string | null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { DownloadState } from "@/types/download";
|
import { DownloadState } from "@/types/download";
|
||||||
import { RawVideoInfo } from "@/types/video";
|
import { RawVideoInfo } from "@/types/video";
|
||||||
import { Settings } from "@/types/settings";
|
import { DownloadConfiguration, Settings } from "@/types/settings";
|
||||||
import { KvStore } from "@/types/kvStore";
|
import { KvStore } from "@/types/kvStore";
|
||||||
import { Update } from "@tauri-apps/plugin-updater";
|
import { Update } from "@tauri-apps/plugin-updater";
|
||||||
|
import { Log } from "@/types/logs";
|
||||||
|
|
||||||
export interface BasePathsStore {
|
export interface BasePathsStore {
|
||||||
ffmpegPath: string | null;
|
ffmpegPath: string | null;
|
||||||
@@ -23,22 +24,49 @@ export interface CurrentVideoMetadataStore {
|
|||||||
isMetadataLoading: boolean;
|
isMetadataLoading: boolean;
|
||||||
requestedUrl: string;
|
requestedUrl: string;
|
||||||
autoSubmitSearch: boolean;
|
autoSubmitSearch: boolean;
|
||||||
|
searchPid: number | null;
|
||||||
|
showSearchError: boolean;
|
||||||
setVideoUrl: (url: string) => void;
|
setVideoUrl: (url: string) => void;
|
||||||
setVideoMetadata: (metadata: RawVideoInfo | null) => void;
|
setVideoMetadata: (metadata: RawVideoInfo | null) => void;
|
||||||
setIsMetadataLoading: (isLoading: boolean) => void;
|
setIsMetadataLoading: (isLoading: boolean) => void;
|
||||||
setRequestedUrl: (url: string) => void;
|
setRequestedUrl: (url: string) => void;
|
||||||
setAutoSubmitSearch: (autoSubmit: boolean) => void;
|
setAutoSubmitSearch: (autoSubmit: boolean) => void;
|
||||||
|
setSearchPid: (pid: number | null) => void;
|
||||||
|
setShowSearchError: (showError: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloaderPageStatesStore {
|
export interface DownloaderPageStatesStore {
|
||||||
|
activeDownloadModeTab: string;
|
||||||
|
activeDownloadConfigurationTab: string;
|
||||||
isStartingDownload: boolean;
|
isStartingDownload: boolean;
|
||||||
selctedDownloadFormat: string;
|
selectedDownloadFormat: string;
|
||||||
|
selectedCombinableVideoFormat: string;
|
||||||
|
selectedCombinableAudioFormat: string;
|
||||||
selectedSubtitles: string[];
|
selectedSubtitles: string[];
|
||||||
selectedPlaylistVideoIndex: string;
|
selectedPlaylistVideoIndex: string;
|
||||||
|
downloadConfiguration: DownloadConfiguration;
|
||||||
|
isErrored: boolean;
|
||||||
|
isErrorExpected: boolean;
|
||||||
|
erroredDownloadId: string | null;
|
||||||
|
setActiveDownloadModeTab: (tab: string) => void;
|
||||||
|
setActiveDownloadConfigurationTab: (tab: string) => void;
|
||||||
setIsStartingDownload: (isStarting: boolean) => void;
|
setIsStartingDownload: (isStarting: boolean) => void;
|
||||||
setSelctedDownloadFormat: (format: string) => void;
|
setSelectedDownloadFormat: (format: string) => void;
|
||||||
|
setSelectedCombinableVideoFormat: (format: string) => void;
|
||||||
|
setSelectedCombinableAudioFormat: (format: string) => void;
|
||||||
setSelectedSubtitles: (subtitles: string[]) => void;
|
setSelectedSubtitles: (subtitles: string[]) => void;
|
||||||
setSelectedPlaylistVideoIndex: (index: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryPageStatesStore {
|
||||||
|
activeTab: string;
|
||||||
|
setActiveTab: (tab: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadActionStatesStore {
|
export interface DownloadActionStatesStore {
|
||||||
@@ -58,6 +86,8 @@ export interface DownloadActionStatesStore {
|
|||||||
|
|
||||||
export interface SettingsPageStatesStore {
|
export interface SettingsPageStatesStore {
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
|
activeSubAppTab: string;
|
||||||
|
activeSubExtTab: string;
|
||||||
appVersion: string | null;
|
appVersion: string | null;
|
||||||
isFetchingAppVersion: boolean;
|
isFetchingAppVersion: boolean;
|
||||||
ytDlpVersion: string | null;
|
ytDlpVersion: string | null;
|
||||||
@@ -72,6 +102,8 @@ export interface SettingsPageStatesStore {
|
|||||||
isUpdatingApp: boolean;
|
isUpdatingApp: boolean;
|
||||||
appUpdateDownloadProgress: number;
|
appUpdateDownloadProgress: number;
|
||||||
setActiveTab: (tab: string) => void;
|
setActiveTab: (tab: string) => void;
|
||||||
|
setActiveSubAppTab: (tab: string) => void;
|
||||||
|
setActiveSubExtTab: (tab: string) => void;
|
||||||
setAppVersion: (version: string | null) => void;
|
setAppVersion: (version: string | null) => void;
|
||||||
setIsFetchingAppVersion: (isFetching: boolean) => void;
|
setIsFetchingAppVersion: (isFetching: boolean) => void;
|
||||||
setYtDlpVersion: (version: string | null) => void;
|
setYtDlpVersion: (version: string | null) => void;
|
||||||
@@ -93,4 +125,11 @@ export interface KvPairsStatesStore {
|
|||||||
kvPairs: KvStore
|
kvPairs: KvStore
|
||||||
setKvPairsKey: (key: string, value: unknown) => void;
|
setKvPairsKey: (key: string, value: unknown) => void;
|
||||||
setKvPairs: (kvPairs: KvStore) => void;
|
setKvPairs: (kvPairs: KvStore) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogsStore {
|
||||||
|
logs: Log[];
|
||||||
|
setLogs: (logs: Log[]) => void;
|
||||||
|
addLog: (log: Log) => void;
|
||||||
|
clearLogs: () => void;
|
||||||
}
|
}
|
||||||
140
src/utils.ts
140
src/utils.ts
@@ -21,32 +21,130 @@ export function getRouteName(location: string, routes: Array<RoutesObj> = AllRou
|
|||||||
return lastPart ? lastPart.toUpperCase() : 'Dashboard';
|
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 => {
|
export const parseProgressLine = (line: string): DownloadProgress => {
|
||||||
const progress: Partial<DownloadProgress> = {
|
const progress: Partial<DownloadProgress> = {
|
||||||
status: 'downloading'
|
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 => {
|
line.split(',').forEach(pair => {
|
||||||
const [key, value] = pair.split(':');
|
const [key, value] = pair.split(':');
|
||||||
switch (key) {
|
if (key && value) {
|
||||||
case 'status':
|
switch (key.trim()) {
|
||||||
progress.status = value.trim();
|
case 'status':
|
||||||
break;
|
progress.status = value.trim();
|
||||||
case 'progress':
|
break;
|
||||||
progress.progress = parseFloat(value.replace('%', '').trim());
|
case 'progress':
|
||||||
break;
|
progress.progress = parseFloat(value.replace('%', '').trim());
|
||||||
case 'speed':
|
break;
|
||||||
progress.speed = parseFloat(value);
|
case 'speed':
|
||||||
break;
|
progress.speed = parseFloat(value);
|
||||||
case 'downloaded':
|
break;
|
||||||
progress.downloaded = parseInt(value, 10);
|
case 'downloaded':
|
||||||
break;
|
progress.downloaded = parseInt(value, 10);
|
||||||
case 'total':
|
break;
|
||||||
progress.total = parseInt(value, 10);
|
case 'total':
|
||||||
break;
|
progress.total = parseInt(value, 10);
|
||||||
case 'eta':
|
break;
|
||||||
progress.eta = parseInt(value, 10);
|
case 'eta':
|
||||||
break;
|
if (value.trim() !== 'NA') {
|
||||||
|
progress.eta = parseInt(value, 10);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,6 +209,10 @@ export const formatCodec = (codec: string) => {
|
|||||||
return codec.toUpperCase();
|
return codec.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const generateID = () => {
|
||||||
|
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export const generateDownloadId = (videoId: string, host: string) => {
|
export const generateDownloadId = (videoId: string, host: string) => {
|
||||||
host = host.trim().split('.')[0];
|
host = host.trim().split('.')[0];
|
||||||
return `${host}_${videoId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
return `${host}_${videoId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
/* Paths */
|
/* Paths */
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ const host = process.env.TAURI_DEV_HOST;
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 1024, // 1MB
|
||||||
|
},
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
// 1. prevent vite from obscuring rust errors
|
// 1. prevent vite from obscuring rust errors
|
||||||
|
|||||||
Reference in New Issue
Block a user