1
1
mirror of https://github.com/neosubhamoy/neodlp.git synced 2026-03-22 11:25:49 +05:30

feat: added support for youtube po token generation

This commit is contained in:
2026-02-18 14:19:11 +05:30
Verified
parent 1292758b1e
commit dfa5cace82
24 changed files with 1088 additions and 27 deletions

View File

@@ -2,7 +2,7 @@
# NeoDLP - Neo Downloader Plus # NeoDLP - Neo Downloader Plus
Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration
[![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 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) [![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 ## ✨ 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.) - Download Video/Audio in your preffered format (MP4, WEBM, MKV, MP3 etc.)
- Supports both Video and Playlist download - Supports both Video and Playlist/Batch download
- Supports Combining Video, Audio streams of your choice - Supports Combining Video, Audio streams of your choice
- Supports Multi-Lingual Subtitle/Caption (CC) embeding - Supports Multi-Lingual Subtitle/Caption (CC) embeding
- Different Video/Audio metadata embeding options (info, chapters, thumbnail etc.) - Different Video/Audio metadata embeding options (info, chapters, thumbnail etc.)
- SponsorBlock support (mark/remove video segments) - SponsorBlock support (mark/remove video segments)
- Aria2 support (for blazing fast downloads)
- Network controls (proxy, rate limit etc.) - Network controls (proxy, rate limit etc.)
- Highly customizable and many more...😉 - Highly customizable and many more...😉
@@ -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 - [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) - [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)) - [Deno](https://deno.com) [MIT] - Provides sandboxed javascript runtime environment for yt-dlp (Required for YT downloads, as per the new yt-dlp [announcement](https://github.com/yt-dlp/yt-dlp/issues/14404))
- [BgUtils POT Provider (Rust)](https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs) [GPLv3+] - Provides PO (Proof-of-Origin) Token for YT downloads
## System Pre-Requirements ## System Pre-Requirements
@@ -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 is made possible by the joint efforts of [yt-dlp](https://github.com/yt-dlp/yt-dlp) and [FFmpeg](https://www.ffmpeg.org). Lots of NeoDLP features are actually powered by these tools under the hood! So huge thanks to all the developers/contributers for making these great tools! 🙏
- NeoDLP's 'Format Selection' options are inspired from the [Seal](https://github.com/JunkFood02/Seal) app by [@JunkFood02](https://github.com/JunkFood02) - NeoDLP's 'Format Selection' options are inspired from the [Seal](https://github.com/JunkFood02/Seal) app by [@JunkFood02](https://github.com/JunkFood02)
- Aria2 Linux x86_64 static binaries are built by [@asdo92](https://github.com/asdo92/aria2-static-builds) - Aria2 Linux x86_64 static binaries are built by [@asdo92](https://github.com/asdo92/aria2-static-builds)
- NeoDLP's 'POT Server' is based on [@jim60105's Rust Implementation](https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs) of [Brainicism/bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider)
## ⚖️ License and Usage ## ⚖️ License and Usage

View File

@@ -2,7 +2,7 @@
"name": "neodlp", "name": "neodlp",
"private": true, "private": true,
"version": "0.4.0", "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", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -20,6 +20,7 @@ const versions = {
'ffmpeg-ffprobe': 'latest', 'ffmpeg-ffprobe': 'latest',
'deno': 'latest', 'deno': 'latest',
'aria2c': '1.37.0', 'aria2c': '1.37.0',
'neodlp-pot': 'latest'
}; };
const binaries = { const binaries = {
@@ -353,6 +354,73 @@ const binaries = {
path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1`) 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')
]
}
] ]
} }

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "neodlp" name = "neodlp"
version = "0.4.0" 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 <hey@neosubhamoy.com>"] authors = ["neosubhamoy <hey@neosubhamoy.com>"]
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"

View File

@@ -35,6 +35,11 @@
"args": true, "args": true,
"sidecar": true "sidecar": true
}, },
{
"name": "binaries/neodlp-pot",
"args": true,
"sidecar": true
},
{ {
"name": "ffmpeg", "name": "ffmpeg",
"cmd": "ffmpeg", "cmd": "ffmpeg",
@@ -45,6 +50,11 @@
"cmd": "aria2c", "cmd": "aria2c",
"args": true "args": true
}, },
{
"name": "deno",
"cmd": "deno",
"args": true
},
{ {
"name": "pkexec", "name": "pkexec",
"cmd": "pkexec", "cmd": "pkexec",
@@ -85,6 +95,11 @@
"args": true, "args": true,
"sidecar": true "sidecar": true
}, },
{
"name": "binaries/neodlp-pot",
"args": true,
"sidecar": true
},
{ {
"name": "ffmpeg", "name": "ffmpeg",
"cmd": "ffmpeg", "cmd": "ffmpeg",
@@ -94,6 +109,11 @@
"name": "aria2c", "name": "aria2c",
"cmd": "aria2c", "cmd": "aria2c",
"args": true "args": true
},
{
"name": "deno",
"cmd": "deno",
"args": true
} }
] ]
} }

View File

@@ -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__']

View File

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

View File

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

View File

@@ -39,8 +39,12 @@
"externalBin": [ "externalBin": [
"binaries/yt-dlp", "binaries/yt-dlp",
"binaries/aria2c", "binaries/aria2c",
"binaries/deno" "binaries/deno",
"binaries/neodlp-pot"
], ],
"resources": {
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
},
"linux": { "linux": {
"deb": { "deb": {
"depends": ["ffmpeg"], "depends": ["ffmpeg"],

View File

@@ -39,8 +39,12 @@
"externalBin": [ "externalBin": [
"binaries/yt-dlp", "binaries/yt-dlp",
"binaries/aria2c", "binaries/aria2c",
"binaries/deno" "binaries/deno",
"binaries/neodlp-pot"
], ],
"resources": {
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
},
"linux": { "linux": {
"deb": { "deb": {
"depends": ["ffmpeg"], "depends": ["ffmpeg"],

View File

@@ -39,13 +39,15 @@
"binaries/yt-dlp", "binaries/yt-dlp",
"binaries/ffmpeg", "binaries/ffmpeg",
"binaries/ffprobe", "binaries/ffprobe",
"binaries/deno" "binaries/deno",
"binaries/neodlp-pot"
], ],
"resources": { "resources": {
"target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost", "target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json", "resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json", "resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist" "resources/autostart/macos/autostart.plist": "neodlp-autostart.plist",
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
}, },
"macOS": { "macOS": {
"providerShortName": "neosubhamoy" "providerShortName": "neosubhamoy"

View File

@@ -39,13 +39,15 @@
"binaries/yt-dlp", "binaries/yt-dlp",
"binaries/ffmpeg", "binaries/ffmpeg",
"binaries/ffprobe", "binaries/ffprobe",
"binaries/deno" "binaries/deno",
"binaries/neodlp-pot"
], ],
"resources": { "resources": {
"target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost", "target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json", "resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json", "resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist" "resources/autostart/macos/autostart.plist": "neodlp-autostart.plist",
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
}, },
"macOS": { "macOS": {
"providerShortName": "neosubhamoy" "providerShortName": "neosubhamoy"

View File

@@ -41,12 +41,14 @@
"binaries/ffmpeg", "binaries/ffmpeg",
"binaries/ffprobe", "binaries/ffprobe",
"binaries/aria2c", "binaries/aria2c",
"binaries/deno" "binaries/deno",
"binaries/neodlp-pot"
], ],
"resources": { "resources": {
"target/release/neodlp-msghost.exe": "neodlp-msghost.exe", "target/release/neodlp-msghost.exe": "neodlp-msghost.exe",
"resources/msghost-manifest/windows/chrome.json": "chrome.json", "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": { "windows": {
"wix": { "wix": {

View File

@@ -25,6 +25,7 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
import { toast } from "sonner"; import { toast } from "sonner";
import { useLogger } from "@/helpers/use-logger"; import { useLogger } from "@/helpers/use-logger";
import useDownloader from "@/helpers/use-downloader"; import useDownloader from "@/helpers/use-downloader";
import usePotServer from "@/helpers/use-pot-server";
export default function App({ children }: { children: React.ReactNode }) { export default function App({ children }: { children: React.ReactNode }) {
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates(); const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
@@ -39,6 +40,7 @@ export default function App({ children }: { children: React.ReactNode }) {
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings); const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey); const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
const appVersion = useSettingsPageStatesStore(state => state.appVersion); const appVersion = useSettingsPageStatesStore(state => state.appVersion);
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
const ytDlpVersion = useSettingsPageStatesStore(state => state.ytDlpVersion); const ytDlpVersion = useSettingsPageStatesStore(state => state.ytDlpVersion);
const setYtDlpVersion = useSettingsPageStatesStore((state) => state.setYtDlpVersion); const setYtDlpVersion = useSettingsPageStatesStore((state) => state.setYtDlpVersion);
const setIsFetchingYtDlpVersion = useSettingsPageStatesStore((state) => state.setIsFetchingYtDlpVersion); const setIsFetchingYtDlpVersion = useSettingsPageStatesStore((state) => state.setIsFetchingYtDlpVersion);
@@ -50,6 +52,7 @@ export default function App({ children }: { children: React.ReactNode }) {
download_dir: DOWNLOAD_DIR, download_dir: DOWNLOAD_DIR,
theme: APP_THEME, theme: APP_THEME,
color_scheme: APP_COLOR_SCHEME, color_scheme: APP_COLOR_SCHEME,
use_potoken: USE_POTOKEN,
} = useSettingsPageStatesStore(state => state.settings); } = useSettingsPageStatesStore(state => state.settings);
const erroredDownloadIds = useDownloaderPageStatesStore((state) => state.erroredDownloadIds); const erroredDownloadIds = useDownloaderPageStatesStore((state) => state.erroredDownloadIds);
@@ -64,9 +67,11 @@ export default function App({ children }: { children: React.ReactNode }) {
const { updateYtDlp } = useYtDlpUpdater(); const { updateYtDlp } = useYtDlpUpdater();
const { registerToMac } = useMacOsRegisterer(); const { registerToMac } = useMacOsRegisterer();
const { checkForAppUpdate } = useAppUpdater(); const { checkForAppUpdate } = useAppUpdater();
const { startPotServer, stopPotServer } = usePotServer();
const setKvPairsKey = useKvPairsStatesStore((state) => state.setKvPairsKey); const setKvPairsKey = useKvPairsStatesStore((state) => state.setKvPairsKey);
const ytDlpUpdateLastCheck = useKvPairsStatesStore(state => state.kvPairs.ytdlp_update_last_check); const ytDlpUpdateLastCheck = useKvPairsStatesStore(state => state.kvPairs.ytdlp_update_last_check);
const macOsRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.macos_registered_version); const macOsRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.macos_registered_version);
const linuxRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.linux_registered_version);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const downloadStatusUpdater = useUpdateDownloadStatus(); const downloadStatusUpdater = useUpdateDownloadStatus();
@@ -76,7 +81,9 @@ export default function App({ children }: { children: React.ReactNode }) {
const hasRunYtDlpAutoUpdateRef = useRef(false); const hasRunYtDlpAutoUpdateRef = useRef(false);
const hasRunAppUpdateCheckRef = useRef(false); const hasRunAppUpdateCheckRef = useRef(false);
const hasRunPotServerStatusCheckRef = useRef(false);
const isRegisteredToMacOsRef = useRef(false); const isRegisteredToMacOsRef = useRef(false);
const isRegisteredToLinuxRef = useRef(false);
const pendingErrorUpdatesRef = useRef<Set<string>>(new Set()); const pendingErrorUpdatesRef = useRef<Set<string>>(new Set());
const { fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload, processQueuedDownloads } = useDownloader(); const { fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload, processQueuedDownloads } = useDownloader();
@@ -98,6 +105,19 @@ export default function App({ children }: { children: React.ReactNode }) {
appWindow.onCloseRequested(handleCloseRequested); 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 // Listen for websocket messages
useEffect(() => { useEffect(() => {
const unlisten = listen<WebSocketMessage>('websocket-message', (event) => { const unlisten = listen<WebSocketMessage>('websocket-message', (event) => {
@@ -272,6 +292,32 @@ export default function App({ children }: { children: React.ReactNode }) {
} }
}, [isSettingsStatePropagated, isKvPairsStatePropagated]); }, [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 // Check for MacOS auto-registration
useEffect(() => { useEffect(() => {
// Only run once when both settings and KV pairs are loaded // 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]); }, [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(() => { useEffect(() => {
if (isSuccessFetchingDownloadStates && downloadStates) { if (isSuccessFetchingDownloadStates && downloadStates) {
// console.log("Download States fetched successfully:", 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<typeof setTimeout>[] = [];
unexpectedErrors.forEach((downloadId) => { unexpectedErrors.forEach((downloadId) => {
pendingErrorUpdatesRef.current.add(downloadId); pendingErrorUpdatesRef.current.add(downloadId);

View File

@@ -7,7 +7,7 @@ import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { toast } from "sonner"; 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 { cn } from "@/lib/utils";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import { Input } from "@/components/ui/input"; 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 neosubhamoyImage from "@/assets/images/neosubhamoy.jpg";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { NumberInput } from "@/components/custom/numberInput"; import { NumberInput } from "@/components/custom/numberInput";
import usePotServer from "@/helpers/use-pot-server";
const proxyUrlSchema = z.object({ const proxyUrlSchema = z.object({
url: z.url({ url: z.url({
@@ -106,6 +107,19 @@ const requestSleepIntervalSchema = z.object({
}), }),
}) })
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() { function AppGeneralSettings() {
const { saveSettingsKey } = useSettings(); 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<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() { function AppNotificationSettings() {
const { saveSettingsKey } = useSettings(); const { saveSettingsKey } = useSettings();
@@ -1296,12 +1459,14 @@ function AppNotificationSettings() {
function AppCommandSettings() { function AppCommandSettings() {
const { saveSettingsKey } = useSettings(); const { saveSettingsKey } = useSettings();
const { startPotServer, stopPotServer } = usePotServer();
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger); const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset); const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
const customCommands = useSettingsPageStatesStore(state => state.settings.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 setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration); const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
@@ -1379,9 +1544,14 @@ function AppCommandSettings() {
<Switch <Switch
id="use-custom-commands" id="use-custom-commands"
checked={useCustomCommands} checked={useCustomCommands}
onCheckedChange={(checked) => { onCheckedChange={async(checked) => {
saveSettingsKey('use_custom_commands', checked) saveSettingsKey('use_custom_commands', checked)
resetDownloadConfiguration(); resetDownloadConfiguration();
if (checked && usePotoken) {
await stopPotServer();
} else if (!checked && usePotoken) {
await startPotServer();
}
}} }}
/> />
<Label htmlFor="use-custom-commands">Use Custom Commands</Label> <Label htmlFor="use-custom-commands">Use Custom Commands</Label>
@@ -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: '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: '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: '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 = [ 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' }, { 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: <AppCookiesSettings /> }, { key: 'cookies', label: 'Cookies', icon: Cookie, component: <AppCookiesSettings /> },
{ key: 'sponsorblock', label: 'Sponsorblock', icon: ShieldMinus, component: <AppSponsorblockSettings /> }, { key: 'sponsorblock', label: 'Sponsorblock', icon: ShieldMinus, component: <AppSponsorblockSettings /> },
{ key: 'delay', label: 'Delay', icon: Timer, component: <AppDelaySettings /> }, { 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: 'notifications', label: 'Notifications', icon: BellRing, component: <AppNotificationSettings /> },
{ key: 'commands', label: 'Commands', icon: SquareTerminal, component: <AppCommandSettings /> }, { key: 'commands', label: 'Commands', icon: SquareTerminal, component: <AppCommandSettings /> },
{ key: 'debug', label: 'Debug', icon: Bug, component: <AppDebugSettings /> }, { key: 'debug', label: 'Debug', icon: Bug, component: <AppDebugSettings /> },
@@ -1810,7 +1982,7 @@ export function ApplicationSettings() {
</TabsList> </TabsList>
<div className="min-h-full flex flex-col w-full border-l border-border pl-4"> <div className="min-h-full flex flex-col w-full border-l border-border pl-4">
{tabsList.map((tab) => ( {tabsList.map((tab) => (
<TabsContent key={tab.key} value={tab.key} className={clsx("flex flex-col gap-4 min-h-120", 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} {tab.component}
</TabsContent> </TabsContent>
))} ))}

View File

@@ -69,7 +69,11 @@ export default function useDownloader() {
max_sleep_interval: MAX_SLEEP_INTERVAL, max_sleep_interval: MAX_SLEEP_INTERVAL,
request_sleep_interval: REQUEST_SLEEP_INTERVAL, request_sleep_interval: REQUEST_SLEEP_INTERVAL,
delay_playlist_only: DELAY_PLAYLIST_ONLY, delay_playlist_only: DELAY_PLAYLIST_ONLY,
use_potoken: USE_POTOKEN,
disable_innertube: DISABLE_INNERTUBE,
pot_server_port: POT_SERVER_PORT,
} = useSettingsPageStatesStore(state => state.settings); } = useSettingsPageStatesStore(state => state.settings);
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds); const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds);
const addErroredDownload = useDownloaderPageStatesStore((state) => state.addErroredDownload); 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()); 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); 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).`); 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)) { if (resumeState || (!USE_CUSTOM_COMMANDS && USE_ARIA2)) {
args.push('--continue'); args.push('--continue');
} else { } else {

View 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 };
}

View File

@@ -3,17 +3,26 @@ import * as fs from "@tauri-apps/plugin-fs";
import { useKvPairs } from "@/helpers/use-kvpairs"; import { useKvPairs } from "@/helpers/use-kvpairs";
import { useSettingsPageStatesStore } from "@/services/store"; import { useSettingsPageStatesStore } from "@/services/store";
interface FileMap {
source: string;
destination: string;
dir: string;
}
export function useMacOsRegisterer() { export function useMacOsRegisterer() {
const { saveKvPair } = useKvPairs(); const { saveKvPair } = useKvPairs();
const appVersion = useSettingsPageStatesStore(state => state.appVersion); const appVersion = useSettingsPageStatesStore(state => state.appVersion);
const registerToMac = async () => { const registerToMac = async () => {
try { try {
const filesToCopy = [ const filesToCopy: FileMap[] = [
{ source: 'neodlp-autostart.plist', destination: 'Library/LaunchAgents/com.neosubhamoy.neodlp.plist', dir: 'Library/LaunchAgents/' }, { 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/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.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: '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 resourceDirPath = await resourceDir();

View 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};
}

View File

@@ -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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { ExtensionSettings } from "@/components/pages/settings/extensionSettings"; import { ExtensionSettings } from "@/components/pages/settings/extensionSettings";
import { ApplicationSettings } from "@/components/pages/settings/applicationSettings"; import { ApplicationSettings } from "@/components/pages/settings/applicationSettings";
import usePotServer from "@/helpers/use-pot-server";
export default function SettingsPage() { export default function SettingsPage() {
const { setTheme } = useTheme(); const { setTheme } = useTheme();
@@ -17,10 +18,12 @@ export default function SettingsPage() {
const setActiveTab = useSettingsPageStatesStore(state => state.setActiveTab); const setActiveTab = useSettingsPageStatesStore(state => state.setActiveTab);
const isUsingDefaultSettings = useSettingsPageStatesStore(state => state.isUsingDefaultSettings); const isUsingDefaultSettings = useSettingsPageStatesStore(state => state.isUsingDefaultSettings);
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
const appTheme = useSettingsPageStatesStore(state => state.settings.theme); const appTheme = useSettingsPageStatesStore(state => state.settings.theme);
const appColorScheme = useSettingsPageStatesStore(state => state.settings.color_scheme); const appColorScheme = useSettingsPageStatesStore(state => state.settings.color_scheme);
const { resetSettings } = useSettings(); const { resetSettings } = useSettings();
const { stopPotServer } = usePotServer();
useEffect(() => { useEffect(() => {
const updateTheme = async () => { const updateTheme = async () => {
@@ -60,8 +63,11 @@ export default function SettingsPage() {
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={ <AlertDialogAction onClick={
() => { async () => {
resetSettings() resetSettings();
if (isRunningPotServer) {
await stopPotServer();
}
} }
}>Reset</AlertDialogAction> }>Reset</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>

View File

@@ -218,6 +218,9 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
max_sleep_interval: 20, max_sleep_interval: 20,
request_sleep_interval: 1, request_sleep_interval: 1,
delay_playlist_only: true, delay_playlist_only: true,
use_potoken: false,
disable_innertube: false,
pot_server_port: 4416,
// extension settings // extension settings
websocket_port: 53511 websocket_port: 53511
}, },
@@ -230,6 +233,10 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
appUpdateDownloadProgress: 0, appUpdateDownloadProgress: 0,
formResetTrigger: 0, formResetTrigger: 0,
resetAcknowledgements: 0, resetAcknowledgements: 0,
isRunningPotServer: false,
isStartingPotServer: false,
isChangingPotServerPort: false,
potServerPid: null,
setActiveTab: (tab) => set(() => ({ activeTab: tab })), setActiveTab: (tab) => set(() => ({ activeTab: tab })),
setActiveSubAppTab: (tab) => set(() => ({ activeSubAppTab: tab })), setActiveSubAppTab: (tab) => set(() => ({ activeSubAppTab: tab })),
setActiveSubExtTab: (tab) => set(() => ({ activeSubExtTab: tab })), setActiveSubExtTab: (tab) => set(() => ({ activeSubExtTab: tab })),
@@ -296,6 +303,9 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
max_sleep_interval: 20, max_sleep_interval: 20,
request_sleep_interval: 1, request_sleep_interval: 1,
delay_playlist_only: true, delay_playlist_only: true,
use_potoken: false,
disable_innertube: false,
pot_server_port: 4416,
// extension settings // extension settings
websocket_port: 53511 websocket_port: 53511
}, },
@@ -315,12 +325,17 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
acknowledgeFormReset: () => set((state) => ({ acknowledgeFormReset: () => set((state) => ({
resetAcknowledgements: state.resetAcknowledgements + 1 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) => ({ export const useKvPairsStatesStore = create<KvPairsStatesStore>((set) => ({
kvPairs: { kvPairs: {
ytdlp_update_last_check: null, ytdlp_update_last_check: null,
macos_registered_version: null macos_registered_version: null,
linux_registered_version: null
}, },
setKvPairsKey: (key, value) => set((state) => ({ setKvPairsKey: (key, value) => set((state) => ({
kvPairs: { kvPairs: {

View File

@@ -6,4 +6,5 @@ export interface KvStoreTable {
export interface KvStore { export interface KvStore {
ytdlp_update_last_check: number | null; ytdlp_update_last_check: number | null;
macos_registered_version: string | null; macos_registered_version: string | null;
linux_registered_version: string | null;
} }

View File

@@ -61,6 +61,9 @@ export interface Settings {
max_sleep_interval: number; max_sleep_interval: number;
request_sleep_interval: number; request_sleep_interval: number;
delay_playlist_only: boolean; delay_playlist_only: boolean;
use_potoken: boolean;
disable_innertube: boolean;
pot_server_port: number;
// extension settings // extension settings
websocket_port: number; websocket_port: number;
} }

View File

@@ -110,6 +110,10 @@ export interface SettingsPageStatesStore {
appUpdateDownloadProgress: number; appUpdateDownloadProgress: number;
formResetTrigger: number; formResetTrigger: number;
resetAcknowledgements: number; resetAcknowledgements: number;
isRunningPotServer: boolean;
isStartingPotServer: boolean;
isChangingPotServerPort: boolean;
potServerPid: number | null;
setActiveTab: (tab: string) => void; setActiveTab: (tab: string) => void;
setActiveSubAppTab: (tab: string) => void; setActiveSubAppTab: (tab: string) => void;
setActiveSubExtTab: (tab: string) => void; setActiveSubExtTab: (tab: string) => void;
@@ -130,6 +134,10 @@ export interface SettingsPageStatesStore {
setAppUpdateDownloadProgress: (progress: number) => void; setAppUpdateDownloadProgress: (progress: number) => void;
triggerFormReset: () => void; triggerFormReset: () => void;
acknowledgeFormReset: () => void; acknowledgeFormReset: () => void;
setIsRunningPotServer: (isRunning: boolean) => void;
setIsStartingPotServer: (isStarting: boolean) => void;
setIsChangingPotServerPort: (isChanging: boolean) => void;
setPotServerPid: (pid: number | null) => void;
} }
export interface KvPairsStatesStore { export interface KvPairsStatesStore {