omnius 1.0.31 → 1.0.32
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.
- package/dist/index.js +962 -873
- package/dist/scripts/web_scrape.py +151 -6
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -140,6 +140,26 @@ def log_message(msg: str, level: str = "INFO") -> None:
|
|
|
140
140
|
print(f"[{ts}] [{level.upper()}] {msg}")
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def _bounded_int(value, default: int, minimum: int, maximum: int) -> int:
|
|
144
|
+
try:
|
|
145
|
+
n = int(float(value))
|
|
146
|
+
except Exception:
|
|
147
|
+
n = default
|
|
148
|
+
return max(minimum, min(maximum, n))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _bounded_float(value, default: float, minimum: float, maximum: float) -> float:
|
|
152
|
+
try:
|
|
153
|
+
n = float(value)
|
|
154
|
+
except Exception:
|
|
155
|
+
n = default
|
|
156
|
+
return max(minimum, min(maximum, n))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _truthy(value) -> bool:
|
|
160
|
+
return str(value).lower() in ("1", "true", "yes", "on")
|
|
161
|
+
|
|
162
|
+
|
|
143
163
|
class Tools:
|
|
144
164
|
_driver: Optional[webdriver.Chrome] = None
|
|
145
165
|
|
|
@@ -163,7 +183,13 @@ class Tools:
|
|
|
163
183
|
return None
|
|
164
184
|
|
|
165
185
|
@staticmethod
|
|
166
|
-
def open_browser(
|
|
186
|
+
def open_browser(
|
|
187
|
+
headless: bool = False,
|
|
188
|
+
force_new: bool = False,
|
|
189
|
+
width: int = 1280,
|
|
190
|
+
height: int = 720,
|
|
191
|
+
device_scale_factor: float = 1.0,
|
|
192
|
+
) -> str:
|
|
167
193
|
if force_new and Tools._driver:
|
|
168
194
|
try:
|
|
169
195
|
Tools._driver.quit()
|
|
@@ -184,12 +210,16 @@ class Tools:
|
|
|
184
210
|
or "/usr/bin/chromium"
|
|
185
211
|
)
|
|
186
212
|
|
|
213
|
+
width = _bounded_int(width, 1280, 320, 3840)
|
|
214
|
+
height = _bounded_int(height, 720, 240, 2160)
|
|
215
|
+
device_scale_factor = _bounded_float(device_scale_factor, 1.0, 0.25, 3.0)
|
|
216
|
+
|
|
187
217
|
opts = Options()
|
|
188
218
|
if chrome_bin:
|
|
189
219
|
opts.binary_location = chrome_bin
|
|
190
220
|
if headless:
|
|
191
221
|
opts.add_argument("--headless=new")
|
|
192
|
-
opts.add_argument("--window-size=
|
|
222
|
+
opts.add_argument(f"--window-size={width},{height}")
|
|
193
223
|
opts.add_argument("--disable-gpu")
|
|
194
224
|
opts.add_argument("--no-sandbox")
|
|
195
225
|
opts.add_argument("--disable-dev-shm-usage")
|
|
@@ -199,6 +229,7 @@ class Tools:
|
|
|
199
229
|
try:
|
|
200
230
|
log_message("[open_browser] Trying Selenium-Manager…", "DEBUG")
|
|
201
231
|
Tools._driver = webdriver.Chrome(options=opts)
|
|
232
|
+
Tools.set_viewport(width, height, device_scale_factor)
|
|
202
233
|
log_message("[open_browser] Launched via Selenium-Manager.", "SUCCESS")
|
|
203
234
|
return "Browser launched (selenium-manager)"
|
|
204
235
|
except WebDriverException as e:
|
|
@@ -209,6 +240,7 @@ class Tools:
|
|
|
209
240
|
try:
|
|
210
241
|
log_message(f"[open_browser] Using snap chromedriver at {snap_drv}", "DEBUG")
|
|
211
242
|
Tools._driver = webdriver.Chrome(service=Service(snap_drv), options=opts)
|
|
243
|
+
Tools.set_viewport(width, height, device_scale_factor)
|
|
212
244
|
log_message("[open_browser] Launched via snap chromedriver.", "SUCCESS")
|
|
213
245
|
return "Browser launched (snap chromedriver)"
|
|
214
246
|
except WebDriverException as e:
|
|
@@ -219,6 +251,7 @@ class Tools:
|
|
|
219
251
|
try:
|
|
220
252
|
log_message(f"[open_browser] Trying system chromedriver at {sys_drv}", "DEBUG")
|
|
221
253
|
Tools._driver = webdriver.Chrome(service=Service(sys_drv), options=opts)
|
|
254
|
+
Tools.set_viewport(width, height, device_scale_factor)
|
|
222
255
|
log_message("[open_browser] Launched via system chromedriver.", "SUCCESS")
|
|
223
256
|
return "Browser launched (system chromedriver)"
|
|
224
257
|
except WebDriverException as e:
|
|
@@ -242,6 +275,7 @@ class Tools:
|
|
|
242
275
|
drv = shutil.which("chromedriver")
|
|
243
276
|
log_message(f"[open_browser] Installed ARM64 driver at {drv}", "DEBUG")
|
|
244
277
|
Tools._driver = webdriver.Chrome(service=Service(drv), options=opts)
|
|
278
|
+
Tools.set_viewport(width, height, device_scale_factor)
|
|
245
279
|
log_message("[open_browser] Launched via downloaded ARM64 chromedriver.", "SUCCESS")
|
|
246
280
|
return "Browser launched (downloaded ARM64 chromedriver)"
|
|
247
281
|
except Exception as e:
|
|
@@ -257,6 +291,7 @@ class Tools:
|
|
|
257
291
|
log_message(f"[open_browser] Installing ChromeDriver {browser_major} via webdriver-manager", "DEBUG")
|
|
258
292
|
drv_path = ChromeDriverManager(driver_version=browser_major).install()
|
|
259
293
|
Tools._driver = webdriver.Chrome(service=Service(drv_path), options=opts)
|
|
294
|
+
Tools.set_viewport(width, height, device_scale_factor)
|
|
260
295
|
log_message("[open_browser] Launched via webdriver-manager.", "SUCCESS")
|
|
261
296
|
return "Browser launched (webdriver-manager)"
|
|
262
297
|
except Exception as e:
|
|
@@ -266,6 +301,7 @@ class Tools:
|
|
|
266
301
|
log_message("[open_browser] Attempting `sudo snap install chromium`…", "DEBUG")
|
|
267
302
|
subprocess.check_call(["sudo", "snap", "install", "chromium"])
|
|
268
303
|
Tools._driver = webdriver.Chrome(service=Service(snap_drv), options=opts)
|
|
304
|
+
Tools.set_viewport(width, height, device_scale_factor)
|
|
269
305
|
log_message("[open_browser] Launched via newly-installed snap chromium.", "SUCCESS")
|
|
270
306
|
return "Browser launched (snap install fallback)"
|
|
271
307
|
except Exception as e:
|
|
@@ -292,6 +328,46 @@ class Tools:
|
|
|
292
328
|
def is_browser_open() -> bool:
|
|
293
329
|
return Tools._driver is not None
|
|
294
330
|
|
|
331
|
+
@staticmethod
|
|
332
|
+
def set_viewport(width: int = 1280, height: int = 720, device_scale_factor: float = 1.0) -> Dict[str, float]:
|
|
333
|
+
if not Tools._driver:
|
|
334
|
+
raise RuntimeError("browser not open")
|
|
335
|
+
width = _bounded_int(width, 1280, 320, 3840)
|
|
336
|
+
height = _bounded_int(height, 720, 240, 2160)
|
|
337
|
+
device_scale_factor = _bounded_float(device_scale_factor, 1.0, 0.25, 3.0)
|
|
338
|
+
try:
|
|
339
|
+
Tools._driver.set_window_size(width, height)
|
|
340
|
+
except Exception as exc:
|
|
341
|
+
log_message(f"[viewport] set_window_size failed: {exc}", "WARNING")
|
|
342
|
+
try:
|
|
343
|
+
Tools._driver.execute_cdp_cmd("Emulation.setDeviceMetricsOverride", {
|
|
344
|
+
"mobile": False,
|
|
345
|
+
"width": width,
|
|
346
|
+
"height": height,
|
|
347
|
+
"deviceScaleFactor": device_scale_factor,
|
|
348
|
+
})
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
log_message(f"[viewport] CDP override failed: {exc}", "DEBUG")
|
|
351
|
+
return Tools.viewport()
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def viewport() -> Dict[str, float]:
|
|
355
|
+
if not Tools._driver:
|
|
356
|
+
return {"width": 0, "height": 0, "deviceScaleFactor": 1}
|
|
357
|
+
try:
|
|
358
|
+
data = Tools._driver.execute_script(
|
|
359
|
+
"return {width: window.innerWidth, height: window.innerHeight, deviceScaleFactor: window.devicePixelRatio || 1};"
|
|
360
|
+
)
|
|
361
|
+
if isinstance(data, dict):
|
|
362
|
+
return {
|
|
363
|
+
"width": int(data.get("width") or 0),
|
|
364
|
+
"height": int(data.get("height") or 0),
|
|
365
|
+
"deviceScaleFactor": float(data.get("deviceScaleFactor") or 1),
|
|
366
|
+
}
|
|
367
|
+
except Exception as exc:
|
|
368
|
+
log_message(f"[viewport] read failed: {exc}", "DEBUG")
|
|
369
|
+
return {"width": 0, "height": 0, "deviceScaleFactor": 1}
|
|
370
|
+
|
|
295
371
|
@staticmethod
|
|
296
372
|
def navigate(url: str) -> str:
|
|
297
373
|
if not Tools._driver:
|
|
@@ -357,9 +433,36 @@ class Tools:
|
|
|
357
433
|
return f"Error scrolling: {exc}"
|
|
358
434
|
|
|
359
435
|
@staticmethod
|
|
360
|
-
def screenshot(filename: str = "screenshot.png") -> str:
|
|
436
|
+
def screenshot(filename: str = "screenshot.png", full_page: bool = False) -> str:
|
|
361
437
|
if not Tools._driver:
|
|
362
438
|
return "Error: browser not open"
|
|
439
|
+
# Prefer Chrome DevTools Protocol capture. It renders the browser
|
|
440
|
+
# surface directly, so this path cannot accidentally fall back to an
|
|
441
|
+
# OS desktop screenshot tool.
|
|
442
|
+
try:
|
|
443
|
+
drv = Tools._driver
|
|
444
|
+
layout = drv.execute_cdp_cmd("Page.getLayoutMetrics", {})
|
|
445
|
+
if full_page:
|
|
446
|
+
content = layout.get("contentSize") or {}
|
|
447
|
+
width = int(content.get("width") or 1280)
|
|
448
|
+
height = int(content.get("height") or 720)
|
|
449
|
+
clip = {"x": 0, "y": 0, "width": width, "height": height, "scale": 1}
|
|
450
|
+
else:
|
|
451
|
+
viewport = Tools.viewport()
|
|
452
|
+
width = int(viewport.get("width") or 1280)
|
|
453
|
+
height = int(viewport.get("height") or 720)
|
|
454
|
+
clip = {"x": 0, "y": 0, "width": width, "height": height, "scale": 1}
|
|
455
|
+
data = drv.execute_cdp_cmd("Page.captureScreenshot", {
|
|
456
|
+
"format": "png",
|
|
457
|
+
"fromSurface": True,
|
|
458
|
+
"captureBeyondViewport": bool(full_page),
|
|
459
|
+
"clip": clip,
|
|
460
|
+
})
|
|
461
|
+
raw = base64.b64decode(data.get("data") or "")
|
|
462
|
+
Path(filename).write_bytes(raw)
|
|
463
|
+
return filename
|
|
464
|
+
except Exception as exc:
|
|
465
|
+
log_message(f"[screenshot] CDP capture failed, falling back to webdriver screenshot: {exc}", "WARNING")
|
|
363
466
|
Tools._driver.save_screenshot(filename)
|
|
364
467
|
return filename
|
|
365
468
|
|
|
@@ -827,8 +930,17 @@ def session_start():
|
|
|
827
930
|
return _error("unauthorized", 401)
|
|
828
931
|
payload = request.get_json(silent=True) or {}
|
|
829
932
|
headless = bool(payload.get("headless", HEADLESS_DEFAULT))
|
|
933
|
+
width = _bounded_int(payload.get("width"), 1280, 320, 3840)
|
|
934
|
+
height = _bounded_int(payload.get("height"), 720, 240, 2160)
|
|
935
|
+
device_scale_factor = _bounded_float(payload.get("deviceScaleFactor") or payload.get("device_scale_factor"), 1.0, 0.25, 3.0)
|
|
830
936
|
with _slot():
|
|
831
|
-
msg = Tools.open_browser(
|
|
937
|
+
msg = Tools.open_browser(
|
|
938
|
+
headless=headless,
|
|
939
|
+
force_new=True,
|
|
940
|
+
width=width,
|
|
941
|
+
height=height,
|
|
942
|
+
device_scale_factor=device_scale_factor,
|
|
943
|
+
)
|
|
832
944
|
if not _result_ok(msg):
|
|
833
945
|
return _error(msg, 500)
|
|
834
946
|
sid = uuid.uuid4().hex
|
|
@@ -842,7 +954,7 @@ def session_start():
|
|
|
842
954
|
"frames": {},
|
|
843
955
|
}
|
|
844
956
|
_queue_event(sid, {"type": "status", "msg": "browser_started", "detail": msg, "sid": sid, "ts": int(time.time() * 1000)})
|
|
845
|
-
return _ok(session_id=sid, message=msg, headless=headless)
|
|
957
|
+
return _ok(session_id=sid, message=msg, headless=headless, viewport=Tools.viewport())
|
|
846
958
|
|
|
847
959
|
|
|
848
960
|
@app.post("/session/close")
|
|
@@ -855,6 +967,29 @@ def session_close():
|
|
|
855
967
|
return _ok(message=msg)
|
|
856
968
|
|
|
857
969
|
|
|
970
|
+
@app.route("/viewport", methods=["GET", "POST"])
|
|
971
|
+
def viewport():
|
|
972
|
+
if not _auth_ok(request):
|
|
973
|
+
return _error("unauthorized", 401)
|
|
974
|
+
if not Tools.is_browser_open():
|
|
975
|
+
return _error("browser not open", 409)
|
|
976
|
+
if request.method == "POST":
|
|
977
|
+
payload = request.get_json(silent=True) or {}
|
|
978
|
+
width = _bounded_int(payload.get("width"), 1280, 320, 3840)
|
|
979
|
+
height = _bounded_int(payload.get("height"), 720, 240, 2160)
|
|
980
|
+
device_scale_factor = _bounded_float(
|
|
981
|
+
payload.get("deviceScaleFactor") or payload.get("device_scale_factor"),
|
|
982
|
+
1.0,
|
|
983
|
+
0.25,
|
|
984
|
+
3.0,
|
|
985
|
+
)
|
|
986
|
+
with _slot():
|
|
987
|
+
vp = Tools.set_viewport(width, height, device_scale_factor)
|
|
988
|
+
else:
|
|
989
|
+
vp = Tools.viewport()
|
|
990
|
+
return _ok(viewport=vp, width=vp.get("width"), height=vp.get("height"), deviceScaleFactor=vp.get("deviceScaleFactor"))
|
|
991
|
+
|
|
992
|
+
|
|
858
993
|
@app.post("/navigate")
|
|
859
994
|
def navigate():
|
|
860
995
|
if not _auth_ok(request):
|
|
@@ -1200,10 +1335,20 @@ def screenshot():
|
|
|
1200
1335
|
if not _auth_ok(request):
|
|
1201
1336
|
return _error("unauthorized", 401)
|
|
1202
1337
|
sid = request.args.get("sid") or next(iter(_SESSIONS), "")
|
|
1338
|
+
width_raw = request.args.get("width")
|
|
1339
|
+
height_raw = request.args.get("height")
|
|
1340
|
+
scale_raw = request.args.get("deviceScaleFactor") or request.args.get("device_scale_factor")
|
|
1341
|
+
if width_raw or height_raw or scale_raw:
|
|
1342
|
+
width = _bounded_int(width_raw, 1280, 320, 3840)
|
|
1343
|
+
height = _bounded_int(height_raw, 720, 240, 2160)
|
|
1344
|
+
device_scale_factor = _bounded_float(scale_raw, 1.0, 0.25, 3.0)
|
|
1345
|
+
with _slot():
|
|
1346
|
+
Tools.set_viewport(width, height, device_scale_factor)
|
|
1347
|
+
full_page = _truthy(request.args.get("full_page") or request.args.get("fullPage"))
|
|
1203
1348
|
fname = f"{uuid.uuid4().hex}.png"
|
|
1204
1349
|
fpath = OUT_DIR / fname
|
|
1205
1350
|
with _slot():
|
|
1206
|
-
msg = Tools.screenshot(str(fpath))
|
|
1351
|
+
msg = Tools.screenshot(str(fpath), full_page=full_page)
|
|
1207
1352
|
if not _result_ok(msg):
|
|
1208
1353
|
return _error(msg, 500)
|
|
1209
1354
|
try:
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnius",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "omnius",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.32",
|
|
10
10
|
"bundleDependencies": [
|
|
11
11
|
"image-to-ascii"
|
|
12
12
|
],
|
package/package.json
CHANGED