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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtools-cli
3
- Version: 1.3.0
3
+ Version: 1.3.2
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.3.0"
7
+ version = "1.3.2"
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()
@@ -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 = _get_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
- </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">
@@ -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="px-3 py-2 bg-red-600 hover:bg-red-500 text-white rounded-lg transition-all" title="Save to Mega Cloud">
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="px-3 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-xs font-semibold transition-all flex items-center gap-1">
925
- <svg class="w-3 h-3" 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>
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>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtools-cli
3
- Version: 1.3.0
3
+ Version: 1.3.2
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