webtools-cli 1.2.9__tar.gz → 1.3.1__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.2.9 → webtools_cli-1.3.1}/PKG-INFO +1 -1
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/pyproject.toml +1 -1
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools/core.py +199 -232
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools/web/index.html +55 -55
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools_cli.egg-info/PKG-INFO +1 -1
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/LICENSE +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/README.md +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/setup.cfg +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools/__init__.py +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools/__main__.py +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools/cli.py +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools/install.py +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools/mega_client.py +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools/web/script.js +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools/web/style.css +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools_cli.egg-info/SOURCES.txt +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools_cli.egg-info/dependency_links.txt +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools_cli.egg-info/entry_points.txt +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/webtools_cli.egg-info/requires.txt +0 -0
- {webtools_cli-1.2.9 → webtools_cli-1.3.1}/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()
|
|
@@ -1054,6 +1005,7 @@ def cloud_download(filepath):
|
|
|
1054
1005
|
|
|
1055
1006
|
@app.route('/api/adb/status', methods=['GET'])
|
|
1056
1007
|
def adb_status():
|
|
1008
|
+
global _active_device
|
|
1057
1009
|
ok, devices_out = _adb_run([ADB_CMD, 'devices'])
|
|
1058
1010
|
connected, offline = [], []
|
|
1059
1011
|
if ok:
|
|
@@ -1066,10 +1018,11 @@ def adb_status():
|
|
|
1066
1018
|
offline.append({'id': parts[0], 'status': parts[1]})
|
|
1067
1019
|
|
|
1068
1020
|
if connected:
|
|
1021
|
+
_active_device = connected[0]
|
|
1069
1022
|
return jsonify({
|
|
1070
1023
|
'status': 'connected',
|
|
1071
1024
|
'device_id': connected[0],
|
|
1072
|
-
'device_model': _adb_get_device_info(),
|
|
1025
|
+
'device_model': _adb_get_device_info(connected[0]),
|
|
1073
1026
|
'other_devices': len(connected) - 1
|
|
1074
1027
|
})
|
|
1075
1028
|
elif offline:
|
|
@@ -3532,10 +3485,6 @@ def main_launcher():
|
|
|
3532
3485
|
menu_commands = ['/web', '/cli', '/image', '/adb', '/help', '/clear', '/quit', '/history', '/w', '/c', '/i', '/a', '/h', '/q', '/hi', '--help']
|
|
3533
3486
|
setup_autocomplete(menu_commands)
|
|
3534
3487
|
|
|
3535
|
-
# Auto-install Playwright browsers at startup if needed
|
|
3536
|
-
if PLAYWRIGHT_AVAILABLE and not check_playwright_browsers():
|
|
3537
|
-
install_playwright_browsers()
|
|
3538
|
-
|
|
3539
3488
|
while True:
|
|
3540
3489
|
try:
|
|
3541
3490
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|
@@ -3674,7 +3623,15 @@ def _get_adb_cmd():
|
|
|
3674
3623
|
|
|
3675
3624
|
return 'adb'
|
|
3676
3625
|
|
|
3677
|
-
ADB_CMD =
|
|
3626
|
+
ADB_CMD = None # Lazy initialized when needed
|
|
3627
|
+
_active_device = None # Track the active device ID for multi-device support
|
|
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
|
|
3678
3635
|
|
|
3679
3636
|
KNOWN_BLOATWARE = {
|
|
3680
3637
|
# Facebook
|
|
@@ -3753,8 +3710,13 @@ CRITICAL_PACKAGES = {
|
|
|
3753
3710
|
'com.android.vending', 'com.android.providers.settings',
|
|
3754
3711
|
}
|
|
3755
3712
|
|
|
3756
|
-
def _adb_run(cmd, timeout=15):
|
|
3757
|
-
"""Run an adb command and return (success, stdout)"""
|
|
3713
|
+
def _adb_run(cmd, timeout=15, device=None):
|
|
3714
|
+
"""Run an adb command and return (success, stdout). If device is specified, adds -s <device> flag."""
|
|
3715
|
+
# Auto-inject -s <device_id> for device-specific commands
|
|
3716
|
+
if device and len(cmd) > 1 and cmd[0] == ADB_CMD and cmd[1] != 'devices':
|
|
3717
|
+
cmd = [cmd[0], '-s', device] + cmd[1:]
|
|
3718
|
+
elif _active_device and len(cmd) > 1 and cmd[0] == ADB_CMD and cmd[1] not in ('devices', 'version', 'pair', 'connect'):
|
|
3719
|
+
cmd = [cmd[0], '-s', _active_device] + cmd[1:]
|
|
3758
3720
|
try:
|
|
3759
3721
|
result = subprocess.run(
|
|
3760
3722
|
cmd, capture_output=True, text=True, timeout=timeout
|
|
@@ -3770,9 +3732,9 @@ def _adb_run(cmd, timeout=15):
|
|
|
3770
3732
|
except Exception as e:
|
|
3771
3733
|
return False, str(e)
|
|
3772
3734
|
|
|
3773
|
-
def _adb_get_device_info():
|
|
3735
|
+
def _adb_get_device_info(device=None):
|
|
3774
3736
|
"""Get connected device model name"""
|
|
3775
|
-
ok, out = _adb_run([ADB_CMD, 'shell', 'getprop', 'ro.product.model'])
|
|
3737
|
+
ok, out = _adb_run([ADB_CMD, 'shell', 'getprop', 'ro.product.model'], device=device)
|
|
3776
3738
|
if ok and out:
|
|
3777
3739
|
return out
|
|
3778
3740
|
return "Unknown Device"
|
|
@@ -3862,6 +3824,10 @@ def _adb_display_packages(packages, search_filter=None):
|
|
|
3862
3824
|
|
|
3863
3825
|
def run_adb_mode():
|
|
3864
3826
|
"""ADB Bloatware Remover - Interactive Mode"""
|
|
3827
|
+
global _active_device
|
|
3828
|
+
# Ensure ADB is downloaded first
|
|
3829
|
+
_ensure_adb()
|
|
3830
|
+
|
|
3865
3831
|
# Step 1: Check if ADB is available
|
|
3866
3832
|
ok, version_out = _adb_run([ADB_CMD, 'version'])
|
|
3867
3833
|
if not ok:
|
|
@@ -3902,7 +3868,8 @@ def run_adb_mode():
|
|
|
3902
3868
|
offline_devices.append((parts[0], parts[1]))
|
|
3903
3869
|
|
|
3904
3870
|
if connected_devices:
|
|
3905
|
-
|
|
3871
|
+
_active_device = connected_devices[0]
|
|
3872
|
+
device_name = _adb_get_device_info(connected_devices[0])
|
|
3906
3873
|
print(f"\n {Fore.GREEN}✅ Connected: {device_name} ({connected_devices[0]}){Style.RESET_ALL}")
|
|
3907
3874
|
if len(connected_devices) > 1:
|
|
3908
3875
|
print(f" {Fore.YELLOW} + {len(connected_devices)-1} more device(s){Style.RESET_ALL}")
|
|
@@ -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">
|
|
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
|
|
File without changes
|