webtools-cli 1.3.0__tar.gz → 1.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/PKG-INFO +1 -1
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/pyproject.toml +1 -1
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools/core.py +183 -226
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools/web/index.html +122 -55
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools/web/script.js +3 -4
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools_cli.egg-info/PKG-INFO +1 -1
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/LICENSE +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/README.md +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/setup.cfg +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools/__init__.py +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools/__main__.py +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools/cli.py +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools/install.py +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools/mega_client.py +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools/web/style.css +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools_cli.egg-info/SOURCES.txt +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools_cli.egg-info/dependency_links.txt +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools_cli.egg-info/entry_points.txt +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools_cli.egg-info/requires.txt +0 -0
- {webtools_cli-1.3.0 → webtools_cli-1.3.2}/webtools_cli.egg-info/top_level.txt +0 -0
|
@@ -7,64 +7,7 @@ if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
|
|
|
7
7
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
8
8
|
except Exception: pass
|
|
9
9
|
|
|
10
|
-
# ---
|
|
11
|
-
def ensure_dependencies():
|
|
12
|
-
"""Performs an aggressive forced repair of the environment if issues are detected"""
|
|
13
|
-
# Dependencies to verify (module_name -> (pip_pkg, min_v, max_v))
|
|
14
|
-
mandatory = {
|
|
15
|
-
"mega": ("mega.py", None, None),
|
|
16
|
-
"Crypto": ("pycryptodome", None, None),
|
|
17
|
-
"tenacity": ("tenacity>=8.2.3,<9.0.0", "8", "9")
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
needs_repair = False
|
|
21
|
-
|
|
22
|
-
# 1. Check for corrupted Crypto namespace (Mixed pycrypto/pycryptodome)
|
|
23
|
-
try:
|
|
24
|
-
from Crypto.PublicKey import RSA
|
|
25
|
-
except (SyntaxError, AttributeError, ImportError):
|
|
26
|
-
needs_repair = True
|
|
27
|
-
|
|
28
|
-
# 2. Check for missing or outdated modules
|
|
29
|
-
if not needs_repair:
|
|
30
|
-
for module, (pkg, min_v, max_v) in mandatory.items():
|
|
31
|
-
try:
|
|
32
|
-
mod = __import__(module)
|
|
33
|
-
if min_v or max_v:
|
|
34
|
-
from importlib.metadata import version as get_v
|
|
35
|
-
v_str = get_v(module)
|
|
36
|
-
major = int(v_str.split('.')[0])
|
|
37
|
-
if (min_v and major < int(min_v)) or (max_v and major >= int(max_v)):
|
|
38
|
-
needs_repair = True; break
|
|
39
|
-
except:
|
|
40
|
-
needs_repair = True; break
|
|
41
|
-
|
|
42
|
-
if needs_repair:
|
|
43
|
-
print("\n[!] Environment instability detected (Dependency Conflict).")
|
|
44
|
-
print("[*] Performing Aggressive Environment Repair (Python 3.11+ Stability Patch)...")
|
|
45
|
-
try:
|
|
46
|
-
# A: Deep clean the Crypto and Tenacity namespaces
|
|
47
|
-
# We uninstall everything that could possibly clash
|
|
48
|
-
bad_pkgs = ["pycrypto", "pycryptodome", "pycryptodomex", "tenacity", "mega.py"]
|
|
49
|
-
subprocess.call([sys.executable, "-m", "pip", "uninstall", "-y"] + bad_pkgs,
|
|
50
|
-
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
51
|
-
|
|
52
|
-
# B: Forced Reinstall of the MODERN stack
|
|
53
|
-
# We use --force-reinstall to ensure no broken files remain
|
|
54
|
-
print("[*] Rebuilding core libraries...")
|
|
55
|
-
subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "--force-reinstall",
|
|
56
|
-
"pycryptodome", "tenacity>=8.2.3,<9.0.0", "requests"])
|
|
57
|
-
|
|
58
|
-
# C: Install mega.py WITHOUT its own dependencies (to prevent it from breaking tenacity again)
|
|
59
|
-
subprocess.check_call([sys.executable, "-m", "pip", "install", "mega.py", "--no-deps"])
|
|
60
|
-
|
|
61
|
-
print("[+] Environment successfully repaired! Please restart the app.\n")
|
|
62
|
-
sys.exit(0) # Exit to allow the user to restart with the fresh environment
|
|
63
|
-
except Exception as e:
|
|
64
|
-
print(f"[-] Auto-repair failed: {e}")
|
|
65
|
-
print("[!] Please run: pip uninstall -y pycrypto pycryptodome tenacity mega.py && pip install pycryptodome tenacity>=8.2.3,<9.0.0 mega.py --no-deps")
|
|
66
|
-
|
|
67
|
-
ensure_dependencies()
|
|
10
|
+
# --- Dependencies are managed by pyproject.toml / pip install ---
|
|
68
11
|
|
|
69
12
|
|
|
70
13
|
# --- PACKAGE PATHS ---
|
|
@@ -109,176 +52,182 @@ from collections import Counter
|
|
|
109
52
|
from flask import Flask, render_template_string, send_from_directory, request, jsonify, send_file, after_this_request
|
|
110
53
|
from PIL import Image,ExifTags,ImageChops,ImageEnhance
|
|
111
54
|
from io import BytesIO
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
encrypt_key, str_to_a32, mpi_to_int, base64_url_encode
|
|
118
|
-
)
|
|
119
|
-
from Crypto.PublicKey import RSA
|
|
120
|
-
import binascii
|
|
121
|
-
except ImportError:
|
|
122
|
-
class RequestError(Exception): pass
|
|
55
|
+
# --- MEGA.NZ SUPPORT (Lazy: installed on first use via --no-deps) ---
|
|
56
|
+
MEGA_AVAILABLE = False
|
|
57
|
+
mega_engine = None
|
|
58
|
+
mega_sessions = {}
|
|
59
|
+
active_mega_email = None
|
|
123
60
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if isinstance(kwargs[k], str):
|
|
144
|
-
kwargs[k] = kwargs[k].encode('latin-1')
|
|
145
|
-
|
|
146
|
-
return _original_aes_new(key, *new_args, **kwargs)
|
|
61
|
+
def _ensure_mega():
|
|
62
|
+
"""Install mega.py (without its broken deps) on first use and initialize"""
|
|
63
|
+
global MEGA_AVAILABLE, mega_engine
|
|
64
|
+
if MEGA_AVAILABLE:
|
|
65
|
+
return True
|
|
66
|
+
try:
|
|
67
|
+
from mega import Mega
|
|
68
|
+
MEGA_AVAILABLE = True
|
|
69
|
+
except ImportError:
|
|
70
|
+
print("\n📦 Installing mega.py (cloud storage support)...")
|
|
71
|
+
try:
|
|
72
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "mega.py", "--no-deps"],
|
|
73
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
74
|
+
from mega import Mega
|
|
75
|
+
MEGA_AVAILABLE = True
|
|
76
|
+
print("✅ mega.py installed successfully!\n")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"❌ Failed to install mega.py: {e}")
|
|
79
|
+
return False
|
|
147
80
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
mega_module.AES.new = _patched_aes_new
|
|
152
|
-
except Exception:
|
|
153
|
-
pass
|
|
154
|
-
|
|
155
|
-
class RobustMega(Mega):
|
|
156
|
-
"""
|
|
157
|
-
Patched version of Mega library to handle fragile API responses.
|
|
158
|
-
The original library crashes if the API returns non-JSON or an empty string.
|
|
159
|
-
"""
|
|
160
|
-
def _login_process(self, resp, password):
|
|
161
|
-
"""
|
|
162
|
-
Overridden to fix 'Invalid RSA public exponent' error in newer pycryptodome.
|
|
163
|
-
The original code passes 0 as the exponent, which is rejected.
|
|
164
|
-
"""
|
|
165
|
-
encrypted_master_key = base64_to_a32(resp['k'])
|
|
166
|
-
self.master_key = decrypt_key(encrypted_master_key, password)
|
|
167
|
-
if 'tsid' in resp:
|
|
168
|
-
tsid = base64_url_decode(resp['tsid'])
|
|
169
|
-
key_encrypted = a32_to_str(
|
|
170
|
-
encrypt_key(str_to_a32(tsid[:16]), self.master_key)
|
|
171
|
-
)
|
|
172
|
-
if key_encrypted == tsid[-16:]:
|
|
173
|
-
self.sid = resp['tsid']
|
|
174
|
-
elif 'csid' in resp:
|
|
175
|
-
encrypted_rsa_private_key = base64_to_a32(resp['privk'])
|
|
176
|
-
rsa_private_key = decrypt_key(
|
|
177
|
-
encrypted_rsa_private_key, self.master_key
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
private_key_str = a32_to_str(rsa_private_key)
|
|
181
|
-
self.rsa_private_key = [0, 0, 0, 0]
|
|
182
|
-
|
|
183
|
-
# This loop parses the 4 components: p, q, d, u
|
|
184
|
-
for i in range(4):
|
|
185
|
-
l = int(((private_key_str[0]) * 256 + (private_key_str[1]) + 7) / 8) + 2
|
|
186
|
-
self.rsa_private_key[i] = mpi_to_int(private_key_str[:l])
|
|
187
|
-
private_key_str = private_key_str[l:]
|
|
188
|
-
|
|
189
|
-
encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
|
|
190
|
-
|
|
191
|
-
# The Fix: Calculate the real 'e' from p, q, d
|
|
192
|
-
p = self.rsa_private_key[0]
|
|
193
|
-
q = self.rsa_private_key[1]
|
|
194
|
-
d = self.rsa_private_key[2]
|
|
195
|
-
n = p * q
|
|
196
|
-
phi = (p - 1) * (q - 1)
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
# Calculate real e = d^-1 mod phi
|
|
200
|
-
e = pow(d, -1, phi)
|
|
201
|
-
rsa_decrypter = RSA.construct((n, e, d, p, q))
|
|
202
|
-
except:
|
|
203
|
-
# Fallback to 65537 if inverse fails (shouldn't happen)
|
|
204
|
-
e = 65537
|
|
205
|
-
rsa_decrypter = RSA.construct((n, e, d, p, q), consistency_check=False)
|
|
206
|
-
|
|
207
|
-
# Handle potential library differences in how the key is accessed
|
|
208
|
-
key_obj = rsa_decrypter.key if hasattr(rsa_decrypter, 'key') else rsa_decrypter
|
|
209
|
-
|
|
210
|
-
# Decrypt sid integer
|
|
211
|
-
sid_int = key_obj._decrypt(encrypted_sid)
|
|
212
|
-
|
|
213
|
-
# Convert to hex then to bytes, ensuring we handle the 0 padding if any
|
|
214
|
-
sid_hex = '%x' % sid_int
|
|
215
|
-
if len(sid_hex) % 2:
|
|
216
|
-
sid_hex = '0' + sid_hex
|
|
217
|
-
sid_bytes = binascii.unhexlify(sid_hex)
|
|
218
|
-
|
|
219
|
-
self.sid = base64_url_encode(sid_bytes[:43])
|
|
220
|
-
|
|
221
|
-
def _api_request(self, data):
|
|
222
|
-
params = {'id': self.sequence_num}
|
|
223
|
-
self.sequence_num += 1
|
|
224
|
-
|
|
225
|
-
if self.sid:
|
|
226
|
-
params.update({'sid': self.sid})
|
|
81
|
+
# Import mega sub-modules and apply patches
|
|
82
|
+
_init_mega_internals()
|
|
83
|
+
return True
|
|
227
84
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
85
|
+
def _init_mega_internals():
|
|
86
|
+
"""Initialize mega imports, monkey patches, and RobustMega after mega is available"""
|
|
87
|
+
global mega_engine, RequestError
|
|
88
|
+
|
|
89
|
+
from mega import Mega
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
from mega.errors import RequestError as _RequestError
|
|
93
|
+
from mega.crypto import (
|
|
94
|
+
base64_to_a32, decrypt_key, base64_url_decode, a32_to_str,
|
|
95
|
+
encrypt_key, str_to_a32, mpi_to_int, base64_url_encode
|
|
96
|
+
)
|
|
97
|
+
from Crypto.PublicKey import RSA
|
|
98
|
+
import binascii
|
|
99
|
+
globals()['RequestError'] = _RequestError
|
|
100
|
+
globals()['base64_to_a32'] = base64_to_a32
|
|
101
|
+
globals()['decrypt_key'] = decrypt_key
|
|
102
|
+
globals()['base64_url_decode'] = base64_url_decode
|
|
103
|
+
globals()['a32_to_str'] = a32_to_str
|
|
104
|
+
globals()['encrypt_key'] = encrypt_key
|
|
105
|
+
globals()['str_to_a32'] = str_to_a32
|
|
106
|
+
globals()['mpi_to_int'] = mpi_to_int
|
|
107
|
+
globals()['base64_url_encode'] = base64_url_encode
|
|
108
|
+
globals()['RSA'] = RSA
|
|
109
|
+
globals()['binascii'] = binascii
|
|
110
|
+
except ImportError:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# Monkey patch for mega.py (fixes TypeError in AES calls)
|
|
114
|
+
try:
|
|
115
|
+
from Crypto.Cipher import AES
|
|
116
|
+
import mega.mega as mega_module
|
|
117
|
+
_original_aes_new = AES.new
|
|
239
118
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
119
|
+
def _patched_aes_new(key, *args, **kwargs):
|
|
120
|
+
if isinstance(key, str):
|
|
121
|
+
key = key.encode('latin-1')
|
|
122
|
+
new_args = list(args)
|
|
123
|
+
for i in range(len(new_args)):
|
|
124
|
+
if isinstance(new_args[i], str):
|
|
125
|
+
new_args[i] = new_args[i].encode('latin-1')
|
|
126
|
+
for k in kwargs:
|
|
127
|
+
if isinstance(kwargs[k], str):
|
|
128
|
+
kwargs[k] = kwargs[k].encode('latin-1')
|
|
129
|
+
return _original_aes_new(key, *new_args, **kwargs)
|
|
130
|
+
|
|
131
|
+
AES.new = _patched_aes_new
|
|
132
|
+
if hasattr(mega_module, 'AES'):
|
|
133
|
+
mega_module.AES.new = _patched_aes_new
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
# Build RobustMega class dynamically
|
|
138
|
+
class RobustMega(Mega):
|
|
139
|
+
"""Patched version of Mega library to handle fragile API responses."""
|
|
140
|
+
def _login_process(self, resp, password):
|
|
141
|
+
encrypted_master_key = base64_to_a32(resp['k'])
|
|
142
|
+
self.master_key = decrypt_key(encrypted_master_key, password)
|
|
143
|
+
if 'tsid' in resp:
|
|
144
|
+
tsid = base64_url_decode(resp['tsid'])
|
|
145
|
+
key_encrypted = a32_to_str(
|
|
146
|
+
encrypt_key(str_to_a32(tsid[:16]), self.master_key)
|
|
147
|
+
)
|
|
148
|
+
if key_encrypted == tsid[-16:]:
|
|
149
|
+
self.sid = resp['tsid']
|
|
150
|
+
elif 'csid' in resp:
|
|
151
|
+
encrypted_rsa_private_key = base64_to_a32(resp['privk'])
|
|
152
|
+
rsa_private_key = decrypt_key(
|
|
153
|
+
encrypted_rsa_private_key, self.master_key
|
|
154
|
+
)
|
|
155
|
+
private_key_str = a32_to_str(rsa_private_key)
|
|
156
|
+
self.rsa_private_key = [0, 0, 0, 0]
|
|
157
|
+
for i in range(4):
|
|
158
|
+
l = int(((private_key_str[0]) * 256 + (private_key_str[1]) + 7) / 8) + 2
|
|
159
|
+
self.rsa_private_key[i] = mpi_to_int(private_key_str[:l])
|
|
160
|
+
private_key_str = private_key_str[l:]
|
|
161
|
+
encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
|
|
162
|
+
p = self.rsa_private_key[0]
|
|
163
|
+
q = self.rsa_private_key[1]
|
|
164
|
+
d = self.rsa_private_key[2]
|
|
165
|
+
n = p * q
|
|
166
|
+
phi = (p - 1) * (q - 1)
|
|
167
|
+
try:
|
|
168
|
+
e = pow(d, -1, phi)
|
|
169
|
+
rsa_decrypter = RSA.construct((n, e, d, p, q))
|
|
170
|
+
except:
|
|
171
|
+
e = 65537
|
|
172
|
+
rsa_decrypter = RSA.construct((n, e, d, p, q), consistency_check=False)
|
|
173
|
+
key_obj = rsa_decrypter.key if hasattr(rsa_decrypter, 'key') else rsa_decrypter
|
|
174
|
+
sid_int = key_obj._decrypt(encrypted_sid)
|
|
175
|
+
sid_hex = '%x' % sid_int
|
|
176
|
+
if len(sid_hex) % 2:
|
|
177
|
+
sid_hex = '0' + sid_hex
|
|
178
|
+
sid_bytes = binascii.unhexlify(sid_hex)
|
|
179
|
+
self.sid = base64_url_encode(sid_bytes[:43])
|
|
180
|
+
|
|
181
|
+
def _api_request(self, data):
|
|
182
|
+
params = {'id': self.sequence_num}
|
|
183
|
+
self.sequence_num += 1
|
|
184
|
+
if self.sid:
|
|
185
|
+
params.update({'sid': self.sid})
|
|
186
|
+
if not isinstance(data, list):
|
|
187
|
+
data = [data]
|
|
188
|
+
url = '{0}://g.api.{1}/cs'.format(self.schema, self.domain)
|
|
189
|
+
headers = {
|
|
190
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
|
191
|
+
'Accept': '*/*',
|
|
192
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
193
|
+
'Origin': 'https://mega.nz',
|
|
194
|
+
'Referer': 'https://mega.nz/'
|
|
195
|
+
}
|
|
256
196
|
try:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
197
|
+
req = requests.post(
|
|
198
|
+
url, params=params, data=json.dumps(data),
|
|
199
|
+
headers=headers, timeout=self.timeout,
|
|
200
|
+
)
|
|
201
|
+
content = req.text.strip() if req.text else ""
|
|
202
|
+
if not content:
|
|
203
|
+
if req.status_code == 402:
|
|
204
|
+
raise Exception("Mega.nz is temporarily blocking this connection (Status 402). This usually happens due to rate-limiting. Please try again later or use a different network.")
|
|
205
|
+
raise Exception(f"Mega API returned empty response (Status {req.status_code})")
|
|
260
206
|
try:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
207
|
+
json_resp = json.loads(content)
|
|
208
|
+
except json.JSONDecodeError:
|
|
209
|
+
try:
|
|
210
|
+
val = int(content)
|
|
211
|
+
raise RequestError(val)
|
|
212
|
+
except (ValueError, RequestError):
|
|
213
|
+
if content.startswith("-") and content[1:].isdigit():
|
|
214
|
+
raise RequestError(int(content))
|
|
215
|
+
raise Exception(f"Mega API error: {content[:100]}")
|
|
216
|
+
if isinstance(json_resp, int):
|
|
217
|
+
if json_resp == -3:
|
|
218
|
+
time.sleep(0.5)
|
|
219
|
+
return self._api_request(data=data)
|
|
220
|
+
raise RequestError(json_resp)
|
|
221
|
+
return json_resp[0]
|
|
222
|
+
except Exception:
|
|
223
|
+
raise
|
|
224
|
+
|
|
225
|
+
globals()['RobustMega'] = RobustMega
|
|
226
|
+
mega_engine = RobustMega()
|
|
227
|
+
globals()['mega_engine'] = mega_engine
|
|
277
228
|
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
mega_sessions = {} # Mapping: email -> {'client': MegaClient, 'info': user_info, 'quota': quota}
|
|
281
|
-
active_mega_email = None
|
|
229
|
+
# Placeholder for RequestError when mega is not installed
|
|
230
|
+
class RequestError(Exception): pass
|
|
282
231
|
|
|
283
232
|
try:
|
|
284
233
|
from playwright.sync_api import sync_playwright
|
|
@@ -730,6 +679,8 @@ def _get_or_create_node(name, parent_id, client):
|
|
|
730
679
|
@app.route('/api/cloud/login', methods=['POST'])
|
|
731
680
|
def cloud_login():
|
|
732
681
|
global mega_sessions, active_mega_email
|
|
682
|
+
if not _ensure_mega():
|
|
683
|
+
return jsonify({'success': False, 'error': 'mega.py is not available. Install it with: pip install mega.py --no-deps'}), 500
|
|
733
684
|
try:
|
|
734
685
|
data = request.json or {}
|
|
735
686
|
email = data.get('email', '').strip()
|
|
@@ -3534,10 +3485,6 @@ def main_launcher():
|
|
|
3534
3485
|
menu_commands = ['/web', '/cli', '/image', '/adb', '/help', '/clear', '/quit', '/history', '/w', '/c', '/i', '/a', '/h', '/q', '/hi', '--help']
|
|
3535
3486
|
setup_autocomplete(menu_commands)
|
|
3536
3487
|
|
|
3537
|
-
# Auto-install Playwright browsers at startup if needed
|
|
3538
|
-
if PLAYWRIGHT_AVAILABLE and not check_playwright_browsers():
|
|
3539
|
-
install_playwright_browsers()
|
|
3540
|
-
|
|
3541
3488
|
while True:
|
|
3542
3489
|
try:
|
|
3543
3490
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|
@@ -3676,9 +3623,16 @@ def _get_adb_cmd():
|
|
|
3676
3623
|
|
|
3677
3624
|
return 'adb'
|
|
3678
3625
|
|
|
3679
|
-
ADB_CMD =
|
|
3626
|
+
ADB_CMD = None # Lazy initialized when needed
|
|
3680
3627
|
_active_device = None # Track the active device ID for multi-device support
|
|
3681
3628
|
|
|
3629
|
+
def _ensure_adb():
|
|
3630
|
+
"""Ensure ADB is downloaded and return the command path"""
|
|
3631
|
+
global ADB_CMD
|
|
3632
|
+
if ADB_CMD is None:
|
|
3633
|
+
ADB_CMD = _get_adb_cmd()
|
|
3634
|
+
return ADB_CMD
|
|
3635
|
+
|
|
3682
3636
|
KNOWN_BLOATWARE = {
|
|
3683
3637
|
# Facebook
|
|
3684
3638
|
'com.facebook.appmanager': 'Facebook App Manager',
|
|
@@ -3871,6 +3825,9 @@ def _adb_display_packages(packages, search_filter=None):
|
|
|
3871
3825
|
def run_adb_mode():
|
|
3872
3826
|
"""ADB Bloatware Remover - Interactive Mode"""
|
|
3873
3827
|
global _active_device
|
|
3828
|
+
# Ensure ADB is downloaded first
|
|
3829
|
+
_ensure_adb()
|
|
3830
|
+
|
|
3874
3831
|
# Step 1: Check if ADB is available
|
|
3875
3832
|
ok, version_out = _adb_run([ADB_CMD, 'version'])
|
|
3876
3833
|
if not ok:
|
|
@@ -361,71 +361,71 @@
|
|
|
361
361
|
</div>
|
|
362
362
|
</div>
|
|
363
363
|
</div>
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
364
|
+
|
|
365
|
+
<!-- IMAGE ANALYSIS MODE -->
|
|
366
|
+
<div id="mode-image"
|
|
367
|
+
class="hidden flex flex-col gap-4 animate-in fade-in slide-in-from-right-8 duration-300">
|
|
368
|
+
<div class="border-2 border-dashed border-slate-700/50 rounded-2xl p-4 text-center hover:border-indigo-500/50 hover:bg-slate-800/30 transition-all cursor-pointer relative group min-h-[200px] flex items-center justify-center"
|
|
369
|
+
id="drop-zone" onclick="document.getElementById('imageInput').click()">
|
|
370
|
+
<input type="file" id="imageInput" class="hidden" accept="image/*"
|
|
371
|
+
onchange="handleImageUpload(this)">
|
|
372
|
+
<div id="drop-content" class="transition-all duration-300 group-hover:scale-105">
|
|
373
|
+
<div
|
|
374
|
+
class="w-12 h-12 bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-2 border border-slate-700 shadow-lg">
|
|
375
|
+
<svg class="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor"
|
|
376
|
+
viewBox="0 0 24 24">
|
|
377
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
378
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
|
|
379
|
+
</path>
|
|
380
|
+
</svg>
|
|
381
|
+
</div>
|
|
382
|
+
<h3 class="text-white font-semibold text-base mb-0.5">Drop image here</h3>
|
|
383
|
+
<p class="text-slate-400 text-xs">or click to browse files</p>
|
|
384
|
+
</div>
|
|
385
|
+
<div id="image-preview-container"
|
|
386
|
+
class="hidden absolute inset-0 bg-slate-900 z-10 rounded-2xl overflow-hidden flex items-center justify-center">
|
|
387
|
+
<img id="image-preview" class="max-h-[80%] max-w-[80%] object-contain p-2 rounded-lg">
|
|
388
|
+
<button onclick="event.stopPropagation(); clearImage()"
|
|
389
|
+
class="absolute top-2 right-2 bg-black/50 hover:bg-red-500 text-white p-1.5 rounded-full backdrop-blur-md transition-all">
|
|
390
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
391
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
392
|
+
d="M6 18L18 6M6 6l12 12"></path>
|
|
393
|
+
</svg>
|
|
394
|
+
</button>
|
|
381
395
|
</div>
|
|
382
|
-
<h3 class="text-white font-semibold text-base mb-0.5">Drop image here</h3>
|
|
383
|
-
<p class="text-slate-400 text-xs">or click to browse files</p>
|
|
384
396
|
</div>
|
|
385
|
-
<div id="
|
|
386
|
-
class="
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
391
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
392
|
-
d="M6 18L18 6M6 6l12 12"></path>
|
|
393
|
-
</svg>
|
|
394
|
-
</button>
|
|
397
|
+
<div id="url-separator" class="relative flex py-2 items-center transition-all duration-300">
|
|
398
|
+
<div class="flex-grow border-t border-slate-700/50"></div>
|
|
399
|
+
<span class="flex-shrink-0 mx-4 text-slate-500 text-xs font-bold uppercase tracking-widest">OR
|
|
400
|
+
USE URL</span>
|
|
401
|
+
<div class="flex-grow border-t border-slate-700/50"></div>
|
|
395
402
|
</div>
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
<
|
|
407
|
-
|
|
403
|
+
<div id="url-input-container" class="flex gap-2 relative transition-all duration-300">
|
|
404
|
+
<div class="flex-1 relative">
|
|
405
|
+
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
|
406
|
+
<svg class="w-5 h-5 text-slate-500" fill="none" stroke="currentColor"
|
|
407
|
+
viewBox="0 0 24 24">
|
|
408
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
409
|
+
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1">
|
|
410
|
+
</path>
|
|
411
|
+
</svg>
|
|
412
|
+
</div>
|
|
413
|
+
<input type="url" id="imageUrlInput" placeholder="Paste direct image link..."
|
|
414
|
+
class="w-full bg-gray-900/80 text-white placeholder-slate-500 border border-slate-700 rounded-xl py-3 pl-12 pr-4 focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 transition-all shadow-inner">
|
|
415
|
+
</div>
|
|
416
|
+
<button id="analyzeImageBtn" onclick="analyzeImage()"
|
|
417
|
+
class="scrape-btn-custom relative overflow-hidden text-white font-bold px-6 py-2.5 flex items-center gap-2 whitespace-nowrap transform active:scale-95 transition-all hidden">
|
|
418
|
+
<svg class="w-5 h-5 relative z-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
408
419
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
409
|
-
d="
|
|
420
|
+
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">
|
|
410
421
|
</path>
|
|
411
422
|
</svg>
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
class="w-full bg-gray-900/80 text-white placeholder-slate-500 border border-slate-700 rounded-xl py-3 pl-12 pr-4 focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 transition-all shadow-inner">
|
|
423
|
+
<span class="relative z-10">Analyze</span>
|
|
424
|
+
</button>
|
|
415
425
|
</div>
|
|
416
|
-
<button id="analyzeImageBtn" onclick="analyzeImage()"
|
|
417
|
-
class="scrape-btn-custom relative overflow-hidden text-white font-bold px-6 py-2.5 flex items-center gap-2 whitespace-nowrap transform active:scale-95 transition-all hidden">
|
|
418
|
-
<svg class="w-5 h-5 relative z-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
419
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
420
|
-
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">
|
|
421
|
-
</path>
|
|
422
|
-
</svg>
|
|
423
|
-
<span class="relative z-10">Analyze</span>
|
|
424
|
-
</button>
|
|
425
426
|
</div>
|
|
426
427
|
</div>
|
|
427
428
|
</div>
|
|
428
|
-
</div>
|
|
429
429
|
<div id="history-section" class="w-full hidden opacity-0 translate-y-4 transition-all duration-500">
|
|
430
430
|
<div class="glass-dark rounded-2xl p-6 border border-white/5 h-full">
|
|
431
431
|
<h3 class="text-lg font-semibold text-slate-400 mb-4 uppercase tracking-wider flex items-center gap-2">
|
|
@@ -733,6 +733,13 @@
|
|
|
733
733
|
</svg>
|
|
734
734
|
<span class="hidden md:inline">Videos</span>
|
|
735
735
|
</button>
|
|
736
|
+
<button onclick="switchTab('seo')" id="tab-seo"
|
|
737
|
+
class="tab-btn flex items-center gap-2 px-3 md:px-4 py-2 text-sm font-semibold text-slate-300 hover:text-white transition-all whitespace-nowrap">
|
|
738
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
739
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
|
740
|
+
</svg>
|
|
741
|
+
<span class="hidden md:inline">Analytics</span>
|
|
742
|
+
</button>
|
|
736
743
|
</div>
|
|
737
744
|
<div id="main-content-container"
|
|
738
745
|
class="glass-dark rounded-2xl p-3 border border-white/5 transition-all duration-300 tab-gradient-border">
|
|
@@ -1460,6 +1467,66 @@
|
|
|
1460
1467
|
</button>
|
|
1461
1468
|
</div>
|
|
1462
1469
|
</dialog>
|
|
1470
|
+
|
|
1471
|
+
<!-- About Floating Button -->
|
|
1472
|
+
<button onclick="document.getElementById('about-modal').showModal()"
|
|
1473
|
+
class="fixed bottom-6 right-6 p-3 bg-white/10 backdrop-blur-md border border-white/10 hover:bg-white/20 hover:border-white/20 text-indigo-300 hover:text-indigo-200 rounded-full shadow-lg hover:shadow-[0_0_15px_rgba(255,255,255,0.1)] hover:scale-110 transition-all z-50 group flex items-center justify-center">
|
|
1474
|
+
<svg class="w-5 h-5" fill="currentColor" stroke="none" viewBox="0 0 24 24">
|
|
1475
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
|
|
1476
|
+
</svg>
|
|
1477
|
+
<span class="absolute right-full mr-3 bg-slate-900/80 backdrop-blur-md text-slate-200 text-xs px-2.5 py-1 rounded-md opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap border border-white/10 pointer-events-none shadow-xl">About</span>
|
|
1478
|
+
</button>
|
|
1479
|
+
|
|
1480
|
+
<!-- About Modal -->
|
|
1481
|
+
<dialog id="about-modal"
|
|
1482
|
+
class="backdrop:bg-black/60 bg-transparent glass-dark border border-white/10 rounded-2xl p-0 w-[90vw] max-w-md shadow-[0_0_50px_rgba(0,0,0,0.5)] focus:outline-none m-auto"
|
|
1483
|
+
onclick="if(event.target === this) this.close()">
|
|
1484
|
+
<div class="relative overflow-hidden rounded-2xl">
|
|
1485
|
+
<!-- Header -->
|
|
1486
|
+
<div class="bg-white/5 p-6 pb-8 text-center border-b border-white/10 relative overflow-hidden">
|
|
1487
|
+
<div class="absolute inset-0 bg-gradient-to-b from-indigo-500/15 to-transparent"></div>
|
|
1488
|
+
<div class="relative z-10 flex justify-center mb-3">
|
|
1489
|
+
<img src="https://i.postimg.cc/Gh7Gnn18/Web-Tools.png" alt="WebTools Logo" class="w-16 h-16 object-contain drop-shadow-[0_0_15px_rgba(79,70,229,0.4)] hover:scale-110 transition-transform">
|
|
1490
|
+
</div>
|
|
1491
|
+
<h2 class="relative z-10 text-xl font-bold text-white mb-1">WebTools CLI</h2>
|
|
1492
|
+
|
|
1493
|
+
<button onclick="document.getElementById('about-modal').close()"
|
|
1494
|
+
class="absolute top-4 right-4 p-2 bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white rounded-full transition-all z-20">
|
|
1495
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1496
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
1497
|
+
</svg>
|
|
1498
|
+
</button>
|
|
1499
|
+
</div>
|
|
1500
|
+
|
|
1501
|
+
<!-- Content -->
|
|
1502
|
+
<div class="p-6">
|
|
1503
|
+
<p class="text-slate-300 text-sm leading-relaxed mb-6 text-center drop-shadow-md">
|
|
1504
|
+
An advanced, self-hosted web intelligence suite for researchers and developers. OSINT, SEO, AI Analysis, and Forensics—all from your terminal.
|
|
1505
|
+
</p>
|
|
1506
|
+
|
|
1507
|
+
<div class="grid grid-cols-2 gap-3 mb-6">
|
|
1508
|
+
<div class="bg-white/5 p-3 rounded-xl border border-white/10 text-center flex flex-col items-center shadow-lg">
|
|
1509
|
+
<div class="text-indigo-400 mb-1"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg></div>
|
|
1510
|
+
<h4 class="text-[10px] font-bold text-white uppercase tracking-wider mb-0.5">Author</h4>
|
|
1511
|
+
<p class="text-xs text-slate-300 font-medium whitespace-nowrap overflow-hidden text-ellipsis w-full drop-shadow-md">Abhinav Adarsh</p>
|
|
1512
|
+
</div>
|
|
1513
|
+
<div class="bg-white/5 p-3 rounded-xl border border-white/10 text-center flex flex-col items-center shadow-lg">
|
|
1514
|
+
<div class="text-indigo-400 mb-1"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg></div>
|
|
1515
|
+
<h4 class="text-[10px] font-bold text-white uppercase tracking-wider mb-0.5">License</h4>
|
|
1516
|
+
<p class="text-xs text-slate-300 font-medium drop-shadow-md">MIT Open Source</p>
|
|
1517
|
+
</div>
|
|
1518
|
+
</div>
|
|
1519
|
+
|
|
1520
|
+
<div class="flex justify-center mt-2">
|
|
1521
|
+
<a href="https://webtoolscli.pages.dev/" target="_blank"
|
|
1522
|
+
class="w-full py-2.5 bg-white/5 hover:bg-white/10 text-indigo-400 border border-white/10 rounded-xl transition-all font-semibold text-sm flex items-center justify-center gap-2">
|
|
1523
|
+
Visit Official Website
|
|
1524
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
|
1525
|
+
</a>
|
|
1526
|
+
</div>
|
|
1527
|
+
</div>
|
|
1528
|
+
</div>
|
|
1529
|
+
</dialog>
|
|
1463
1530
|
</body>
|
|
1464
1531
|
|
|
1465
1532
|
</html>
|
|
@@ -918,12 +918,11 @@ async function scrapeWebsite() {
|
|
|
918
918
|
<span class="hidden sm:inline">Play</span>
|
|
919
919
|
</button>
|
|
920
920
|
<button onclick="event.stopPropagation(); saveToCloud('video', '${vid.url}', '${data.title || 'WEB Tools'}')"
|
|
921
|
-
class="
|
|
921
|
+
class="p-2 bg-red-600 hover:bg-red-500 text-white rounded-lg transition-all flex items-center justify-center" title="Save to Mega Cloud">
|
|
922
922
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.5 14h-2v-4.5l-2.5 3-2.5-3V16h-2V8h2.5l2 2.5 2-2.5H16v8z"/></svg>
|
|
923
923
|
</button>
|
|
924
|
-
<a href="${vid.url}" download class="
|
|
925
|
-
<svg class="w-
|
|
926
|
-
<span class="hidden sm:inline">Download</span>
|
|
924
|
+
<a href="${vid.url}" download class="p-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg transition-all flex items-center justify-center" title="Download">
|
|
925
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
|
|
927
926
|
</a>
|
|
928
927
|
</div>
|
|
929
928
|
</div>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|