webtools-cli 1.2.0__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/PKG-INFO +6 -1
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/README.md +3 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/pyproject.toml +3 -1
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools/core.py +528 -3
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools/web/index.html +248 -13
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools/web/script.js +670 -8
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools_cli.egg-info/PKG-INFO +6 -1
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools_cli.egg-info/requires.txt +2 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/LICENSE +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/setup.cfg +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools/__init__.py +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools/__main__.py +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools/cli.py +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools/install.py +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools/web/style.css +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools_cli.egg-info/SOURCES.txt +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools_cli.egg-info/dependency_links.txt +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools_cli.egg-info/entry_points.txt +0 -0
- {webtools_cli-1.2.0 → webtools_cli-1.2.2}/webtools_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webtools-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI
|
|
5
5
|
Author: Abhinav Adarsh
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,6 +29,8 @@ Requires-Dist: Pillow
|
|
|
29
29
|
Requires-Dist: mtranslate
|
|
30
30
|
Requires-Dist: colorama
|
|
31
31
|
Requires-Dist: playwright
|
|
32
|
+
Requires-Dist: mega.py
|
|
33
|
+
Requires-Dist: pycryptodome
|
|
32
34
|
Requires-Dist: pyreadline3; platform_system == "Windows"
|
|
33
35
|
Provides-Extra: playwright
|
|
34
36
|
Requires-Dist: playwright; extra == "playwright"
|
|
@@ -52,6 +54,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
|
|
|
52
54
|
|
|
53
55
|
- **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
|
|
54
56
|
- **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
|
|
57
|
+
- **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
|
|
55
58
|
- **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
|
|
56
59
|
- **💻 Terminal-First**: Designed for power users who live in the command line.
|
|
57
60
|
- **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
|
|
@@ -84,6 +87,8 @@ pip install webtools-cli --upgrade
|
|
|
84
87
|
### Advanced Scraping & Stealth
|
|
85
88
|
- **Smart Proxy Rotation**: Automatically rotates User-Agents and Proxies to evade detection.
|
|
86
89
|
- **Turbo-Fetch**: Parallel chunk downloads for large media (Videos/Images).
|
|
90
|
+
- **Mega Cloud Drive**: Native integration with Mega.nz for multi-account storage and file management.
|
|
91
|
+
- **One-Click Save**: Instantly save scraped images and videos directly to your cloud storage.
|
|
87
92
|
- **Deep Crawl**: Recursive link mapping up to 3 levels deep.
|
|
88
93
|
- **Headless Fallback**: Integrated Playwright support for auth-walled or SPA environments.
|
|
89
94
|
|
|
@@ -16,6 +16,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
|
|
|
16
16
|
|
|
17
17
|
- **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
|
|
18
18
|
- **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
|
|
19
|
+
- **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
|
|
19
20
|
- **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
|
|
20
21
|
- **💻 Terminal-First**: Designed for power users who live in the command line.
|
|
21
22
|
- **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
|
|
@@ -48,6 +49,8 @@ pip install webtools-cli --upgrade
|
|
|
48
49
|
### Advanced Scraping & Stealth
|
|
49
50
|
- **Smart Proxy Rotation**: Automatically rotates User-Agents and Proxies to evade detection.
|
|
50
51
|
- **Turbo-Fetch**: Parallel chunk downloads for large media (Videos/Images).
|
|
52
|
+
- **Mega Cloud Drive**: Native integration with Mega.nz for multi-account storage and file management.
|
|
53
|
+
- **One-Click Save**: Instantly save scraped images and videos directly to your cloud storage.
|
|
51
54
|
- **Deep Crawl**: Recursive link mapping up to 3 levels deep.
|
|
52
55
|
- **Headless Fallback**: Integrated Playwright support for auth-walled or SPA environments.
|
|
53
56
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "webtools-cli"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.2"
|
|
8
8
|
description = "Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -36,6 +36,8 @@ dependencies = [
|
|
|
36
36
|
"mtranslate",
|
|
37
37
|
"colorama",
|
|
38
38
|
"playwright",
|
|
39
|
+
"mega.py",
|
|
40
|
+
"pycryptodome",
|
|
39
41
|
"pyreadline3; platform_system == 'Windows'",
|
|
40
42
|
]
|
|
41
43
|
|
|
@@ -7,6 +7,32 @@ 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 DEPENDENCIES ---
|
|
11
|
+
def ensure_dependencies():
|
|
12
|
+
"""Checks and installs missing dependencies at runtime"""
|
|
13
|
+
dependencies = {
|
|
14
|
+
"mega": "mega.py",
|
|
15
|
+
"Crypto": "pycryptodome"
|
|
16
|
+
}
|
|
17
|
+
missing = []
|
|
18
|
+
for module_name, package_name in dependencies.items():
|
|
19
|
+
try:
|
|
20
|
+
__import__(module_name)
|
|
21
|
+
except ImportError:
|
|
22
|
+
missing.append(package_name)
|
|
23
|
+
|
|
24
|
+
if missing:
|
|
25
|
+
print(f"\n[!] Missing dependencies detected: {', '.join(missing)}")
|
|
26
|
+
print("[*] Attempting automatic installation...")
|
|
27
|
+
try:
|
|
28
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install"] + missing)
|
|
29
|
+
print("[+] Dependencies installed successfully!\n")
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"[-] Auto-installation failed: {e}")
|
|
32
|
+
print("[!] Please run: pip install mega.py pycryptodome")
|
|
33
|
+
|
|
34
|
+
ensure_dependencies()
|
|
35
|
+
|
|
10
36
|
|
|
11
37
|
# --- PACKAGE PATHS ---
|
|
12
38
|
PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -23,9 +49,179 @@ except ImportError:
|
|
|
23
49
|
import numpy as np
|
|
24
50
|
from bs4 import BeautifulSoup
|
|
25
51
|
from collections import Counter
|
|
26
|
-
from flask import Flask, render_template_string, send_from_directory, request, jsonify, send_file
|
|
52
|
+
from flask import Flask, render_template_string, send_from_directory, request, jsonify, send_file, after_this_request
|
|
27
53
|
from PIL import Image,ExifTags,ImageChops,ImageEnhance
|
|
28
54
|
from io import BytesIO
|
|
55
|
+
from mega import Mega
|
|
56
|
+
try:
|
|
57
|
+
from mega.errors import RequestError
|
|
58
|
+
from mega.crypto import (
|
|
59
|
+
base64_to_a32, decrypt_key, base64_url_decode, a32_to_str,
|
|
60
|
+
encrypt_key, str_to_a32, mpi_to_int, base64_url_encode
|
|
61
|
+
)
|
|
62
|
+
from Crypto.PublicKey import RSA
|
|
63
|
+
import binascii
|
|
64
|
+
except ImportError:
|
|
65
|
+
class RequestError(Exception): pass
|
|
66
|
+
|
|
67
|
+
# --- MONKEY PATCH FOR MEGA.PY (Fixes TypeError in AES calls) ---
|
|
68
|
+
try:
|
|
69
|
+
from Crypto.Cipher import AES
|
|
70
|
+
import mega.mega as mega_module
|
|
71
|
+
_original_aes_new = AES.new
|
|
72
|
+
|
|
73
|
+
def _patched_aes_new(key, *args, **kwargs):
|
|
74
|
+
# Convert key to bytes if string
|
|
75
|
+
if isinstance(key, str):
|
|
76
|
+
key = key.encode('latin-1')
|
|
77
|
+
|
|
78
|
+
# Convert positional arguments (like IV/mode) to bytes if strings
|
|
79
|
+
new_args = list(args)
|
|
80
|
+
for i in range(len(new_args)):
|
|
81
|
+
if isinstance(new_args[i], str):
|
|
82
|
+
new_args[i] = new_args[i].encode('latin-1')
|
|
83
|
+
|
|
84
|
+
# Convert keyword arguments (IV, nonce, etc.) to bytes if strings
|
|
85
|
+
for k in kwargs:
|
|
86
|
+
if isinstance(kwargs[k], str):
|
|
87
|
+
kwargs[k] = kwargs[k].encode('latin-1')
|
|
88
|
+
|
|
89
|
+
return _original_aes_new(key, *new_args, **kwargs)
|
|
90
|
+
|
|
91
|
+
# Apply patch globally
|
|
92
|
+
AES.new = _patched_aes_new
|
|
93
|
+
if hasattr(mega_module, 'AES'):
|
|
94
|
+
mega_module.AES.new = _patched_aes_new
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
class RobustMega(Mega):
|
|
99
|
+
"""
|
|
100
|
+
Patched version of Mega library to handle fragile API responses.
|
|
101
|
+
The original library crashes if the API returns non-JSON or an empty string.
|
|
102
|
+
"""
|
|
103
|
+
def _login_process(self, resp, password):
|
|
104
|
+
"""
|
|
105
|
+
Overridden to fix 'Invalid RSA public exponent' error in newer pycryptodome.
|
|
106
|
+
The original code passes 0 as the exponent, which is rejected.
|
|
107
|
+
"""
|
|
108
|
+
encrypted_master_key = base64_to_a32(resp['k'])
|
|
109
|
+
self.master_key = decrypt_key(encrypted_master_key, password)
|
|
110
|
+
if 'tsid' in resp:
|
|
111
|
+
tsid = base64_url_decode(resp['tsid'])
|
|
112
|
+
key_encrypted = a32_to_str(
|
|
113
|
+
encrypt_key(str_to_a32(tsid[:16]), self.master_key)
|
|
114
|
+
)
|
|
115
|
+
if key_encrypted == tsid[-16:]:
|
|
116
|
+
self.sid = resp['tsid']
|
|
117
|
+
elif 'csid' in resp:
|
|
118
|
+
encrypted_rsa_private_key = base64_to_a32(resp['privk'])
|
|
119
|
+
rsa_private_key = decrypt_key(
|
|
120
|
+
encrypted_rsa_private_key, self.master_key
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
private_key_str = a32_to_str(rsa_private_key)
|
|
124
|
+
self.rsa_private_key = [0, 0, 0, 0]
|
|
125
|
+
|
|
126
|
+
# This loop parses the 4 components: p, q, d, u
|
|
127
|
+
for i in range(4):
|
|
128
|
+
l = int(((private_key_str[0]) * 256 + (private_key_str[1]) + 7) / 8) + 2
|
|
129
|
+
self.rsa_private_key[i] = mpi_to_int(private_key_str[:l])
|
|
130
|
+
private_key_str = private_key_str[l:]
|
|
131
|
+
|
|
132
|
+
encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
|
|
133
|
+
|
|
134
|
+
# The Fix: Calculate the real 'e' from p, q, d
|
|
135
|
+
p = self.rsa_private_key[0]
|
|
136
|
+
q = self.rsa_private_key[1]
|
|
137
|
+
d = self.rsa_private_key[2]
|
|
138
|
+
n = p * q
|
|
139
|
+
phi = (p - 1) * (q - 1)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
# Calculate real e = d^-1 mod phi
|
|
143
|
+
e = pow(d, -1, phi)
|
|
144
|
+
rsa_decrypter = RSA.construct((n, e, d, p, q))
|
|
145
|
+
except:
|
|
146
|
+
# Fallback to 65537 if inverse fails (shouldn't happen)
|
|
147
|
+
e = 65537
|
|
148
|
+
rsa_decrypter = RSA.construct((n, e, d, p, q), consistency_check=False)
|
|
149
|
+
|
|
150
|
+
# Handle potential library differences in how the key is accessed
|
|
151
|
+
key_obj = rsa_decrypter.key if hasattr(rsa_decrypter, 'key') else rsa_decrypter
|
|
152
|
+
|
|
153
|
+
# Decrypt sid integer
|
|
154
|
+
sid_int = key_obj._decrypt(encrypted_sid)
|
|
155
|
+
|
|
156
|
+
# Convert to hex then to bytes, ensuring we handle the 0 padding if any
|
|
157
|
+
sid_hex = '%x' % sid_int
|
|
158
|
+
if len(sid_hex) % 2:
|
|
159
|
+
sid_hex = '0' + sid_hex
|
|
160
|
+
sid_bytes = binascii.unhexlify(sid_hex)
|
|
161
|
+
|
|
162
|
+
self.sid = base64_url_encode(sid_bytes[:43])
|
|
163
|
+
|
|
164
|
+
def _api_request(self, data):
|
|
165
|
+
params = {'id': self.sequence_num}
|
|
166
|
+
self.sequence_num += 1
|
|
167
|
+
|
|
168
|
+
if self.sid:
|
|
169
|
+
params.update({'sid': self.sid})
|
|
170
|
+
|
|
171
|
+
if not isinstance(data, list):
|
|
172
|
+
data = [data]
|
|
173
|
+
|
|
174
|
+
url = '{0}://g.api.{1}/cs'.format(self.schema, self.domain)
|
|
175
|
+
headers = {
|
|
176
|
+
'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',
|
|
177
|
+
'Accept': '*/*',
|
|
178
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
179
|
+
'Origin': 'https://mega.nz',
|
|
180
|
+
'Referer': 'https://mega.nz/'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
req = requests.post(
|
|
185
|
+
url,
|
|
186
|
+
params=params,
|
|
187
|
+
data=json.dumps(data),
|
|
188
|
+
headers=headers,
|
|
189
|
+
timeout=self.timeout,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
content = req.text.strip() if req.text else ""
|
|
193
|
+
|
|
194
|
+
if not content:
|
|
195
|
+
if req.status_code == 402:
|
|
196
|
+
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.")
|
|
197
|
+
raise Exception(f"Mega API returned empty response (Status {req.status_code})")
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
json_resp = json.loads(content)
|
|
201
|
+
except json.JSONDecodeError:
|
|
202
|
+
# Try to parse as integer error code
|
|
203
|
+
try:
|
|
204
|
+
val = int(content)
|
|
205
|
+
raise RequestError(val)
|
|
206
|
+
except (ValueError, RequestError):
|
|
207
|
+
if content.startswith("-") and content[1:].isdigit():
|
|
208
|
+
raise RequestError(int(content))
|
|
209
|
+
raise Exception(f"Mega API error: {content[:100]}")
|
|
210
|
+
|
|
211
|
+
if isinstance(json_resp, int):
|
|
212
|
+
if json_resp == -3:
|
|
213
|
+
time.sleep(0.5)
|
|
214
|
+
return self._api_request(data=data)
|
|
215
|
+
raise RequestError(json_resp)
|
|
216
|
+
|
|
217
|
+
return json_resp[0]
|
|
218
|
+
except Exception:
|
|
219
|
+
raise
|
|
220
|
+
|
|
221
|
+
# Global Mega session state
|
|
222
|
+
mega_engine = RobustMega()
|
|
223
|
+
mega_sessions = {} # Mapping: email -> {'client': MegaClient, 'info': user_info, 'quota': quota}
|
|
224
|
+
active_mega_email = None
|
|
29
225
|
|
|
30
226
|
try:
|
|
31
227
|
from playwright.sync_api import sync_playwright
|
|
@@ -144,6 +340,8 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
144
340
|
# Directories setup kar rahe hain
|
|
145
341
|
os.makedirs(os.path.join(SCRAPED_DIR, 'images'), exist_ok=True)
|
|
146
342
|
os.makedirs(os.path.join(SCRAPED_DIR, 'videos'), exist_ok=True)
|
|
343
|
+
CLOUD_DIR = os.path.join(os.getcwd(), 'cloud_storage')
|
|
344
|
+
os.makedirs(CLOUD_DIR, exist_ok=True)
|
|
147
345
|
|
|
148
346
|
# --- PERFORMANCE AUDITOR ---
|
|
149
347
|
class PerformanceTracker:
|
|
@@ -429,12 +627,339 @@ def serve_js():
|
|
|
429
627
|
def serve_favicon():
|
|
430
628
|
return send_from_directory(WEB_DIR, 'Web_Tools.png')
|
|
431
629
|
|
|
432
|
-
@app.route('/download/<path:filename>')
|
|
433
|
-
|
|
434
630
|
@app.route('/download/<path:filename>')
|
|
435
631
|
def serve_scraped_file(filename):
|
|
436
632
|
return send_from_directory(SCRAPED_DIR, filename)
|
|
437
633
|
|
|
634
|
+
# --- CLOUD STORAGE APIs (MEGA.NZ) ---
|
|
635
|
+
|
|
636
|
+
def _get_node_by_path(path, client=None):
|
|
637
|
+
if not client:
|
|
638
|
+
global active_mega_email, mega_sessions
|
|
639
|
+
if not active_mega_email or active_mega_email not in mega_sessions:
|
|
640
|
+
return None
|
|
641
|
+
client = mega_sessions[active_mega_email]['client']
|
|
642
|
+
|
|
643
|
+
if not path or path == '/':
|
|
644
|
+
files = client.get_files()
|
|
645
|
+
for k, v in files.items():
|
|
646
|
+
if v.get('t') == 2: # Root node
|
|
647
|
+
return k
|
|
648
|
+
return None
|
|
649
|
+
else:
|
|
650
|
+
node = client.find(path)
|
|
651
|
+
if node:
|
|
652
|
+
if isinstance(node, tuple):
|
|
653
|
+
return node[0]
|
|
654
|
+
elif hasattr(node, '__getitem__'):
|
|
655
|
+
return node[0]
|
|
656
|
+
return None
|
|
657
|
+
|
|
658
|
+
def _get_or_create_node(name, parent_id, client):
|
|
659
|
+
"""Find a folder by name under parent_id, or create it if missing."""
|
|
660
|
+
files = client.get_files()
|
|
661
|
+
for k, v in files.items():
|
|
662
|
+
if v.get('p') == parent_id and v.get('t') == 1 and v.get('a', {}).get('n') == name:
|
|
663
|
+
return k
|
|
664
|
+
# Not found, create it
|
|
665
|
+
new_node = client.create_folder(name, dest=parent_id)
|
|
666
|
+
# create_folder returns the new dict or node id depending on version
|
|
667
|
+
if isinstance(new_node, dict):
|
|
668
|
+
# find it again to be sure
|
|
669
|
+
files = client.get_files()
|
|
670
|
+
for k, v in files.items():
|
|
671
|
+
if v.get('p') == parent_id and v.get('t') == 1 and v.get('a', {}).get('n') == name:
|
|
672
|
+
return k
|
|
673
|
+
return new_node
|
|
674
|
+
|
|
675
|
+
@app.route('/api/cloud/login', methods=['POST'])
|
|
676
|
+
def cloud_login():
|
|
677
|
+
global mega_sessions, active_mega_email
|
|
678
|
+
try:
|
|
679
|
+
data = request.json or {}
|
|
680
|
+
email = data.get('email', '').strip()
|
|
681
|
+
password = data.get('password', '').strip()
|
|
682
|
+
if not email or not password:
|
|
683
|
+
return jsonify({'success': False, 'error': 'Email and password required'}), 400
|
|
684
|
+
|
|
685
|
+
client = mega_engine.login(email, password)
|
|
686
|
+
user_info = client.get_user()
|
|
687
|
+
quota_raw = client.get_quota()
|
|
688
|
+
# Normalize quota (some versions use total/space_used, others total/used)
|
|
689
|
+
quota = {
|
|
690
|
+
'total': quota_raw.get('total', 0) if isinstance(quota_raw, dict) else 0,
|
|
691
|
+
'used': quota_raw.get('space_used', quota_raw.get('used', 0)) if isinstance(quota_raw, dict) else 0
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
# Store in sessions
|
|
695
|
+
mega_sessions[email] = {
|
|
696
|
+
'client': client,
|
|
697
|
+
'info': user_info,
|
|
698
|
+
'quota': quota
|
|
699
|
+
}
|
|
700
|
+
active_mega_email = email
|
|
701
|
+
|
|
702
|
+
return jsonify({
|
|
703
|
+
'success': True,
|
|
704
|
+
'message': f'Logged in as {email}',
|
|
705
|
+
'user': user_info,
|
|
706
|
+
'quota': quota,
|
|
707
|
+
'active_email': active_mega_email,
|
|
708
|
+
'all_accounts': list(mega_sessions.keys())
|
|
709
|
+
})
|
|
710
|
+
except Exception as e:
|
|
711
|
+
error_msg = str(e)
|
|
712
|
+
if "-9" in error_msg: error_msg = "Invalid email or password"
|
|
713
|
+
elif "-11" in error_msg: error_msg = "Access denied (Blocked/Banned)"
|
|
714
|
+
elif "-3" in error_msg: error_msg = "Too many requests, please wait"
|
|
715
|
+
return jsonify({'success': False, 'error': error_msg}), 401
|
|
716
|
+
|
|
717
|
+
@app.route('/api/cloud/switch_account', methods=['POST'])
|
|
718
|
+
def cloud_switch_account():
|
|
719
|
+
global active_mega_email, mega_sessions
|
|
720
|
+
data = request.json or {}
|
|
721
|
+
email = data.get('email')
|
|
722
|
+
if email in mega_sessions:
|
|
723
|
+
active_mega_email = email
|
|
724
|
+
return jsonify({'success': True, 'active_email': active_mega_email})
|
|
725
|
+
return jsonify({'success': False, 'error': 'Account not found'}), 404
|
|
726
|
+
|
|
727
|
+
@app.route('/api/cloud/check_auth', methods=['GET'])
|
|
728
|
+
def cloud_check_auth():
|
|
729
|
+
global active_mega_email, mega_sessions
|
|
730
|
+
accounts = []
|
|
731
|
+
for email, session in mega_sessions.items():
|
|
732
|
+
try:
|
|
733
|
+
# Quick check if still valid
|
|
734
|
+
quota_raw = session['client'].get_quota()
|
|
735
|
+
quota = {
|
|
736
|
+
'total': quota_raw.get('total', 0) if isinstance(quota_raw, dict) else 0,
|
|
737
|
+
'used': quota_raw.get('space_used', quota_raw.get('used', 0)) if isinstance(quota_raw, dict) else 0
|
|
738
|
+
}
|
|
739
|
+
session['quota'] = quota # update quota
|
|
740
|
+
accounts.append({
|
|
741
|
+
'email': email,
|
|
742
|
+
'user': session['info'],
|
|
743
|
+
'quota': quota
|
|
744
|
+
})
|
|
745
|
+
except:
|
|
746
|
+
# Session might have died
|
|
747
|
+
pass
|
|
748
|
+
|
|
749
|
+
return jsonify({
|
|
750
|
+
'success': True,
|
|
751
|
+
'authenticated': len(accounts) > 0,
|
|
752
|
+
'active_email': active_mega_email,
|
|
753
|
+
'accounts': accounts
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
@app.route('/api/cloud/logout', methods=['POST'])
|
|
757
|
+
def cloud_logout():
|
|
758
|
+
global mega_sessions, active_mega_email
|
|
759
|
+
data = request.json or {}
|
|
760
|
+
email = data.get('email', active_mega_email)
|
|
761
|
+
if email in mega_sessions:
|
|
762
|
+
del mega_sessions[email]
|
|
763
|
+
if active_mega_email == email:
|
|
764
|
+
active_mega_email = list(mega_sessions.keys())[0] if mega_sessions else None
|
|
765
|
+
return jsonify({'success': True, 'message': 'Logged out', 'active_email': active_mega_email})
|
|
766
|
+
|
|
767
|
+
@app.route('/api/cloud/files', methods=['GET'])
|
|
768
|
+
def cloud_list_files():
|
|
769
|
+
global mega_sessions, active_mega_email
|
|
770
|
+
email = request.args.get('email', active_mega_email)
|
|
771
|
+
if not email or email not in mega_sessions:
|
|
772
|
+
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
|
|
773
|
+
|
|
774
|
+
try:
|
|
775
|
+
client = mega_sessions[email]['client']
|
|
776
|
+
req_path = request.args.get('path', '').strip('/')
|
|
777
|
+
parent_id = _get_node_by_path(req_path, client)
|
|
778
|
+
if not parent_id:
|
|
779
|
+
return jsonify({'success': False, 'error': 'Directory not found'}), 404
|
|
780
|
+
|
|
781
|
+
files = client.get_files()
|
|
782
|
+
items = []
|
|
783
|
+
for k, v in files.items():
|
|
784
|
+
if v.get('p') == parent_id:
|
|
785
|
+
items.append({
|
|
786
|
+
'name': v.get('a', {}).get('n'),
|
|
787
|
+
'is_dir': v.get('t') == 1,
|
|
788
|
+
'size': v.get('s', 0),
|
|
789
|
+
'modified_at': v.get('ts', 0)
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
items.sort(key=lambda x: (not x['is_dir'], (x['name'] or '').lower()))
|
|
793
|
+
return jsonify({'success': True, 'path': req_path, 'items': items})
|
|
794
|
+
except Exception as e:
|
|
795
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
796
|
+
|
|
797
|
+
@app.route('/api/cloud/import_local', methods=['POST'])
|
|
798
|
+
def cloud_import_local():
|
|
799
|
+
global mega_sessions, active_mega_email
|
|
800
|
+
try:
|
|
801
|
+
data = request.json or {}
|
|
802
|
+
local_path = data.get('path') # relative to SCRAPED_DIR or static/uploads?
|
|
803
|
+
file_type = data.get('type') # 'image' or 'video'
|
|
804
|
+
page_title = data.get('title', 'Imported Media').strip() or 'Imported Media'
|
|
805
|
+
email = data.get('email', active_mega_email)
|
|
806
|
+
|
|
807
|
+
if not email or email not in mega_sessions:
|
|
808
|
+
return jsonify({'success': False, 'error': 'Cloud login required'}), 401
|
|
809
|
+
|
|
810
|
+
if not local_path:
|
|
811
|
+
return jsonify({'success': False, 'error': 'No file path provided'}), 400
|
|
812
|
+
|
|
813
|
+
# Handle local path check
|
|
814
|
+
# Usually scraped files are in SCRAPED_DIR
|
|
815
|
+
# If path starts with /download/ we strip it
|
|
816
|
+
clean_path = local_path.split('?')[0] # remove cache busters
|
|
817
|
+
if clean_path.startswith('/download/'):
|
|
818
|
+
filename = clean_path.replace('/download/', '')
|
|
819
|
+
disk_path = os.path.join(SCRAPED_DIR, filename)
|
|
820
|
+
else:
|
|
821
|
+
# Ad-hoc filenames
|
|
822
|
+
filename = os.path.basename(clean_path)
|
|
823
|
+
disk_path = os.path.join(SCRAPED_DIR, filename)
|
|
824
|
+
|
|
825
|
+
if not os.path.exists(disk_path):
|
|
826
|
+
return jsonify({'success': False, 'error': f'File not found: {filename}'}), 404
|
|
827
|
+
|
|
828
|
+
client = mega_sessions[email]['client']
|
|
829
|
+
|
|
830
|
+
# 1. Ensure Root Folder for this Page
|
|
831
|
+
root_node = _get_node_by_path('', client)
|
|
832
|
+
page_node = _get_or_create_node(page_title, root_node, client)
|
|
833
|
+
|
|
834
|
+
# 2. Ensure Type Folder (Images / Videos)
|
|
835
|
+
type_folder_name = "Images" if file_type == 'image' else "Videos"
|
|
836
|
+
type_node = _get_or_create_node(type_folder_name, page_node, client)
|
|
837
|
+
|
|
838
|
+
# 3. Upload
|
|
839
|
+
client.upload(disk_path, dest=type_node)
|
|
840
|
+
|
|
841
|
+
return jsonify({'success': True, 'message': f'Saved to {page_title}/{type_folder_name}'})
|
|
842
|
+
except Exception as e:
|
|
843
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
844
|
+
|
|
845
|
+
@app.route('/api/cloud/upload', methods=['POST'])
|
|
846
|
+
def cloud_upload():
|
|
847
|
+
global mega_sessions, active_mega_email
|
|
848
|
+
email = request.form.get('email', active_mega_email)
|
|
849
|
+
if not email or email not in mega_sessions:
|
|
850
|
+
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
|
|
851
|
+
try:
|
|
852
|
+
client = mega_sessions[email]['client']
|
|
853
|
+
req_path = request.form.get('path', '').strip('/')
|
|
854
|
+
parent_id = _get_node_by_path(req_path, client)
|
|
855
|
+
if not parent_id:
|
|
856
|
+
return jsonify({'success': False, 'error': 'Invalid path'}), 403
|
|
857
|
+
|
|
858
|
+
if 'file' not in request.files:
|
|
859
|
+
return jsonify({'success': False, 'error': 'No file part'}), 400
|
|
860
|
+
|
|
861
|
+
file = request.files['file']
|
|
862
|
+
if file.filename == '':
|
|
863
|
+
return jsonify({'success': False, 'error': 'No selected file'}), 400
|
|
864
|
+
|
|
865
|
+
from werkzeug.utils import secure_filename
|
|
866
|
+
filename = secure_filename(file.filename)
|
|
867
|
+
|
|
868
|
+
temp_dir = os.path.join(PACKAGE_DIR, 'tmp_mega')
|
|
869
|
+
os.makedirs(temp_dir, exist_ok=True)
|
|
870
|
+
temp_path = os.path.join(temp_dir, filename)
|
|
871
|
+
file.save(temp_path)
|
|
872
|
+
|
|
873
|
+
client.upload(temp_path, parent_id)
|
|
874
|
+
|
|
875
|
+
try: os.remove(temp_path)
|
|
876
|
+
except: pass
|
|
877
|
+
|
|
878
|
+
return jsonify({'success': True, 'message': 'File uploaded successfully'})
|
|
879
|
+
except Exception as e:
|
|
880
|
+
import traceback
|
|
881
|
+
traceback.print_exc()
|
|
882
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
883
|
+
|
|
884
|
+
@app.route('/api/cloud/mkdir', methods=['POST'])
|
|
885
|
+
def cloud_mkdir():
|
|
886
|
+
global mega_sessions, active_mega_email
|
|
887
|
+
data = request.json or {}
|
|
888
|
+
email = data.get('email', active_mega_email)
|
|
889
|
+
if not email or email not in mega_sessions:
|
|
890
|
+
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
|
|
891
|
+
try:
|
|
892
|
+
client = mega_sessions[email]['client']
|
|
893
|
+
req_path = data.get('path', '').strip('/')
|
|
894
|
+
folder_name = data.get('folder_name', '').strip()
|
|
895
|
+
|
|
896
|
+
if not folder_name:
|
|
897
|
+
return jsonify({'success': False, 'error': 'Folder name required'}), 400
|
|
898
|
+
|
|
899
|
+
parent_id = _get_node_by_path(req_path, client)
|
|
900
|
+
if not parent_id:
|
|
901
|
+
return jsonify({'success': False, 'error': 'Invalid path'}), 403
|
|
902
|
+
|
|
903
|
+
client.create_folder(folder_name, parent_id)
|
|
904
|
+
return jsonify({'success': True, 'message': 'Folder created'})
|
|
905
|
+
except Exception as e:
|
|
906
|
+
import traceback
|
|
907
|
+
traceback.print_exc()
|
|
908
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
909
|
+
|
|
910
|
+
@app.route('/api/cloud/delete', methods=['DELETE'])
|
|
911
|
+
def cloud_delete():
|
|
912
|
+
global mega_sessions, active_mega_email
|
|
913
|
+
data = request.json or {}
|
|
914
|
+
email = data.get('email', active_mega_email)
|
|
915
|
+
if not email or email not in mega_sessions:
|
|
916
|
+
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
|
|
917
|
+
try:
|
|
918
|
+
client = mega_sessions[email]['client']
|
|
919
|
+
req_path = data.get('path', '').strip('/')
|
|
920
|
+
item_name = data.get('item_name', '').strip()
|
|
921
|
+
|
|
922
|
+
target_path = f"{req_path}/{item_name}".strip('/')
|
|
923
|
+
node_id = _get_node_by_path(target_path, client)
|
|
924
|
+
|
|
925
|
+
if not node_id:
|
|
926
|
+
return jsonify({'success': False, 'error': 'Item not found'}), 404
|
|
927
|
+
|
|
928
|
+
client.destroy(node_id)
|
|
929
|
+
return jsonify({'success': True, 'message': 'Item deleted'})
|
|
930
|
+
except Exception as e:
|
|
931
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
932
|
+
|
|
933
|
+
@app.route('/api/cloud/download/<path:filepath>')
|
|
934
|
+
def cloud_download(filepath):
|
|
935
|
+
global mega_sessions, active_mega_email
|
|
936
|
+
email = request.args.get('email', active_mega_email)
|
|
937
|
+
if not email or email not in mega_sessions:
|
|
938
|
+
return "Not authenticated", 401
|
|
939
|
+
try:
|
|
940
|
+
client = mega_sessions[email]['client']
|
|
941
|
+
node_id = _get_node_by_path(filepath, client)
|
|
942
|
+
if not node_id:
|
|
943
|
+
return "File not found", 404
|
|
944
|
+
|
|
945
|
+
# Download to local temp folder, then serve it
|
|
946
|
+
temp_dir = os.path.join(PACKAGE_DIR, 'tmp_mega_dl')
|
|
947
|
+
os.makedirs(temp_dir, exist_ok=True)
|
|
948
|
+
# find node object first
|
|
949
|
+
files = client.get_files()
|
|
950
|
+
node = {node_id: files[node_id]}
|
|
951
|
+
downloaded_path = client.download(node, temp_dir)
|
|
952
|
+
|
|
953
|
+
@after_this_request
|
|
954
|
+
def remove_file(response):
|
|
955
|
+
try: os.remove(downloaded_path)
|
|
956
|
+
except: pass
|
|
957
|
+
return response
|
|
958
|
+
|
|
959
|
+
return send_file(downloaded_path, as_attachment=True)
|
|
960
|
+
except Exception as e:
|
|
961
|
+
return str(e), 500
|
|
962
|
+
|
|
438
963
|
def _run_playwright_scrape(url):
|
|
439
964
|
"""Internal: Actually run the Playwright scrape"""
|
|
440
965
|
with sync_playwright() as p:
|