mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2026-03-22 19:25:49 +05:30
Compare commits
7 Commits
15bd9c244f
...
v0.4.1
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,24 +1,16 @@
|
||||
### ✨ Changelog
|
||||
|
||||
- Added support for selective-batch/full-playlist download
|
||||
- Added support for selecting multiple audio streams on combine mode
|
||||
- Added support for embedding original auto-generated subtitles
|
||||
- Added option to crop thubnails to square (1:1) before embedding
|
||||
- Added 'errored' download state (to better identify errored downloads, which you can retry later)
|
||||
- Added app interface color scheme options on appearance settings
|
||||
- Added app info page under settings
|
||||
- Added copy/clear log buttons in log viewer
|
||||
- Added sponsorblock 'hook' category
|
||||
- Fixed sidebar state not persisting on app re-start
|
||||
- Fixed linux native (deb/rpm) installation downloading appimage update
|
||||
- Bumped up shadcn/ui to v3.5 and lots of under the hood ui improvements
|
||||
- Optimized database and backend performance
|
||||
- Lots of other fixes and improvements
|
||||
- Added delay/sleep configuration settings (delay is now also configurable on search)
|
||||
- Added support for YouTube PO Token generation (based on [bgutil-ytdlp-pot-provider-rs](https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs))
|
||||
- Implemented custom app titlebar on windows and linux
|
||||
- Further improved and persisted app logs (stored in [platform specific log directory](https://v2.tauri.app/plugin/logging/#persisting-logs))
|
||||
- Fixed webview window creation is failing on wayland with nvidia gpu
|
||||
- Other minor fixes and improvements
|
||||
|
||||
### 📝 Notes
|
||||
|
||||
> [!CAUTION]
|
||||
> This update introduces few breaking changes! 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.
|
||||
> 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)
|
||||
@@ -29,9 +21,9 @@
|
||||
|
||||
### 📦 Shipped Binaries
|
||||
|
||||
| yt-dlp (updateable) | ffmpeg | ffprobe | aria2c | deno |
|
||||
| :---- | :---- | :---- | :---- | :---- |
|
||||
| v2026.01.19.233146 (nightly) | v7.1.1 | v7.1.1 | v1.37.0 | v2.6.5 |
|
||||
| yt-dlp (updateable) | ffmpeg | ffprobe | aria2c | deno | bgutil-pot-rs |
|
||||
| :---- | :---- | :---- | :---- | :---- | :---- |
|
||||
| v2026.02.17.233631 (nightly) | v7.1.1 | v7.1.1 | v1.37.0 | v2.6.10 | v0.7.1-1.2.2 |
|
||||
|
||||
> ‼️ 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)
|
||||
|
||||
|
||||
10
README.md
10
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
# 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
|
||||
|
||||
[](https://github.com/neosubhamoy/neodlp/releases/latest)
|
||||
[](https://github.com/neosubhamoy/neodlp/releases)
|
||||
@@ -18,13 +18,15 @@ Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Int
|
||||
|
||||
## ✨ Highlighted Features
|
||||
|
||||
- Download Video/Audio from popular sites (YT, FB, IG, X and other 2.5k+ [supported sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md))
|
||||
- Download Video/Audio 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.)
|
||||
- Supports both Video and Playlist download
|
||||
- Supports both Video and Playlist/Batch download
|
||||
- Supports Combining Video, Audio streams of your choice
|
||||
- Supports Multi-Lingual Subtitle/Caption (CC) embeding
|
||||
- Different Video/Audio metadata embeding options (info, chapters, thumbnail etc.)
|
||||
- SponsorBlock support (mark/remove video segments)
|
||||
- Aria2 support (for blazing fast downloads)
|
||||
- Network controls (proxy, rate limit etc.)
|
||||
- Highly customizable and many more...😉
|
||||
|
||||
@@ -58,6 +60,7 @@ After installing the extension you can do the following directly from the browse
|
||||
- [FFmpeg & FFprobe](https://www.ffmpeg.org) [LGPLv2.1+] - Used for video/audio post-processing
|
||||
- [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) [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
|
||||
|
||||
@@ -195,6 +198,7 @@ Noticed any Bug? or Want to give us some suggetions? Always feel free to let us
|
||||
- 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)
|
||||
- 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 and Usage
|
||||
|
||||
|
||||
380
package-lock.json
generated
380
package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "neodlp",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neodlp",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tanstack/devtools-vite": "^0.5.1",
|
||||
"@tanstack/react-devtools": "^0.9.5",
|
||||
"@tanstack/react-pacer": "^0.19.4",
|
||||
"@tanstack/react-devtools": "^0.9.6",
|
||||
"@tanstack/react-pacer": "^0.20.0",
|
||||
"@tanstack/react-pacer-devtools": "^0.5.2",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-query-devtools": "^5.91.3",
|
||||
@@ -29,30 +29,30 @@
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
"lucide-react": "^0.574.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-resizable-panels": "^4.6.2",
|
||||
"react-resizable-panels": "^4.6.4",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-merge": "^3.4.1",
|
||||
"ulid": "^3.0.2",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tailwindcss/postcss": "^4.2.0",
|
||||
"@tailwindcss/vite": "^4.2.0",
|
||||
"@tauri-apps/cli": "^2.10.0",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
@@ -2746,49 +2746,49 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
|
||||
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz",
|
||||
"integrity": "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"enhanced-resolve": "^5.19.0",
|
||||
"jiti": "^2.6.1",
|
||||
"lightningcss": "1.30.2",
|
||||
"lightningcss": "1.31.1",
|
||||
"magic-string": "^0.30.21",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.18"
|
||||
"tailwindcss": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
|
||||
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.0.tgz",
|
||||
"integrity": "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
|
||||
"@tailwindcss/oxide-android-arm64": "4.2.0",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.2.0",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.2.0",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.2.0",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.0",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.2.0",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.2.0",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.2.0",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.2.0",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.0",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz",
|
||||
"integrity": "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2799,13 +2799,13 @@
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz",
|
||||
"integrity": "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2816,13 +2816,13 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz",
|
||||
"integrity": "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2833,13 +2833,13 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz",
|
||||
"integrity": "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2850,13 +2850,13 @@
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
|
||||
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz",
|
||||
"integrity": "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2867,13 +2867,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.0.tgz",
|
||||
"integrity": "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2884,13 +2884,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.0.tgz",
|
||||
"integrity": "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2901,13 +2901,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.0.tgz",
|
||||
"integrity": "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2918,13 +2918,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.0.tgz",
|
||||
"integrity": "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2935,13 +2935,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
|
||||
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.0.tgz",
|
||||
"integrity": "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
@@ -2957,21 +2957,21 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@emnapi/core": "^1.8.1",
|
||||
"@emnapi/runtime": "^1.8.1",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.1",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"tslib": "^2.4.0"
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz",
|
||||
"integrity": "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2982,13 +2982,13 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.0.tgz",
|
||||
"integrity": "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2999,42 +2999,42 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
|
||||
"integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.0.tgz",
|
||||
"integrity": "sha512-u6YBacGpOm/ixPfKqfgrJEjMfrYmPD7gEFRoygS/hnQaRtV0VCBdpkx5Ouw9pnaLRwwlgGCuJw8xLpaR0hOrQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/node": "4.1.18",
|
||||
"@tailwindcss/oxide": "4.1.18",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "4.1.18"
|
||||
"@tailwindcss/node": "4.2.0",
|
||||
"@tailwindcss/oxide": "4.2.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz",
|
||||
"integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.0.tgz",
|
||||
"integrity": "sha512-da9mFCaHpoOgtQiWtDGIikTrSpUFBtIZCG3jy/u2BGV+l/X1/pbxzmIUxNt6JWm19N3WtGi4KlJdSH/Si83WOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.1.18",
|
||||
"@tailwindcss/oxide": "4.1.18",
|
||||
"tailwindcss": "4.1.18"
|
||||
"@tailwindcss/node": "4.2.0",
|
||||
"@tailwindcss/oxide": "4.2.0",
|
||||
"tailwindcss": "4.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/devtools": {
|
||||
"version": "0.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/devtools/-/devtools-0.10.6.tgz",
|
||||
"integrity": "sha512-STB3pS49gPoe7UHgDshOMkWPXPZmezsQBLkCrh6l+mcsRs+/Jk1OvfVF8HspiMA1RTuNRkTeGXZDA8LoGWmxyQ==",
|
||||
"version": "0.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/devtools/-/devtools-0.10.7.tgz",
|
||||
"integrity": "sha512-ScwnFjJTMRUd6miQax7sEhq9winalQIVhm0MTX3YfjoGjMzB/jzjzYlLOraen8hcxMHH9CifTjio8ZVdqSRBRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@solid-primitives/event-listener": "^2.4.3",
|
||||
@@ -3193,13 +3193,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/pacer": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/pacer/-/pacer-0.18.0.tgz",
|
||||
"integrity": "sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/pacer/-/pacer-0.19.0.tgz",
|
||||
"integrity": "sha512-MRXCiG8IcjrN/3LGu7Wy6lKZkbwOb5YelOBYtHxnxKYj2WlO2FrqASILSiJcwdES5Sz2QJEIeuvO5JY8cKaGQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/devtools-event-client": "^0.4.0",
|
||||
"@tanstack/store": "^0.8.0"
|
||||
"@tanstack/store": "^0.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -3234,6 +3234,16 @@
|
||||
"@tanstack/pacer": ">=0.16.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/pacer/node_modules/@tanstack/store": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.1.tgz",
|
||||
"integrity": "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.90.20",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
|
||||
@@ -3255,12 +3265,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-devtools": {
|
||||
"version": "0.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-devtools/-/react-devtools-0.9.5.tgz",
|
||||
"integrity": "sha512-/YsSSobbWfSZ0khLZ5n4cz/isa8Ac21PAVdgrX0qOEkPkS6J63JTEgFR0Ch2n2ka511dm2pIEuTvCsL7WVu1XQ==",
|
||||
"version": "0.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-devtools/-/react-devtools-0.9.6.tgz",
|
||||
"integrity": "sha512-4wnhqQ1o5PnmEDV8L3yLWaE2ZWD2xjdUw1X8Uv5NK9Ekrz/Qr6iuYl+X4Kq9+Ix2luVGMqd3toFvEwkr3uMFBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/devtools": "0.10.6"
|
||||
"@tanstack/devtools": "0.10.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -3277,13 +3287,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-pacer": {
|
||||
"version": "0.19.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-pacer/-/react-pacer-0.19.4.tgz",
|
||||
"integrity": "sha512-coj8ULAuR0qFpjAKD44gTgRuZyjxU6Xu+IX5MwwYvr4e61OtZcJshaExoOBKpCGde0Edb12jDnzzj2Im13Qm9Q==",
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-pacer/-/react-pacer-0.20.0.tgz",
|
||||
"integrity": "sha512-5p7rHTBUroUl6vxYhvREaqpUWKCoe0bXaFH6y6CLYpcuU5aCl78IxXJKY5IujCN1sTRaq07jsMInTPmTBHvTiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/pacer": "0.18.0",
|
||||
"@tanstack/react-store": "^0.8.0"
|
||||
"@tanstack/pacer": "0.19.0",
|
||||
"@tanstack/react-store": "^0.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -3354,12 +3364,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-store": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.0.tgz",
|
||||
"integrity": "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==",
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.1.tgz",
|
||||
"integrity": "sha512-XItJt+rG8c5Wn/2L/bnxys85rBpm0BfMbhb4zmPVLXAKY9POrp1xd6IbU4PKoOI+jSEGc3vntPRfLGSgXfE2Ig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/store": "0.8.0",
|
||||
"@tanstack/store": "0.8.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -3371,6 +3381,16 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-store/node_modules/@tanstack/store": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.1.tgz",
|
||||
"integrity": "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/solid-store": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-store/-/solid-store-0.8.0.tgz",
|
||||
@@ -4001,14 +4021,14 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.4",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
|
||||
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
|
||||
"version": "5.19.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
||||
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
"tapable": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
@@ -4180,9 +4200,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
|
||||
"integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
|
||||
"devOptional": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
@@ -4196,23 +4216,23 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.30.2",
|
||||
"lightningcss-darwin-arm64": "1.30.2",
|
||||
"lightningcss-darwin-x64": "1.30.2",
|
||||
"lightningcss-freebsd-x64": "1.30.2",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||
"lightningcss-linux-x64-musl": "1.30.2",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||
"lightningcss-android-arm64": "1.31.1",
|
||||
"lightningcss-darwin-arm64": "1.31.1",
|
||||
"lightningcss-darwin-x64": "1.31.1",
|
||||
"lightningcss-freebsd-x64": "1.31.1",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.31.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.31.1",
|
||||
"lightningcss-linux-arm64-musl": "1.31.1",
|
||||
"lightningcss-linux-x64-gnu": "1.31.1",
|
||||
"lightningcss-linux-x64-musl": "1.31.1",
|
||||
"lightningcss-win32-arm64-msvc": "1.31.1",
|
||||
"lightningcss-win32-x64-msvc": "1.31.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
|
||||
"integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4230,9 +4250,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
|
||||
"integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4250,9 +4270,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
|
||||
"integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4270,9 +4290,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
|
||||
"integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4290,9 +4310,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
|
||||
"integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4310,9 +4330,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
|
||||
"integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4330,9 +4350,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
|
||||
"integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4350,9 +4370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
|
||||
"integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4370,9 +4390,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
|
||||
"integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4390,9 +4410,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
|
||||
"integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4410,9 +4430,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
|
||||
"integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4439,9 +4459,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.563.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
|
||||
"integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
|
||||
"version": "0.574.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.574.0.tgz",
|
||||
"integrity": "sha512-dJ8xb5juiZVIbdSn3HTyHsjjIwUwZ4FNwV0RtYDScOyySOeie1oXZTymST6YPJ4Qwt3Po8g4quhYl4OxtACiuQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -4853,9 +4873,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.6.2.tgz",
|
||||
"integrity": "sha512-d6hyD6s7ewNAI+oINrZznR/08GUyAszrowXouUDztePEn/tQ2z/LEI2qRvrizYBe3TpgBi0cCjc10pXTTOc4jw==",
|
||||
"version": "4.6.4",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.6.4.tgz",
|
||||
"integrity": "sha512-E7Szs1xyaMZ7xOI2gG4TECNz4r/gmpV1AsXyZRnER6OQnfFf9uclFmrHHZR3h/iF8vQS+nQ1LKyZv9bzwGxPSg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
@@ -5046,9 +5066,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.1.tgz",
|
||||
"integrity": "sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5056,9 +5076,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz",
|
||||
"integrity": "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
20
package.json
20
package.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "neodlp",
|
||||
"private": true,
|
||||
"version": "0.4.0",
|
||||
"description": "Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration",
|
||||
"version": "0.4.1",
|
||||
"description": "Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -22,8 +22,8 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tanstack/devtools-vite": "^0.5.1",
|
||||
"@tanstack/react-devtools": "^0.9.5",
|
||||
"@tanstack/react-pacer": "^0.19.4",
|
||||
"@tanstack/react-devtools": "^0.9.6",
|
||||
"@tanstack/react-pacer": "^0.20.0",
|
||||
"@tanstack/react-pacer-devtools": "^0.5.2",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-query-devtools": "^5.91.3",
|
||||
@@ -41,30 +41,30 @@
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
"lucide-react": "^0.574.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-resizable-panels": "^4.6.2",
|
||||
"react-resizable-panels": "^4.6.4",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-merge": "^3.4.1",
|
||||
"ulid": "^3.0.2",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tailwindcss/postcss": "^4.2.0",
|
||||
"@tailwindcss/vite": "^4.2.0",
|
||||
"@tauri-apps/cli": "^2.10.0",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
|
||||
@@ -20,6 +20,7 @@ const versions = {
|
||||
'ffmpeg-ffprobe': 'latest',
|
||||
'deno': 'latest',
|
||||
'aria2c': '1.37.0',
|
||||
'neodlp-pot': 'latest'
|
||||
};
|
||||
|
||||
const binaries = {
|
||||
@@ -145,10 +146,54 @@ const binaries = {
|
||||
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://evermeet.cx/ffmpeg/get/zip`,
|
||||
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: {
|
||||
@@ -170,7 +215,7 @@ const binaries = {
|
||||
{
|
||||
name: 'ffprobe-universal-apple-darwin',
|
||||
platform: 'darwin',
|
||||
url: `https://evermeet.cx/ffmpeg/get/ffprobe/zip`,
|
||||
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: {
|
||||
@@ -353,6 +398,73 @@ const binaries = {
|
||||
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' ? '/'+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' ? '/'+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' ? '/'+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' ? '/'+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' ? '/'+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')
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
320
src-tauri/Cargo.lock
generated
320
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "neodlp"
|
||||
version = "0.4.0"
|
||||
description = "Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration"
|
||||
version = "0.4.1"
|
||||
description = "Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration"
|
||||
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/neodlp-pot",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
"cmd": "ffmpeg",
|
||||
@@ -45,6 +50,11 @@
|
||||
"cmd": "aria2c",
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "deno",
|
||||
"cmd": "deno",
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "pkexec",
|
||||
"cmd": "pkexec",
|
||||
@@ -85,6 +95,11 @@
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "binaries/neodlp-pot",
|
||||
"args": true,
|
||||
"sidecar": true
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
"cmd": "ffmpeg",
|
||||
@@ -94,6 +109,11 @@
|
||||
"name": "aria2c",
|
||||
"cmd": "aria2c",
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "deno",
|
||||
"cmd": "deno",
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = '1.2.2'
|
||||
|
||||
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_challenge_data = self.ie._search_regex(
|
||||
r'''(?sx)window\.ytAtR\s*=\s*(?P<raw_cd>(?P<q>['"])
|
||||
(?:
|
||||
\\.|
|
||||
(?!(?P=q)).
|
||||
)*
|
||||
(?P=q))\s*;''',
|
||||
webpage,
|
||||
'raw challenge data',
|
||||
default=None,
|
||||
group='raw_cd',
|
||||
)
|
||||
att_txt = traverse_obj(
|
||||
raw_challenge_data,
|
||||
({js_to_json}, {json.loads}, {json.loads}, 'bgChallenge')
|
||||
)
|
||||
if not att_txt:
|
||||
self.logger.warning(
|
||||
'Failed to extract initial attestation from the webpage'
|
||||
)
|
||||
return None
|
||||
return att_txt
|
||||
|
||||
|
||||
__all__ = ['__version__']
|
||||
@@ -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__]
|
||||
@@ -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__]
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "NeoDLP",
|
||||
"mainBinaryName": "neodlp",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"identifier": "com.neosubhamoy.neodlp",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -39,8 +39,12 @@
|
||||
"externalBin": [
|
||||
"binaries/yt-dlp",
|
||||
"binaries/aria2c",
|
||||
"binaries/deno"
|
||||
"binaries/deno",
|
||||
"binaries/neodlp-pot"
|
||||
],
|
||||
"resources": {
|
||||
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["ffmpeg"],
|
||||
|
||||
@@ -39,8 +39,12 @@
|
||||
"externalBin": [
|
||||
"binaries/yt-dlp",
|
||||
"binaries/aria2c",
|
||||
"binaries/deno"
|
||||
"binaries/deno",
|
||||
"binaries/neodlp-pot"
|
||||
],
|
||||
"resources": {
|
||||
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["ffmpeg"],
|
||||
|
||||
@@ -39,13 +39,15 @@
|
||||
"binaries/yt-dlp",
|
||||
"binaries/ffmpeg",
|
||||
"binaries/ffprobe",
|
||||
"binaries/deno"
|
||||
"binaries/deno",
|
||||
"binaries/neodlp-pot"
|
||||
],
|
||||
"resources": {
|
||||
"target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
||||
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
|
||||
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist",
|
||||
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||
},
|
||||
"macOS": {
|
||||
"providerShortName": "neosubhamoy"
|
||||
|
||||
@@ -39,13 +39,15 @@
|
||||
"binaries/yt-dlp",
|
||||
"binaries/ffmpeg",
|
||||
"binaries/ffprobe",
|
||||
"binaries/deno"
|
||||
"binaries/deno",
|
||||
"binaries/neodlp-pot"
|
||||
],
|
||||
"resources": {
|
||||
"target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
||||
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
|
||||
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist",
|
||||
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||
},
|
||||
"macOS": {
|
||||
"providerShortName": "neosubhamoy"
|
||||
|
||||
@@ -41,12 +41,14 @@
|
||||
"binaries/ffmpeg",
|
||||
"binaries/ffprobe",
|
||||
"binaries/aria2c",
|
||||
"binaries/deno"
|
||||
"binaries/deno",
|
||||
"binaries/neodlp-pot"
|
||||
],
|
||||
"resources": {
|
||||
"target/release/neodlp-msghost.exe": "neodlp-msghost.exe",
|
||||
"resources/msghost-manifest/windows/chrome.json": "chrome.json",
|
||||
"resources/msghost-manifest/windows/firefox.json": "firefox.json"
|
||||
"resources/msghost-manifest/windows/firefox.json": "firefox.json",
|
||||
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||
},
|
||||
"windows": {
|
||||
"wix": {
|
||||
|
||||
85
src/App.tsx
85
src/App.tsx
@@ -25,6 +25,8 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||
import { toast } from "sonner";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
import useDownloader from "@/helpers/use-downloader";
|
||||
import usePotServer from "@/helpers/use-pot-server";
|
||||
import { useLinuxRegisterer } from "@/helpers/use-linux-registerer";
|
||||
|
||||
export default function App({ children }: { children: React.ReactNode }) {
|
||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
||||
@@ -39,6 +41,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
|
||||
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
|
||||
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
||||
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
|
||||
const ytDlpVersion = useSettingsPageStatesStore(state => state.ytDlpVersion);
|
||||
const setYtDlpVersion = useSettingsPageStatesStore((state) => state.setYtDlpVersion);
|
||||
const setIsFetchingYtDlpVersion = useSettingsPageStatesStore((state) => state.setIsFetchingYtDlpVersion);
|
||||
@@ -50,6 +53,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
download_dir: DOWNLOAD_DIR,
|
||||
theme: APP_THEME,
|
||||
color_scheme: APP_COLOR_SCHEME,
|
||||
use_potoken: USE_POTOKEN,
|
||||
} = useSettingsPageStatesStore(state => state.settings);
|
||||
|
||||
const erroredDownloadIds = useDownloaderPageStatesStore((state) => state.erroredDownloadIds);
|
||||
@@ -63,10 +67,13 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const currentPlatform = platform();
|
||||
const { updateYtDlp } = useYtDlpUpdater();
|
||||
const { registerToMac } = useMacOsRegisterer();
|
||||
const { registerToLinux } = useLinuxRegisterer();
|
||||
const { checkForAppUpdate } = useAppUpdater();
|
||||
const { startPotServer, stopPotServer } = usePotServer();
|
||||
const setKvPairsKey = useKvPairsStatesStore((state) => state.setKvPairsKey);
|
||||
const ytDlpUpdateLastCheck = useKvPairsStatesStore(state => state.kvPairs.ytdlp_update_last_check);
|
||||
const macOsRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.macos_registered_version);
|
||||
const linuxRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.linux_registered_version);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const downloadStatusUpdater = useUpdateDownloadStatus();
|
||||
@@ -76,7 +83,9 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
||||
const hasRunAppUpdateCheckRef = useRef(false);
|
||||
const hasRunPotServerStatusCheckRef = useRef(false);
|
||||
const isRegisteredToMacOsRef = useRef(false);
|
||||
const isRegisteredToLinuxRef = useRef(false);
|
||||
const pendingErrorUpdatesRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const { fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload, processQueuedDownloads } = useDownloader();
|
||||
@@ -98,6 +107,19 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
appWindow.onCloseRequested(handleCloseRequested);
|
||||
}, []);
|
||||
|
||||
// Cleanup before page refresh/unload
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (_event: BeforeUnloadEvent) => {
|
||||
if (isRunningPotServer) {
|
||||
stopPotServer();
|
||||
}
|
||||
};
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [stopPotServer]);
|
||||
|
||||
// Listen for websocket messages
|
||||
useEffect(() => {
|
||||
const unlisten = listen<WebSocketMessage>('websocket-message', (event) => {
|
||||
@@ -272,6 +294,32 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||
|
||||
// Check POT server status and auto-start if enabled
|
||||
useEffect(() => {
|
||||
// Only run once when both settings and KV pairs are loaded
|
||||
if (!isSettingsStatePropagated || !isKvPairsStatePropagated) {
|
||||
console.log("Skipping POT server status check, waiting for configs to load...");
|
||||
return;
|
||||
}
|
||||
// Skip if we've already run the POT server status check once
|
||||
if (hasRunPotServerStatusCheckRef.current) {
|
||||
console.log("POT server status check already performed in this session, skipping");
|
||||
return;
|
||||
}
|
||||
hasRunPotServerStatusCheckRef.current = true;
|
||||
console.log("Checking POT server status with loaded config values:", {
|
||||
usePotoken: USE_POTOKEN,
|
||||
});
|
||||
if (USE_POTOKEN) {
|
||||
console.log("Auto-starting POT server...");
|
||||
startPotServer().catch((error) => {
|
||||
console.error("Error starting POT server:", error);
|
||||
});
|
||||
} else {
|
||||
console.log("Skipping POT server auto-start, not enabled.");
|
||||
}
|
||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||
|
||||
// Check for MacOS auto-registration
|
||||
useEffect(() => {
|
||||
// Only run once when both settings and KV pairs are loaded
|
||||
@@ -307,6 +355,41 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||
|
||||
// Check for Linux auto-registration
|
||||
useEffect(() => {
|
||||
// Only run once when both settings and KV pairs are loaded
|
||||
if (!isSettingsStatePropagated || !isKvPairsStatePropagated) {
|
||||
console.log("Skipping Linux auto registration, waiting for configs to load...");
|
||||
return;
|
||||
}
|
||||
// Skip if we've already run the linux auto-registration once
|
||||
if (isRegisteredToLinuxRef.current) {
|
||||
console.log("Linux auto registration check already performed in this session, skipping");
|
||||
return;
|
||||
}
|
||||
isRegisteredToLinuxRef.current = true;
|
||||
console.log("Checking Linux auto registration with loaded config values:", {
|
||||
appVersion: appVersion,
|
||||
registeredVersion: linuxRegisteredVersion
|
||||
});
|
||||
if (currentPlatform === 'linux' && (!linuxRegisteredVersion || linuxRegisteredVersion !== appVersion)) {
|
||||
console.log("Running Linux auto registration...");
|
||||
LOG.info('NEODLP', 'Running Linux registration');
|
||||
registerToLinux().then((result: { success: boolean, message: string }) => {
|
||||
if (result.success) {
|
||||
console.log("Linux registration successful:", result.message);
|
||||
LOG.info('NEODLP', 'Linux registration successful');
|
||||
} else {
|
||||
console.error("Linux registration failed:", result.message);
|
||||
LOG.error('NEODLP', `Linux registration failed: ${result.message}`);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error("Error during Linux registration:", error);
|
||||
LOG.error('NEODLP', `Error during Linux registration: ${error}`);
|
||||
});
|
||||
}
|
||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccessFetchingDownloadStates && downloadStates) {
|
||||
// console.log("Download States fetched successfully:", downloadStates);
|
||||
@@ -341,7 +424,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
});
|
||||
});
|
||||
|
||||
const timeoutIds: NodeJS.Timeout[] = [];
|
||||
const timeoutIds: ReturnType<typeof setTimeout>[] = [];
|
||||
unexpectedErrors.forEach((downloadId) => {
|
||||
pendingErrorUpdatesRef.current.add(downloadId);
|
||||
|
||||
|
||||
110
src/components/custom/numberInput.tsx
Normal file
110
src/components/custom/numberInput.tsx
Normal 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 }
|
||||
@@ -47,8 +47,8 @@ export default function Navbar() {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Logs</p>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Logs</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DialogContent className="sm:max-w-150">
|
||||
|
||||
@@ -352,7 +352,7 @@ export function CompletedDownloads({ downloads }: CompletedDownloadsProps) {
|
||||
<Empty className="mt-10">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<CircleArrowDown />
|
||||
<CircleArrowDown className="stroke-primary" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Completed Downloads</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
|
||||
@@ -280,7 +280,7 @@ export function IncompleteDownloads({ downloads }: IncompleteDownloadsProps) {
|
||||
<Empty className="mt-10">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<CircleCheck />
|
||||
<CircleCheck className="stroke-primary" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Incomplete Downloads</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { BadgeCheck, BellRing, BrushCleaning, Bug, Cookie, ExternalLink, FilePen, FileVideo, Folder, FolderOpen, Github, Globe, Heart, Info, Loader2, LucideIcon, Mail, Monitor, Moon, Package, Scale, ShieldMinus, SquareTerminal, Sun, Terminal, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||
import { BadgeCheck, BellRing, BrushCleaning, Bug, Cookie, ExternalLink, FilePen, FileVideo, Folder, FolderOpen, Github, Globe, Heart, Info, KeyRound, Loader2, LucideIcon, Mail, Monitor, Moon, Package, Scale, ShieldMinus, SquareTerminal, Sun, Terminal, Timer, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -34,6 +34,8 @@ import { config } from "@/config";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import neosubhamoyImage from "@/assets/images/neosubhamoy.jpg";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { NumberInput } from "@/components/custom/numberInput";
|
||||
import usePotServer from "@/helpers/use-pot-server";
|
||||
|
||||
const proxyUrlSchema = z.object({
|
||||
url: z.url({
|
||||
@@ -66,6 +68,59 @@ const filenameTemplateShcema = z.object({
|
||||
template: z.string().min(1, { message: "Filename Template is required" }),
|
||||
});
|
||||
|
||||
const minMaxSleepIntervalSchema = z.object({
|
||||
min_sleep_interval: z.coerce.number<number>({
|
||||
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||
? "Minimum Sleep Interval is required"
|
||||
: "Minimum Sleep Interval must be a valid number"
|
||||
}).int({
|
||||
message: "Minimum Sleep Interval must be an integer"
|
||||
}).min(1, {
|
||||
message: "Minimum Sleep Interval must be at least 1 second"
|
||||
}).max(3600, {
|
||||
message: "Minimum Sleep Interval must be at most 3600 seconds (1 hour)"
|
||||
}),
|
||||
max_sleep_interval: z.coerce.number<number>({
|
||||
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||
? "Maximum Sleep Interval is required"
|
||||
: "Maximum Sleep Interval must be a valid number"
|
||||
}).int({
|
||||
message: "Maximum Sleep Interval must be an integer"
|
||||
}).min(1, {
|
||||
message: "Maximum Sleep Interval must be at least 1 second"
|
||||
}).max(3600, {
|
||||
message: "Maximum Sleep Interval must be at most 3600 seconds (1 hour)"
|
||||
}),
|
||||
})
|
||||
|
||||
const requestSleepIntervalSchema = z.object({
|
||||
request_sleep_interval: z.coerce.number<number>({
|
||||
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||
? "Request Sleep Interval is required"
|
||||
: "Request Sleep Interval must be a valid number"
|
||||
}).int({
|
||||
message: "Request Sleep Interval must be an integer"
|
||||
}).min(1, {
|
||||
message: "Request Sleep Interval must be at least 1 second"
|
||||
}).max(3600, {
|
||||
message: "Request Sleep Interval must be at most 3600 seconds (1 hour)"
|
||||
}),
|
||||
})
|
||||
|
||||
const potServerPortSchema = z.object({
|
||||
port: z.coerce.number<number>({
|
||||
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||
? "POT Server Port is required"
|
||||
: "POT Server Port must be a valid number"
|
||||
}).int({
|
||||
message: "POT Server Port must be an integer"
|
||||
}).min(4000, {
|
||||
message: "POT Server Port must be at least 4000"
|
||||
}).max(5000, {
|
||||
message: "POT Server Port must be at most 5000"
|
||||
}),
|
||||
});
|
||||
|
||||
function AppGeneralSettings() {
|
||||
const { saveSettingsKey } = useSettings();
|
||||
|
||||
@@ -668,9 +723,10 @@ function AppNetworkSettings() {
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
<NumberInput
|
||||
className="w-full"
|
||||
placeholder="Enter rate limit in bytes/s"
|
||||
min={0}
|
||||
readOnly={useCustomCommands}
|
||||
{...field}
|
||||
/>
|
||||
@@ -853,7 +909,7 @@ function AppSponsorblockSettings() {
|
||||
return (
|
||||
<>
|
||||
<div className="sponsorblock">
|
||||
<h3 className="font-semibold">Sponsor Block</h3>
|
||||
<h3 className="font-semibold">Sponsorblock</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
@@ -978,6 +1034,369 @@ function AppSponsorblockSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
function AppDelaySettings() {
|
||||
const { saveSettingsKey } = useSettings();
|
||||
|
||||
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
||||
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
||||
|
||||
const useDelay = useSettingsPageStatesStore(state => state.settings.use_delay);
|
||||
const useSearchDelay = useSettingsPageStatesStore(state => state.settings.use_search_delay);
|
||||
const delayMode = useSettingsPageStatesStore(state => state.settings.delay_mode);
|
||||
const minSleepInterval = useSettingsPageStatesStore(state => state.settings.min_sleep_interval);
|
||||
const maxSleepInterval = useSettingsPageStatesStore(state => state.settings.max_sleep_interval);
|
||||
const requestSleepInterval = useSettingsPageStatesStore(state => state.settings.request_sleep_interval);
|
||||
const delayPlaylistOnly = useSettingsPageStatesStore(state => state.settings.delay_playlist_only);
|
||||
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||
|
||||
const minMaxSleepIntervalForm = useForm<z.infer<typeof minMaxSleepIntervalSchema>>({
|
||||
resolver: zodResolver(minMaxSleepIntervalSchema),
|
||||
defaultValues: {
|
||||
min_sleep_interval: minSleepInterval,
|
||||
max_sleep_interval: maxSleepInterval,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
const watchedMinSleepInterval = minMaxSleepIntervalForm.watch("min_sleep_interval");
|
||||
const watchedMaxSleepInterval = minMaxSleepIntervalForm.watch("max_sleep_interval");
|
||||
const { errors: minMaxSleepIntervalFormErrors } = minMaxSleepIntervalForm.formState;
|
||||
|
||||
function handleMinMaxSleepIntervalSubmit(values: z.infer<typeof minMaxSleepIntervalSchema>) {
|
||||
try {
|
||||
saveSettingsKey('min_sleep_interval', values.min_sleep_interval);
|
||||
saveSettingsKey('max_sleep_interval', values.max_sleep_interval);
|
||||
toast.success("Sleep Intervals updated", {
|
||||
description: `Minimum Sleep Interval changed to ${values.min_sleep_interval} seconds, Maximum Sleep Interval changed to ${values.max_sleep_interval} seconds`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing sleep intervals:", error);
|
||||
toast.error("Failed to change sleep intervals", {
|
||||
description: "An error occurred while trying to change the sleep intervals. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const requestSleepIntervalForm = useForm<z.infer<typeof requestSleepIntervalSchema>>({
|
||||
resolver: zodResolver(requestSleepIntervalSchema),
|
||||
defaultValues: {
|
||||
request_sleep_interval: requestSleepInterval,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
const watchedRequestSleepInterval = requestSleepIntervalForm.watch("request_sleep_interval");
|
||||
const { errors: requestSleepIntervalFormErrors } = requestSleepIntervalForm.formState;
|
||||
|
||||
function handleRequestSleepIntervalSubmit(values: z.infer<typeof requestSleepIntervalSchema>) {
|
||||
try {
|
||||
saveSettingsKey('request_sleep_interval', values.request_sleep_interval);
|
||||
toast.success("Request Sleep Interval updated", {
|
||||
description: `Request Sleep Interval changed to ${values.request_sleep_interval} seconds`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing request sleep interval:", error);
|
||||
toast.error("Failed to change request sleep interval", {
|
||||
description: "An error occurred while trying to change the request sleep interval. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (formResetTrigger > 0) {
|
||||
minMaxSleepIntervalForm.reset();
|
||||
requestSleepIntervalForm.reset();
|
||||
acknowledgeFormReset();
|
||||
}
|
||||
}, [formResetTrigger]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="delay">
|
||||
<h3 className="font-semibold">Delay</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use delay to prevent potential issues with some sites (bypass rate-limit, temporary ban, etc.)</p>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Switch
|
||||
id="use-delay"
|
||||
checked={useDelay}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_delay', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-delay">Use Delay in Downloads</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="use-search-delay"
|
||||
checked={useSearchDelay}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_search_delay', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-search-delay">Use Delay in Search</Label>
|
||||
</div>
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4"
|
||||
value={delayMode}
|
||||
onValueChange={(value) => saveSettingsKey('delay_mode', value)}
|
||||
disabled={(!useDelay && !useSearchDelay) || useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="delay-auto" />
|
||||
<Label htmlFor="delay-auto">Auto (Default)</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="custom" id="delay-custom" />
|
||||
<Label htmlFor="delay-custom">Custom</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<div className="flex flex-col gap-2 mt-5">
|
||||
<Label className="text-xs mb-1">Minimum, Maximum Sleep Interval (in Seconds)</Label>
|
||||
<Form {...minMaxSleepIntervalForm}>
|
||||
<form onSubmit={minMaxSleepIntervalForm.handleSubmit(handleMinMaxSleepIntervalSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||
<FormField
|
||||
control={minMaxSleepIntervalForm.control}
|
||||
name="min_sleep_interval"
|
||||
disabled={delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<NumberInput
|
||||
className="w-full"
|
||||
placeholder="Min sleep"
|
||||
min={0}
|
||||
readOnly={useCustomCommands}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={minMaxSleepIntervalForm.control}
|
||||
name="max_sleep_interval"
|
||||
disabled={delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<NumberInput
|
||||
className="w-full"
|
||||
placeholder="Max sleep"
|
||||
min={0}
|
||||
readOnly={useCustomCommands}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={(!watchedMinSleepInterval || Number(watchedMinSleepInterval) === minSleepInterval) && (!watchedMaxSleepInterval || Number(watchedMaxSleepInterval) === maxSleepInterval) || Object.keys(minMaxSleepIntervalFormErrors).length > 0 || delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-4 mb-2">
|
||||
<Label className="text-xs mb-1">Request Sleep Interval (in Seconds)</Label>
|
||||
<Form {...requestSleepIntervalForm}>
|
||||
<form onSubmit={requestSleepIntervalForm.handleSubmit(handleRequestSleepIntervalSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||
<FormField
|
||||
control={requestSleepIntervalForm.control}
|
||||
name="request_sleep_interval"
|
||||
disabled={delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<NumberInput
|
||||
className="w-full"
|
||||
placeholder="Request sleep"
|
||||
min={0}
|
||||
readOnly={useCustomCommands}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!watchedRequestSleepInterval || Number(watchedRequestSleepInterval) === requestSleepInterval || Object.keys(requestSleepIntervalFormErrors).length > 0 || delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<Label className="text-xs text-muted-foreground">(Configured: {minSleepInterval}s - {maxSleepInterval}s & {requestSleepInterval}s, Mode: {delayMode === 'auto' ? 'Auto' : 'Custom'}, Status: {useDelay && delayPlaylistOnly ? 'Playlist Only' : useDelay ? 'Downloads' : ''}{useDelay && useSearchDelay ? ', Search' : useSearchDelay ? 'Search' : !useDelay && !useSearchDelay ? 'Disabled' : ''}) (Default: 10s - 20s & 1s, Range: 1s - 3600s)</Label>
|
||||
</div>
|
||||
<div className="delay-playlist-only">
|
||||
<h3 className="font-semibold">Delay Playlist Only</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Only apply delay for playlist/batch downloads, single video downloads will not be affected (recommended)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="delay-playlist-only"
|
||||
checked={delayPlaylistOnly}
|
||||
onCheckedChange={(checked) => saveSettingsKey('delay_playlist_only', checked)}
|
||||
disabled={!useDelay || useCustomCommands}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AppPoTokenSettings() {
|
||||
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
||||
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
||||
|
||||
const usePotoken = useSettingsPageStatesStore(state => state.settings.use_potoken);
|
||||
const disableInnertube = useSettingsPageStatesStore(state => state.settings.disable_innertube);
|
||||
const potServerPort = useSettingsPageStatesStore(state => state.settings.pot_server_port);
|
||||
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
|
||||
const isStartingPotServer = useSettingsPageStatesStore(state => state.isStartingPotServer);
|
||||
const isChangingPotServerPort = useSettingsPageStatesStore(state => state.isChangingPotServerPort);
|
||||
const setIsChangingPotServerPort = useSettingsPageStatesStore(state => state.setIsChangingPotServerPort);
|
||||
|
||||
const { saveSettingsKey } = useSettings();
|
||||
const { startPotServer, stopPotServer } = usePotServer();
|
||||
|
||||
const potServerPortForm = useForm<z.infer<typeof potServerPortSchema>>({
|
||||
resolver: zodResolver(potServerPortSchema),
|
||||
defaultValues: {
|
||||
port: potServerPort,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
const watchedPotServerPort = potServerPortForm.watch("port");
|
||||
const { errors: potServerPortFormErrors } = potServerPortForm.formState;
|
||||
|
||||
async function handlePotServerPortSubmit(values: z.infer<typeof potServerPortSchema>) {
|
||||
setIsChangingPotServerPort(true);
|
||||
try {
|
||||
saveSettingsKey('pot_server_port', values.port);
|
||||
if (isRunningPotServer) {
|
||||
await stopPotServer();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await startPotServer(values.port);
|
||||
}
|
||||
toast.success("POT Server Port updated", {
|
||||
description: `PO Token Server Port changed to ${values.port}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing PO Token Server Port:", error);
|
||||
toast.error("Failed to change POT Server Port", {
|
||||
description: "An error occurred while trying to change the PO Token Server Port. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsChangingPotServerPort(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (formResetTrigger > 0) {
|
||||
potServerPortForm.reset();
|
||||
acknowledgeFormReset();
|
||||
}
|
||||
}, [formResetTrigger]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="potoken">
|
||||
<h3 className="font-semibold">PO Token</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Generate proof-of-origin token for youtube to make seem your traffic more legitimate (bypasses some bot-protection checks, sometimes requires cookies)</p>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Switch
|
||||
id="use-potoken"
|
||||
checked={usePotoken}
|
||||
onCheckedChange={async (checked) => {
|
||||
saveSettingsKey('use_potoken', checked);
|
||||
if (checked) {
|
||||
await startPotServer();
|
||||
} else {
|
||||
await stopPotServer();
|
||||
}
|
||||
}}
|
||||
disabled={useCustomCommands || isStartingPotServer || isChangingPotServerPort}
|
||||
/>
|
||||
<Label htmlFor="use-potoken">Use PO Token</Label>
|
||||
</div>
|
||||
<Label className="text-xs text-muted-foreground flex items-center">
|
||||
<span className="mr-1">NeoDLP POT Server is</span>
|
||||
{isStartingPotServer ? (
|
||||
<span className="text-amber-600 dark:text-amber-500 underline">Starting</span>
|
||||
) : isRunningPotServer ? (
|
||||
<span className="text-emerald-600 dark:text-emerald-500 underline">Running</span>
|
||||
) : (
|
||||
<span className="text-red-600 dark:text-red-500 underline">Not Running</span>
|
||||
)}
|
||||
{isRunningPotServer && potServerPort ? (
|
||||
<span className="ml-1">on Port {potServerPort}</span>
|
||||
) : null}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="disable-innertube">
|
||||
<h3 className="font-semibold">Disable Innertube</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Disable the usage of innertube api for potoken generation (falls back to legacy mode, use only if normal potoken is not working)</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="disable-innertube"
|
||||
checked={disableInnertube}
|
||||
onCheckedChange={(checked) => saveSettingsKey('disable_innertube', checked)}
|
||||
disabled={useCustomCommands || !usePotoken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pot-server-port">
|
||||
<h3 className="font-semibold">POT Server Port</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Change neodlp proof-of-origin token server port</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Form {...potServerPortForm}>
|
||||
<form onSubmit={potServerPortForm.handleSubmit(handlePotServerPortSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||
<FormField
|
||||
control={potServerPortForm.control}
|
||||
name="port"
|
||||
disabled={!usePotoken || useCustomCommands || isChangingPotServerPort || isStartingPotServer}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<NumberInput
|
||||
className="w-full"
|
||||
placeholder="Enter port number"
|
||||
min={0}
|
||||
readOnly={useCustomCommands}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Label htmlFor="port" className="text-xs text-muted-foreground">(Current: {potServerPort}) (Default: 4416, Range: 4000-5000)</Label>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!watchedPotServerPort || Number(watchedPotServerPort) === potServerPort || Object.keys(potServerPortFormErrors).length > 0 || !usePotoken || useCustomCommands || isChangingPotServerPort || isStartingPotServer}
|
||||
>
|
||||
{isChangingPotServerPort ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Changing
|
||||
</>
|
||||
) : (
|
||||
'Change'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AppNotificationSettings() {
|
||||
const { saveSettingsKey } = useSettings();
|
||||
|
||||
@@ -1040,12 +1459,14 @@ function AppNotificationSettings() {
|
||||
|
||||
function AppCommandSettings() {
|
||||
const { saveSettingsKey } = useSettings();
|
||||
const { startPotServer, stopPotServer } = usePotServer();
|
||||
|
||||
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
||||
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
||||
|
||||
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||
const customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands);
|
||||
const usePotoken = useSettingsPageStatesStore(state => state.settings.use_potoken);
|
||||
|
||||
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
|
||||
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
|
||||
@@ -1123,9 +1544,14 @@ function AppCommandSettings() {
|
||||
<Switch
|
||||
id="use-custom-commands"
|
||||
checked={useCustomCommands}
|
||||
onCheckedChange={(checked) => {
|
||||
onCheckedChange={async(checked) => {
|
||||
saveSettingsKey('use_custom_commands', checked)
|
||||
resetDownloadConfiguration();
|
||||
if (checked && usePotoken) {
|
||||
await stopPotServer();
|
||||
} else if (!checked && usePotoken) {
|
||||
await startPotServer();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="use-custom-commands">Use Custom Commands</Label>
|
||||
@@ -1268,6 +1694,7 @@ function AppInfoSettings() {
|
||||
{ key: 'ffprobe', name: 'FFprobe', desc: 'Multimedia stream analyzer for retrieving media information', url: 'https://ffmpeg.org/ffprobe.html', license: 'LGPLv2.1+', licenseUrl: 'https://ffmpeg.org/legal.html' },
|
||||
{ key: 'deno', name: 'Deno', desc: 'The modern JavaScript/TypeScript runtime', url: 'https://deno.land/', license: 'MIT', licenseUrl: 'https://github.com/denoland/deno/blob/main/LICENSE.md' },
|
||||
{ key: 'aria2', name: 'Aria2', desc: 'Lightweight multi-protocol & multi-source download utility', url: 'https://aria2.github.io/', license: 'GPLv2+', licenseUrl: 'https://github.com/aria2/aria2/blob/master/COPYING' },
|
||||
{ Key: 'bgutil-pot-rs', name: 'BgUtils POT Provider (Rust)', desc: 'A high-performance YouTube POT (Proof-of-Origin Token) provider implemented in Rust', url: 'https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs', license: 'GPLv3+', licenseUrl: 'https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/blob/master/LICENSE' },
|
||||
];
|
||||
const langDepsList = [
|
||||
{ key: 'tauri', name: 'Tauri', desc: 'Framework for building cross-platform, tiny and blazing fast binaries', url: 'https://tauri.app/', license: 'MIT, Apache-2.0', licenseUrl: 'https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT' },
|
||||
@@ -1468,6 +1895,8 @@ export function ApplicationSettings() {
|
||||
{ key: 'network', label: 'Network', icon: Wifi, component: <AppNetworkSettings /> },
|
||||
{ key: 'cookies', label: 'Cookies', icon: Cookie, component: <AppCookiesSettings /> },
|
||||
{ key: 'sponsorblock', label: 'Sponsorblock', icon: ShieldMinus, component: <AppSponsorblockSettings /> },
|
||||
{ key: 'delay', label: 'Delay', icon: Timer, component: <AppDelaySettings /> },
|
||||
{ key: 'potoken', label: 'Potoken', icon: KeyRound, component: <AppPoTokenSettings /> },
|
||||
{ key: 'notifications', label: 'Notifications', icon: BellRing, component: <AppNotificationSettings /> },
|
||||
{ key: 'commands', label: 'Commands', icon: SquareTerminal, component: <AppCommandSettings /> },
|
||||
{ key: 'debug', label: 'Debug', icon: Bug, component: <AppDebugSettings /> },
|
||||
@@ -1553,7 +1982,7 @@ export function ApplicationSettings() {
|
||||
</TabsList>
|
||||
<div className="min-h-full flex flex-col w-full border-l border-border pl-4">
|
||||
{tabsList.map((tab) => (
|
||||
<TabsContent key={tab.key} value={tab.key} className={clsx("flex flex-col gap-4 min-h-108.75", tab.key === "info" ? "max-w-[80%]" : "max-w-[70%]")}>
|
||||
<TabsContent key={tab.key} value={tab.key} className={clsx("flex flex-col gap-4 min-h-130", tab.key === "info" ? "max-w-[80%]" : "max-w-[70%]")}>
|
||||
{tab.component}
|
||||
</TabsContent>
|
||||
))}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { ArrowDownToLine, ArrowRight, EthernetPort, Loader2, Radio, RotateCw } from "lucide-react";
|
||||
import { useSettings } from "@/helpers/use-settings";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
@@ -15,6 +14,7 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from "@/component
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { SlidingButton } from "@/components/custom/slidingButton";
|
||||
import clsx from "clsx";
|
||||
import { NumberInput } from "@/components/custom/numberInput";
|
||||
|
||||
const websocketPortSchema = z.object({
|
||||
port: z.coerce.number<number>({
|
||||
@@ -167,9 +167,10 @@ function ExtPortSettings() {
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
<NumberInput
|
||||
className="w-full"
|
||||
placeholder="Enter port number"
|
||||
min={0}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -61,8 +61,19 @@ export default function useDownloader() {
|
||||
log_verbose: LOG_VERBOSE,
|
||||
log_progress: LOG_PROGRESS,
|
||||
enable_notifications: ENABLE_NOTIFICATIONS,
|
||||
download_completion_notification: DOWNLOAD_COMPLETION_NOTIFICATION
|
||||
download_completion_notification: DOWNLOAD_COMPLETION_NOTIFICATION,
|
||||
use_delay: USE_DELAY,
|
||||
use_search_delay: USE_SEARCH_DELAY,
|
||||
delay_mode: DELAY_MODE,
|
||||
min_sleep_interval: MIN_SLEEP_INTERVAL,
|
||||
max_sleep_interval: MAX_SLEEP_INTERVAL,
|
||||
request_sleep_interval: REQUEST_SLEEP_INTERVAL,
|
||||
delay_playlist_only: DELAY_PLAYLIST_ONLY,
|
||||
use_potoken: USE_POTOKEN,
|
||||
disable_innertube: DISABLE_INNERTUBE,
|
||||
pot_server_port: POT_SERVER_PORT,
|
||||
} = useSettingsPageStatesStore(state => state.settings);
|
||||
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
|
||||
|
||||
const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds);
|
||||
const addErroredDownload = useDownloaderPageStatesStore((state) => state.addErroredDownload);
|
||||
@@ -103,7 +114,12 @@ export default function useDownloader() {
|
||||
const fetchVideoMetadata = async (params: FetchVideoMetadataParams): Promise<RawVideoInfo | null> => {
|
||||
const { url, formatId, playlistIndices, selectedSubtitles, resumeState, downloadConfig } = params;
|
||||
try {
|
||||
const args = [url, '--dump-single-json', '--no-warnings'];
|
||||
const args = [url, '--dump-single-json'];
|
||||
if (DEBUG_MODE && LOG_VERBOSE) {
|
||||
args.push('--verbose');
|
||||
} else {
|
||||
args.push('--no-warnings');
|
||||
}
|
||||
if (formatId) {
|
||||
const isMultipleAudioFormatSelected = formatId.split('+').length > 2;
|
||||
args.push('--format', formatId);
|
||||
@@ -162,37 +178,64 @@ export default function useDownloader() {
|
||||
args.push('--sponsorblock-mark', sponsorblockMark);
|
||||
}
|
||||
};
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_SEARCH_DELAY) {
|
||||
if (DELAY_MODE === 'auto') {
|
||||
args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20');
|
||||
} else if (DELAY_MODE === 'custom') {
|
||||
args.push('--sleep-requests', REQUEST_SLEEP_INTERVAL.toString(), '--sleep-interval', MIN_SLEEP_INTERVAL.toString(), '--max-sleep-interval', MAX_SLEEP_INTERVAL.toString());
|
||||
}
|
||||
}
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_POTOKEN) {
|
||||
if (!isRunningPotServer) {
|
||||
LOG.warning("NEODLP", "Looks like you want to use PO Token! But, NeoDLP POT Server is not running. PO Token generation will most likely fail!");
|
||||
}
|
||||
if (DISABLE_INNERTUBE) {
|
||||
args.push('--extractor-args', `youtubepot-bgutilhttp:base_url=http://localhost:${POT_SERVER_PORT};disable_innertube=1`);
|
||||
} else {
|
||||
args.push('--extractor-args', `youtubepot-bgutilhttp:base_url=http://localhost:${POT_SERVER_PORT}`);
|
||||
}
|
||||
}
|
||||
|
||||
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||
|
||||
let jsonOutput = '';
|
||||
|
||||
return new Promise<RawVideoInfo | null>((resolve) => {
|
||||
command.stdout.on('data', line => {
|
||||
jsonOutput += line;
|
||||
if (line.startsWith('{')) {
|
||||
jsonOutput = line;
|
||||
} else {
|
||||
if (line.trim() !== '') LOG.info('YT-DLP', line);
|
||||
}
|
||||
});
|
||||
|
||||
command.stderr.on('data', line => {
|
||||
if (line.trim() !== '') LOG.info('YT-DLP', line);
|
||||
});
|
||||
|
||||
command.on('close', async (data) => {
|
||||
if (data.code !== 0) {
|
||||
console.error(`yt-dlp failed to fetch metadata with code ${data.code}`);
|
||||
console.error(`yt-dlp exited with code ${data.code} while fetching metadata for URL: ${url}`);
|
||||
LOG.error('NEODLP', `yt-dlp exited with code ${data.code} while fetching metadata for URL: ${url} (ignore if you manually cancelled)`);
|
||||
resolve(null);
|
||||
} else {
|
||||
try {
|
||||
const matchedJson = jsonOutput.match(/{.*}/);
|
||||
if (!matchedJson) {
|
||||
console.error(`Failed to match JSON: ${jsonOutput}`);
|
||||
LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url})`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const parsedJson: RawVideoInfo = JSON.parse(matchedJson[0]);
|
||||
resolve(parsedJson);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Failed to parse JSON: ${e}`);
|
||||
LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url}) with error: ${e}`);
|
||||
LOG.info('NEODLP', `yt-dlp exited with code ${data.code} while fetching metadata for URL: ${url}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const matchedJson = jsonOutput.match(/{.*}/);
|
||||
if (!matchedJson) {
|
||||
console.error(`Failed to match JSON: ${jsonOutput}`);
|
||||
LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url})`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const parsedJson: RawVideoInfo = JSON.parse(matchedJson[0]);
|
||||
resolve(parsedJson);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Failed to parse JSON: ${e}`);
|
||||
LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url}) with error: ${e}`);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -305,14 +348,41 @@ export default function useDownloader() {
|
||||
args.push('--output', `${FILENAME_TEMPLATE}[${downloadId}].%(ext)s`);
|
||||
}
|
||||
|
||||
if (isMultiplePlaylistItems) {
|
||||
const playlistLength = playlistIndices.split(',').length;
|
||||
if (playlistLength > 5 && playlistLength < 100) {
|
||||
args.push('--sleep-requests', '1', '--sleep-interval', '5', '--max-sleep-interval', '15');
|
||||
} else if (playlistLength >= 100 && playlistLength < 500) {
|
||||
args.push('--sleep-requests', '1.5', '--sleep-interval', '10', '--max-sleep-interval', '40');
|
||||
} else if (playlistLength >= 500) {
|
||||
args.push('--sleep-requests', '2.5', '--sleep-interval', '20', '--max-sleep-interval', '60');
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_DELAY) {
|
||||
if (!DELAY_PLAYLIST_ONLY) {
|
||||
if (DELAY_MODE === 'auto') {
|
||||
if (isMultiplePlaylistItems) {
|
||||
const playlistLength = playlistIndices.split(',').length;
|
||||
if (playlistLength <= 5) {
|
||||
args.push('--sleep-requests', '1', '--sleep-interval', '5', '--max-sleep-interval', '10');
|
||||
} else if (playlistLength > 5 && playlistLength < 100) {
|
||||
args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20');
|
||||
} else if (playlistLength >= 100 && playlistLength < 500) {
|
||||
args.push('--sleep-requests', '2', '--sleep-interval', '20', '--max-sleep-interval', '40');
|
||||
} else if (playlistLength >= 500) {
|
||||
args.push('--sleep-requests', '2', '--sleep-interval', '40', '--max-sleep-interval', '60');
|
||||
}
|
||||
} else {
|
||||
args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20');
|
||||
}
|
||||
} else if (DELAY_MODE === 'custom') {
|
||||
args.push('--sleep-requests', REQUEST_SLEEP_INTERVAL.toString(), '--sleep-interval', MIN_SLEEP_INTERVAL.toString(), '--max-sleep-interval', MAX_SLEEP_INTERVAL.toString());
|
||||
}
|
||||
} else if (DELAY_PLAYLIST_ONLY && isMultiplePlaylistItems) {
|
||||
if (DELAY_MODE === 'auto') {
|
||||
const playlistLength = playlistIndices.split(',').length;
|
||||
if (playlistLength <= 5) {
|
||||
args.push('--sleep-requests', '1', '--sleep-interval', '5', '--max-sleep-interval', '10');
|
||||
} else if (playlistLength > 5 && playlistLength < 100) {
|
||||
args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20');
|
||||
} else if (playlistLength >= 100 && playlistLength < 500) {
|
||||
args.push('--sleep-requests', '2', '--sleep-interval', '20', '--max-sleep-interval', '40');
|
||||
} else if (playlistLength >= 500) {
|
||||
args.push('--sleep-requests', '2', '--sleep-interval', '40', '--max-sleep-interval', '60');
|
||||
}
|
||||
} else if (DELAY_MODE === 'custom') {
|
||||
args.push('--sleep-requests', REQUEST_SLEEP_INTERVAL.toString(), '--sleep-interval', MIN_SLEEP_INTERVAL.toString(), '--max-sleep-interval', MAX_SLEEP_INTERVAL.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +539,17 @@ export default function useDownloader() {
|
||||
LOG.warning('NEODLP', `Looks like you are using aria2 for this yt-dlp download: ${downloadId}. Make sure aria2 is installed on your system if you are on macOS for this to work. Also, pause/resume might not work as expected especially on windows (using aria2 is not recommended for most downloads).`);
|
||||
}
|
||||
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_POTOKEN) {
|
||||
if (!isRunningPotServer) {
|
||||
LOG.warning("NEODLP", "Looks like you want to use PO Token! But, NeoDLP POT Server is not running. PO Token generation will most likely fail!");
|
||||
}
|
||||
if (DISABLE_INNERTUBE) {
|
||||
args.push('--extractor-args', `youtubepot-bgutilhttp:base_url=http://localhost:${POT_SERVER_PORT};disable_innertube=1`);
|
||||
} else {
|
||||
args.push('--extractor-args', `youtubepot-bgutilhttp:base_url=http://localhost:${POT_SERVER_PORT}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (resumeState || (!USE_CUSTOM_COMMANDS && USE_ARIA2)) {
|
||||
args.push('--continue');
|
||||
} else {
|
||||
|
||||
52
src/helpers/use-linux-registerer.ts
Normal file
52
src/helpers/use-linux-registerer.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { join, resourceDir, homeDir } from "@tauri-apps/api/path";
|
||||
import * as fs from "@tauri-apps/plugin-fs";
|
||||
import { useKvPairs } from "@/helpers/use-kvpairs";
|
||||
import { useSettingsPageStatesStore } from "@/services/store";
|
||||
|
||||
interface FileMap {
|
||||
source: string;
|
||||
destination: string;
|
||||
dir: string;
|
||||
}
|
||||
|
||||
export function useLinuxRegisterer() {
|
||||
const { saveKvPair } = useKvPairs();
|
||||
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
||||
|
||||
const registerToLinux = async () => {
|
||||
try {
|
||||
const filesToCopy: FileMap[] = [
|
||||
{ source: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil.py', destination: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil.py', dir: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/' },
|
||||
{ source: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_cli.py', destination: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_cli.py', dir: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/' },
|
||||
{ source: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_http.py', destination: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_http.py', dir: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/' },
|
||||
];
|
||||
|
||||
const resourceDirPath = await resourceDir();
|
||||
const homeDirPath = await homeDir();
|
||||
|
||||
for (const file of filesToCopy) {
|
||||
const sourcePath = await join(resourceDirPath, file.source);
|
||||
const destinationDir = await join(homeDirPath, file.dir);
|
||||
const destinationPath = await join(homeDirPath, file.destination);
|
||||
|
||||
const dirExists = await fs.exists(destinationDir);
|
||||
if (dirExists) {
|
||||
await fs.copyFile(sourcePath, destinationPath);
|
||||
console.log(`File ${file.source} copied successfully to ${destinationPath}`);
|
||||
} else {
|
||||
await fs.mkdir(destinationDir, { recursive: true })
|
||||
console.log(`Created dir ${destinationDir}`);
|
||||
await fs.copyFile(sourcePath, destinationPath);
|
||||
console.log(`File ${file.source} copied successfully to ${destinationPath}`);
|
||||
}
|
||||
}
|
||||
saveKvPair('linux_registered_version', appVersion);
|
||||
return { success: true, message: 'Registered successfully' }
|
||||
} catch (error) {
|
||||
console.error('Error copying files:', error);
|
||||
return { success: false, message: 'Failed to register' }
|
||||
}
|
||||
}
|
||||
|
||||
return { registerToLinux };
|
||||
}
|
||||
@@ -3,17 +3,26 @@ import * as fs from "@tauri-apps/plugin-fs";
|
||||
import { useKvPairs } from "@/helpers/use-kvpairs";
|
||||
import { useSettingsPageStatesStore } from "@/services/store";
|
||||
|
||||
interface FileMap {
|
||||
source: string;
|
||||
destination: string;
|
||||
dir: string;
|
||||
}
|
||||
|
||||
export function useMacOsRegisterer() {
|
||||
const { saveKvPair } = useKvPairs();
|
||||
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
||||
|
||||
const registerToMac = async () => {
|
||||
try {
|
||||
const filesToCopy = [
|
||||
const filesToCopy: FileMap[] = [
|
||||
{ source: 'neodlp-autostart.plist', destination: 'Library/LaunchAgents/com.neosubhamoy.neodlp.plist', dir: 'Library/LaunchAgents/' },
|
||||
{ source: 'neodlp-msghost.json', destination: 'Library/Application Support/Google/Chrome/NativeMessagingHosts/com.neosubhamoy.neodlp.json', dir: 'Library/Application Support/Google/Chrome/NativeMessagingHosts/' },
|
||||
{ source: 'neodlp-msghost.json', destination: 'Library/Application Support/Chromium/NativeMessagingHosts/com.neosubhamoy.neodlp.json', dir: 'Library/Application Support/Chromium/NativeMessagingHosts/' },
|
||||
{ source: 'neodlp-msghost-moz.json', destination: 'Library/Application Support/Mozilla/NativeMessagingHosts/com.neosubhamoy.neodlp.json', dir: 'Library/Application Support/Mozilla/NativeMessagingHosts/' },
|
||||
{ source: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil.py', destination: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil.py', dir: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/' },
|
||||
{ source: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_cli.py', destination: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_cli.py', dir: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/' },
|
||||
{ source: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_http.py', destination: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_http.py', dir: 'yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/' },
|
||||
];
|
||||
|
||||
const resourceDirPath = await resourceDir();
|
||||
|
||||
86
src/helpers/use-pot-server.ts
Normal file
86
src/helpers/use-pot-server.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useSettingsPageStatesStore } from "@/services/store";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
import { Command } from "@tauri-apps/plugin-shell";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export default function usePotServer() {
|
||||
const setIsRunningPotServer = useSettingsPageStatesStore(state => state.setIsRunningPotServer);
|
||||
const setIsStartingPotServer = useSettingsPageStatesStore(state => state.setIsStartingPotServer);
|
||||
const potServerPid = useSettingsPageStatesStore(state => state.potServerPid);
|
||||
const setPotServerPid = useSettingsPageStatesStore(state => state.setPotServerPid);
|
||||
const potServerPort = useSettingsPageStatesStore(state => state.settings.pot_server_port);
|
||||
const LOG = useLogger();
|
||||
|
||||
const stripAnsiAndLogPrefix = (line: string): string => {
|
||||
const stripped = line.replace(/\x1b\[\d+m/g, '');
|
||||
return stripped.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+\w+\s+[\w:]+:\s*/, '');
|
||||
};
|
||||
|
||||
const startPotServer = async (port?: number) => {
|
||||
const runCommand = Command.sidecar('binaries/neodlp-pot', [
|
||||
'server',
|
||||
'--port',
|
||||
port ? port.toString() : potServerPort.toString(),
|
||||
]);
|
||||
|
||||
try {
|
||||
setIsStartingPotServer(true);
|
||||
LOG.info("NEODLP POT-SERVER", `Starting POT Server on port: ${port ?? potServerPort}`);
|
||||
|
||||
runCommand.on("close", (data) => {
|
||||
if (data.code === 0) {
|
||||
LOG.info("NEODLP POT-SERVER", `POT Server process exited with code: ${data.code}`);
|
||||
} else {
|
||||
LOG.error("NEODLP POT-SERVER", `POT Server process exited with code: ${data.code} (ignore if you manually stopped the server)`);
|
||||
}
|
||||
setIsRunningPotServer(false);
|
||||
setPotServerPid(null);
|
||||
});
|
||||
|
||||
runCommand.on("error", (error) => {
|
||||
LOG.error("NEODLP POT-SERVER", `Error running POT Server: ${error}`);
|
||||
setIsRunningPotServer(false);
|
||||
setPotServerPid(null);
|
||||
});
|
||||
|
||||
runCommand.stdout.on("data", (line) => {
|
||||
const cleanedLine = stripAnsiAndLogPrefix(line).trim();
|
||||
if (cleanedLine !== '') LOG.info("NEODLP POT-SERVER", cleanedLine);
|
||||
if (cleanedLine.startsWith("POT server")) {
|
||||
setIsRunningPotServer(true);
|
||||
}
|
||||
});
|
||||
|
||||
runCommand.stderr.on("data", (line) => {
|
||||
const cleanedLine = stripAnsiAndLogPrefix(line).trim();
|
||||
if (cleanedLine !== '') LOG.error("NEODLP POT-SERVER", cleanedLine);
|
||||
});
|
||||
|
||||
const child = await runCommand.spawn();
|
||||
setPotServerPid(child.pid);
|
||||
} catch (error) {
|
||||
LOG.error("NEODLP POT-SERVER", `Error starting POT Server: ${error}`);
|
||||
} finally {
|
||||
setIsStartingPotServer(false);
|
||||
}
|
||||
}
|
||||
|
||||
const stopPotServer = async () => {
|
||||
if (!potServerPid) {
|
||||
LOG.warning("NEODLP POT-SERVER", "No POT Server process found to stop.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
LOG.info("NEODLP POT-SERVER", `Stopping POT Server with PID: ${potServerPid}`);
|
||||
await invoke('kill_all_process', { pid: potServerPid });
|
||||
LOG.info("NEODLP POT-SERVER", "POT Server stopped successfully.");
|
||||
setIsRunningPotServer(false);
|
||||
setPotServerPid(null);
|
||||
} catch (error) {
|
||||
LOG.error("NEODLP POT-SERVER", `Error stopping POT Server: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { startPotServer, stopPotServer};
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
@@ -9,6 +9,7 @@ import { useSettings } from "@/helpers/use-settings";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||
import { ExtensionSettings } from "@/components/pages/settings/extensionSettings";
|
||||
import { ApplicationSettings } from "@/components/pages/settings/applicationSettings";
|
||||
import usePotServer from "@/helpers/use-pot-server";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { setTheme } = useTheme();
|
||||
@@ -17,10 +18,12 @@ export default function SettingsPage() {
|
||||
const setActiveTab = useSettingsPageStatesStore(state => state.setActiveTab);
|
||||
|
||||
const isUsingDefaultSettings = useSettingsPageStatesStore(state => state.isUsingDefaultSettings);
|
||||
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
|
||||
const appTheme = useSettingsPageStatesStore(state => state.settings.theme);
|
||||
const appColorScheme = useSettingsPageStatesStore(state => state.settings.color_scheme);
|
||||
|
||||
const { resetSettings } = useSettings();
|
||||
const { stopPotServer } = usePotServer();
|
||||
|
||||
useEffect(() => {
|
||||
const updateTheme = async () => {
|
||||
@@ -60,8 +63,11 @@ export default function SettingsPage() {
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={
|
||||
() => {
|
||||
resetSettings()
|
||||
async () => {
|
||||
resetSettings();
|
||||
if (isRunningPotServer) {
|
||||
await stopPotServer();
|
||||
}
|
||||
}
|
||||
}>Reset</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
||||
@@ -211,6 +211,16 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
enable_notifications: false,
|
||||
update_notification: true,
|
||||
download_completion_notification: false,
|
||||
use_delay: true,
|
||||
use_search_delay: false,
|
||||
delay_mode: 'auto',
|
||||
min_sleep_interval: 10,
|
||||
max_sleep_interval: 20,
|
||||
request_sleep_interval: 1,
|
||||
delay_playlist_only: true,
|
||||
use_potoken: false,
|
||||
disable_innertube: false,
|
||||
pot_server_port: 4416,
|
||||
// extension settings
|
||||
websocket_port: 53511
|
||||
},
|
||||
@@ -223,6 +233,10 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
appUpdateDownloadProgress: 0,
|
||||
formResetTrigger: 0,
|
||||
resetAcknowledgements: 0,
|
||||
isRunningPotServer: false,
|
||||
isStartingPotServer: false,
|
||||
isChangingPotServerPort: false,
|
||||
potServerPid: null,
|
||||
setActiveTab: (tab) => set(() => ({ activeTab: tab })),
|
||||
setActiveSubAppTab: (tab) => set(() => ({ activeSubAppTab: tab })),
|
||||
setActiveSubExtTab: (tab) => set(() => ({ activeSubExtTab: tab })),
|
||||
@@ -282,6 +296,16 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
enable_notifications: false,
|
||||
update_notification: true,
|
||||
download_completion_notification: false,
|
||||
use_delay: true,
|
||||
use_search_delay: false,
|
||||
delay_mode: 'auto',
|
||||
min_sleep_interval: 10,
|
||||
max_sleep_interval: 20,
|
||||
request_sleep_interval: 1,
|
||||
delay_playlist_only: true,
|
||||
use_potoken: false,
|
||||
disable_innertube: false,
|
||||
pot_server_port: 4416,
|
||||
// extension settings
|
||||
websocket_port: 53511
|
||||
},
|
||||
@@ -301,12 +325,17 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
acknowledgeFormReset: () => set((state) => ({
|
||||
resetAcknowledgements: state.resetAcknowledgements + 1
|
||||
})),
|
||||
setIsRunningPotServer: (isRunning) => set(() => ({ isRunningPotServer: isRunning })),
|
||||
setIsStartingPotServer: (isStarting) => set(() => ({ isStartingPotServer: isStarting })),
|
||||
setIsChangingPotServerPort: (isChanging) => set(() => ({ isChangingPotServerPort: isChanging })),
|
||||
setPotServerPid: (pid) => set(() => ({ potServerPid: pid }))
|
||||
}));
|
||||
|
||||
export const useKvPairsStatesStore = create<KvPairsStatesStore>((set) => ({
|
||||
kvPairs: {
|
||||
ytdlp_update_last_check: null,
|
||||
macos_registered_version: null
|
||||
macos_registered_version: null,
|
||||
linux_registered_version: null
|
||||
},
|
||||
setKvPairsKey: (key, value) => set((state) => ({
|
||||
kvPairs: {
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface KvStoreTable {
|
||||
export interface KvStore {
|
||||
ytdlp_update_last_check: number | null;
|
||||
macos_registered_version: string | null;
|
||||
linux_registered_version: string | null;
|
||||
}
|
||||
@@ -54,6 +54,16 @@ export interface Settings {
|
||||
enable_notifications: boolean;
|
||||
update_notification: boolean;
|
||||
download_completion_notification: boolean;
|
||||
use_delay: boolean;
|
||||
use_search_delay: boolean;
|
||||
delay_mode: string;
|
||||
min_sleep_interval: number;
|
||||
max_sleep_interval: number;
|
||||
request_sleep_interval: number;
|
||||
delay_playlist_only: boolean;
|
||||
use_potoken: boolean;
|
||||
disable_innertube: boolean;
|
||||
pot_server_port: number;
|
||||
// extension settings
|
||||
websocket_port: number;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,10 @@ export interface SettingsPageStatesStore {
|
||||
appUpdateDownloadProgress: number;
|
||||
formResetTrigger: number;
|
||||
resetAcknowledgements: number;
|
||||
isRunningPotServer: boolean;
|
||||
isStartingPotServer: boolean;
|
||||
isChangingPotServerPort: boolean;
|
||||
potServerPid: number | null;
|
||||
setActiveTab: (tab: string) => void;
|
||||
setActiveSubAppTab: (tab: string) => void;
|
||||
setActiveSubExtTab: (tab: string) => void;
|
||||
@@ -130,6 +134,10 @@ export interface SettingsPageStatesStore {
|
||||
setAppUpdateDownloadProgress: (progress: number) => void;
|
||||
triggerFormReset: () => void;
|
||||
acknowledgeFormReset: () => void;
|
||||
setIsRunningPotServer: (isRunning: boolean) => void;
|
||||
setIsStartingPotServer: (isStarting: boolean) => void;
|
||||
setIsChangingPotServerPort: (isChanging: boolean) => void;
|
||||
setPotServerPid: (pid: number | null) => void;
|
||||
}
|
||||
|
||||
export interface KvPairsStatesStore {
|
||||
|
||||
@@ -348,7 +348,7 @@ export const determineFileType = (
|
||||
const audioCodec = (acodec || '').toLowerCase();
|
||||
|
||||
const isNone = (str: string): boolean => {
|
||||
return ['none', 'n/a', '-', ''].includes(str);
|
||||
return ['none', 'auto', 'n/a', '-', ''].includes(str);
|
||||
};
|
||||
|
||||
const hasVideo = !isNone(videoCodec);
|
||||
@@ -591,7 +591,7 @@ export const getMergedBestFormat = (
|
||||
} else {
|
||||
return {
|
||||
...baseFormat,
|
||||
format: 'Best Video (Automatic)',
|
||||
format: 'Best Quality (Auto)',
|
||||
format_id: 'best',
|
||||
format_note: 'auto',
|
||||
ext: 'auto',
|
||||
|
||||
Reference in New Issue
Block a user