1
1
mirror of https://github.com/neosubhamoy/neodlp.git synced 2026-03-27 20:45:49 +05:30

117 Commits

186 changed files with 15212 additions and 12252 deletions

2
.gitattributes vendored
View File

@@ -1,3 +1 @@
* text=auto eol=lf * text=auto eol=lf
src-tauri/binaries/* filter=lfs diff=lfs merge=lfs -text

View File

@@ -1,6 +1,4 @@
on: on: workflow_dispatch
release:
types: [published]
name: 🚀 Publish to AUR name: 🚀 Publish to AUR
jobs: jobs:
@@ -27,11 +25,11 @@ jobs:
# If manually triggered, fetch latest release # If manually triggered, fetch latest release
RELEASE_TAG=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r '.tag_name') RELEASE_TAG=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r '.tag_name')
fi fi
# Extract version number from tag # Extract version number from tag
VERSION=$(echo "$RELEASE_TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$/\1/') VERSION=$(echo "$RELEASE_TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$/\1/')
SUFFIX=$(echo "$RELEASE_TAG" | sed -E 's/^v[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/\1/') SUFFIX=$(echo "$RELEASE_TAG" | sed -E 's/^v[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/\1/')
echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "suffix=$SUFFIX" >> $GITHUB_OUTPUT echo "suffix=$SUFFIX" >> $GITHUB_OUTPUT
@@ -39,15 +37,15 @@ jobs:
- name: 🔑 Setup SSH for AUR - name: 🔑 Setup SSH for AUR
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
# Write key with proper newline handling # Write key with proper newline handling
echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" | sed 's/\\n/\n/g' > ~/.ssh/id_rsa echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" | sed 's/\\n/\n/g' > ~/.ssh/id_rsa
# Set proper permissions # Set proper permissions
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts chmod 600 ~/.ssh/known_hosts
# Create SSH config file # Create SSH config file
cat > ~/.ssh/config << EOF cat > ~/.ssh/config << EOF
Host aur.archlinux.org Host aur.archlinux.org
@@ -66,7 +64,7 @@ jobs:
git config --global user.name "${{ secrets.AUR_USER }}" git config --global user.name "${{ secrets.AUR_USER }}"
git config --global user.email "${{ secrets.AUR_EMAIL }}" git config --global user.email "${{ secrets.AUR_EMAIL }}"
git config --global --add safe.directory '*' git config --global --add safe.directory '*'
# Clone AUR repository # Clone AUR repository
GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=accept-new" \ GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=accept-new" \
git clone "ssh://aur@aur.archlinux.org/neodlp.git" aur-repo git clone "ssh://aur@aur.archlinux.org/neodlp.git" aur-repo
@@ -77,21 +75,21 @@ jobs:
# Update PKGBUILD version # Update PKGBUILD version
sed -i "s/pkgver=.*/pkgver=${VERSION}/" PKGBUILD sed -i "s/pkgver=.*/pkgver=${VERSION}/" PKGBUILD
# Create non-root user for makepkg (which refuses to run as root) # Create non-root user for makepkg (which refuses to run as root)
useradd -m builder useradd -m builder
chown -R builder:builder . chown -R builder:builder .
# Generate .SRCINFO using makepkg # Generate .SRCINFO using makepkg
su builder -c "makepkg --printsrcinfo" > .SRCINFO su builder -c "makepkg --printsrcinfo" > .SRCINFO
# Debug output # Debug output
echo "PKGBUILD:" echo "PKGBUILD:"
cat PKGBUILD cat PKGBUILD
echo ".SRCINFO:" echo ".SRCINFO:"
cat .SRCINFO cat .SRCINFO
# Check if there are any changes to commit # Check if there are any changes to commit
if [ -n "$(git status --porcelain)" ]; then if [ -n "$(git status --porcelain)" ]; then
echo "Changes detected, committing and pushing..." echo "Changes detected, committing and pushing..."
@@ -108,4 +106,4 @@ jobs:
- name: 🔍 Verify update - name: 🔍 Verify update
run: | run: |
echo "Successfully updated AUR package to version ${{ steps.release_info.outputs.version }}${{ steps.release_info.outputs.suffix }}" echo "Successfully updated AUR package to version ${{ steps.release_info.outputs.version }}${{ steps.release_info.outputs.suffix }}"
echo "View the updated package at: https://aur.archlinux.org/packages/neodlp" echo "View the updated package at: https://aur.archlinux.org/packages/neodlp"

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: neosubhamoy
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

40
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

View 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
View 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]

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
.github/images/completed-downloads.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
.github/images/downloader.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
.github/images/flathub/downloader.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
.github/images/flathub/settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 541 KiB

After

Width:  |  Height:  |  Size: 541 KiB

BIN
.github/images/ongoing-downloads.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
.github/images/settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -25,9 +25,7 @@ jobs:
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: 🛠️ Install dependencies - name: 🛠️ Install dependencies
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
@@ -36,7 +34,7 @@ jobs:
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'
@@ -54,6 +52,9 @@ jobs:
- name: 🛠️ Install frontend dependencies - name: 🛠️ Install frontend dependencies
run: npm install run: npm install
- name: 📥 Download binaries
run: npm run download
- name: 📄 Read and Process CHANGELOG (Unix) - name: 📄 Read and Process CHANGELOG (Unix)
if: matrix.platform != 'windows-latest' if: matrix.platform != 'windows-latest'
id: changelog_unix id: changelog_unix

19
.gitignore vendored
View File

@@ -1,3 +1,14 @@
node_modules
dist
dist-ssr
*.local
.github/workflows/.secrets
/target/
src-tauri/binaries/*
!src-tauri/binaries/.gitkeep
src-tauri/resources/downloads/*
!src-tauri/resources/downloads/.gitkeep
# Logs # Logs
logs logs
*.log *.log
@@ -7,12 +18,6 @@ yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.github/workflows/.secrets
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
@@ -22,4 +27,4 @@ dist-ssr
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?

View File

@@ -1,15 +1,19 @@
### ✨ Changelog ### ✨ Changelog
- Fixed audio video not merging on macOS - Added filename sanitization settings
- Removed bundling ffmpeg and ffprobe with linux (deb, rpm) builds as it conflicts (comes pre-installed) on lots of distros (ffmpeg is a dependency now which will be auto installed by your package manager) - Fixed po-token server process not terminating on app update
- Added linux appimage builds (appimage builds does not support neodlp browser integration features due to their sandboxed nature) - Fixed yt-dlp auto-update on linux flatpak
- Fixed potoken server on linux flatpak
- Improved linux package dependencies
- Other minor fixes and improvements - Other minor fixes and improvements
### 📝 Notes ### 📝 Notes
> **🔴 DANGER:** 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. > [!CAUTION]
> Users are always adviced to complete/cancel all paused downloads before updating to a newer version, otherwise paused downloads may not resume properly and 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) > [!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 neodlp, which You can also disable from neodlp settings if you don't want to auto-update yt-dlp) (ignore this if you are installing AppImage/Flatpak)
> 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)
@@ -17,25 +21,29 @@
### 📦 Shipped Binaries ### 📦 Shipped Binaries
| yt-dlp (updateable) | ffmpeg | ffprobe | aria2c | deno | | yt-dlp (updateable) | ffmpeg | aria2c | deno | bgutil-pot-rs |
| :---- | :---- | :---- | :---- | :---- | | :---- | :---- | :---- | :---- | :---- |
| v2025.10.11.232807 (nightly) | v7.1.1 | v7.1.1 | v1.37.0 | v2.5.4 | | v2026.03.21.233500 (nightly) | v8.0.1 | v1.37.0 | v2.7.8 | v0.8.1 |
> ‼️ 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) > ‼️ Linux builds (deb, rpm) does not ships with `ffmpeg` and `aria2c` (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) > ‼️ 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) (though it will be auto installed as a dependency if you install neodlp via Homebrew)
### ⬇️ Download Section ### ⬇️ Download Section
| Arch\OS | Windows (msi) ⬆️ | Windows (exe) ⬆️ | Linux (deb) | Linux (rpm) | Linux (appimage) ⬆️ | MacOS (dmg) ⬆️ | MacOS (app) ⬆️ | | Architecture | Windows (msi) ⬆️ | Windows (exe) ⬆️ | Linux (deb) | Linux (rpm) | Linux (AppImage) ⬆️ | MacOS (dmg) ⬆️ | MacOS (app) ⬆️ |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- |
| x86_64 | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64_en-US.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>_amd64.Appimage) | [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.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>_amd64.AppImage) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64.dmg) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_x64.app.tar.gz) |
| ARM64 | N/A | 🪟 [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>_arm64.deb) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP-<version>-1.aarch64.rpm) | 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.exe) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_arm64.deb) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP-<version>-1.aarch64.rpm) | 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) |
> ⬆️ icon indicates this packaging format supports in-built app-updater > ⬆️ icon indicates this packaging format supports in-built app-updater
> 🪟 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) > 🪟 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 does not support neodlp's browser intergration features due to it's sandboxed nature (it is highly recommended to use native (deb, rpm, AUR) builds if possible for the full experiance, otherwise appimages are good for portable usage) > 🚫 Linux AppImage builds are experimental and does not support neodlp's browser intergration features and yt-dlp updates due to it's limitations. 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 > ⚠️ 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, There are few ways you can bypass these restrictions:
> 1. Using [Homebrew](https://neodlp.neosubhamoy.com/download) (Recommended)
> 2. Using our automated [Curl-Bash Installer](https://neodlp.neosubhamoy.com/download)
> 3. You can also manually remove the .dmg file/.app folder from macOS quarantine using these commands: `xattr -d com.apple.quarantine NeoDLP_x.x.x_aarch64.dmg` (for .dmg file) -OR- `xattr -r -d com.apple.quarantine /Applications/NeoDLP.app` (for .app folder)
> 4. Or you can [compile NeoDLP from source](https://github.com/neosubhamoy/neodlp?tab=readme-ov-file#%EF%B8%8F-building-from-source) in your Mac (Then you don't have to download the pre-compiled binaries at all, though it is a much longer process and is intended for advanced users only)

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 Subhamoy Biswas Copyright (c) 2025 - Present Subhamoy Biswas
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

207
README.md
View File

@@ -1,90 +1,110 @@
![NeoDLP](./.github/banner.svg) ![NeoDLP](./.github/images/banner.svg)
# 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 based on YT-DLP with Modern UI and Browser Integration
[![status](https://img.shields.io/badge/status-active-brightgreen.svg?style=flat)](https://github.com/neosubhamoy/neodlp) [![github release](https://img.shields.io/github/v/release/neosubhamoy/neodlp?color=lime-green&style=for-the-badge)](https://github.com/neosubhamoy/neodlp/releases/latest)
[![github tag](https://img.shields.io/github/v/tag/neosubhamoy/neodlp?color=yellow)](https://github.com/neosubhamoy/neodlp) [![github downloads](https://img.shields.io/github/downloads/neosubhamoy/neodlp/total?style=for-the-badge)](https://github.com/neosubhamoy/neodlp/releases)
[![github downloads](https://img.shields.io/github/downloads/neosubhamoy/neodlp/total)](https://github.com/neosubhamoy/neodlp/releases) [![github stars](https://img.shields.io/github/stars/neosubhamoy/neodlp?color=yellow&style=for-the-badge)](https://github.com/neosubhamoy/neodlp/stargazers)
[![PRs](https://img.shields.io/badge/PRs-welcome-blue.svg?style=flat)](https://github.com/neosubhamoy/neodlp) [![github license](https://img.shields.io/github/license/neosubhamoy/neodlp?color=blue&style=for-the-badge)](https://github.com/neosubhamoy/neodlp/blob/main/LICENSE)
> **🥰 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!** > [!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!**
[![Packaging status](https://repology.org/badge/vertical-allrepos/neodlp.svg)](https://repology.org/project/neodlp/versions) [![winget version](https://img.shields.io/winget/v/neosubhamoy.neodlp?color=lime-green&style=flat-square)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/n/neosubhamoy/neodlp)
[![flathub version](https://img.shields.io/flathub/v/com.neosubhamoy.neodlp?color=lime-green&style=flat-square)](https://flathub.org/en/apps/com.neosubhamoy.neodlp)
[![aur version](https://img.shields.io/aur/version/neodlp?color=lime-green&style=flat-square)](https://aur.archlinux.org/packages/neodlp)
### ✨ 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)) ## ✨ Highlighted Features
- Download Video/Audio from thousands of popular sites (YT, FB, IG, X and other 2.5k+ [supported sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md))
- Fully Configured YT-DLP Environment Out-of-the-Box (with JS Runtime, PO Token Server, Real-Time Logs etc.)
- Download Video/Audio in your preffered format (MP4, WEBM, MKV, MP3 etc.) - Download Video/Audio in your preffered format (MP4, WEBM, MKV, MP3 etc.)
- Supports both Video and Playlist download - Supports both Video and Playlist/Batch download
- Supports Combining Video, Audio streams of your choice - Supports Combining Video, Audio streams of your choice
- Supports Multi-Language Subtitle/Caption (CC) embeding - Supports Multi-Lingual Subtitle/Caption (CC) embeding
- Different Video/Audio metadata embeding options (info, chapters, thumbnail etc.) - Different Video/Audio metadata embeding options (info, chapters, thumbnail etc.)
- SponsorBlock support (mark/remove video segments) - SponsorBlock support (mark/remove video segments)
- Aria2 support (for blazing fast downloads)
- Network controls (proxy, rate limit etc.) - Network controls (proxy, rate limit etc.)
- Highly customizable and many more...😉 - Highly customizable and many more...😉
### 🧩 Browser Integration ## 🧩 Browser Integration
You can integrate NeoDLP with your favourite browser (any Chrome/Chromium/Firefox based browser) Just, install [NeoDLP Extension](https://github.com/neosubhamoy/neodlp-extension) to get started! You can integrate NeoDLP with your favourite browser (any 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: 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) - 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) - Right Click Context Menu Action (Search with Neo Downloader Plus - Link, Selection, Media Source)
### 👀 Sneak Peek ## 👀 Sneak Peek
![NeoDLP-Mockup](./.github/mockup.svg) ![NeoDLP-Mockup](./.github/images/mockup.svg)
### 💻 Supported Platforms | Downloader | Completed Downloads | Ongoing Downloads | Settings |
| :---- | :---- | :---- | :---- |
| ![Downloader](./.github/images/downloader.png) | ![Completed-Downloads](./.github/images/completed-downloads.png) | ![Ongoing-Downloads](./.github/images/ongoing-downloads.png) | ![Settings](./.github/images/settings.png) |
## 💻 Supported Platforms
- Windows (10 / 11) - Windows (10 / 11)
- Linux (Debian / Fedora / RHEL / SUSE / Arch Linux base) - Linux (Mostly all modern distros)
- MacOS (>11) - MacOS (>=11)
### 🤝 External Dependencies ## 🤝 External Dependencies
- [YT-DLP](https://github.com/yt-dlp/yt-dlp) - The core CLI tool used to download video/audio from the web (Hero of the show 😎) - [YT-DLP](https://github.com/yt-dlp/yt-dlp) [Unlicense] - 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 - [FFmpeg & FFprobe](https://www.ffmpeg.org) [LGPLv2.1+] - 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) - [Aria2](https://aria2.github.io) [GPLv2+] - 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)) - [Deno](https://deno.com) [MIT] - 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))
- [BgUtils POT Provider (Rust)](https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs) [GPLv3+] - Provides PO (Proof-of-Origin) Token for YT downloads
### System Pre-Requirements ## System Pre-Requirements
- **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) - **Windows:** [Microsoft Visual C++ Redistributable 2015+](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) `winget install Microsoft.VCRedist.2015+.x64` (Will be auto-installed if you install NeoDLP via winget)
- **MacOS:** XCode Command Line Tools `xcode-select --install` (Mostly, comes pre-installed on modern macos, still if you encounter any issue then try installing it manually) - **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)) - **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 (listed below) 1. Download the latest NeoDLP release based on your OS and CPU Architecture, then install it! -OR- Install it directly from an available distribution channel (listed below)
| Arch\OS | Windows | Linux | MacOS | | Architecture | Windows | Linux | MacOS |
| :---- | :---- | :---- | :---- | | :---- | :---- | :---- | :---- |
| x86_64 | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) | | 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 | ✅ Emulation | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) | ✅ [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) > [!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 / ARM64 | WinGet | `winget install neodlp` | | Windows x86_64 / ARM64 | WinGet | `winget install neosubhamoy.neodlp` |
| MacOS x86_64 / ARM64 | Homebrew | `brew install neosubhamoy/tap/neodlp` |
| MacOS x86_64 / ARM64 | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/macos_installer.sh \| bash` | | MacOS x86_64 / ARM64 | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/macos_installer.sh \| bash` |
| Linux x86_64 / ARM64 | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/linux_installer.sh \| bash` | | Linux x86_64 / ARM64 (Flatpak) | Flathub | `flatpak install flathub com.neosubhamoy.neodlp` |
| Arch Linux x86_64 / ARM64 | AUR | `yay -S neodlp` | | Linux x86_64 / ARM64 (Native) | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/linux_installer.sh \| bash` |
| Arch Linux x86_64 / ARM64 (Native) | AUR | `yay -S neodlp` or `paru -S neodlp` |
### 🧪 Package Testing Status ## 🧪 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. 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 😊)
<details>
<summary>Test Coverage</summary>
| Platform | Status | Platform | Status | | Platform | Status | Platform | Status |
| :---- | :---- | :---- | :---- | | :---- | :---- | :---- | :---- |
| Windows 10 (x64) | ✅ Tested | Windows 10 (ARM64) | ⚠️ Untested | | Windows 10 (x64) | ✅ Tested | Windows 10 (ARM64) | ⚠️ Untested |
| Windows 11 (x64) | ✅ Tested | Windows 11 (ARM64) | ✅ Tested | | Windows 11 (x64) | ✅ Tested | Windows 11 (ARM64) | ✅ Tested |
| MacOS 14 (x64) | ✅ Tested | MacOS 14 (ARM64) | ⚠️ Untested | | MacOS 14 (x64) | ✅ Tested | MacOS 14 (ARM64) | ✅ Tested |
| MacOS 15 (x64) | ⚠️ Untested | MacOS 15 (ARM64) | ✅ Tested | | MacOS 15 (x64) | ⚠️ Untested | MacOS 15 (ARM64) | ✅ Tested |
| MacOS 26 (x64) | ⚠️ Untested | MacOS 26 (ARM64) | ✅ Tested | | MacOS 26 (x64) | ⚠️ Untested | MacOS 26 (ARM64) | ✅ Tested |
| Ubuntu 24.04 LTS (x64) | ✅ Tested | Ubuntu 24.04 LTS (ARM64) | ⚠️ Untested | | Ubuntu 24.04 LTS (x64) | ✅ Tested | Ubuntu 24.04 LTS (ARM64) | ⚠️ Untested |
@@ -93,82 +113,91 @@ Though NeoDLP is supported on most platforms but not all packages are tested on
| openSUSE 16 (x64) | ⚠️ Untested | openSUSE 16 (ARM64) | ⚠️ Untested | | openSUSE 16 (x64) | ⚠️ Untested | openSUSE 16 (ARM64) | ⚠️ Untested |
| RHEL 10 (x64) | ⚠️ Untested | RHEL 10 (ARM64) | ⚠️ Untested | | RHEL 10 (x64) | ⚠️ Untested | RHEL 10 (ARM64) | ⚠️ Untested |
### 💝 Support the Development </details>
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...🤗 ## 🪜 Roadmap
<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 support for yt-dlp
- [x] Add basic settings and customization - [x] Add basic settings and customization
- [x] Integrate with browsers - [x] Integrate with browsers
- [x] Add aria2c support - [x] Add aria2c support
- [ ] Add more advanced settings and achive stability **(ongoing)** - [x] Add custom command support
- [ ] Add media converter - [x] Add full-playlist/batch download support
- [ ] Add multiple downloader engines - [ ] Improve browser integration **(ongoing)**
- [ ] Add advanced web extractor - [ ] Implement NeoDLP API
- [ ] Build web interface
- [ ] Implement plugin system
- [ ] Add more cool stuffs 😉 - [ ] Add more cool stuffs 😉
### ⚡ Technologies Used ## ⚡ Technologies Used
![Tauri](https://img.shields.io/badge/tauri-%2324C8DB.svg?style=for-the-badge&logo=tauri&logoColor=%23FFFFFF) [![Tauri](https://img.shields.io/badge/tauri-%2324C8DB.svg?style=for-the-badge&logo=tauri&logoColor=%23FFFFFF)](https://tauri.app)
![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white) [![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white)](https://rust-lang.org)
![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) [![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)](https://react.dev)
![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) [![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
![ShadCnUi](https://img.shields.io/badge/shadcn%2Fui-000000?style=for-the-badge&logo=shadcnui&logoColor=white) [![ShadCnUi](https://img.shields.io/badge/shadcn%2Fui-000000?style=for-the-badge&logo=shadcnui&logoColor=white)](https://ui.shadcn.com)
### 🛠️ Contributing / Building from Source ## 🛠️ 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 build/compile NeoDLP from the source code? Follow these simple steps to create a production build:
* Make sure to install [Rust](https://www.rust-lang.org/tools/install), [Node.js](https://nodejs.org/en), [Git](https://git-scm.com/downloads) and [Git-LFS](https://git-lfs.com/) before proceeding. * Make sure to install [Rust](https://www.rust-lang.org/tools/install), [Node.js](https://nodejs.org/en), and [Git](https://git-scm.com/downloads) before proceeding.
* Install [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS / platform * Install [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS / platform
1. Fork this repo in your github account. 1. Clone this repo in your local machine: `git clone https://github.com/neosubhamoy/neodlp.git`
2. Git clone the forked repo in your local machine. 2. Go inside the cloned project directory: `cd neodlp`
3. Create a git branch (related to the feature you are working on) (Optional - Recommended) 3. Install Node.js dependencies: `npm install`
4. Install Node.js dependencies: `npm install` 4. Download required external binaries (for your platform): `npm run download`
5. Run development / build process 5. Run build process (run the command based on your platform and architecture)
> ⚠️ **IMPORTANT:** Make sure to run the build command once before running the dev command for the first time to avoid compile time errors ```shell
```code # command for windows users
# for windows users npm run tauri build # for both x64/ARM64 devices
npm run tauri dev # for development
npm run tauri build # for production build
# for linux users # commands for linux users
npm run tauri dev -- --config "./src-tauri/tauri.linux-aarch64.conf.json" # for ARM64 devices, development npm run tauri:build:linux-x64 # for x64 devices
npm run tauri build -- --config "./src-tauri/tauri.linux-aarch64.conf.json" # for ARM64 devices, production build npm run tauri:build:linux-arm64 # for ARM64 devices
npm run tauri dev -- --config "./src-tauri/tauri.linux-x86_64.conf.json" # for x64 devices, development # commands for macOS users
npm run tauri build -- --config "./src-tauri/tauri.linux-x86_64.conf.json" # for x64 devices, production build npm run tauri:build:macos-arm64 # for apple silicon macs
npm run tauri:build:macos-x64 # for intel x86 macs
# 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 build -- --config "./src-tauri/tauri.macos-aarch64.conf.json" # for apple silicon macs, production build
npm run tauri dev -- --config "./src-tauri/tauri.macos-x86_64.conf.json" # for intel x86 macs, development
npm run tauri build -- --config "./src-tauri/tauri.macos-x86_64.conf.json" # for intel x86 macs, production build
``` ```
6. Do the changes, Send a Pull Request with proper Description (NOTE: Pull Requests Without Proper Description will be Rejected) 6. Give it the time to compile (~5-10min) (if you get an error, something like this at the end: `Error A public key has been found, but no private key. Make sure to set 'TAURI_SIGNING_PRIVATE_KEY' environment variable.` simply ignore it! Your build is successfull!). You can find the compiled packages under: `src-tauri/target/release/bundle` directory.
### ⭕ Bug Report ## 🐞 Bug Report and Discussions
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...!! Noticed any Bug? or Want to give us some suggetions? Always feel free to let us know! We would love to hear from you...!! You can reach us out via the following methods:
### 💫 Credits - GitHub Issues (Recommended): [Report a Bug](https://github.com/neosubhamoy/neodlp/issues/new?template=bug_report.md) -OR- [Request a Feature](https://github.com/neosubhamoy/neodlp/issues/new?template=feature_request.md)
- Mailing List: If you prefer the good old mailing list way, You can just simply write us on [support@neodlp.neosubhamoy.com](mailto:support@neodlp.neosubhamoy.com) (Kindly follow the Bug Report/Feature Request Template on that case)
- Reddit Community: If you have any other general pourpose query/discussion related to NeoDLP, post it on our subreddit community [r/NeoDLP](https://www.reddit.com/r/NeoDLP)
## 📦 Sources
- [Official Website](https://neodlp.neosubhamoy.com)
- Official Repositories
- [GitHub (Primary)](https://github.com/neosubhamoy/neodlp)
- [Gitea (Mirror)](https://gitea.neosubhamoy.com/neosubhamoy/neodlp)
- [SourceForge (Releases Only)](https://sourceforge.net/projects/neodlp)
- Official Distribution Channels
- [WinGet (for Windows)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/n/neosubhamoy/neodlp)
- [Flathub (for Linux)](https://flathub.org/en/apps/com.neosubhamoy.neodlp)
- [AUR (for Arch Linux)](https://aur.archlinux.org/packages/neodlp)
- Related Projects
- [NeoDLP Extension](https://github.com/neosubhamoy/neodlp-extension)
- [NeoDLP Website](https://github.com/neosubhamoy/neodlp-website)
## 💫 Credits
- NeoDLP is made possible by the joint efforts of [yt-dlp](https://github.com/yt-dlp/yt-dlp) and [FFmpeg](https://www.ffmpeg.org). Lots of NeoDLP features are actually powered by these tools under the hood! So huge thanks to all the developers/contributers for making these great tools! 🙏
- NeoDLP's 'Format Selection' options are inspired from the [Seal](https://github.com/JunkFood02/Seal) app by [@JunkFood02](https://github.com/JunkFood02) - 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) - Aria2 Linux x86_64 static binaries are built by [@asdo92](https://github.com/asdo92/aria2-static-builds)
- NeoDLP's 'POT Server' is based on [@jim60105's Rust Implementation](https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs) of [Brainicism/bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider)
### 📝 License ## ⚖️ License and Usage
NeoDLP is Licensed under the [MIT license](https://github.com/neosubhamoy/neodlp/blob/main/LICENSE). Anyone can view, modify, use (personal and commercial) or distribute it's sources without any attribution and extra permissions. NeoDLP is a Fully Open-Source Software 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 extra permission (Just include the LICENSE file :)
> [!WARNING]
> NeoDLP facilitates downloading from various Online Platforms with different Policies and Terms of Use which Users must follow. We strictly do not promote any unauthorized downloading of copyrighted content. NeoDLP is only made for downloading content that the user holds the copyright to or has the authority for. Users must use the downloaded content wisely and solely at their own legal responsibility. The developer is not responsible for any action taken by the user, and takes zero direct or indirect liability for that matter.
**** ****
An Open Sourced Project - Developed with ❤️ by **Subhamoy** An Open Sourced Project - Developed with ❤️ by **Subhamoy**

View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=NeoDLP
Comment=Modern feature-rich video/audio downloader based on yt-dlp.
Icon=com.neosubhamoy.neodlp
Exec=neodlp
Terminal=false
Categories=Utility;
Keywords=neodlp;downloader;yt-dlp-gui;

View File

@@ -0,0 +1,53 @@
<?xml version='1.0' encoding='UTF-8'?>
<component type="desktop-application">
<id>com.neosubhamoy.neodlp</id>
<name>NeoDLP</name>
<summary>Modern feature-rich video/audio downloader based on yt-dlp</summary>
<developer id="com.neosubhamoy">
<name>Subhamoy Biswas</name>
</developer>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
<url type="homepage">https://neodlp.neosubhamoy.com</url>
<url type="vcs-browser">https://github.com/neosubhamoy/neodlp</url>
<url type="bugtracker">https://github.com/neosubhamoy/neodlp/issues</url>
<description>
<p>
NeoDLP is a cross-platform desktop application designed for downloading videos and audio from various online sources based on yt-dlp.
It offers modern user interface, lots of features and customization options.
</p>
</description>
<launchable type="desktop-id">com.neosubhamoy.neodlp.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/neosubhamoy/neodlp/main/.github/images/flathub/downloader.png</image>
<caption>Downloader page of NeoDLP</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/neosubhamoy/neodlp/main/.github/images/flathub/completed-downloads.png</image>
<caption>Completed downloads page of NeoDLP</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/neosubhamoy/neodlp/main/.github/images/flathub/ongoing-downloads.png</image>
<caption>Ongoing downloads page of NeoDLP</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/neosubhamoy/neodlp/main/.github/images/flathub/settings.png</image>
<caption>Settings page of NeoDLP</caption>
</screenshot>
</screenshots>
<branding>
<color type="primary" scheme_preference="light">#404559</color>
<color type="primary" scheme_preference="dark">#404559</color>
</branding>
<content_rating type="oars-1.1" />
<releases>
<release version="0.4.4" date="2026-03-27">
<url type="details">https://github.com/neosubhamoy/neodlp/releases/tag/v0.4.4</url>
</release>
<release version="0.4.3" date="2026-03-07">
<url type="details">https://github.com/neosubhamoy/neodlp/releases/tag/v0.4.3</url>
</release>
</releases>
</component>

View File

@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/neodlp.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NeoDLP</title> <title>NeoDLP</title>
</head> </head>

View File

@@ -1,50 +0,0 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Define array of binary source directories
const binSrcDirs = [
path.join(__dirname, 'src-tauri', 'binaries'),
];
function makeFilesExecutable() {
let totalCount = 0;
let successDirs = 0;
for (const binSrc of binSrcDirs) {
try {
if (!fs.existsSync(binSrc)) {
console.error(`Binaries directory does not exist: ${binSrc}`);
continue;
}
const files = fs.readdirSync(binSrc);
const nonExeFiles = files.filter(file => !file.endsWith('.exe'));
let count = 0;
for (const file of nonExeFiles) {
const filePath = path.join(binSrc, file);
if (fs.statSync(filePath).isFile()) {
execSync(`chmod +x "${filePath}"`);
console.log(`Made executable: ${path.relative(__dirname, filePath)}`);
count++;
}
}
console.log(`Successfully made ${count} files executable in ${binSrc}`);
totalCount += count;
successDirs++;
} catch (error) {
console.error(`Error processing directory ${binSrc}: ${error.message}`);
}
}
console.log(`\nSummary: Made ${totalCount} files executable across ${successDirs} directories`);
}
console.log(`RUNNING: 🛠️ Build Script makeFilesExecutable.js`);
makeFilesExecutable();

3832
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +1,70 @@
{ {
"name": "neodlp", "name": "neodlp",
"private": true, "private": true,
"version": "0.3.1", "version": "0.4.4",
"description": "Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri" "tauri": "tauri",
"tauri:dev:linux-x64": "npm run tauri dev -- --config ./src-tauri/tauri.linux-x86_64.conf.json",
"tauri:build:linux-x64": "npm run tauri build -- --config ./src-tauri/tauri.linux-x86_64.conf.json",
"tauri:dev:linux-arm64": "npm run tauri dev -- --config ./src-tauri/tauri.linux-aarch64.conf.json",
"tauri:build:linux-arm64": "npm run tauri build -- --config ./src-tauri/tauri.linux-aarch64.conf.json",
"tauri:dev:macos-x64": "npm run tauri dev -- --config ./src-tauri/tauri.macos-x86_64.conf.json",
"tauri:build:macos-x64": "npm run tauri build -- --config ./src-tauri/tauri.macos-x86_64.conf.json",
"tauri:dev:macos-arm64": "npm run tauri dev -- --config ./src-tauri/tauri.macos-aarch64.conf.json",
"tauri:build:macos-arm64": "npm run tauri build -- --config ./src-tauri/tauri.macos-aarch64.conf.json",
"download": "node ./scripts/download-bins.js"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12", "@tanstack/devtools-vite": "^0.6.0",
"@radix-ui/react-alert-dialog": "^1.1.15", "@tanstack/react-devtools": "^0.10.0",
"@radix-ui/react-aspect-ratio": "^1.1.7", "@tanstack/react-pacer": "^0.20.0",
"@radix-ui/react-avatar": "^1.1.10", "@tanstack/react-pacer-devtools": "^0.5.5",
"@radix-ui/react-checkbox": "^1.3.3", "@tanstack/react-query": "^5.91.2",
"@radix-ui/react-collapsible": "^1.1.12", "@tanstack/react-query-devtools": "^5.91.3",
"@radix-ui/react-context-menu": "^2.2.16", "@tauri-apps/api": "^2.10.1",
"@radix-ui/react-dialog": "^1.1.15", "@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@tauri-apps/plugin-dialog": "^2.6.0",
"@radix-ui/react-hover-card": "^1.1.15", "@tauri-apps/plugin-fs": "^2.4.5",
"@radix-ui/react-label": "^2.1.7", "@tauri-apps/plugin-log": "^2.8.0",
"@radix-ui/react-menubar": "^1.1.16", "@tauri-apps/plugin-notification": "^2.3.3",
"@radix-ui/react-navigation-menu": "^1.2.14", "@tauri-apps/plugin-opener": "^2.5.3",
"@radix-ui/react-popover": "^1.1.15", "@tauri-apps/plugin-os": "^2.3.2",
"@radix-ui/react-progress": "^1.1.7", "@tauri-apps/plugin-process": "^2.3.1",
"@radix-ui/react-radio-group": "^1.3.8", "@tauri-apps/plugin-shell": "^2.3.5",
"@radix-ui/react-scroll-area": "^1.2.10", "@tauri-apps/plugin-sql": "^2.3.2",
"@radix-ui/react-select": "^2.2.6", "@tauri-apps/plugin-updater": "^2.10.0",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query-devtools": "^5.90.2",
"@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-opener": "^2.5.0",
"@tauri-apps/plugin-os": "^2.3.1",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.1",
"@tauri-apps/plugin-sql": "^2.3.0",
"@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", "lucide-react": "^0.577.0",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.545.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.0", "radix-ui": "^1.4.3",
"react-day-picker": "^9.11.1", "react": "^19.2.4",
"react-dom": "^19.2.0", "react-dom": "^19.2.4",
"react-hook-form": "^7.65.0", "react-hook-form": "^7.71.2",
"react-resizable-panels": "^3.0.6", "react-resizable-panels": "^4.7.3",
"react-router-dom": "^7.9.4", "react-router-dom": "^7.13.1",
"recharts": "^3.2.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.5.0",
"ulid": "^3.0.1", "ulid": "^3.0.2",
"vaul": "^1.1.2", "zod": "^4.3.6",
"zod": "^4.1.12", "zustand": "^5.0.12"
"zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/vite": "^4.2.2",
"@tailwindcss/vite": "^4.1.14", "@tauri-apps/cli": "^2.10.1",
"@tauri-apps/cli": "^2.8.4", "@types/node": "^25.5.0",
"@types/node": "^24.7.2", "@types/react": "^19.2.14",
"@types/react": "^19.2.2", "@types/react-dom": "^19.2.3",
"@types/react-dom": "^19.2.1", "@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-react": "^5.0.4", "tailwindcss": "^4.2.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.1.9" "vite": "^8.0.1"
} }
} }

View File

@@ -1,5 +0,0 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
}

11
public/neodlp.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1024" height="1024" rx="200" fill="url(#paint0_linear_10_2)"/>
<path d="M529.252 811.098C519.472 820.96 503.528 820.96 493.748 811.098L256.265 571.603C240.619 555.824 251.796 529 274.017 529H748.983C771.204 529 782.381 555.824 766.735 571.603L529.252 811.098Z" fill="#FAFAFA"/>
<rect x="355" y="222" width="313" height="346" rx="25" fill="#FAFAFA"/>
<defs>
<linearGradient id="paint0_linear_10_2" x1="129.5" y1="148.5" x2="921" y2="863" gradientUnits="userSpaceOnUse">
<stop stop-color="#4444FF"/>
<stop offset="1" stop-color="#FF43D0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@@ -1,6 +0,0 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

554
scripts/download-bins.js Normal file
View File

@@ -0,0 +1,554 @@
import os from 'os';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
const downloadDir = path.join(projectRoot, 'src-tauri', 'resources', 'downloads');
const binDir = path.join(projectRoot, 'src-tauri', 'binaries');
const platform = os.platform();
const targetPlatform = process.argv[2];
const targetBin = process.argv[3];
const versions = {
'yt-dlp': '2026.03.21.233500',
'ffmpeg-ffprobe': 'latest',
'deno': '2.7.8',
'aria2c': '1.37.0',
'neodlp-pot': '0.8.1'
};
const binaries = {
'yt-dlp': [
{
name: 'yt-dlp-x86_64-pc-windows-msvc',
platform: 'win32',
url: `https://github.com/yt-dlp/yt-dlp-nightly-builds/releases${versions['yt-dlp'] === 'latest' ? '/latest' : ''}/download${versions['yt-dlp'] !== 'latest' ? '/'+versions['yt-dlp'] : ''}/yt-dlp.exe`,
src: path.join(downloadDir, 'yt-dlp-x86_64-pc-windows-msvc.exe'),
dest: [
path.join(binDir, 'yt-dlp-x86_64-pc-windows-msvc.exe')
],
archive: null,
cleanup: [
path.join(downloadDir, 'yt-dlp-x86_64-pc-windows-msvc.exe')
]
},
{
name: 'yt-dlp-x86_64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/yt-dlp/yt-dlp-nightly-builds/releases${versions['yt-dlp'] === 'latest' ? '/latest' : ''}/download${versions['yt-dlp'] !== 'latest' ? '/'+versions['yt-dlp'] : ''}/yt-dlp_linux`,
src: path.join(downloadDir, 'yt-dlp-x86_64-unknown-linux-gnu'),
dest: [
path.join(binDir, 'yt-dlp-x86_64-unknown-linux-gnu')
],
archive: null,
cleanup: [
path.join(downloadDir, 'yt-dlp-x86_64-unknown-linux-gnu')
]
},
{
name: 'yt-dlp-aarch64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/yt-dlp/yt-dlp-nightly-builds/releases${versions['yt-dlp'] === 'latest' ? '/latest' : ''}/download${versions['yt-dlp'] !== 'latest' ? '/'+versions['yt-dlp'] : ''}/yt-dlp_linux_aarch64`,
src: path.join(downloadDir, 'yt-dlp-aarch64-unknown-linux-gnu'),
dest: [
path.join(binDir, 'yt-dlp-aarch64-unknown-linux-gnu')
],
archive: null,
cleanup: [
path.join(downloadDir, 'yt-dlp-aarch64-unknown-linux-gnu')
]
},
{
name: 'yt-dlp-universal-apple-darwin',
platform: 'darwin',
url: `https://github.com/yt-dlp/yt-dlp-nightly-builds/releases${versions['yt-dlp'] === 'latest' ? '/latest' : ''}/download${versions['yt-dlp'] !== 'latest' ? '/'+versions['yt-dlp'] : ''}/yt-dlp_macos`,
src: path.join(downloadDir, 'yt-dlp-universal-apple-darwin'),
dest: [
path.join(binDir, 'yt-dlp-x86_64-apple-darwin'),
path.join(binDir, 'yt-dlp-aarch64-apple-darwin')
],
archive: null,
cleanup: [
path.join(downloadDir, 'yt-dlp-universal-apple-darwin')
]
},
],
'ffmpeg-ffprobe': [
{
name: 'ffmpeg-ffprobe-x86_64-pc-windows-msvc',
platform: 'win32',
url: `https://github.com/yt-dlp/FFmpeg-Builds/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffmpeg-master-latest-win64-gpl.zip`,
src: path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl.zip'),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl', 'bin', 'ffmpeg.exe'),
path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl', 'bin', 'ffprobe.exe')
],
binDest: [
path.join(binDir, 'ffmpeg-x86_64-pc-windows-msvc.exe'),
path.join(binDir, 'ffprobe-x86_64-pc-windows-msvc.exe')
]
},
cleanup: [
path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl.zip'),
path.join(downloadDir, 'ffmpeg-master-latest-win64-gpl')
]
},
{
name: 'ffmpeg-ffprobe-x86_64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/yt-dlp/FFmpeg-Builds/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffmpeg-master-latest-linux64-gpl.tar.xz`,
src: path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl.tar.xz'),
dest: null,
archive: {
type: 'tar.xz',
binSrc: [
path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl', 'bin', 'ffmpeg'),
path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl', 'bin', 'ffprobe')
],
binDest: [
path.join(binDir, 'ffmpeg-x86_64-unknown-linux-gnu'),
path.join(binDir, 'ffprobe-x86_64-unknown-linux-gnu')
]
},
cleanup: [
path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl.tar.xz'),
path.join(downloadDir, 'ffmpeg-master-latest-linux64-gpl')
]
},
{
name: 'ffmpeg-ffprobe-aarch64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/yt-dlp/FFmpeg-Builds/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffmpeg-master-latest-linuxarm64-gpl.tar.xz`,
src: path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl.tar.xz'),
dest: null,
archive: {
type: 'tar.xz',
binSrc: [
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl', 'bin', 'ffmpeg'),
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl', 'bin', 'ffprobe')
],
binDest: [
path.join(binDir, 'ffmpeg-aarch64-unknown-linux-gnu'),
path.join(binDir, 'ffprobe-aarch64-unknown-linux-gnu')
]
},
cleanup: [
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl.tar.xz'),
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl')
]
},
// {
// name: 'ffmpeg-universal-apple-darwin',
// platform: 'darwin',
// url: `https://evermeet.cx/ffmpeg/get/zip`,
// src: path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
// dest: null,
// archive: {
// type: 'zip',
// binSrc: [
// path.join(downloadDir, 'ffmpeg'),
// path.join(downloadDir, 'ffmpeg')
// ],
// binDest: [
// path.join(binDir, 'ffmpeg-x86_64-apple-darwin'),
// path.join(binDir, 'ffmpeg-aarch64-apple-darwin')
// ]
// },
// cleanup: [
// path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
// path.join(downloadDir, 'ffmpeg')
// ]
// },
// {
// name: 'ffprobe-universal-apple-darwin',
// platform: 'darwin',
// url: `https://evermeet.cx/ffmpeg/get/ffprobe/zip`,
// src: path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
// dest: null,
// archive: {
// type: 'zip',
// binSrc: [
// path.join(downloadDir, 'ffprobe'),
// path.join(downloadDir, 'ffprobe')
// ],
// binDest: [
// path.join(binDir, 'ffprobe-x86_64-apple-darwin'),
// path.join(binDir, 'ffprobe-aarch64-apple-darwin')
// ]
// },
// cleanup: [
// path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
// path.join(downloadDir, 'ffprobe')
// ]
// },
{
name: 'ffmpeg-universal-apple-darwin',
platform: 'darwin',
url: `https://github.com/neosubhamoy/evermeet-static-ffmpeg/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffmpeg-universal-apple-darwin.zip`,
src: path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, 'ffmpeg'),
path.join(downloadDir, 'ffmpeg')
],
binDest: [
path.join(binDir, 'ffmpeg-x86_64-apple-darwin'),
path.join(binDir, 'ffmpeg-aarch64-apple-darwin')
]
},
cleanup: [
path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
path.join(downloadDir, 'ffmpeg')
]
},
{
name: 'ffprobe-universal-apple-darwin',
platform: 'darwin',
url: `https://github.com/neosubhamoy/evermeet-static-ffmpeg/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffprobe-universal-apple-darwin.zip`,
src: path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, 'ffprobe'),
path.join(downloadDir, 'ffprobe')
],
binDest: [
path.join(binDir, 'ffprobe-x86_64-apple-darwin'),
path.join(binDir, 'ffprobe-aarch64-apple-darwin')
]
},
cleanup: [
path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
path.join(downloadDir, 'ffprobe')
]
}
],
'deno': [
{
name: 'deno-x86_64-pc-windows-msvc',
platform: 'win32',
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-x86_64-pc-windows-msvc.zip`,
src: path.join(downloadDir, 'deno-x86_64-pc-windows-msvc.zip'),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, 'deno.exe')
],
binDest: [
path.join(binDir, 'deno-x86_64-pc-windows-msvc.exe')
]
},
cleanup: [
path.join(downloadDir, 'deno-x86_64-pc-windows-msvc.zip'),
path.join(downloadDir, 'deno.exe')
]
},
{
name: 'deno-x86_64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-x86_64-unknown-linux-gnu.zip`,
src: path.join(downloadDir, 'deno-x86_64-unknown-linux-gnu.zip'),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, 'deno')
],
binDest: [
path.join(binDir, 'deno-x86_64-unknown-linux-gnu')
]
},
cleanup: [
path.join(downloadDir, 'deno-x86_64-unknown-linux-gnu.zip'),
path.join(downloadDir, 'deno')
]
},
{
name: 'deno-aarch64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-aarch64-unknown-linux-gnu.zip`,
src: path.join(downloadDir, 'deno-aarch64-unknown-linux-gnu.zip'),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, 'deno')
],
binDest: [
path.join(binDir, 'deno-aarch64-unknown-linux-gnu')
]
},
cleanup: [
path.join(downloadDir, 'deno-aarch64-unknown-linux-gnu.zip'),
path.join(downloadDir, 'deno')
]
},
{
name: 'deno-x86_64-apple-darwin',
platform: 'darwin',
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-x86_64-apple-darwin.zip`,
src: path.join(downloadDir, 'deno-x86_64-apple-darwin.zip'),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, 'deno')
],
binDest: [
path.join(binDir, 'deno-x86_64-apple-darwin')
]
},
cleanup: [
path.join(downloadDir, 'deno-x86_64-apple-darwin.zip'),
path.join(downloadDir, 'deno')
]
},
{
name: 'deno-aarch64-apple-darwin',
platform: 'darwin',
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-aarch64-apple-darwin.zip`,
src: path.join(downloadDir, 'deno-aarch64-apple-darwin.zip'),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, 'deno')
],
binDest: [
path.join(binDir, 'deno-aarch64-apple-darwin')
]
},
cleanup: [
path.join(downloadDir, 'deno-aarch64-apple-darwin.zip'),
path.join(downloadDir, 'deno')
]
}
],
'aria2c': [
{
name: 'aria2c-x86_64-pc-windows-msvc',
platform: 'win32',
url: `https://github.com/aria2/aria2/releases/download/release-${versions['aria2c']}/aria2-${versions['aria2c']}-win-64bit-build1.zip`,
src: path.join(downloadDir, `aria2-${versions['aria2c']}-win-64bit-build1.zip`),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, `aria2-${versions['aria2c']}-win-64bit-build1`, 'aria2c.exe')
],
binDest: [
path.join(binDir, 'aria2c-x86_64-pc-windows-msvc.exe')
]
},
cleanup: [
path.join(downloadDir, `aria2-${versions['aria2c']}-win-64bit-build1.zip`),
path.join(downloadDir, `aria2-${versions['aria2c']}-win-64bit-build1`)
]
},
{
name: 'aria2c-x86_64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/asdo92/aria2-static-builds/releases/download/v${versions['aria2c']}/aria2-${versions['aria2c']}-linux-gnu-64bit-build1.tar.bz2`,
src: path.join(downloadDir, `aria2-${versions['aria2c']}-linux-gnu-64bit-build1.tar.bz2`),
dest: null,
archive: {
type: 'tar.bz2',
binSrc: [
path.join(downloadDir, `aria2-${versions['aria2c']}-linux-gnu-64bit-build1`, 'aria2c')
],
binDest: [
path.join(binDir, 'aria2c-x86_64-unknown-linux-gnu')
]
},
cleanup: [
path.join(downloadDir, `aria2-${versions['aria2c']}-linux-gnu-64bit-build1.tar.bz2`),
path.join(downloadDir, `aria2-${versions['aria2c']}-linux-gnu-64bit-build1`)
]
},
{
name: 'aria2c-aarch64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/aria2/aria2/releases/download/release-${versions['aria2c']}/aria2-${versions['aria2c']}-aarch64-linux-android-build1.zip`,
src: path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1.zip`),
dest: null,
archive: {
type: 'zip',
binSrc: [
path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1`, 'aria2c')
],
binDest: [
path.join(binDir, 'aria2c-aarch64-unknown-linux-gnu')
]
},
cleanup: [
path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1.zip`),
path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1`)
]
}
],
'neodlp-pot': [
{
name: 'neodlp-pot-x86_64-pc-windows-msvc',
platform: 'win32',
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-windows-x86_64.exe`,
src: path.join(downloadDir, 'bgutil-pot-windows-x86_64.exe'),
dest: [
path.join(binDir, 'neodlp-pot-x86_64-pc-windows-msvc.exe')
],
archive: null,
cleanup: [
path.join(downloadDir, 'bgutil-pot-windows-x86_64.exe')
]
},
{
name: 'neodlp-pot-x86_64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-linux-x86_64`,
src: path.join(downloadDir, 'bgutil-pot-linux-x86_64'),
dest: [
path.join(binDir, 'neodlp-pot-x86_64-unknown-linux-gnu')
],
archive: null,
cleanup: [
path.join(downloadDir, 'bgutil-pot-linux-x86_64')
]
},
{
name: 'neodlp-pot-aarch64-unknown-linux-gnu',
platform: 'linux',
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-linux-aarch64`,
src: path.join(downloadDir, 'bgutil-pot-linux-aarch64'),
dest: [
path.join(binDir, 'neodlp-pot-aarch64-unknown-linux-gnu')
],
archive: null,
cleanup: [
path.join(downloadDir, 'bgutil-pot-linux-aarch64')
]
},
{
name: 'neodlp-pot-x86_64-apple-darwin',
platform: 'darwin',
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-macos-x86_64`,
src: path.join(downloadDir, 'bgutil-pot-macos-x86_64'),
dest: [
path.join(binDir, 'neodlp-pot-x86_64-apple-darwin')
],
archive: null,
cleanup: [
path.join(downloadDir, 'bgutil-pot-macos-x86_64')
]
},
{
name: 'neodlp-pot-aarch64-apple-darwin',
platform: 'darwin',
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-macos-aarch64`,
src: path.join(downloadDir, 'bgutil-pot-macos-aarch64'),
dest: [
path.join(binDir, 'neodlp-pot-aarch64-apple-darwin')
],
archive: null,
cleanup: [
path.join(downloadDir, 'bgutil-pot-macos-aarch64')
]
}
]
}
function downloadAndProcess(bin) {
console.log(`=> Processing: ${bin.name}`);
console.log(`Downloading: ${bin.url}`);
if (platform === 'win32') {
execSync(`powershell -Command "Invoke-WebRequest -Uri '${bin.url}' -OutFile '${bin.src}'"`, { stdio: 'inherit' });
} else {
execSync(`curl -L "${bin.url}" -o "${bin.src}"`, { stdio: 'inherit' });
}
if (bin.archive) {
console.log(`Extracting: ${bin.src}`);
if (platform === 'win32' && bin.archive.type === 'zip') {
execSync(`powershell -Command "Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${bin.src}', '${downloadDir}')"`, { stdio: 'inherit' });
} else if (bin.archive.type === 'tar.bz2') {
execSync(`tar -xjf "${bin.src}" -C "${downloadDir}"`, { stdio: 'inherit' });
} else if (bin.archive.type === 'zip') {
execSync(`unzip -o "${bin.src}" -d "${downloadDir}"`, { stdio: 'inherit' });
} else {
execSync(`tar -xf "${bin.src}" -C "${downloadDir}"`, { stdio: 'inherit' });
}
bin.archive.binSrc.forEach((src, index) => {
const dest = bin.archive.binDest[index];
console.log(`Moving: "${src}" to "${dest}"`);
fs.copyFileSync(src, dest);
if (platform !== 'win32') {
fs.chmodSync(dest, 0o755);
}
});
} else if (bin.dest) {
bin.dest.forEach((dest) => {
console.log(`Moving: "${bin.src}" to "${dest}"`);
fs.copyFileSync(bin.src, dest);
if (platform !== 'win32') {
fs.chmodSync(dest, 0o755);
}
});
}
bin.cleanup.forEach((item) => {
if (fs.existsSync(item)) {
console.log(`Cleaning: "${item}"`);
const stats = fs.statSync(item);
if (stats.isDirectory()) {
fs.rmSync(item, { recursive: true, force: true });
} else {
fs.unlinkSync(item);
}
}
});
}
if (targetPlatform && !['win32', 'linux', 'darwin', 'all'].includes(targetPlatform)) {
console.error(`ERROR: Invalid platform specified: '${targetPlatform}'. Use one of: win32, linux, darwin, or all`);
process.exit(1);
}
if (targetBin && !binaries.hasOwnProperty(targetBin) && targetBin !== 'all') {
console.error(`ERROR: Invalid binary specified: '${targetBin}'. Use one of: ${Object.keys(binaries).join(', ')}, or all`);
process.exit(1);
}
const effectivePlatform = targetPlatform || platform;
const effectiveBin = targetBin || 'all';
console.log(`RUNNING: 📦 Binary Downloader (platform: ${effectivePlatform} | binary: ${effectiveBin})`);
Object.keys(binaries).forEach((binKey) => {
if (effectiveBin !== 'all' && binKey !== effectiveBin) {
return;
}
binaries[binKey].forEach((bin) => {
if (effectivePlatform !== 'all' && bin.platform !== effectivePlatform) {
return;
}
downloadAndProcess(bin);
});
});
console.log('✅ Downloads Completed');

2858
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "neodlp" name = "neodlp"
version = "0.3.1" version = "0.4.4"
description = "NeoDLP" description = "Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration"
authors = ["neosubhamoy <hey@neosubhamoy.com>"] authors = ["neosubhamoy <hey@neosubhamoy.com>"]
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@@ -22,13 +22,15 @@ tauri-build = { version = "2", features = [] }
tauri = { version = "2", features = ["tray-icon"] } tauri = { version = "2", features = ["tray-icon"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.13", features = ["json"] }
tokio = { version = "1", features = ["full"] } 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 = "6.0" directories = "6.0"
futures-util = "0.3" futures-util = "0.3"
log = "0.4"
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 +38,9 @@ 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"
tauri-plugin-notification = "2"
tauri-plugin-log = "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"

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,10 @@
"core:window:allow-hide", "core:window:allow-hide",
"core:window:allow-show", "core:window:allow-show",
"core:window:allow-set-focus", "core:window:allow-set-focus",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-start-dragging",
"opener:default", "opener:default",
"shell:default", "shell:default",
"fs:default", "fs:default",
@@ -22,14 +26,10 @@
"fs:allow-app-write-recursive", "fs:allow-app-write-recursive",
"updater:default", "updater:default",
"process:default", "process:default",
{ "clipboard-manager:allow-read-text",
"identifier": "opener:allow-open-path", "clipboard-manager:allow-write-text",
"allow": [ "notification:default",
{ "log:default",
"path": "**"
}
]
},
{ {
"identifier": "fs:scope", "identifier": "fs:scope",
"allow": [ "allow": [
@@ -37,6 +37,14 @@
"path": "**" "path": "**"
} }
] ]
},
{
"identifier": "opener:allow-open-path",
"allow": [
{
"path": "**"
}
]
} }
] ]
} }

View File

@@ -35,6 +35,16 @@
"args": true, "args": true,
"sidecar": true "sidecar": true
}, },
{
"name": "binaries/neodlp-pot",
"args": true,
"sidecar": true
},
{
"name": "yt-dlp",
"cmd": "yt-dlp",
"args": true
},
{ {
"name": "ffmpeg", "name": "ffmpeg",
"cmd": "ffmpeg", "cmd": "ffmpeg",
@@ -45,10 +55,25 @@
"cmd": "aria2c", "cmd": "aria2c",
"args": true "args": true
}, },
{
"name": "deno",
"cmd": "deno",
"args": true
},
{ {
"name": "pkexec", "name": "pkexec",
"cmd": "pkexec", "cmd": "pkexec",
"args": true "args": true
},
{
"name": "powershell",
"cmd": "powershell",
"args": true
},
{
"name": "sh",
"cmd": "sh",
"args": true
} }
] ]
}, },
@@ -80,6 +105,16 @@
"args": true, "args": true,
"sidecar": true "sidecar": true
}, },
{
"name": "binaries/neodlp-pot",
"args": true,
"sidecar": true
},
{
"name": "yt-dlp",
"cmd": "yt-dlp",
"args": true
},
{ {
"name": "ffmpeg", "name": "ffmpeg",
"cmd": "ffmpeg", "cmd": "ffmpeg",
@@ -89,6 +124,26 @@
"name": "aria2c", "name": "aria2c",
"cmd": "aria2c", "cmd": "aria2c",
"args": true "args": true
},
{
"name": "deno",
"cmd": "deno",
"args": true
},
{
"name": "pkexec",
"cmd": "pkexec",
"args": true
},
{
"name": "powershell",
"cmd": "powershell",
"args": true
},
{
"name": "sh",
"cmd": "sh",
"args": true
} }
] ]
} }

View File

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

View File

@@ -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="&quot;[INSTALLDIR]neodlp.exe&quot; --hidden" KeyPath="no" /> <RegistryValue Name="NeoDLP" Type="string" Value="&quot;[INSTALLDIR]neodlp.exe&quot; --hidden" KeyPath="no" />
@@ -15,4 +15,4 @@
</Component> </Component>
</DirectoryRef> </DirectoryRef>
</Fragment> </Fragment>
</Wix> </Wix>

View File

View File

@@ -0,0 +1,83 @@
from __future__ import annotations
__version__ = '0.8.1'
import abc
import json
from yt_dlp.extractor.youtube.pot.provider import (
ExternalRequestFeature,
PoTokenContext,
PoTokenProvider,
PoTokenProviderRejectedRequest,
)
from yt_dlp.extractor.youtube.pot.utils import WEBPO_CLIENTS
from yt_dlp.utils import js_to_json
from yt_dlp.utils.traversal import traverse_obj
class BgUtilPTPBase(PoTokenProvider, abc.ABC):
PROVIDER_VERSION = __version__
BUG_REPORT_LOCATION = (
'https://github.com/jim60105/bgutil-ytdlp-pot-provider/issues'
)
_SUPPORTED_EXTERNAL_REQUEST_FEATURES = (
ExternalRequestFeature.PROXY_SCHEME_HTTP,
ExternalRequestFeature.PROXY_SCHEME_HTTPS,
ExternalRequestFeature.PROXY_SCHEME_SOCKS4,
ExternalRequestFeature.PROXY_SCHEME_SOCKS4A,
ExternalRequestFeature.PROXY_SCHEME_SOCKS5,
ExternalRequestFeature.PROXY_SCHEME_SOCKS5H,
ExternalRequestFeature.SOURCE_ADDRESS,
ExternalRequestFeature.DISABLE_TLS_VERIFICATION,
)
_SUPPORTED_CLIENTS = WEBPO_CLIENTS
_SUPPORTED_CONTEXTS = (
PoTokenContext.GVS,
PoTokenContext.PLAYER,
PoTokenContext.SUBS,
)
_GETPOT_TIMEOUT = 20.0
_GET_SERVER_VSN_TIMEOUT = 5.0
_MIN_NODE_VSN = (18, 0, 0)
def _info_and_raise(self, msg, raise_from=None):
self.logger.info(msg)
raise PoTokenProviderRejectedRequest(msg) from raise_from
def _warn_and_raise(self, msg, once=True, raise_from=None):
self.logger.warning(msg, once=once)
raise PoTokenProviderRejectedRequest(msg) from raise_from
def _get_attestation(self, webpage: str | None):
if not webpage:
return None
raw_cd = (
traverse_obj(
self.ie._search_regex(
r'''(?sx)window\s*\.\s*ytAtN\s*\(\s*
(?P<js>\{.+?}\s*)
\s*\)\s*;''',
webpage,
'ytAtN challenge',
default=None),
({js_to_json}, {json.loads}, 'R'))
or traverse_obj(
self.ie._search_regex(
r'''(?sx)window\.ytAtR\s*=\s*(?P<raw_cd>(?P<q>['"])
(?:
\\.|
(?!(?P=q)).
)*
(?P=q))\s*;''',
webpage,
'ytAtR challenge',
default=None),
({js_to_json}, {json.loads})))
if att_txt := traverse_obj(raw_cd, ({json.loads}, 'bgChallenge')):
return att_txt
self.logger.warning('Failed to extract initial attestation from the webpage')
return None
__all__ = ['__version__']

View File

@@ -0,0 +1,211 @@
from __future__ import annotations
import functools
import json
import os.path
import shutil
import subprocess
from yt_dlp.extractor.youtube.pot.provider import (
PoTokenProviderError,
PoTokenRequest,
PoTokenResponse,
register_preference,
register_provider,
)
from yt_dlp.extractor.youtube.pot.utils import get_webpo_content_binding
from yt_dlp.utils import Popen
from yt_dlp_plugins.extractor.getpot_bgutil import BgUtilPTPBase
@register_provider
class BgUtilCliPTP(BgUtilPTPBase):
PROVIDER_NAME = 'bgutil:cli'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._check_cli = functools.cache(self._check_cli_impl)
@functools.cached_property
def _cli_path(self):
cli_path = self._configuration_arg(
'cli_path', casesense=True, default=[None])[0]
if cli_path:
return os.path.expandvars(cli_path)
# check deprecated arg
deprecated_cli_path = self.ie._configuration_arg(
ie_key='youtube', key='getpot_bgutil_script', default=[None])[0]
if deprecated_cli_path:
self._warn_and_raise(
"'youtube:getpot_bgutil_script' extractor arg is deprecated, "
"use 'youtubepot-bgutilcli:cli_path' instead")
# default if no arg was passed
# First, try to find the executable in PATH
if self._get_executable_path('bgutil-pot'):
self.logger.debug('Found bgutil-pot in PATH')
return 'bgutil-pot'
# Then check common file locations
file_paths = [
os.path.join(
os.getcwd(), 'target', 'debug', 'bgutil-pot'
),
os.path.join(
os.getcwd(), 'target', 'release', 'bgutil-pot'
),
os.path.expanduser(
'~/bgutil-ytdlp-pot-provider/target/debug/bgutil-pot'
),
os.path.expanduser(
'~/bgutil-ytdlp-pot-provider/target/release/'
'bgutil-pot'
),
]
for path in file_paths:
if self._get_executable_path(path):
self.logger.debug(f'Found bgutil-pot at: {path}')
return path
# Fallback to PATH name if no file found
default_path = 'bgutil-pot'
self.logger.debug(
f'No CLI path found, defaulting to {default_path}')
return default_path
def is_available(self):
return self._check_cli(self._cli_path)
def _get_executable_path(self, cli_path):
"""Get the actual executable path, checking PATH or file existence."""
# For relative names (like 'bgutil-pot-generate'), search in PATH
if os.path.sep not in cli_path:
executable_path = shutil.which(cli_path)
if executable_path:
return executable_path
# For absolute/relative paths, check file existence directly
if os.path.isfile(cli_path):
return cli_path
return None
def _check_cli_impl(self, cli_path):
executable_path = self._get_executable_path(cli_path)
if not executable_path:
self.logger.debug(
f"Executable path doesn't exist: {cli_path}")
return False
stdout, stderr, returncode = Popen.run(
[executable_path, '--version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=self._GET_SERVER_VSN_TIMEOUT
)
if returncode:
self.logger.warning(
f'Failed to check executable version. '
f'Executable returned {returncode} exit status. '
f'stdout: {stdout}; stderr: {stderr}',
once=True)
return False
else:
self.logger.debug(f'bgutil-pot version: {stdout.strip()}')
return True
def _real_request_pot(
self,
request: PoTokenRequest,
) -> PoTokenResponse:
# used for CI check
self.logger.trace(
f'Generating POT via Rust executable: {self._cli_path}')
executable_path = self._get_executable_path(self._cli_path)
if not executable_path:
raise PoTokenProviderError(
f'Executable not found: {self._cli_path}')
command_args = [executable_path]
if proxy := request.request_proxy:
command_args.extend(['-p', proxy])
command_args.extend(['-c', get_webpo_content_binding(request)[0]])
if request.bypass_cache:
command_args.append('--bypass-cache')
if request.request_source_address:
command_args.extend(
['--source-address', request.request_source_address])
if request.request_verify_tls is False:
command_args.append('--disable-tls-verification')
self.logger.info(
f'Generating a {request.context.value} PO Token for '
f'{request.internal_client_name} client via bgutil '
f'Rust executable',
)
self.logger.debug(
f'Executing command to get POT via Rust executable: '
f'{" ".join(command_args)}'
)
try:
stdout, stderr, returncode = Popen.run(
command_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=self._GETPOT_TIMEOUT
)
except subprocess.TimeoutExpired as e:
raise PoTokenProviderError(
f'_get_pot_via_cli failed: Timeout expired when trying '
f'to run executable (caused by {e!r})'
)
except Exception as e:
raise PoTokenProviderError(
f'_get_pot_via_cli failed: Unable to run executable '
f'(caused by {e!r})'
) from e
msg = ''
if stdout_extra := stdout.strip().splitlines()[:-1]:
msg = f'stdout:\n{stdout_extra}\n'
if stderr_stripped := stderr.strip(): # Empty strings are falsy
msg += f'stderr:\n{stderr_stripped}\n'
msg = msg.strip()
if msg:
self.logger.trace(msg)
if returncode:
raise PoTokenProviderError(
f'_get_pot_via_cli failed with returncode {returncode}')
try:
json_resp = stdout.splitlines()[-1]
self.logger.trace(f'JSON response:\n{json_resp}')
# The JSON response is always the last line
cli_data_resp = json.loads(json_resp)
except json.JSONDecodeError as e:
raise PoTokenProviderError(
f'Error parsing JSON response from _get_pot_via_cli '
f'(caused by {e!r})'
) from e
if 'poToken' not in cli_data_resp:
raise PoTokenProviderError(
'The executable did not respond with a po_token')
return PoTokenResponse(po_token=cli_data_resp['poToken'])
@register_preference(BgUtilCliPTP)
def bgutil_cli_getpot_preference(provider, request):
return 1
__all__ = [BgUtilCliPTP.__name__,
bgutil_cli_getpot_preference.__name__]

View File

@@ -0,0 +1,207 @@
from __future__ import annotations
import functools
import json
import time
from yt_dlp.extractor.youtube.pot.provider import (
PoTokenProviderError,
PoTokenProviderRejectedRequest,
PoTokenRequest,
PoTokenResponse,
register_preference,
register_provider,
)
from yt_dlp.extractor.youtube.pot.utils import get_webpo_content_binding
from yt_dlp.networking.common import Request
from yt_dlp.networking.exceptions import HTTPError, TransportError
from yt_dlp_plugins.extractor.getpot_bgutil import BgUtilPTPBase
@register_provider
class BgUtilHTTPPTP(BgUtilPTPBase):
PROVIDER_NAME = 'bgutil:http'
DEFAULT_BASE_URL = 'http://127.0.0.1:4416'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._last_server_check = 0
self._server_available = True
@functools.cached_property
def _base_url(self):
base_url = self._configuration_arg('base_url', default=[None])[0]
if base_url:
return base_url
# check deprecated arg
deprecated_base_url = self.ie._configuration_arg(
ie_key='youtube', key='getpot_bgutil_baseurl', default=[None])[0]
if deprecated_base_url:
self._warn_and_raise(
"'youtube:getpot_bgutil_baseurl' extractor arg is deprecated, "
"use 'youtubepot-bgutilhttp:base_url' instead"
)
# default if no arg was passed
self.logger.debug(
f'No base_url provided, defaulting to {self.DEFAULT_BASE_URL}')
return self.DEFAULT_BASE_URL
def _check_server_availability(self, ctx: PoTokenRequest):
if self._last_server_check + 60 > time.time():
return self._server_available
self._server_available = False
try:
self.logger.trace(
f'Checking server availability at {self._base_url}/ping')
response = json.load(self._request_webpage(Request(
f'{self._base_url}/ping',
extensions={'timeout': self._GET_SERVER_VSN_TIMEOUT},
proxies={'all': None}
),
note=False))
except TransportError as e:
# the server may be down
script_path_provided = self.ie._configuration_arg(
ie_key='youtubepot-bgutilscript',
key='script_path',
default=[None]
)[0] is not None
warning_base = (
f'Error reaching GET {self._base_url}/ping '
f'(caused by {e.__class__.__name__}). '
)
if script_path_provided: # server down is expected, log info
self._info_and_raise(
warning_base +
'This is expected if you are using the script method.'
)
else:
self._warn_and_raise(
warning_base +
f'Please make sure that the server is reachable at '
f'{self._base_url}.'
)
return
except HTTPError as e:
# may be an old server, don't raise
self.logger.warning(
f'HTTP Error reaching GET /ping (caused by {e!r})', once=True)
return
except json.JSONDecodeError as e:
# invalid server
self._warn_and_raise(
f'Error parsing ping response JSON (caused by {e!r})')
return
except Exception as e:
self._warn_and_raise(
f'Unknown error reaching GET /ping (caused by {e!r})',
raise_from=e
)
return
else:
version = response.get("version", "unknown")
self.logger.debug(f'HTTP server version: {version}')
self._server_available = True
return True
finally:
self._last_server_check = time.time()
def is_available(self):
return (self._server_available or
self._last_server_check + 60 < int(time.time()))
def _real_request_pot(
self,
request: PoTokenRequest,
) -> PoTokenResponse:
if not self._check_server_availability(request):
raise PoTokenProviderRejectedRequest(
f'{self.PROVIDER_NAME} server is not available')
# used for CI check
self.logger.trace('Generating POT via HTTP server')
disable_innertube = bool(
self._configuration_arg('disable_innertube', default=[None])[0]
)
challenge = self._get_attestation(
None if disable_innertube else request.video_webpage
)
# The challenge is falsy when the webpage and the challenge are
# unavailable. In this case, we need to disable /att/get since
# it's broken for web_music
if not challenge and request.internal_client_name == 'web_music':
if not disable_innertube: # if not already set, warn the user
self.logger.warning(
'BotGuard challenges could not be obtained from the '
'webpage, overriding disable_innertube=True because '
'InnerTube challenges are currently broken for the '
'web_music client. Pass disable_innertube=1 to suppress '
'this warning.'
)
disable_innertube = True
try:
response = self._request_webpage(
request=Request(
f'{self._base_url}/get_pot', data=json.dumps({
'bypass_cache': request.bypass_cache,
'challenge': challenge,
'content_binding': get_webpo_content_binding(
request
)[0],
'disable_innertube': disable_innertube,
'disable_tls_verification': (
not request.request_verify_tls
),
'proxy': request.request_proxy,
'innertube_context': request.innertube_context,
'source_address': request.request_source_address,
}).encode(), headers={'Content-Type': 'application/json'},
extensions={'timeout': self._GETPOT_TIMEOUT},
proxies={'all': None}
),
note=f'Generating a {request.context.value} PO Token for '
f'{request.internal_client_name} client via bgutil '
f'HTTP server',
)
except Exception as e:
raise PoTokenProviderError(
f'Error reaching POST /get_pot (caused by {e!r})') from e
try:
response_json = json.load(response)
except Exception as e:
response_data = response.read().decode()
raise PoTokenProviderError(
f'Error parsing response JSON (caused by {e!r}). '
f'response = {response_data}'
) from e
if error_msg := response_json.get('error'):
raise PoTokenProviderError(error_msg)
if 'poToken' not in response_json:
raise PoTokenProviderError(
f'Server did not respond with a poToken. '
f'Received response: {response}'
)
po_token = response_json['poToken']
self.logger.trace(f'Generated POT: {po_token}')
return PoTokenResponse(po_token=po_token)
@register_preference(BgUtilHTTPPTP)
def bgutil_HTTP_getpot_preference(provider, request):
return 130
__all__ = [BgUtilHTTPPTP.__name__,
bgutil_HTTP_getpot_preference.__name__]

View File

@@ -21,6 +21,7 @@ use std::{
use tauri::{ use tauri::{
menu::{Menu, MenuItem}, menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
path::BaseDirectory,
Emitter, Manager, State, Emitter, Manager, State,
}; };
use tauri_plugin_opener::OpenerExt; use tauri_plugin_opener::OpenerExt;
@@ -30,6 +31,7 @@ use tokio::{
time::sleep, time::sleep,
}; };
use tokio_tungstenite::accept_async; use tokio_tungstenite::accept_async;
use log::{info, error};
struct ImageCache(StdMutex<HashMap<String, String>>); struct ImageCache(StdMutex<HashMap<String, String>>);
@@ -174,6 +176,26 @@ fn get_config_file_path() -> Result<String, String> {
} }
} }
#[tauri::command]
fn get_current_app_path() -> Result<String, String> {
let exe_path = std::env::current_exe().map_err(|e| e.to_string())?;
Ok(exe_path
.parent()
.ok_or("Failed to get parent directory")?
.to_string_lossy()
.into_owned())
}
#[tauri::command]
fn is_flatpak() -> bool {
std::env::var("FLATPAK").is_ok()
}
#[tauri::command]
fn get_appimage_path() -> Option<String> {
std::env::var("APPDIR").ok()
}
#[tauri::command] #[tauri::command]
async fn update_config( async fn update_config(
new_config: Config, new_config: Config,
@@ -336,21 +358,48 @@ async fn open_file_with_app(
) -> Result<(), String> { ) -> Result<(), String> {
if let Some(name) = &app_name { if let Some(name) = &app_name {
if name == "explorer" { if name == "explorer" {
println!("Revealing file: {} in explorer", file_path); info!("Revealing file: {} in explorer", file_path);
return app_handle return app_handle
.opener() .opener()
.reveal_item_in_dir(file_path) .reveal_item_in_dir(file_path)
.map_err(|e| e.to_string()); .map_err(|e| {
error!("Failed to reveal file in explorer: {}", e);
e.to_string()
});
} }
println!("Opening file: {} with app: {}", file_path, name); info!("Opening file: {} with app: {}", file_path, name);
} else { } else {
println!("Opening file: {} with default app", file_path); info!("Opening file: {} with default app", file_path);
} }
app_handle app_handle
.opener() .opener()
.open_path(file_path, app_name) .open_path(file_path, app_name)
.map_err(|e| e.to_string()) .map_err(|e| {
error!("Failed to open file: {}", e);
e.to_string()
})
}
#[tauri::command]
async fn open_link_with_app(
app_handle: tauri::AppHandle,
url: String,
app_name: Option<String>,
) -> Result<(), String> {
if let Some(name) = &app_name {
info!("Opening link: {} with app: {}", url, name);
} else {
info!("Opening link: {} with default app", url);
}
app_handle
.opener()
.open_url(url, app_name)
.map_err(|e| {
error!("Failed to open link: {}", e);
e.to_string()
})
} }
#[tauri::command] #[tauri::command]
@@ -452,6 +501,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;
@@ -466,6 +516,11 @@ pub async fn run() {
let start_hidden = args.contains(&"--hidden".to_string()); let start_hidden = args.contains(&"--hidden".to_string());
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_log::Builder::new()
.level(log::LevelFilter::Info)
.max_file_size(5_242_880) /* in bytes = 5MB */
.build(),
)
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
// Focus the main window when attempting to launch another instance // Focus the main window when attempting to launch another instance
@@ -474,10 +529,9 @@ pub async fn run() {
let _ = window.set_focus(); let _ = window.set_focus();
} }
})) }))
.plugin( .plugin(tauri_plugin_sql::Builder::default()
tauri_plugin_sql::Builder::default() .add_migrations("sqlite:database.db", migrations)
.add_migrations("sqlite:database.db", migrations) .build(),
.build(),
) )
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
@@ -485,6 +539,8 @@ 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())
.plugin(tauri_plugin_notification::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| {
@@ -557,6 +613,20 @@ pub async fn run() {
.build(app) .build(app)
.map_err(|e| format!("Failed to create tray: {}", e))?; .map_err(|e| format!("Failed to create tray: {}", e))?;
// Fix tray icon in sandboxed environments (e.g., Flatpak)
// libappindicator uses the full path of the icon in dbus messages,
// so the path needs to be accessible from both the host and the sandbox.
// The default /tmp path doesn't work across sandbox boundaries.
if let Ok(local_data_path) = app
.path()
.resolve("tray-icon", BaseDirectory::AppLocalData)
{
let _ = fs::create_dir_all(&local_data_path);
let _ = tray.set_temp_dir_path(Some(local_data_path));
// Re-set the icon so it gets written to the new temp dir path
let _ = tray.set_icon(Some(app.default_window_icon().unwrap().clone()));
}
app.manage(tray); app.manage(tray);
let window = app.get_webview_window("main").unwrap(); let window = app.get_webview_window("main").unwrap();
@@ -577,6 +647,7 @@ pub async fn run() {
kill_all_process, kill_all_process,
fetch_image, fetch_image,
open_file_with_app, open_file_with_app,
open_link_with_app,
list_ongoing_downloads, list_ongoing_downloads,
pause_ongoing_downloads, pause_ongoing_downloads,
send_to_extension, send_to_extension,
@@ -586,6 +657,9 @@ pub async fn run() {
reset_config, reset_config,
get_config_file_path, get_config_file_path,
restart_websocket_server, restart_websocket_server,
get_current_app_path,
is_flatpak,
get_appimage_path
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@@ -3,5 +3,12 @@
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
#[cfg(target_os = "linux")]
{
if std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").is_err() {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
}
neodlp_lib::run().await neodlp_lib::run().await
} }

View File

@@ -148,5 +148,94 @@ pub fn get_migrations() -> Vec<Migration> {
END; END;
", ",
kind: MigrationKind::Up, kind: MigrationKind::Up,
},
Migration {
version: 3,
description: "add_more_columns_and_indices_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_indices TEXT,
resolution TEXT,
ext TEXT,
abr REAL,
vbr REAL,
acodec TEXT,
vcodec TEXT,
dynamic_range TEXT,
process_id INTEGER,
status TEXT,
item 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,
square_crop_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,
CAST(playlist_index AS TEXT), -- Convert INTEGER playlist_index to TEXT playlist_indices
resolution, ext, abr, vbr,
acodec, vcodec, dynamic_range, process_id, status,
CASE WHEN playlist_id IS NOT NULL THEN '1/1' ELSE NULL END, -- item
progress, total, downloaded, speed, eta,
filepath, filetype, filesize,
output_format, embed_metadata, embed_thumbnail,
0, -- square_crop_thumbnail
sponsorblock_remove, sponsorblock_mark, use_aria2,
custom_command, queue_config, created_at, updated_at
FROM downloads;
-- Remove existing triggers
DROP TRIGGER IF EXISTS update_downloads_updated_at;
-- 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;
-- Add indexes to improve query performance
CREATE INDEX IF NOT EXISTS idx_downloads_video_id ON downloads(video_id);
CREATE INDEX IF NOT EXISTS idx_downloads_playlist_id ON downloads(playlist_id);
CREATE INDEX IF NOT EXISTS idx_downloads_status_updated ON downloads(download_status, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_downloads_id_desc ON downloads(id DESC);
",
kind: MigrationKind::Up,
}] }]
} }

View File

@@ -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.3.1", "version": "0.4.4",
"identifier": "com.neosubhamoy.neodlp", "identifier": "com.neosubhamoy.neodlp",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -1,8 +1,8 @@
{ {
"identifier": "com.neosubhamoy.neodlp", "identifier": "com.neosubhamoy.neodlp",
"build": { "build": {
"beforeDevCommand": "cargo build --target=aarch64-unknown-linux-gnu --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 && npm run dev",
"beforeBuildCommand": "cargo build --release --target=aarch64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run build", "beforeBuildCommand": "cargo build --release --target=aarch64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && npm run build",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1420",
"frontendDist": "../dist" "frontendDist": "../dist"
}, },
@@ -10,8 +10,9 @@
"windows": [ "windows": [
{ {
"title": "NeoDLP", "title": "NeoDLP",
"width": 1067, "width": 1080,
"height": 605, "height": 680,
"decorations": false,
"visible": false "visible": false
} }
], ],
@@ -28,6 +29,7 @@
"targets": ["deb", "rpm"], "targets": ["deb", "rpm"],
"createUpdaterArtifacts": true, "createUpdaterArtifacts": true,
"licenseFile": "../LICENSE", "licenseFile": "../LICENSE",
"category": "Utility",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
@@ -37,12 +39,17 @@
], ],
"externalBin": [ "externalBin": [
"binaries/yt-dlp", "binaries/yt-dlp",
"binaries/aria2c", "binaries/deno",
"binaries/deno" "binaries/neodlp-pot"
], ],
"resources": {
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
},
"linux": { "linux": {
"deb": { "deb": {
"depends": ["ffmpeg"], "depends": ["ffmpeg", "aria2"],
"provides": ["yt-dlp", "deno"],
"conflicts": ["yt-dlp", "deno"],
"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",
@@ -54,7 +61,9 @@
"rpm": { "rpm": {
"epoch": 0, "epoch": 0,
"release": "1", "release": "1",
"depends": ["ffmpeg"], "depends": ["ffmpeg", "aria2"],
"provides": ["yt-dlp", "deno"],
"conflicts": ["yt-dlp", "deno"],
"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",

View File

@@ -0,0 +1,48 @@
{
"identifier": "com.neosubhamoy.neodlp",
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "NeoDLP",
"width": 1080,
"height": 680,
"decorations": false,
"visible": false
}
],
"security": {
"csp": null,
"capabilities": [
"default",
"shell-scope"
]
}
},
"bundle": {
"active": true,
"targets": ["deb"],
"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/deno",
"binaries/neodlp-pot"
],
"resources": {
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
}
}
}

View File

@@ -1,8 +1,8 @@
{ {
"identifier": "com.neosubhamoy.neodlp", "identifier": "com.neosubhamoy.neodlp",
"build": { "build": {
"beforeDevCommand": "cargo build --target=x86_64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run dev", "beforeDevCommand": "cargo build --target=x86_64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
"beforeBuildCommand": "cargo build --release --target=x86_64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run build", "beforeBuildCommand": "cargo build --release --target=x86_64-unknown-linux-gnu --manifest-path=./src-tauri/msghost/Cargo.toml && npm run build",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1420",
"frontendDist": "../dist" "frontendDist": "../dist"
}, },
@@ -10,8 +10,9 @@
"windows": [ "windows": [
{ {
"title": "NeoDLP", "title": "NeoDLP",
"width": 1067, "width": 1080,
"height": 605, "height": 680,
"decorations": false,
"visible": false "visible": false
} }
], ],
@@ -28,6 +29,7 @@
"targets": ["deb", "rpm", "appimage"], "targets": ["deb", "rpm", "appimage"],
"createUpdaterArtifacts": true, "createUpdaterArtifacts": true,
"licenseFile": "../LICENSE", "licenseFile": "../LICENSE",
"category": "Utility",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
@@ -37,12 +39,17 @@
], ],
"externalBin": [ "externalBin": [
"binaries/yt-dlp", "binaries/yt-dlp",
"binaries/aria2c", "binaries/deno",
"binaries/deno" "binaries/neodlp-pot"
], ],
"resources": {
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
},
"linux": { "linux": {
"deb": { "deb": {
"depends": ["ffmpeg"], "depends": ["ffmpeg", "aria2"],
"provides": ["yt-dlp", "deno"],
"conflicts": ["yt-dlp", "deno"],
"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",
@@ -54,7 +61,9 @@
"rpm": { "rpm": {
"epoch": 0, "epoch": 0,
"release": "1", "release": "1",
"depends": ["ffmpeg"], "depends": ["ffmpeg", "aria2"],
"provides": ["yt-dlp", "deno"],
"conflicts": ["yt-dlp", "deno"],
"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",
@@ -66,7 +75,8 @@
"appimage": { "appimage": {
"files": { "files": {
"/usr/bin/ffmpeg": "./binaries/ffmpeg-x86_64-unknown-linux-gnu", "/usr/bin/ffmpeg": "./binaries/ffmpeg-x86_64-unknown-linux-gnu",
"/usr/bin/ffprobe": "./binaries/ffprobe-x86_64-unknown-linux-gnu" "/usr/bin/ffprobe": "./binaries/ffprobe-x86_64-unknown-linux-gnu",
"/usr/bin/aria2c": "./binaries/aria2c-x86_64-unknown-linux-gnu"
} }
} }
} }

View File

@@ -1,8 +1,8 @@
{ {
"identifier": "com.neosubhamoy.neodlp", "identifier": "com.neosubhamoy.neodlp",
"build": { "build": {
"beforeDevCommand": "cargo build --target=aarch64-apple-darwin --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 && npm run dev",
"beforeBuildCommand": "cargo build --release --target=aarch64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run build", "beforeBuildCommand": "cargo build --release --target=aarch64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && npm run build",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1420",
"frontendDist": "../dist" "frontendDist": "../dist"
}, },
@@ -39,13 +39,15 @@
"binaries/yt-dlp", "binaries/yt-dlp",
"binaries/ffmpeg", "binaries/ffmpeg",
"binaries/ffprobe", "binaries/ffprobe",
"binaries/deno" "binaries/deno",
"binaries/neodlp-pot"
], ],
"resources": { "resources": {
"target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost", "target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
"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",
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
}, },
"macOS": { "macOS": {
"providerShortName": "neosubhamoy" "providerShortName": "neosubhamoy"

View File

@@ -1,8 +1,8 @@
{ {
"identifier": "com.neosubhamoy.neodlp", "identifier": "com.neosubhamoy.neodlp",
"build": { "build": {
"beforeDevCommand": "cargo build --target=x86_64-apple-darwin --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 && npm run dev",
"beforeBuildCommand": "cargo build --release --target=x86_64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run build", "beforeBuildCommand": "cargo build --release --target=x86_64-apple-darwin --manifest-path=./src-tauri/msghost/Cargo.toml && npm run build",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1420",
"frontendDist": "../dist" "frontendDist": "../dist"
}, },
@@ -39,13 +39,15 @@
"binaries/yt-dlp", "binaries/yt-dlp",
"binaries/ffmpeg", "binaries/ffmpeg",
"binaries/ffprobe", "binaries/ffprobe",
"binaries/deno" "binaries/deno",
"binaries/neodlp-pot"
], ],
"resources": { "resources": {
"target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost", "target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
"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",
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
}, },
"macOS": { "macOS": {
"providerShortName": "neosubhamoy" "providerShortName": "neosubhamoy"

View File

@@ -10,8 +10,9 @@
"windows": [ "windows": [
{ {
"title": "NeoDLP", "title": "NeoDLP",
"width": 1067, "width": 1080,
"height": 605, "height": 680,
"decorations": false,
"visible": false "visible": false
} }
], ],
@@ -40,12 +41,14 @@
"binaries/ffmpeg", "binaries/ffmpeg",
"binaries/ffprobe", "binaries/ffprobe",
"binaries/aria2c", "binaries/aria2c",
"binaries/deno" "binaries/deno",
"binaries/neodlp-pot"
], ],
"resources": { "resources": {
"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",
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
}, },
"windows": { "windows": {
"wix": { "wix": {

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,5 +1,5 @@
import * as React from "react" import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { VideoFormat } from "@/types/video" import { VideoFormat } from "@/types/video"
import { determineFileType, formatBitrate, formatCodec, formatFileSize } from "@/utils" import { determineFileType, formatBitrate, formatCodec, formatFileSize } from "@/utils"
@@ -57,8 +57,8 @@ const FormatSelectionGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item <RadioGroupPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative w-full rounded-lg border-2 border-border bg-card px-3 py-2 shadow-sm transition-all", "relative w-full rounded-lg border-2 border-border bg-background px-3 py-2 shadow-sm transition-all",
"data-[state=checked]:border-primary data-[state=checked]:border-2 data-[state=checked]:bg-muted/70", "data-[state=checked]:border-primary data-[state=checked]:bg-primary/10",
"hover:bg-muted/70", "hover:bg-muted/70",
"disabled:cursor-not-allowed disabled:opacity-50", "disabled:cursor-not-allowed disabled:opacity-50",
className className
@@ -79,4 +79,4 @@ const FormatSelectionGroupItem = React.forwardRef<
}) })
FormatSelectionGroupItem.displayName = "FormatSelectionGroupItem" FormatSelectionGroupItem.displayName = "FormatSelectionGroupItem"
export { FormatSelectionGroup, FormatSelectionGroupItem } export { FormatSelectionGroup, FormatSelectionGroupItem }

View File

@@ -0,0 +1,114 @@
import * as React from "react";
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle";
import { VideoFormat } from "@/types/video";
import { determineFileType, formatBitrate, formatCodec, formatFileSize } from "@/utils";
import { Music, Video, File } from "lucide-react";
const FormatToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & { toggleType?: "single" | "multiple" }
>({
size: "default",
variant: "default",
toggleType: "multiple",
});
type FormatToggleGroupProps =
| (Omit<React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>, "type"> &
VariantProps<typeof toggleVariants> & { type: "single", value?: string, onValueChange?: (value: string) => void })
| (Omit<React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>, "type"> &
VariantProps<typeof toggleVariants> & { type: "multiple", value?: string[], onValueChange?: (value: string[]) => void });
export const FormatToggleGroup = React.forwardRef<
React.ComponentRef<typeof ToggleGroupPrimitive.Root>,
FormatToggleGroupProps
>(({ className, variant, size, children, type = "multiple", ...props }, ref) => {
if (type === "single") {
return (
<ToggleGroupPrimitive.Root
ref={ref}
type="single"
className={cn("flex flex-col gap-2", className)}
{...(props as any)}
>
<FormatToggleGroupContext.Provider value={{ variant, size, toggleType: "single" }}>
{children}
</FormatToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}
return (
<ToggleGroupPrimitive.Root
ref={ref}
type="multiple"
className={cn("flex flex-col gap-2", className)}
{...(props as any)}
>
<FormatToggleGroupContext.Provider value={{ variant, size, toggleType: "multiple" }}>
{children}
</FormatToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
});
FormatToggleGroup.displayName = "FormatToggleGroup";
export const FormatToggleGroupItem = React.forwardRef<
React.ComponentRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants> & {
format: VideoFormat
}
>(({ className, children, variant, size, format, value, ...props }, ref) => {
const determineFileTypeIcon = (format: VideoFormat) => {
const fileFormat = determineFileType(/*format.video_ext, format.audio_ext,*/ format.vcodec, format.acodec)
switch (fileFormat) {
case 'video+audio':
return (
<span className="absolute flex items-center right-2 bottom-2">
<Video className="w-3 h-3 mr-2" />
<Music className="w-3 h-3" />
</span>
)
case 'video':
return (
<Video className="w-3 h-3 absolute right-2 bottom-2" />
)
case 'audio':
return (
<Music className="w-3 h-3 absolute right-2 bottom-2" />
)
default:
return (
<File className="w-3 h-3 absolute right-2 bottom-2" />
)
}
}
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full p-2 rounded-lg border-2 border-border bg-background px-3 py-2 shadow-sm transition-all",
"hover:bg-muted/70 data-[state=on]:bg-primary/10",
"data-[state=on]:border-primary",
className
)}
value={value}
{...props}
>
<div className="flex flex-col items-start text-start gap-1">
<h5 className="text-sm">{format.format}</h5>
<p className="text-muted-foreground text-xs">{format.filesize_approx ? formatFileSize(format.filesize_approx) : 'unknown'} {format.tbr ? formatBitrate(format.tbr) : 'unknown'}</p>
<p className="text-muted-foreground text-xs">{format.ext ? format.ext.toUpperCase() : 'unknown'} {
((format.vcodec && format.vcodec !== 'none') || (format.acodec && format.acodec !== 'none')) && (
`(${format.vcodec && format.vcodec !== 'none' ? formatCodec(format.vcodec) : ''}${format.vcodec && format.vcodec !== 'none' && format.acodec && format.acodec !== 'none' ? ' ' : ''}${format.acodec && format.acodec !== 'none' ? formatCodec(format.acodec) : ''})`
)}</p>
{determineFileTypeIcon(format)}
</div>
</ToggleGroupPrimitive.Item>
);
});
FormatToggleGroupItem.displayName = "FormatToggleGroupItem";

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress" import { Progress as ProgressPrimitive } from "radix-ui"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -33,4 +33,4 @@ const IndeterminateProgress = React.forwardRef<
)); ));
IndeterminateProgress.displayName = ProgressPrimitive.Root.displayName; IndeterminateProgress.displayName = ProgressPrimitive.Root.displayName;
export { IndeterminateProgress }; export { IndeterminateProgress };

View File

@@ -1,5 +1,5 @@
import * as React from "react" import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
import { type VariantProps } from "class-variance-authority" import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -56,4 +56,4 @@ const ToggleGroupItem = React.forwardRef<
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem } export { ToggleGroup, ToggleGroupItem }

View File

@@ -0,0 +1,110 @@
import * as React from "react"
import { Minus, Plus } from "lucide-react"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
interface NumberInputProps
extends Omit<React.ComponentProps<"input">, "type" | "onChange" | "value"> {
value?: number
defaultValue?: number
min?: number
max?: number
step?: number
onChange?: (value: number) => void
}
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
(
{
className,
value: controlledValue,
defaultValue = 0,
min = -Infinity,
max = Infinity,
step = 1,
onChange,
disabled,
readOnly,
...props
},
ref
) => {
const [internalValue, setInternalValue] = React.useState(defaultValue)
const isControlled = controlledValue !== undefined
const currentValue = isControlled ? controlledValue : internalValue
const updateValue = (newValue: number) => {
const clamped = Math.min(max, Math.max(min, newValue))
if (!isControlled) {
setInternalValue(clamped)
}
onChange?.(clamped)
}
const handleIncrement = () => {
if (!disabled && !readOnly) {
updateValue(currentValue + step)
}
}
const handleDecrement = () => {
if (!disabled && !readOnly) {
updateValue(currentValue - step)
}
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const parsed = parseFloat(e.target.value)
if (!isNaN(parsed)) {
updateValue(parsed)
} else if (e.target.value === "" || e.target.value === "-") {
if (!isControlled) {
setInternalValue(0)
}
}
}
return (
<div className={cn("relative flex items-center", className)}>
<Input
type="number"
ref={ref}
value={currentValue}
onChange={handleInputChange}
min={min}
max={max}
step={step}
disabled={disabled}
readOnly={readOnly}
className="pr-16 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield] focus-visible:ring-0"
{...props}
/>
<div className="absolute right-0 flex h-full items-center">
<button
type="button"
onClick={handleDecrement}
disabled={disabled || readOnly || currentValue <= min}
className="flex h-full items-center justify-center px-2 text-muted-foreground hover:text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50 transition-colors border-x"
aria-label="Decrement"
tabIndex={-1}
>
<Minus className="size-3.5" />
</button>
<button
type="button"
onClick={handleIncrement}
disabled={disabled || readOnly || currentValue >= max}
className="flex h-full items-center justify-center px-2 text-muted-foreground hover:text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50 transition-colors rounded-r-md"
aria-label="Increment"
tabIndex={-1}
>
<Plus className="size-3.5" />
</button>
</div>
</div>
)
}
)
NumberInput.displayName = "NumberInput"
export { NumberInput }

View File

@@ -0,0 +1,81 @@
import { Paginated } from "@/types/download";
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
export default function PaginationBar({
paginatedData,
setPage,
}: {
paginatedData: Paginated;
setPage: (page: number) => void;
}) {
return (
<Pagination className="mt-4">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setPage(paginatedData.prev_page ?? paginatedData.first_page)}
aria-disabled={!paginatedData.prev_page}
className={!paginatedData.prev_page ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
/>
</PaginationItem>
{paginatedData.pages.map((link, index, array) => {
const currentPage = paginatedData.current_page;
const pageNumber = link.page!;
// Show first page, last page, current page, and 2 pages around current
const showPage =
pageNumber === 1 ||
pageNumber === paginatedData.last_page ||
Math.abs(pageNumber - currentPage) <= 1;
// Show ellipsis if there's a gap
const prevVisiblePage = array
.slice(0, index)
.reverse()
.find((prevLink) => {
const prevPageNum = prevLink.page!;
return (
prevPageNum === 1 ||
prevPageNum === paginatedData.last_page ||
Math.abs(prevPageNum - currentPage) <= 1
);
})?.page;
const showEllipsis = showPage && prevVisiblePage && pageNumber - prevVisiblePage > 1;
if (!showPage) return null;
return (
<div key={link.page} className="contents">
{showEllipsis && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{showPage && (
<PaginationItem>
<PaginationLink
className="cursor-pointer"
onClick={() => setPage(link.page)}
isActive={link.active}
>
{link.label}
</PaginationLink>
</PaginationItem>
)}
</div>
);
})}
<PaginationItem>
<PaginationNext
onClick={() => setPage(paginatedData.next_page ?? paginatedData.last_page)}
aria-disabled={!paginatedData.next_page}
className={!paginatedData.next_page ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)
}

View File

@@ -1,5 +1,5 @@
import * as React from "react" import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { RawVideoInfo } from "@/types/video" import { RawVideoInfo } from "@/types/video"
import { formatDurationString} from "@/utils" import { formatDurationString} from "@/utils"
@@ -35,8 +35,8 @@ const PlaylistSelectionGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item <RadioGroupPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative w-full rounded-lg border-2 border-border bg-card p-2 shadow-sm transition-all", "relative w-full rounded-lg border-2 border-border bg-background p-2 shadow-sm transition-all",
"data-[state=checked]:border-primary data-[state=checked]:border-2 data-[state=checked]:bg-muted/70", "data-[state=checked]:border-primary data-[state=checked]:bg-primary/10",
"hover:bg-muted/70", "hover:bg-muted/70",
"disabled:cursor-not-allowed disabled:opacity-50", "disabled:cursor-not-allowed disabled:opacity-50",
className className
@@ -44,28 +44,28 @@ const PlaylistSelectionGroupItem = React.forwardRef<
{...props} {...props}
> >
<div className="flex gap-2 w-full relative"> <div className="flex gap-2 w-full relative">
<div className="w-[7rem] xl:w-[10rem]"> <div className="w-28 xl:w-40">
<AspectRatio <AspectRatio
ratio={16 / 9} ratio={16 / 9}
className={clsx( className={clsx(
"w-full rounded overflow-hidden border border-border", "w-full rounded overflow-hidden border border-border",
video.aspect_ratio && video.aspect_ratio === 0.56 && "relative" video.aspect_ratio && video.aspect_ratio === 0.56 && "relative"
)} )}
> >
<ProxyImage <ProxyImage
src={video.thumbnail} src={video.thumbnail}
alt="thumbnail" alt="thumbnail"
className={clsx( className={clsx(
video.aspect_ratio && video.aspect_ratio === 0.56 && video.aspect_ratio && video.aspect_ratio === 0.56 &&
"absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" "absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
)} )}
/> />
</AspectRatio> </AspectRatio>
</div> </div>
<div className="flex w-[10rem] lg:w-[12rem] xl:w-[15rem] flex-col items-start text-start"> <div className="flex w-40 lg:w-48 xl:w-60 flex-col items-start text-start">
<h3 className="text-sm text-nowrap w-full overflow-hidden text-ellipsis mb-1" title={video.title}>{video.title}</h3> <h3 className="text-sm text-nowrap w-full overflow-hidden text-ellipsis mb-1" title={video.title}>{video.title}</h3>
<p className="text-xs text-muted-foreground mb-2">{video.channel || video.uploader || 'unknown'}</p> <p className="text-xs text-nowrap w-full overflow-hidden text-ellipsis text-muted-foreground mb-2" title={video.creator || video.channel || video.uploader || 'unknown'}>{video.creator || video.channel || video.uploader || 'unknown'}</p>
<div className="flex items-center"> <div className="flex items-center">
<span className="text-xs text-muted-foreground flex items-center pr-3"> <span className="text-xs text-muted-foreground flex items-center pr-3">
<Clock className="w-4 h-4 mr-2"/> <Clock className="w-4 h-4 mr-2"/>
@@ -79,4 +79,4 @@ const PlaylistSelectionGroupItem = React.forwardRef<
}) })
PlaylistSelectionGroupItem.displayName = "PlaylistSelectionGroupItem" PlaylistSelectionGroupItem.displayName = "PlaylistSelectionGroupItem"
export { PlaylistSelectionGroup, PlaylistSelectionGroupItem } export { PlaylistSelectionGroup, PlaylistSelectionGroupItem }

View File

@@ -1,9 +1,8 @@
import * as React from "react"; import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
import { type VariantProps } from "class-variance-authority"; import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle"; import { toggleVariants } from "@/components/ui/toggle";
import { Checkbox } from "@/components/ui/checkbox";
import { AspectRatio } from "@/components/ui/aspect-ratio"; import { AspectRatio } from "@/components/ui/aspect-ratio";
import { ProxyImage } from "@/components/custom/proxyImage"; import { ProxyImage } from "@/components/custom/proxyImage";
import { Clock } from "lucide-react"; import { Clock } from "lucide-react";
@@ -11,7 +10,6 @@ import clsx from "clsx";
import { formatDurationString } from "@/utils"; import { formatDurationString } from "@/utils";
import { RawVideoInfo } from "@/types/video"; import { RawVideoInfo } from "@/types/video";
// Create a context to share toggle group props
const PlaylistToggleGroupContext = React.createContext< const PlaylistToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & { toggleType?: "single" | "multiple" } VariantProps<typeof toggleVariants> & { toggleType?: "single" | "multiple" }
>({ >({
@@ -20,19 +18,16 @@ const PlaylistToggleGroupContext = React.createContext<
toggleType: "multiple", toggleType: "multiple",
}); });
// Helper type for the PlaylistToggleGroup type PlaylistToggleGroupProps =
type PlaylistToggleGroupProps = | (Omit<React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>, "type"> &
| (Omit<React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>, "type"> &
VariantProps<typeof toggleVariants> & { type: "single", value?: string, onValueChange?: (value: string) => void }) VariantProps<typeof toggleVariants> & { type: "single", value?: string, onValueChange?: (value: string) => void })
| (Omit<React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>, "type"> & | (Omit<React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>, "type"> &
VariantProps<typeof toggleVariants> & { type: "multiple", value?: string[], onValueChange?: (value: string[]) => void }); VariantProps<typeof toggleVariants> & { type: "multiple", value?: string[], onValueChange?: (value: string[]) => void });
// Main PlaylistToggleGroup component with proper type handling
export const PlaylistToggleGroup = React.forwardRef< export const PlaylistToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>, React.ComponentRef<typeof ToggleGroupPrimitive.Root>,
PlaylistToggleGroupProps PlaylistToggleGroupProps
>(({ className, variant, size, children, type = "multiple", ...props }, ref) => { >(({ className, variant, size, children, type = "multiple", ...props }, ref) => {
// Pass props based on the type
if (type === "single") { if (type === "single") {
return ( return (
<ToggleGroupPrimitive.Root <ToggleGroupPrimitive.Root
@@ -47,7 +42,7 @@ export const PlaylistToggleGroup = React.forwardRef<
</ToggleGroupPrimitive.Root> </ToggleGroupPrimitive.Root>
); );
} }
return ( return (
<ToggleGroupPrimitive.Root <ToggleGroupPrimitive.Root
ref={ref} ref={ref}
@@ -63,106 +58,48 @@ export const PlaylistToggleGroup = React.forwardRef<
}); });
PlaylistToggleGroup.displayName = "PlaylistToggleGroup"; PlaylistToggleGroup.displayName = "PlaylistToggleGroup";
// Rest of your component remains the same
// PlaylistToggleGroupItem component with checkbox and item layout
export const PlaylistToggleGroupItem = React.forwardRef< export const PlaylistToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>, React.ComponentRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants> & { VariantProps<typeof toggleVariants> & {
video: RawVideoInfo; video: RawVideoInfo;
} }
>(({ className, children, variant, size, video, value, ...props }, ref) => { >(({ className, children, variant, size, video, value, ...props }, ref) => {
const [isHovered, setIsHovered] = React.useState(false);
const [checked, setChecked] = React.useState(false);
// Instead of a ref + useEffect approach
const [itemElement, setItemElement] = React.useState<HTMLButtonElement | null>(null);
// Handle checkbox click separately by simulating a click on the parent item
const handleCheckboxClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
// Manually trigger the item's click to toggle selection
if (itemElement) {
// This simulates a click on the toggle item itself
itemElement.click();
}
};
// Use an effect that triggers when itemElement changes
React.useEffect(() => {
if (itemElement) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-state') {
setChecked(itemElement.getAttribute('data-state') === 'on');
}
});
});
setChecked(itemElement.getAttribute('data-state') === 'on');
observer.observe(itemElement, { attributes: true });
return () => observer.disconnect();
}
}, [itemElement]);
return ( return (
<ToggleGroupPrimitive.Item <ToggleGroupPrimitive.Item
ref={(el) => { ref={ref}
// Handle both our ref and the forwarded ref
if (typeof ref === 'function') {
ref(el);
} else if (ref) {
ref.current = el;
}
setItemElement(el);
}}
className={cn( className={cn(
"flex w-full p-2 rounded-md transition-colors border-2 border-border", "flex w-full p-2 rounded-lg transition-colors border-2 border-border",
"hover:bg-muted/50 data-[state=on]:bg-muted/70", "hover:bg-muted/70 data-[state=on]:bg-primary/10",
"data-[state=on]:border-primary", "data-[state=on]:border-primary",
className className
)} )}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
value={value} value={value}
{...props} {...props}
> >
<div className="flex gap-2 w-full relative"> <div className="flex gap-2 w-full relative">
<div className="absolute top-2 left-2 z-10"> <div className="w-28 xl:w-40">
<Checkbox <AspectRatio
checked={checked} ratio={16 / 9}
onClick={handleCheckboxClick}
className={cn(
"transition-opacity",
isHovered || checked ? "opacity-100" : "opacity-0"
)}
/>
</div>
<div className="w-[7rem] xl:w-[10rem]">
<AspectRatio
ratio={16 / 9}
className={clsx( className={clsx(
"w-full rounded overflow-hidden border border-border", "w-full rounded overflow-hidden border border-border",
video.aspect_ratio && video.aspect_ratio === 0.56 && "relative" video.aspect_ratio && video.aspect_ratio === 0.56 && "relative"
)} )}
> >
<ProxyImage <ProxyImage
src={video.thumbnail} src={video.thumbnail}
alt="thumbnail" alt="thumbnail"
className={clsx( className={clsx(
video.aspect_ratio && video.aspect_ratio === 0.56 && video.aspect_ratio && video.aspect_ratio === 0.56 &&
"absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" "absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
)} )}
/> />
</AspectRatio> </AspectRatio>
</div> </div>
<div className="flex w-[10rem] lg:w-[12rem] xl:w-[15rem] flex-col items-start text-start"> <div className="flex w-40 lg:w-48 xl:w-60 flex-col items-start text-start">
<h3 className="text-sm text-nowrap w-full overflow-hidden text-ellipsis mb-1">{video.title}</h3> <h3 className="text-sm text-nowrap w-full overflow-hidden text-ellipsis mb-1" title={video.title}>{video.title}</h3>
<p className="text-xs text-muted-foreground mb-2">{video.channel || video.uploader || 'unknown'}</p> <p className="text-xs text-nowrap w-full overflow-hidden text-ellipsis text-muted-foreground mb-2" title={video.creator || video.channel || video.uploader || 'unknown'}>{video.creator || video.channel || video.uploader || 'unknown'}</p>
<div className="flex items-center"> <div className="flex items-center">
<span className="text-xs text-muted-foreground flex items-center pr-3"> <span className="text-xs text-muted-foreground flex items-center pr-3">
<Clock className="w-4 h-4 mr-2"/> <Clock className="w-4 h-4 mr-2"/>
@@ -174,4 +111,4 @@ export const PlaylistToggleGroupItem = React.forwardRef<
</ToggleGroupPrimitive.Item> </ToggleGroupPrimitive.Item>
); );
}); });
PlaylistToggleGroupItem.displayName = "PlaylistToggleGroupItem"; PlaylistToggleGroupItem.displayName = "PlaylistToggleGroupItem";

View File

@@ -23,7 +23,7 @@ export const SlidingButton = ({
return ( return (
<Tag <Tag
className={cn( className={cn(
"px-4 py-2 rounded-md bg-black dark:bg-white dark:text-black text-white text-center relative overflow-hidden cursor-pointer flex justify-center", "px-4 py-2 rounded-md bg-primary text-primary-foreground text-center relative overflow-hidden cursor-pointer flex justify-center",
`group/sliding-button`, `group/sliding-button`,
className className
)} )}
@@ -41,7 +41,7 @@ export const SlidingButton = ({
</span> </span>
<div <div
className={cn( className={cn(
'flex items-center justify-center absolute inset-0 transition duration-500 text-white z-20', 'flex items-center justify-center absolute inset-0 transition duration-500 text-primary-foreground z-20',
`-translate-x-60 group-hover/sliding-button:translate-x-0` `-translate-x-60 group-hover/sliding-button:translate-x-0`
)} )}
> >
@@ -49,4 +49,4 @@ export const SlidingButton = ({
</div> </div>
</Tag> </Tag>
); );
}; };

View File

@@ -2,20 +2,21 @@ import { useLocation } from "react-router-dom";
import { isActive } from "@/utils"; import { isActive } from "@/utils";
import { config } from "@/config"; import { config } from "@/config";
import { useSettingsPageStatesStore } from "@/services/store"; import { useSettingsPageStatesStore } from "@/services/store";
import { Github, Globe } from "lucide-react"; import { Github, Globe, Heart } from "lucide-react";
import { IndianFlagLogo } from "@/components/icons/india";
export default function Footer() { export default function Footer() {
const location = useLocation(); const location = useLocation();
const isSettingsPage = isActive("/settings", location.pathname, true); const isSettingsPage = isActive("/settings", location.pathname, true);
const appVersion = useSettingsPageStatesStore(state => state.appVersion); const appVersion = useSettingsPageStatesStore(state => state.appVersion);
return ( return (
<> <>
{isSettingsPage ? ( {isSettingsPage ? (
<div className="flex items-center justify-between p-4 border-t border-border"> <div className="flex items-center justify-between p-4 border-t border-border">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-sm">{config.appName} v{appVersion} - &copy; {new Date().getFullYear()} &nbsp;|&nbsp; <a href={'https://github.com/' + config.appRepo + '/blob/main/LICENSE'} target="_blank">MIT License</a></span> <span className="text-sm">{config.appName} v{appVersion} &copy; 2025 - {new Date().getFullYear()} &nbsp;|&nbsp; <a href={'https://github.com/' + config.appRepo + '/blob/main/LICENSE'} target="_blank">MIT License</a></span>
<span className="text-xs text-muted-foreground">Made with by <a href={config.appAuthorUrl} target="_blank">{config.appAuthor}</a></span> <span className="text-xs text-muted-foreground">Proudly Made with <Heart className="inline size-3 mb-0.5 fill-primary stroke-primary"/> in India <IndianFlagLogo className="inline size-full w-4 ml-0.5 mb-[0.1rem]" /></span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<a href={config.appHomepage} target="_blank" className="text-sm text-muted-foreground hover:text-foreground" title="Homepage"> <a href={config.appHomepage} target="_blank" className="text-sm text-muted-foreground hover:text-foreground" title="Homepage">

View File

@@ -0,0 +1,12 @@
export function CloseIcon({ className }: { className?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" className={className}>
<path
fill="currentColor"
fillRule="evenodd"
d="m7.116 8l-4.558 4.558l.884.884L8 8.884l4.558 4.558l.884-.884L8.884 8l4.558-4.558l-.884-.884L8 7.116L3.442 2.558l-.884.884z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@@ -0,0 +1,27 @@
export function IndianFlagLogo({ className }: { className?: string }) {
return (
<svg width="1024" height="1024" viewBox="-45 -30 90 60" fill="#07038D" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" className={className}>
<path fill="#FFF" d="m-45-30h90v60h-90z"/>
<path fill="#FF6820" d="m-45-30h90v20h-90z"/>
<path fill="#046A38" d="m-45 10h90v20h-90z"/>
<circle r="9.25"/>
<circle fill="#FFF" r="8"/>
<circle r="1.6"/>
<g id="d">
<g id="c">
<g id="b">
<g id="a">
<path d="m0-8 .3 4.81409L0-.80235-.3-3.18591z"/>
<circle transform="rotate(7.5)" r="0.35" cy="-8"/>
</g>
<use xlinkHref="#a" transform="scale(-1)"/>
</g>
<use xlinkHref="#b" transform="rotate(15)"/>
</g>
<use xlinkHref="#c" transform="rotate(30)"/>
</g>
<use xlinkHref="#d" transform="rotate(60)"/>
<use xlinkHref="#d" transform="rotate(120)"/>
</svg>
);
}

View File

@@ -0,0 +1,7 @@
export function MaximizeIcon({ className }: { className?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" className={className}>
<path fill="currentColor" d="M3 3v10h10V3zm9 9H4V4h8z" />
</svg>
);
}

View File

@@ -0,0 +1,7 @@
export function MinimizeIcon({ className }: { className?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" className={className}>
<path fill="currentColor" d="M14 8v1H3V8z" />
</svg>
);
}

View File

@@ -6,10 +6,10 @@ export function NeoDlpLogo({ className }: { className?: string }) {
<rect x="355" y="222" width="313" height="346" rx="25" fill="#FAFAFA"/> <rect x="355" y="222" width="313" height="346" rx="25" fill="#FAFAFA"/>
<defs> <defs>
<linearGradient id="paint0_linear_10_2" x1="129.5" y1="148.5" x2="921" y2="863" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_10_2" x1="129.5" y1="148.5" x2="921" y2="863" gradientUnits="userSpaceOnUse">
<stop stopColor="#4444FF"/> <stop stopColor="var(--logo-stop-color-1)"/>
<stop offset="1" stopColor="#FF43D0"/> <stop offset="1" stopColor="var(--logo-stop-color-2)"/>
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
) )
} }

View File

@@ -0,0 +1,10 @@
export function UnmaximizeIcon({ className }: { className?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" className={className}>
<g fill="currentColor">
<path d="M3 5v9h9V5zm8 8H4V6h7z" />
<path fillRule="evenodd" d="M5 5h1V4h7v7h-1v1h2V3H5z" clipRule="evenodd" />
</g>
</svg>
);
}

View File

@@ -1,56 +1,99 @@
import { useState } from "react";
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 { BrushCleaning, Check, Copy, 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { useLogger } from "@/helpers/use-logger"; import { useLogger } from "@/helpers/use-logger";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import TitleBar from "@/components/titlebar";
import { platform } from "@tauri-apps/plugin-os";
export default function Navbar() { export default function Navbar() {
const [copied, setCopied] = useState(false);
const location = useLocation(); const location = useLocation();
const logs = useLogger().getLogs(); const currentPlatform = platform();
const logger = useLogger();
const logs = logger.getLogs();
const logText = logs.map(log => `${new Date(log.timestamp).toLocaleTimeString()} [${log.level.toUpperCase()}] ${log.context}: ${log.message}`).join('\n');
const handleCopyLogs = async () => {
await writeText(logText);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
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"> <div className="sticky top-0 z-50">
<div className="flex justify-center"> {currentPlatform === "windows" || currentPlatform === "linux" ? (
<SidebarTrigger /> <TitleBar />
<h1 className="text-lg text-primary font-semibold ml-4">{getRouteName(location.pathname)}</h1> ) : (
</div> null
<div className="flex justify-center items-center"> )}
<Dialog> <nav className="flex justify-between items-center py-3 px-4 backdrop-blur supports-backdrop-filter:bg-background/60 border-b">
<Tooltip> <div className="flex justify-center">
<TooltipTrigger asChild> <SidebarTrigger />
<DialogTrigger asChild> <h1 className="text-lg font-semibold ml-4">{getRouteName(location.pathname)}</h1>
<Button variant="outline" size="icon"> </div>
<Terminal /> <div className="flex justify-center items-center">
<Dialog>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Terminal />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Logs</p>
</TooltipContent>
</Tooltip>
<DialogContent className="sm:max-w-150">
<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-75 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>
<DialogFooter>
<Button
variant="destructive"
disabled={logs.length === 0}
onClick={() => logger.clearLogs()}
>
<BrushCleaning className="size-4" />
Clear Logs
</Button> </Button>
</DialogTrigger> <Button
</TooltipTrigger> className="transition-all duration-300"
<TooltipContent> disabled={logs.length === 0}
<p>Logs</p> onClick={() => handleCopyLogs()}
</TooltipContent> >
</Tooltip> {copied ? (
<DialogContent className="sm:max-w-[600px]"> <Check className="size-4" />
<DialogHeader> ) : (
<DialogTitle>Log Viewer</DialogTitle> <Copy className="size-4" />
<DialogDescription>Monitor real-time app session logs (latest on top)</DialogDescription> )}
</DialogHeader> Copy Logs
<div className="flex flex-col gap-2 p-2 max-h-[300px] overflow-y-scroll overflow-x-hidden bg-muted"> </Button>
{logs.length === 0 ? ( </DialogFooter>
<p className="text-sm text-muted-foreground">NO LOGS TO SHOW!</p> </DialogContent>
) : ( </Dialog>
logs.slice().reverse().map((log, index) => ( </div>
<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' : 'text-foreground'}`}> </nav>
<p className="text-xs"><strong>{new Date(log.timestamp).toLocaleTimeString()}</strong> [{log.level.toUpperCase()}] <em>{log.context}</em> :</p> </div>
<p className="text-xs font-mono break-all">{log.message}</p>
</div>
))
)}
</div>
</DialogContent>
</Dialog>
</div>
</nav>
) )
} }

View File

@@ -0,0 +1,503 @@
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { useAppContext } from "@/providers/appContextProvider";
import { useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
import { formatBitrate, formatFileSize } from "@/utils";
import { Loader2, Music, Video, File, AlertCircleIcon, Settings2 } from "lucide-react";
import { useEffect, useRef } from "react";
import { RawVideoInfo, VideoFormat } from "@/types/video";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Checkbox } from "@/components/ui/checkbox";
interface DownloadConfigDialogProps {
selectedFormatFileType: "video+audio" | "video" | "audio" | "unknown";
}
interface BottomBarProps {
videoMetadata: RawVideoInfo;
selectedFormat: VideoFormat | undefined;
selectedFormatFileType: "video+audio" | "video" | "audio" | "unknown";
selectedVideoFormat: VideoFormat | undefined;
selectedAudioFormats: VideoFormat[] | undefined;
containerRef: React.RefObject<HTMLDivElement | null>;
}
function DownloadConfigDialog({ selectedFormatFileType }: DownloadConfigDialogProps) {
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
const activeDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.activeDownloadConfigurationTab);
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
const selectedCombinableAudioFormats = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormats);
const downloadConfiguration = useDownloaderPageStatesStore((state) => state.downloadConfiguration);
const setActiveDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadConfigurationTab);
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
const embedVideoMetadata = useSettingsPageStatesStore(state => state.settings.embed_video_metadata);
const embedAudioMetadata = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
const embedVideoThumbnail = useSettingsPageStatesStore(state => state.settings.embed_video_thumbnail);
const embedAudioThumbnail = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
const customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands);
const isCombineableAudioSelected = selectedCombinableAudioFormats && selectedCombinableAudioFormats.length > 0;
return (
<Dialog>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button
variant="outline"
size="icon"
disabled={!selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !isCombineableAudioSelected))}
>
<Settings2 className="size-4" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Configurations</p>
</TooltipContent>
</Tooltip>
<DialogContent className="sm:max-w-112.5">
<DialogHeader>
<DialogTitle>Configurations</DialogTitle>
<DialogDescription>Tweak this download's configurations</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2 max-h-75 overflow-y-scroll overflow-x-hidden no-scrollbar">
<Tabs
className=""
value={activeDownloadConfigurationTab}
onValueChange={(tab) => setActiveDownloadConfigurationTab(tab)}
>
<TabsList>
<TabsTrigger value="options">Options</TabsTrigger>
<TabsTrigger value="commands">Commands</TabsTrigger>
</TabsList>
<TabsContent value="options">
{useCustomCommands ? (
<Alert className="mt-2 mb-3">
<AlertCircleIcon className="size-4 stroke-primary" />
<AlertTitle className="text-sm">Options Unavailable!</AlertTitle>
<AlertDescription className="text-xs">
You cannot use these options when custom commands are enabled. To use these options, disable custom commands from Settings.
</AlertDescription>
</Alert>
) : null}
<div className="video-format">
<Label className="text-xs mb-3 mt-2">Output Format ({(selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? 'Video' : selectedFormatFileType && selectedFormatFileType === 'audio' ? 'Audio' : 'Unknown'})</Label>
{(selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? (
<RadioGroup
orientation="horizontal"
className="flex items-center gap-4 flex-wrap my-2"
value={downloadConfiguration.output_format ?? 'auto'}
onValueChange={(value) => setDownloadConfigurationKey('output_format', value)}
disabled={useCustomCommands}
>
<div className="flex items-center gap-3">
<RadioGroupItem value="auto" id="v-auto" />
<Label htmlFor="v-auto">Follow Settings</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="mp4" id="v-mp4" />
<Label htmlFor="v-mp4">MP4</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="webm" id="v-webm" />
<Label htmlFor="v-webm">WEBM</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="mkv" id="v-mkv" />
<Label htmlFor="v-mkv">MKV</Label>
</div>
</RadioGroup>
) : selectedFormatFileType && selectedFormatFileType === 'audio' ? (
<RadioGroup
orientation="horizontal"
className="flex items-center gap-4 flex-wrap my-2"
value={downloadConfiguration.output_format ?? 'auto'}
onValueChange={(value) => setDownloadConfigurationKey('output_format', value)}
disabled={useCustomCommands}
>
<div className="flex items-center gap-3">
<RadioGroupItem value="auto" id="a-auto" />
<Label htmlFor="a-auto">Follow Settings</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="m4a" id="a-m4a" />
<Label htmlFor="a-m4a">M4A</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="opus" id="a-opus" />
<Label htmlFor="a-opus">OPUS</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="mp3" id="a-mp3" />
<Label htmlFor="a-mp3">MP3</Label>
</div>
</RadioGroup>
) : (
<RadioGroup
orientation="horizontal"
className="flex items-center gap-4 flex-wrap my-2"
value={downloadConfiguration.output_format ?? 'auto'}
onValueChange={(value) => setDownloadConfigurationKey('output_format', value)}
disabled={useCustomCommands}
>
<div className="flex items-center gap-3">
<RadioGroupItem value="auto" id="u-auto" />
<Label htmlFor="u-auto">Follow Settings</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="mp4" id="u-mp4" />
<Label htmlFor="u-mp4">MP4</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="webm" id="u-webm" />
<Label htmlFor="u-webm">WEBM</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="mkv" id="u-mkv" />
<Label htmlFor="u-mkv">MKV</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="m4a" id="u-m4a" />
<Label htmlFor="u-m4a">M4A</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="opus" id="u-opus" />
<Label htmlFor="u-opus">OPUS</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="mp3" id="u-mp3" />
<Label htmlFor="u-mp3">MP3</Label>
</div>
</RadioGroup>
)}
</div>
<div className="sponsorblock">
<Label className="text-xs my-3">Sponsorblock Mode</Label>
<RadioGroup
orientation="horizontal"
className="flex items-center gap-4 flex-wrap my-2"
value={downloadConfiguration.sponsorblock ?? 'auto'}
onValueChange={(value) => setDownloadConfigurationKey('sponsorblock', value)}
disabled={useCustomCommands}
>
<div className="flex items-center gap-3">
<RadioGroupItem value="auto" id="sb-auto" />
<Label htmlFor="sb-auto">Follow Settings</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="remove" id="sb-remove" />
<Label htmlFor="sb-remove">Remove</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="mark" id="sb-mark" />
<Label htmlFor="sb-mark">Mark</Label>
</div>
</RadioGroup>
</div>
<div className="embeding-options">
<Label className="text-xs my-3">Embedding Options</Label>
<div className="flex items-center space-x-2 mt-3">
<Switch
id="embed-metadata"
checked={downloadConfiguration.embed_metadata !== null ? downloadConfiguration.embed_metadata : (selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? embedVideoMetadata : selectedFormatFileType && selectedFormatFileType === 'audio' ? embedAudioMetadata : false}
onCheckedChange={(checked) => setDownloadConfigurationKey('embed_metadata', checked)}
disabled={useCustomCommands}
/>
<Label htmlFor="embed-metadata">Embed Metadata</Label>
</div>
<div className="flex items-center space-x-2 mt-3">
<Switch
id="embed-thumbnail"
checked={downloadConfiguration.embed_thumbnail !== null ? downloadConfiguration.embed_thumbnail : (selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? embedVideoThumbnail : selectedFormatFileType && selectedFormatFileType === 'audio' ? embedAudioThumbnail : false}
onCheckedChange={(checked) => setDownloadConfigurationKey('embed_thumbnail', checked)}
disabled={useCustomCommands}
/>
<Label htmlFor="embed-thumbnail">Embed Thumbnail</Label>
<div className="flex items-center gap-3 ml-4">
<Checkbox
id="square-crop-thumbnail"
checked={downloadConfiguration.square_crop_thumbnail !== null ? downloadConfiguration.square_crop_thumbnail : false}
onCheckedChange={(checked) => setDownloadConfigurationKey('square_crop_thumbnail', checked)}
disabled={useCustomCommands || !(downloadConfiguration.embed_thumbnail !== null ? downloadConfiguration.embed_thumbnail : (selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? embedVideoThumbnail : selectedFormatFileType && selectedFormatFileType === 'audio' ? embedAudioThumbnail : false)}
/>
<Label htmlFor="square-crop-thumbnail">Square Crop</Label>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="commands">
{!useCustomCommands ? (
<Alert className="mt-2 mb-3">
<AlertCircleIcon className="size-4 stroke-primary" />
<AlertTitle className="text-sm">Enable Custom Commands!</AlertTitle>
<AlertDescription className="text-xs">
To run custom commands for downloads, please enable it from the Settings.
</AlertDescription>
</Alert>
) : null}
<div className="custom-commands">
<Label className="text-xs mb-3 mt-2">Run Custom Command</Label>
{customCommands.length === 0 ? (
<p className="text-sm text-muted-foreground">NO CUSTOM COMMAND TEMPLATE ADDED YET!</p>
) : (
<RadioGroup
orientation="vertical"
className="flex flex-col gap-2 my-2"
disabled={!useCustomCommands}
value={downloadConfiguration.custom_command}
onValueChange={(value) => setDownloadConfigurationKey('custom_command', value)}
>
{customCommands.map((command) => (
<div className="flex items-center gap-3" key={command.id}>
<RadioGroupItem value={command.id} id={`cmd-${command.id}`} />
<Label htmlFor={`cmd-${command.id}`}>{command.label}</Label>
</div>
))}
</RadioGroup>
)}
</div>
</TabsContent>
</Tabs>
</div>
</DialogContent>
</Dialog>
);
}
export function BottomBar({ videoMetadata, selectedFormat, selectedFormatFileType, selectedVideoFormat, selectedAudioFormats, containerRef }: BottomBarProps) {
const { startDownload } = useAppContext();
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
const selectedCombinableAudioFormats = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormats);
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
const selectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideos);
const downloadConfiguration = useDownloaderPageStatesStore((state) => state.downloadConfiguration);
const setActiveDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadConfigurationTab);
const setIsStartingDownload = useDownloaderPageStatesStore((state) => state.setIsStartingDownload);
const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format);
const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format);
const useSponsorblock = useSettingsPageStatesStore(state => state.settings.use_sponsorblock);
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
const bottomBarRef = useRef<HTMLDivElement>(null);
const isPlaylist = videoMetadata._type === 'playlist';
const isMultiplePlaylistItems = isPlaylist && selectedPlaylistVideos.length > 1;
const isCombineableAudioSelected = selectedCombinableAudioFormats && selectedCombinableAudioFormats.length > 0 && selectedAudioFormats && selectedAudioFormats.length > 0;
const isMultipleCombineableAudioSelected = selectedCombinableAudioFormats && selectedCombinableAudioFormats.length > 1 && selectedAudioFormats && selectedAudioFormats.length > 1;
let selectedFormatExtensionMsg = 'Auto - unknown';
if (activeDownloadModeTab === 'combine') {
if (downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') {
selectedFormatExtensionMsg = `Combined - ${downloadConfiguration.output_format.toUpperCase()}`;
} else if (videoFormat !== 'auto') {
selectedFormatExtensionMsg = `Combined - ${videoFormat.toUpperCase()}`;
} else if (isCombineableAudioSelected && selectedVideoFormat?.ext) {
if (isMultipleCombineableAudioSelected) {
selectedFormatExtensionMsg = `Combined - ${selectedVideoFormat.ext.toUpperCase()} + ${selectedAudioFormats.length} Audio`;
} else {
selectedFormatExtensionMsg = `Combined - ${selectedVideoFormat.ext.toUpperCase()} + ${selectedAudioFormats[0].ext.toUpperCase()}`;
}
} else {
selectedFormatExtensionMsg = `Combined - unknown`;
}
} else if (selectedFormat?.ext) {
if ((selectedFormatFileType === 'video+audio' || selectedFormatFileType === 'video') && ((downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') || videoFormat !== 'auto')) {
selectedFormatExtensionMsg = `Forced - ${(downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') ? downloadConfiguration.output_format.toUpperCase() : videoFormat.toUpperCase()}`;
} else if (selectedFormatFileType === 'audio' && ((downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') || audioFormat !== 'auto')) {
selectedFormatExtensionMsg = `Forced - ${(downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') ? downloadConfiguration.output_format.toUpperCase() : audioFormat.toUpperCase()}`;
} else if (selectedFormatFileType === 'unknown' && downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') {
selectedFormatExtensionMsg = `Forced - ${downloadConfiguration.output_format.toUpperCase()}`;
} else {
selectedFormatExtensionMsg = `Auto - ${selectedFormat.ext.toUpperCase()}`;
}
}
let selectedFormatResolutionMsg = 'unknown';
let totalTbr = 0;
if (activeDownloadModeTab === 'combine') {
if (isCombineableAudioSelected) {
if (isMultipleCombineableAudioSelected) {
const totalAudioTbr = selectedAudioFormats.reduce((acc, format) => acc + (format.tbr ?? 0), 0);
totalTbr = (selectedVideoFormat?.tbr ?? 0) + totalAudioTbr;
selectedFormatResolutionMsg = `${selectedVideoFormat?.resolution ?? 'unknown'} + ${formatBitrate(totalAudioTbr)}`;
} else {
totalTbr = (selectedVideoFormat?.tbr ?? 0) + (selectedAudioFormats && selectedAudioFormats[0].tbr ? selectedAudioFormats[0].tbr : 0);
selectedFormatResolutionMsg = `${selectedVideoFormat?.resolution ?? 'unknown'} + ${selectedAudioFormats && selectedAudioFormats[0].tbr ? formatBitrate(selectedAudioFormats[0].tbr) : 'unknown'}`;
}
} else {
totalTbr = selectedVideoFormat?.tbr ?? 0;
selectedFormatResolutionMsg = `${selectedVideoFormat?.resolution ?? 'unknown'} + unknown`;
}
} else if (selectedFormat?.resolution) {
totalTbr = selectedFormat.tbr ?? 0;
selectedFormatResolutionMsg = selectedFormat.resolution;
}
let selectedFormatDynamicRangeMsg = '';
if (activeDownloadModeTab === 'combine') {
selectedFormatDynamicRangeMsg = selectedVideoFormat?.dynamic_range && selectedVideoFormat.dynamic_range !== 'SDR' && selectedVideoFormat.dynamic_range !== 'auto' ? selectedVideoFormat.dynamic_range : '';
} else if (selectedFormat?.dynamic_range && selectedFormat.dynamic_range !== 'SDR' && selectedFormat.dynamic_range !== 'auto') {
selectedFormatDynamicRangeMsg = selectedFormat.dynamic_range;
}
let selectedFormatFileSizeMsg = 'unknown filesize';
let totalFilesize = 0;
if (activeDownloadModeTab === 'combine') {
if (isCombineableAudioSelected) {
if (isMultipleCombineableAudioSelected) {
totalFilesize = (selectedVideoFormat?.filesize_approx ?? 0) + selectedAudioFormats.reduce((acc, format) => acc + (format.filesize_approx ?? 0), 0);
selectedFormatFileSizeMsg = totalFilesize > 0 ? formatFileSize(totalFilesize) : 'unknown filesize';
} else {
totalFilesize = (selectedVideoFormat?.filesize_approx ?? 0) + (selectedAudioFormats && selectedAudioFormats[0].filesize_approx ? selectedAudioFormats[0].filesize_approx : 0);
selectedFormatFileSizeMsg = (selectedVideoFormat?.filesize_approx && selectedAudioFormats && selectedAudioFormats[0].filesize_approx) ? formatFileSize(selectedVideoFormat.filesize_approx + selectedAudioFormats[0].filesize_approx) : 'unknown filesize';
}
} else {
totalFilesize = selectedVideoFormat?.filesize_approx ?? 0;
selectedFormatFileSizeMsg = selectedVideoFormat?.filesize_approx ? formatFileSize(selectedVideoFormat.filesize_approx) : 'unknown filesize';
}
} else if (selectedFormat?.filesize_approx) {
totalFilesize = selectedFormat.filesize_approx;
selectedFormatFileSizeMsg = formatFileSize(selectedFormat.filesize_approx);
}
let selectedFormatFinalMsg = '';
if (activeDownloadModeTab === 'combine') {
if (selectedCombinableVideoFormat && selectedCombinableAudioFormats.length > 0) {
selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''} ${useSponsorblock || (downloadConfiguration.sponsorblock && downloadConfiguration.sponsorblock !== 'auto') ? `• SPBLOCK` : ''} • ${selectedFormatFileSizeMsg}`;
} else {
selectedFormatFinalMsg = `Choose a video and audio streams to combine`;
}
} else {
if (selectedFormat) {
selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''} ${useSponsorblock || (downloadConfiguration.sponsorblock && downloadConfiguration.sponsorblock !== 'auto') ? `• SPBLOCK` : ''} • ${selectedFormatFileSizeMsg}`;
} else {
selectedFormatFinalMsg = `Select a stream to download`;
}
}
useEffect(() => {
const updateBottomBarWidth = (): void => {
if (containerRef.current && bottomBarRef.current) {
bottomBarRef.current.style.width = `${containerRef.current.offsetWidth}px`;
const containerRect = containerRef.current.getBoundingClientRect();
bottomBarRef.current.style.left = `${containerRect.left}px`;
}
};
updateBottomBarWidth();
const resizeObserver = new ResizeObserver(() => {
updateBottomBarWidth();
});
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
window.addEventListener('resize', updateBottomBarWidth);
window.addEventListener('scroll', updateBottomBarWidth);
return () => {
resizeObserver.disconnect();
window.removeEventListener('resize', updateBottomBarWidth);
window.removeEventListener('scroll', updateBottomBarWidth);
};
}, []);
useEffect(() => {
useCustomCommands ? setActiveDownloadConfigurationTab('commands') : setActiveDownloadConfigurationTab('options');
}, []);
return (
<div className="flex justify-between items-center gap-2 fixed bottom-0 right-0 p-4 w-full bg-background rounded-t-lg border-t border-border z-20" ref={bottomBarRef}>
<div className="flex items-center gap-4">
<div className="flex justify-center items-center p-3 rounded-md border border-border">
{activeDownloadModeTab === 'combine' && (
<Video className="w-4 h-4 stroke-primary" />
)}
{activeDownloadModeTab !== 'combine' && selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio') && (
<Video className="w-4 h-4 stroke-primary" />
)}
{activeDownloadModeTab !== 'combine' && selectedFormatFileType && selectedFormatFileType === 'audio' && (
<Music className="w-4 h-4 stroke-primary" />
)}
{activeDownloadModeTab !== 'combine' && ((!selectedFormatFileType) || (selectedFormatFileType && selectedFormatFileType !== 'video' && selectedFormatFileType !== 'audio' && selectedFormatFileType !== 'video+audio')) && (
<File className="w-4 h-4 stroke-primary" />
)}
</div>
<div className="flex flex-col gap-1">
<span className="text-sm text-nowrap max-w-120 xl:max-w-200 overflow-hidden text-ellipsis">{videoMetadata._type === 'video' ? videoMetadata.title : videoMetadata._type === 'playlist' ? selectedPlaylistVideos.length === 1 ? videoMetadata.entries[Number(selectedPlaylistVideos[0]) - 1].title : `${selectedPlaylistVideos.length} Items` : 'Unknown' }</span>
<span className="text-xs text-muted-foreground">{selectedFormatFinalMsg}</span>
</div>
</div>
<div className="flex items-center gap-2">
<DownloadConfigDialog selectedFormatFileType={selectedFormatFileType} />
<Button
onClick={async () => {
setIsStartingDownload(true);
try {
if (videoMetadata._type === 'playlist') {
await startDownload({
url: videoMetadata.original_url,
selectedFormat: activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormats.join('+')}` : selectedDownloadFormat,
downloadConfig: downloadConfiguration,
selectedSubtitles: selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
playlistItems: selectedPlaylistVideos.sort((a, b) => Number(a) - Number(b)).join(','),
overrideOptions: isMultiplePlaylistItems ? {
filesize: totalFilesize > 0 ? totalFilesize : undefined,
tbr: totalTbr > 0 ? totalTbr : undefined,
} : isMultipleCombineableAudioSelected ? {
filesize: totalFilesize > 0 ? totalFilesize : undefined,
tbr: totalTbr > 0 ? totalTbr : undefined,
} : undefined
});
} else if (videoMetadata._type === 'video') {
await startDownload({
url: videoMetadata.webpage_url,
selectedFormat: activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormats.join('+')}` : selectedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selectedDownloadFormat,
downloadConfig: downloadConfiguration,
selectedSubtitles: selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
overrideOptions: isMultipleCombineableAudioSelected ? {
filesize: totalFilesize > 0 ? totalFilesize : undefined,
tbr: totalTbr > 0 ? totalTbr : undefined,
} : undefined
});
}
// toast({
// title: 'Download Initiated',
// description: 'Download initiated, it will start shortly.',
// });
} catch (error) {
console.error('Download failed to start:', error);
toast.error("Failed to Start Download", {
description: "There was an error initiating the download."
});
} finally {
setIsStartingDownload(false);
}
}}
disabled={isStartingDownload || !selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !isCombineableAudioSelected)) || (useCustomCommands && !downloadConfiguration.custom_command)}
>
{isStartingDownload ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Starting Download
</>
) : (
'Start Download'
)}
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,411 @@
import { useDownloaderPageStatesStore } from "@/services/store";
import { DownloadCloud, Info, ListVideo, AlertCircleIcon } from "lucide-react";
import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { RawVideoInfo, VideoFormat } from "@/types/video";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { PlaylistToggleGroup, PlaylistToggleGroupItem } from "@/components/custom/playlistToggleGroup";
import { getMergedBestFormat } from "@/utils";
import { Switch } from "@/components/ui/switch";
import { FormatToggleGroup, FormatToggleGroupItem } from "@/components/custom/formatToggleGroup";
import { Layout } from "react-resizable-panels";
interface PlaylistPreviewSelectionProps {
videoMetadata: RawVideoInfo;
}
interface SelectivePlaylistDownloadProps {
videoMetadata: RawVideoInfo;
audioOnlyFormats: VideoFormat[] | undefined;
videoOnlyFormats: VideoFormat[] | undefined;
combinedFormats: VideoFormat[] | undefined;
qualityPresetFormats: VideoFormat[] | undefined;
subtitleLanguages: { code: string; lang: string }[];
}
interface CombinedPlaylistDownloadProps {
audioOnlyFormats: VideoFormat[] | undefined;
videoOnlyFormats: VideoFormat[] | undefined;
subtitleLanguages: { code: string; lang: string }[];
}
interface PlaylistDownloaderProps {
videoMetadata: RawVideoInfo;
audioOnlyFormats: VideoFormat[] | undefined;
videoOnlyFormats: VideoFormat[] | undefined;
combinedFormats: VideoFormat[] | undefined;
qualityPresetFormats: VideoFormat[] | undefined;
subtitleLanguages: { code: string; lang: string }[];
}
function PlaylistPreviewSelection({ videoMetadata }: PlaylistPreviewSelectionProps) {
const selectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideos);
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
const setSelectedCombinableAudioFormats = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormats);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const setSelectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.setSelectedPlaylistVideos);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
const totalVideos = videoMetadata.entries.filter((entry) => entry).length;
const allVideoIndices = videoMetadata.entries.filter((entry) => entry).map((entry) => entry.playlist_index.toString());
return (
<div className="flex flex-col w-full pr-4">
<div className="flex items-center justify-between mb-4 mt-2">
<h3 className="text-sm flex items-center gap-2">
<ListVideo className="w-4 h-4" />
<span>Playlist ({videoMetadata.entries[0].n_entries})</span>
</h3>
<div className="flex items-center space-x-2">
<Switch
id="select-all-videos"
checked={selectedPlaylistVideos.length === totalVideos && totalVideos > 0}
onCheckedChange={(checked) => {
if (checked) {
setSelectedPlaylistVideos(allVideoIndices);
} else {
setSelectedPlaylistVideos(["1"]);
}
setSelectedDownloadFormat('best');
setSelectedSubtitles([]);
setSelectedCombinableVideoFormat('');
setSelectedCombinableAudioFormats([]);
resetDownloadConfiguration();
}}
disabled={totalVideos <= 1}
/>
</div>
</div>
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
<h2 className="mb-1">{videoMetadata.entries[0].playlist_title ? videoMetadata.entries[0].playlist_title : 'UNTITLED'}</h2>
<p className="text-muted-foreground text-xs mb-4">{videoMetadata.entries[0].playlist_creator || videoMetadata.entries[0].playlist_channel || videoMetadata.entries[0].playlist_uploader || 'unknown'}</p>
<PlaylistToggleGroup
className="mb-2"
type="multiple"
value={selectedPlaylistVideos}
onValueChange={(value: string[]) => {
if (value.length > 0) {
setSelectedPlaylistVideos(value);
setSelectedDownloadFormat('best');
setSelectedSubtitles([]);
setSelectedCombinableVideoFormat('');
setSelectedCombinableAudioFormats([]);
resetDownloadConfiguration();
}
}}
>
{videoMetadata.entries.map((entry) => entry ? (
<PlaylistToggleGroupItem
key={entry.playlist_index}
value={entry.playlist_index.toString()}
video={entry}
/>
) : null)}
</PlaylistToggleGroup>
<div className="flex items-center text-muted-foreground">
<Info className="w-3 h-3 mr-2" />
<span className="text-xs">Extracted from {videoMetadata.entries[0].extractor ? videoMetadata.entries[0].extractor.charAt(0).toUpperCase() + videoMetadata.entries[0].extractor.slice(1) : 'Unknown'}</span>
</div>
<div className="spacer mb-12"></div>
</div>
</div>
);
}
function SelectivePlaylistDownload({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, subtitleLanguages }: SelectivePlaylistDownloadProps) {
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
const selectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideos);
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
{subtitleLanguages && subtitleLanguages.length > 0 && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
// disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => {
const hasAutoSubSelected = selectedSubtitles.some(code => code.endsWith('-orig'));
const hasNormalSubSelected = selectedSubtitles.some(code => !code.endsWith('-orig'));
const isDisabled = (hasAutoSubSelected && !lang.code.endsWith('-orig')) || (hasNormalSubSelected && lang.code.endsWith('-orig'));
return (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-primary/10 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}
disabled={isDisabled}>
{lang.lang}
</ToggleGroupItem>
);
})}
</div>
</ToggleGroup>
)}
<FormatSelectionGroup
value={selectedDownloadFormat}
onValueChange={(value) => {
setSelectedDownloadFormat(value);
// const currentlySelectedFormat = value === 'best' ? videoMetadata?.entries[Number(value) - 1].requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
// if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
// setSelectedSubtitles([]);
// }
resetDownloadConfiguration();
}}
>
<p className="text-xs">Suggested</p>
<div className="">
<FormatSelectionGroupItem
key="best"
value="best"
format={getMergedBestFormat(videoMetadata.entries, selectedPlaylistVideos) as VideoFormat}
/>
</div>
{qualityPresetFormats && qualityPresetFormats.length > 0 && (
<>
<p className="text-xs">Quality Presets</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{qualityPresetFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video {videoOnlyFormats.every(format => format.acodec === 'none') ? '(no audio)' : ''}</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{combinedFormats && combinedFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{combinedFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
<div className="spacer mb-12"></div>
</div>
);
}
function CombinedPlaylistDownload({ audioOnlyFormats, videoOnlyFormats, subtitleLanguages }: CombinedPlaylistDownloadProps) {
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
const selectedCombinableAudioFormats = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormats);
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
const setSelectedCombinableAudioFormats = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormats);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && subtitleLanguages && subtitleLanguages.length > 0 && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => {
const hasAutoSubSelected = selectedSubtitles.some(code => code.endsWith('-orig'));
const hasNormalSubSelected = selectedSubtitles.some(code => !code.endsWith('-orig'));
const isDisabled = (hasAutoSubSelected && !lang.code.endsWith('-orig')) || (hasNormalSubSelected && lang.code.endsWith('-orig'));
return (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-primary/10 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}
disabled={isDisabled}>
{lang.lang}
</ToggleGroupItem>
);
})}
</div>
</ToggleGroup>
)}
<FormatToggleGroup
type="multiple"
variant="outline"
className="mb-2"
value={selectedCombinableAudioFormats}
onValueChange={(value: string[]) => {
setSelectedCombinableAudioFormats(value);
resetDownloadConfiguration();
}}
>
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatToggleGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatToggleGroup>
<FormatSelectionGroup
value={selectedCombinableVideoFormat}
onValueChange={(value) => {
setSelectedCombinableVideoFormat(value);
resetDownloadConfiguration();
}}
>
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
{(!videoOnlyFormats || videoOnlyFormats.length === 0 || !audioOnlyFormats || audioOnlyFormats.length === 0) && (
<Alert>
<AlertCircleIcon className="size-4 stroke-primary" />
<AlertTitle>Unable to use Combine Mode!</AlertTitle>
<AlertDescription>
Cannot use combine mode for this video as it does not have both audio and video formats available. Use Selective Mode or try another video.
</AlertDescription>
</Alert>
)}
<div className="spacer mb-12"></div>
</div>
);
}
export function PlaylistDownloader({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, subtitleLanguages }: PlaylistDownloaderProps) {
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
const playlistPanelSizes = useDownloaderPageStatesStore((state) => state.playlistPanelSizes);
const setPlaylistPanelSizes = useDownloaderPageStatesStore((state) => state.setPlaylistPanelSizes);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex">
<ResizablePanelGroup
orientation="horizontal"
className="w-full"
onLayoutChanged={(layout: Layout) => {
const firstPanelSize = layout[Object.keys(layout)[0]];
const secondPanelSize = layout[Object.keys(layout)[1]];
setPlaylistPanelSizes([firstPanelSize, secondPanelSize]);
}}
>
<ResizablePanel
defaultSize={`${playlistPanelSizes[0]}%`}
>
<PlaylistPreviewSelection videoMetadata={videoMetadata} />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={`${playlistPanelSizes[1]}%`}
>
<div className="flex flex-col w-full pl-4">
<Tabs
className=""
value={activeDownloadModeTab}
onValueChange={(tab) => {
setActiveDownloadModeTab(tab);
resetDownloadConfiguration();
}}
>
<div className="flex items-center justify-between">
<h3 className="text-sm flex items-center gap-2">
<DownloadCloud className="w-4 h-4" />
<span>Download Options</span>
</h3>
<TabsList>
<TabsTrigger value="selective">Selective</TabsTrigger>
<TabsTrigger value="combine">Combine</TabsTrigger>
</TabsList>
</div>
<TabsContent value="selective">
<SelectivePlaylistDownload
videoMetadata={videoMetadata}
audioOnlyFormats={audioOnlyFormats}
videoOnlyFormats={videoOnlyFormats}
combinedFormats={combinedFormats}
qualityPresetFormats={qualityPresetFormats}
subtitleLanguages={subtitleLanguages}
/>
</TabsContent>
<TabsContent value="combine">
<CombinedPlaylistDownload
audioOnlyFormats={audioOnlyFormats}
videoOnlyFormats={videoOnlyFormats}
subtitleLanguages={subtitleLanguages}
/>
</TabsContent>
</Tabs>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
}

View File

@@ -0,0 +1,388 @@
import clsx from "clsx";
import { ProxyImage } from "@/components/custom/proxyImage";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Separator } from "@/components/ui/separator";
import { useDownloaderPageStatesStore } from "@/services/store";
import { formatBitrate, formatDurationString, formatReleaseDate, formatYtStyleCount, isObjEmpty } from "@/utils";
import { Calendar, Clock, DownloadCloud, Eye, Info, ThumbsUp, AlertCircleIcon } from "lucide-react";
import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { RawVideoInfo, VideoFormat } from "@/types/video";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { FormatToggleGroup, FormatToggleGroupItem } from "@/components/custom/formatToggleGroup";
import { Layout } from "react-resizable-panels";
interface VideoPreviewProps {
videoMetadata: RawVideoInfo;
}
interface SelectiveVideoDownloadProps {
videoMetadata: RawVideoInfo;
audioOnlyFormats: VideoFormat[] | undefined;
videoOnlyFormats: VideoFormat[] | undefined;
combinedFormats: VideoFormat[] | undefined;
qualityPresetFormats: VideoFormat[] | undefined;
subtitleLanguages: { code: string; lang: string }[];
}
interface CombinedVideoDownloadProps {
audioOnlyFormats: VideoFormat[] | undefined;
videoOnlyFormats: VideoFormat[] | undefined;
subtitleLanguages: { code: string; lang: string }[];
}
interface VideoDownloaderProps {
videoMetadata: RawVideoInfo;
audioOnlyFormats: VideoFormat[] | undefined;
videoOnlyFormats: VideoFormat[] | undefined;
combinedFormats: VideoFormat[] | undefined;
qualityPresetFormats: VideoFormat[] | undefined;
subtitleLanguages: { code: string; lang: string }[];
}
function VideoPreview({ videoMetadata }: VideoPreviewProps) {
return (
<div className="flex flex-col w-full pr-4">
<h3 className="text-sm mb-4 mt-2 flex items-center gap-2">
<Info className="w-4 h-4" />
<span>Metadata</span>
</h3>
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
<AspectRatio ratio={16 / 9} className={clsx("w-full rounded-lg overflow-hidden mb-2 border border-border", videoMetadata.aspect_ratio && videoMetadata.aspect_ratio === 0.56 && "relative")}>
<ProxyImage src={videoMetadata.thumbnail} alt="thumbnail" className={clsx(videoMetadata.aspect_ratio && videoMetadata.aspect_ratio === 0.56 && "absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2")} />
</AspectRatio>
<h2 className="mb-1">{videoMetadata.title ? videoMetadata.title : 'UNTITLED'}</h2>
<p className="text-muted-foreground text-xs mb-2">{videoMetadata.creator || videoMetadata.channel || videoMetadata.uploader || 'unknown'}</p>
<div className="flex items-center mb-2">
<span className="text-xs text-muted-foreground flex items-center pr-3"><Clock className="w-4 h-4 mr-2"/> {videoMetadata.duration_string ? formatDurationString(videoMetadata.duration_string) : 'unknown'}</span>
<Separator orientation="vertical" />
<span className="text-xs text-muted-foreground flex items-center px-3"><Eye className="w-4 h-4 mr-2"/> {videoMetadata.view_count ? formatYtStyleCount(videoMetadata.view_count) : 'unknown'}</span>
<Separator orientation="vertical" />
<span className="text-xs text-muted-foreground flex items-center pl-3"><ThumbsUp className="w-4 h-4 mr-2"/> {videoMetadata.like_count ? formatYtStyleCount(videoMetadata.like_count) : 'unknown'}</span>
</div>
<p className="text-xs text-muted-foreground flex items-center gap-2 mb-2">
<Calendar className="w-4 h-4" />
<span className="">{videoMetadata.upload_date ? formatReleaseDate(videoMetadata.upload_date) : 'unknown'}</span>
</p>
<div className="flex flex-wrap gap-2 text-xs mb-2">
{videoMetadata.resolution && (
<span className="border border-border py-1 px-2 rounded">{videoMetadata.resolution}</span>
)}
{videoMetadata.tbr && (
<span className="border border-border py-1 px-2 rounded">{formatBitrate(videoMetadata.tbr)}</span>
)}
{videoMetadata.fps && (
<span className="border border-border py-1 px-2 rounded">{videoMetadata.fps} fps</span>
)}
{videoMetadata.subtitles && !isObjEmpty(videoMetadata.subtitles) && (
<span className="border border-border py-1 px-2 rounded">SUB</span>
)}
{videoMetadata.dynamic_range && videoMetadata.dynamic_range !== 'SDR' && (
<span className="border border-border py-1 px-2 rounded">{videoMetadata.dynamic_range}</span>
)}
</div>
<div className="flex items-center text-muted-foreground">
<Info className="w-3 h-3 mr-2" />
<span className="text-xs">Extracted from {videoMetadata.extractor ? videoMetadata.extractor.charAt(0).toUpperCase() + videoMetadata.extractor.slice(1) : 'Unknown'}</span>
</div>
<div className="spacer mb-12"></div>
</div>
</div>
);
}
function SelectiveVideoDownload({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, subtitleLanguages }: SelectiveVideoDownloadProps) {
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
{subtitleLanguages && subtitleLanguages.length > 0 && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
// disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => {
const hasAutoSubSelected = selectedSubtitles.some(code => code.endsWith('-orig'));
const hasNormalSubSelected = selectedSubtitles.some(code => !code.endsWith('-orig'));
const isDisabled = (hasAutoSubSelected && !lang.code.endsWith('-orig')) || (hasNormalSubSelected && lang.code.endsWith('-orig'));
return (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-primary/10 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}
disabled={isDisabled}>
{lang.lang}
</ToggleGroupItem>
);
})}
</div>
</ToggleGroup>
)}
<FormatSelectionGroup
value={selectedDownloadFormat}
onValueChange={(value) => {
setSelectedDownloadFormat(value);
// const currentlySelectedFormat = value === 'best' ? videoMetadata?.requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
// if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
// setSelectedSubtitles([]);
// }
resetDownloadConfiguration();
}}
>
<p className="text-xs">Suggested</p>
<div className="">
<FormatSelectionGroupItem
key="best"
value="best"
format={videoMetadata.requested_downloads[0]}
/>
</div>
{qualityPresetFormats && qualityPresetFormats.length > 0 && (
<>
<p className="text-xs">Quality Presets</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{qualityPresetFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video {videoOnlyFormats.every(format => format.acodec === 'none') ? '(no audio)' : ''}</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{combinedFormats && combinedFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{combinedFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
<div className="spacer mb-12"></div>
</div>
);
}
function CombinedVideoDownload({ audioOnlyFormats, videoOnlyFormats, subtitleLanguages }: CombinedVideoDownloadProps) {
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
const selectedCombinableAudioFormats = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormats);
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
const setSelectedCombinableAudioFormats = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormats);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
return (
<div className="flex flex-col overflow-y-scroll max-h-[50vh] xl:max-h-[60vh] no-scrollbar">
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && subtitleLanguages && subtitleLanguages.length > 0 && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => {
const hasAutoSubSelected = selectedSubtitles.some(code => code.endsWith('-orig'));
const hasNormalSubSelected = selectedSubtitles.some(code => !code.endsWith('-orig'));
const isDisabled = (hasAutoSubSelected && !lang.code.endsWith('-orig')) || (hasNormalSubSelected && lang.code.endsWith('-orig'));
return (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-primary/10 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}
disabled={isDisabled}>
{lang.lang}
</ToggleGroupItem>
);
})}
</div>
</ToggleGroup>
)}
<FormatToggleGroup
type="multiple"
variant="outline"
className="mb-2"
value={selectedCombinableAudioFormats}
onValueChange={(value: string[]) => {
setSelectedCombinableAudioFormats(value);
resetDownloadConfiguration();
}}
>
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatToggleGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatToggleGroup>
<FormatSelectionGroup
value={selectedCombinableVideoFormat}
onValueChange={(value) => {
setSelectedCombinableVideoFormat(value);
resetDownloadConfiguration();
}}
>
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
{(!videoOnlyFormats || videoOnlyFormats.length === 0 || !audioOnlyFormats || audioOnlyFormats.length === 0) && (
<Alert>
<AlertCircleIcon className="size-4 stroke-primary" />
<AlertTitle>Unable to use Combine Mode!</AlertTitle>
<AlertDescription>
Cannot use combine mode for this video as it does not have both audio and video formats available. Use Selective Mode or try another video.
</AlertDescription>
</Alert>
)}
<div className="spacer mb-12"></div>
</div>
);
}
export function VideoDownloader({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, subtitleLanguages }: VideoDownloaderProps) {
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
const videoPanelSizes = useDownloaderPageStatesStore((state) => state.videoPanelSizes);
const setVideoPanelSizes = useDownloaderPageStatesStore((state) => state.setVideoPanelSizes);
return (
<div className="flex">
<ResizablePanelGroup
orientation="horizontal"
className="w-full"
onLayoutChanged={(layout: Layout) => {
const firstPanelSize = layout[Object.keys(layout)[0]];
const secondPanelSize = layout[Object.keys(layout)[1]];
setVideoPanelSizes([firstPanelSize, secondPanelSize]);
}}
>
<ResizablePanel
defaultSize={`${videoPanelSizes[0]}%`}
>
<VideoPreview videoMetadata={videoMetadata} />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={`${videoPanelSizes[1]}%`}
>
<div className="flex flex-col w-full pl-4">
<Tabs
className=""
value={activeDownloadModeTab}
onValueChange={(tab) => {
setActiveDownloadModeTab(tab);
resetDownloadConfiguration();
}}
>
<div className="flex items-center justify-between">
<h3 className="text-sm flex items-center gap-2">
<DownloadCloud className="w-4 h-4" />
<span>Download Options</span>
</h3>
<TabsList>
<TabsTrigger value="selective">Selective</TabsTrigger>
<TabsTrigger value="combine">Combine</TabsTrigger>
</TabsList>
</div>
<TabsContent value="selective">
<SelectiveVideoDownload
videoMetadata={videoMetadata}
audioOnlyFormats={audioOnlyFormats}
videoOnlyFormats={videoOnlyFormats}
combinedFormats={combinedFormats}
qualityPresetFormats={qualityPresetFormats}
subtitleLanguages={subtitleLanguages}
/>
</TabsContent>
<TabsContent value="combine">
<CombinedVideoDownload
audioOnlyFormats={audioOnlyFormats}
videoOnlyFormats={videoOnlyFormats}
subtitleLanguages={subtitleLanguages}
/>
</TabsContent>
</Tabs>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More