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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtools-cli
3
- Version: 1.2.9
3
+ Version: 1.3.1
4
4
  Summary: Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI
5
5
  Author: Abhinav Adarsh
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "webtools-cli"
7
- version = "1.2.9"
7
+ version = "1.3.1"
8
8
  description = "Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- # --- AUTO-INSTALLER FOR MISSING/BROKEN DEPENDENCIES ---
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
- from mega import Mega
113
- try:
114
- from mega.errors import RequestError
115
- from mega.crypto import (
116
- base64_to_a32, decrypt_key, base64_url_decode, a32_to_str,
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
- # --- MONKEY PATCH FOR MEGA.PY (Fixes TypeError in AES calls) ---
125
- try:
126
- from Crypto.Cipher import AES
127
- import mega.mega as mega_module
128
- _original_aes_new = AES.new
129
-
130
- def _patched_aes_new(key, *args, **kwargs):
131
- # Convert key to bytes if string
132
- if isinstance(key, str):
133
- key = key.encode('latin-1')
134
-
135
- # Convert positional arguments (like IV/mode) to bytes if strings
136
- new_args = list(args)
137
- for i in range(len(new_args)):
138
- if isinstance(new_args[i], str):
139
- new_args[i] = new_args[i].encode('latin-1')
140
-
141
- # Convert keyword arguments (IV, nonce, etc.) to bytes if strings
142
- for k in kwargs:
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
- # Apply patch globally
149
- AES.new = _patched_aes_new
150
- if hasattr(mega_module, 'AES'):
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
- if not isinstance(data, list):
229
- data = [data]
230
-
231
- url = '{0}://g.api.{1}/cs'.format(self.schema, self.domain)
232
- headers = {
233
- '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',
234
- 'Accept': '*/*',
235
- 'Accept-Language': 'en-US,en;q=0.9',
236
- 'Origin': 'https://mega.nz',
237
- 'Referer': 'https://mega.nz/'
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
- try:
241
- req = requests.post(
242
- url,
243
- params=params,
244
- data=json.dumps(data),
245
- headers=headers,
246
- timeout=self.timeout,
247
- )
248
-
249
- content = req.text.strip() if req.text else ""
250
-
251
- if not content:
252
- if req.status_code == 402:
253
- 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.")
254
- raise Exception(f"Mega API returned empty response (Status {req.status_code})")
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
- json_resp = json.loads(content)
258
- except json.JSONDecodeError:
259
- # Try to parse as integer error code
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
- val = int(content)
262
- raise RequestError(val)
263
- except (ValueError, RequestError):
264
- if content.startswith("-") and content[1:].isdigit():
265
- raise RequestError(int(content))
266
- raise Exception(f"Mega API error: {content[:100]}")
267
-
268
- if isinstance(json_resp, int):
269
- if json_resp == -3:
270
- time.sleep(0.5)
271
- return self._api_request(data=data)
272
- raise RequestError(json_resp)
273
-
274
- return json_resp[0]
275
- except Exception:
276
- raise
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
- # Global Mega session state
279
- mega_engine = RobustMega()
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 = _get_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
- device_name = _adb_get_device_info()
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
- </div>
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>
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="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>
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
- </div>
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>
402
- </div>
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">
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="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">
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
- </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">
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">
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtools-cli
3
- Version: 1.2.9
3
+ Version: 1.3.1
4
4
  Summary: Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI
5
5
  Author: Abhinav Adarsh
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes