From dfa5cace82bdd2d9364b88a73e2d17fe1e6a2242 Mon Sep 17 00:00:00 2001 From: Subhamoy Biswas Date: Wed, 18 Feb 2026 14:19:11 +0530 Subject: [PATCH] feat: added support for youtube po token generation --- README.md | 10 +- package.json | 2 +- scripts/download-bins.js | 68 ++++++ src-tauri/Cargo.toml | 2 +- src-tauri/capabilities/shell.json | 20 ++ .../yt_dlp_plugins/extractor/getpot_bgutil.py | 79 +++++++ .../extractor/getpot_bgutil_cli.py | 211 ++++++++++++++++++ .../extractor/getpot_bgutil_http.py | 207 +++++++++++++++++ src-tauri/tauri.linux-aarch64.conf.json | 6 +- src-tauri/tauri.linux-x86_64.conf.json | 6 +- src-tauri/tauri.macos-aarch64.conf.json | 6 +- src-tauri/tauri.macos-x86_64.conf.json | 6 +- src-tauri/tauri.windows.conf.json | 6 +- src/App.tsx | 83 ++++++- .../pages/settings/applicationSettings.tsx | 178 ++++++++++++++- src/helpers/use-downloader.ts | 25 +++ src/helpers/use-linux-registerer.ts | 52 +++++ src/helpers/use-macos-registerer.ts | 21 +- src/helpers/use-pot-server.ts | 86 +++++++ src/pages/settings.tsx | 10 +- src/services/store.ts | 17 +- src/types/kvStore.ts | 3 +- src/types/settings.ts | 3 + src/types/store.ts | 8 + 24 files changed, 1088 insertions(+), 27 deletions(-) create mode 100644 src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil.py create mode 100644 src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_cli.py create mode 100644 src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_http.py create mode 100644 src/helpers/use-linux-registerer.ts create mode 100644 src/helpers/use-pot-server.ts diff --git a/README.md b/README.md index ae87c51..067d018 100644 --- a/README.md +++ b/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 [![github release](https://img.shields.io/github/v/release/neosubhamoy/neodlp?color=lime-green&style=for-the-badge)](https://github.com/neosubhamoy/neodlp/releases/latest) [![github downloads](https://img.shields.io/github/downloads/neosubhamoy/neodlp/total?style=for-the-badge)](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 diff --git a/package.json b/package.json index 6446f1d..171cb10 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "neodlp", "private": true, "version": "0.4.0", - "description": "Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration", + "description": "Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration", "type": "module", "scripts": { "dev": "vite", diff --git a/scripts/download-bins.js b/scripts/download-bins.js index 25206ce..70ff1a5 100644 --- a/scripts/download-bins.js +++ b/scripts/download-bins.js @@ -20,6 +20,7 @@ const versions = { 'ffmpeg-ffprobe': 'latest', 'deno': 'latest', 'aria2c': '1.37.0', + 'neodlp-pot': 'latest' }; const binaries = { @@ -353,6 +354,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') + ] + } ] } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7a1dbe5..9fbd326 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" +description = "Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration" authors = ["neosubhamoy "] edition = "2021" license = "MIT" diff --git a/src-tauri/capabilities/shell.json b/src-tauri/capabilities/shell.json index 6623c75..700908f 100644 --- a/src-tauri/capabilities/shell.json +++ b/src-tauri/capabilities/shell.json @@ -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 } ] } diff --git a/src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil.py b/src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil.py new file mode 100644 index 0000000..644ee72 --- /dev/null +++ b/src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil.py @@ -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(?P['"]) + (?: + \\.| + (?!(?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__'] diff --git a/src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_cli.py b/src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_cli.py new file mode 100644 index 0000000..3988ab0 --- /dev/null +++ b/src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_cli.py @@ -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__] diff --git a/src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_http.py b/src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_http.py new file mode 100644 index 0000000..dca75c4 --- /dev/null +++ b/src-tauri/resources/plugins/yt-dlp-plugins/bgutil-ytdlp-pot-provider/yt_dlp_plugins/extractor/getpot_bgutil_http.py @@ -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__] diff --git a/src-tauri/tauri.linux-aarch64.conf.json b/src-tauri/tauri.linux-aarch64.conf.json index faa2a47..9a36657 100644 --- a/src-tauri/tauri.linux-aarch64.conf.json +++ b/src-tauri/tauri.linux-aarch64.conf.json @@ -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"], diff --git a/src-tauri/tauri.linux-x86_64.conf.json b/src-tauri/tauri.linux-x86_64.conf.json index 27294d5..68782ee 100644 --- a/src-tauri/tauri.linux-x86_64.conf.json +++ b/src-tauri/tauri.linux-x86_64.conf.json @@ -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"], diff --git a/src-tauri/tauri.macos-aarch64.conf.json b/src-tauri/tauri.macos-aarch64.conf.json index b3f7964..fa94e4e 100644 --- a/src-tauri/tauri.macos-aarch64.conf.json +++ b/src-tauri/tauri.macos-aarch64.conf.json @@ -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" diff --git a/src-tauri/tauri.macos-x86_64.conf.json b/src-tauri/tauri.macos-x86_64.conf.json index 63de13d..f87e7c3 100644 --- a/src-tauri/tauri.macos-x86_64.conf.json +++ b/src-tauri/tauri.macos-x86_64.conf.json @@ -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" diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index a9484fb..e18bc50 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -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": { diff --git a/src/App.tsx b/src/App.tsx index 821af72..82964b4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,7 @@ 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"; export default function App({ children }: { children: React.ReactNode }) { const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates(); @@ -39,6 +40,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 +52,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); @@ -64,9 +67,11 @@ export default function App({ children }: { children: React.ReactNode }) { const { updateYtDlp } = useYtDlpUpdater(); const { registerToMac } = useMacOsRegisterer(); 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 +81,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>(new Set()); const { fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload, processQueuedDownloads } = useDownloader(); @@ -98,6 +105,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('websocket-message', (event) => { @@ -272,6 +292,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 +353,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'); + registerToMac().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 +422,7 @@ export default function App({ children }: { children: React.ReactNode }) { }); }); - const timeoutIds: NodeJS.Timeout[] = []; + const timeoutIds: ReturnType[] = []; unexpectedErrors.forEach((downloadId) => { pendingErrorUpdatesRef.current.add(downloadId); diff --git a/src/components/pages/settings/applicationSettings.tsx b/src/components/pages/settings/applicationSettings.tsx index ac9394e..011c669 100644 --- a/src/components/pages/settings/applicationSettings.tsx +++ b/src/components/pages/settings/applicationSettings.tsx @@ -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, Timer, 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"; @@ -35,6 +35,7 @@ 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({ @@ -106,6 +107,19 @@ const requestSleepIntervalSchema = z.object({ }), }) +const potServerPortSchema = z.object({ + port: z.coerce.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(); @@ -1234,6 +1248,155 @@ function AppDelaySettings() { ); } +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>({ + resolver: zodResolver(potServerPortSchema), + defaultValues: { + port: potServerPort, + }, + mode: "onChange", + }); + const watchedPotServerPort = potServerPortForm.watch("port"); + const { errors: potServerPortFormErrors } = potServerPortForm.formState; + + async function handlePotServerPortSubmit(values: z.infer) { + 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 ( + <> +
+

PO Token

+

Generate proof-of-origin token for youtube to make seem your traffic more legitimate (bypasses some bot-protection checks, sometimes requires cookies)

+
+ { + saveSettingsKey('use_potoken', checked); + if (checked) { + await startPotServer(); + } else { + await stopPotServer(); + } + }} + disabled={useCustomCommands || isStartingPotServer || isChangingPotServerPort} + /> + +
+ +
+
+

Disable Innertube

+

Disable the usage of innertube api for potoken generation (falls back to legacy mode, use only if normal potoken is not working)

+
+ saveSettingsKey('disable_innertube', checked)} + disabled={useCustomCommands || !usePotoken} + /> +
+
+
+

POT Server Port

+

Change neodlp proof-of-origin token server port

+
+
+ + ( + + + + + + + + )} + /> + + + +
+
+ + ); +} + function AppNotificationSettings() { const { saveSettingsKey } = useSettings(); @@ -1296,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); @@ -1379,9 +1544,14 @@ function AppCommandSettings() { { + onCheckedChange={async(checked) => { saveSettingsKey('use_custom_commands', checked) resetDownloadConfiguration(); + if (checked && usePotoken) { + await stopPotServer(); + } else if (!checked && usePotoken) { + await startPotServer(); + } }} /> @@ -1524,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' }, @@ -1725,6 +1896,7 @@ export function ApplicationSettings() { { key: 'cookies', label: 'Cookies', icon: Cookie, component: }, { key: 'sponsorblock', label: 'Sponsorblock', icon: ShieldMinus, component: }, { key: 'delay', label: 'Delay', icon: Timer, component: }, + { key: 'potoken', label: 'Potoken', icon: KeyRound, component: }, { key: 'notifications', label: 'Notifications', icon: BellRing, component: }, { key: 'commands', label: 'Commands', icon: SquareTerminal, component: }, { key: 'debug', label: 'Debug', icon: Bug, component: }, @@ -1810,7 +1982,7 @@ export function ApplicationSettings() {
{tabsList.map((tab) => ( - + {tab.component} ))} diff --git a/src/helpers/use-downloader.ts b/src/helpers/use-downloader.ts index 88480d0..96fcd7f 100644 --- a/src/helpers/use-downloader.ts +++ b/src/helpers/use-downloader.ts @@ -69,7 +69,11 @@ export default function useDownloader() { 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); @@ -181,6 +185,16 @@ export default function useDownloader() { 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); @@ -525,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 { diff --git a/src/helpers/use-linux-registerer.ts b/src/helpers/use-linux-registerer.ts new file mode 100644 index 0000000..a2363c3 --- /dev/null +++ b/src/helpers/use-linux-registerer.ts @@ -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 }; +} diff --git a/src/helpers/use-macos-registerer.ts b/src/helpers/use-macos-registerer.ts index 30099c6..a79a241 100644 --- a/src/helpers/use-macos-registerer.ts +++ b/src/helpers/use-macos-registerer.ts @@ -3,27 +3,36 @@ 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(); 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); @@ -44,4 +53,4 @@ export function useMacOsRegisterer() { } return { registerToMac }; -} \ No newline at end of file +} diff --git a/src/helpers/use-pot-server.ts b/src/helpers/use-pot-server.ts new file mode 100644 index 0000000..0c680a4 --- /dev/null +++ b/src/helpers/use-pot-server.ts @@ -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}; +} diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index ec93c58..a3b483a 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -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() { Cancel { - resetSettings() + async () => { + resetSettings(); + if (isRunningPotServer) { + await stopPotServer(); + } } }>Reset diff --git a/src/services/store.ts b/src/services/store.ts index 1b0ca14..cb33561 100644 --- a/src/services/store.ts +++ b/src/services/store.ts @@ -218,6 +218,9 @@ export const useSettingsPageStatesStore = create((set) 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 }, @@ -230,6 +233,10 @@ export const useSettingsPageStatesStore = create((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 })), @@ -296,6 +303,9 @@ export const useSettingsPageStatesStore = create((set) 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 }, @@ -315,12 +325,17 @@ export const useSettingsPageStatesStore = create((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((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: { diff --git a/src/types/kvStore.ts b/src/types/kvStore.ts index 7ce07c0..36e501c 100644 --- a/src/types/kvStore.ts +++ b/src/types/kvStore.ts @@ -6,4 +6,5 @@ export interface KvStoreTable { export interface KvStore { ytdlp_update_last_check: number | null; macos_registered_version: string | null; -} \ No newline at end of file + linux_registered_version: string | null; +} diff --git a/src/types/settings.ts b/src/types/settings.ts index 7dd2889..ce4d009 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -61,6 +61,9 @@ export interface Settings { 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; } diff --git a/src/types/store.ts b/src/types/store.ts index d160af3..5643eae 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -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 {