PlaywrightCapture 1.41.6__tar.gz → 1.41.7__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.
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/PKG-INFO +3 -3
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/playwrightcapture/capture.py +58 -7
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/pyproject.toml +4 -4
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/LICENSE +0 -0
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/README.md +0 -0
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/playwrightcapture/__init__.py +0 -0
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/playwrightcapture/exceptions.py +0 -0
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/playwrightcapture/helpers.py +0 -0
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/playwrightcapture/py.typed +0 -0
- {playwrightcapture-1.41.6 → playwrightcapture-1.41.7}/playwrightcapture/socks5dnslookup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PlaywrightCapture
|
|
3
|
-
Version: 1.41.
|
|
3
|
+
Version: 1.41.7
|
|
4
4
|
Summary: A simple library to capture websites using playwright
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
License-File: LICENSE
|
|
@@ -25,11 +25,11 @@ Requires-Dist: async-timeout (>=5.0.1) ; python_version < "3.11"
|
|
|
25
25
|
Requires-Dist: beautifulsoup4[charset-normalizer,lxml] (>=4.15.0)
|
|
26
26
|
Requires-Dist: charset-normalizer (>=3.4.6,<4.0.0)
|
|
27
27
|
Requires-Dist: dnspython (>=2.7.0,<3.0.0)
|
|
28
|
-
Requires-Dist: lookyloo-models (>=0.4.
|
|
28
|
+
Requires-Dist: lookyloo-models (>=0.4.6)
|
|
29
29
|
Requires-Dist: orjson (>=3.12,<4.0.0)
|
|
30
30
|
Requires-Dist: playwright (>=1.63.0)
|
|
31
31
|
Requires-Dist: playwright-stealth (>=2.0.3)
|
|
32
|
-
Requires-Dist: pure-magic-rs (>=0.5)
|
|
32
|
+
Requires-Dist: pure-magic-rs (>=0.5.1)
|
|
33
33
|
Requires-Dist: pydub-ng (>=0.2.0) ; extra == "recaptcha"
|
|
34
34
|
Requires-Dist: pyfaup-rs (>=0.4.6,<0.5.0)
|
|
35
35
|
Requires-Dist: python-socks (>=3.0.0,<4.0.0)
|
|
@@ -17,7 +17,8 @@ import time
|
|
|
17
17
|
from base64 import b64decode, b64encode
|
|
18
18
|
from io import BytesIO
|
|
19
19
|
from logging import LoggerAdapter, Logger
|
|
20
|
-
from
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
21
22
|
from typing import Any, Literal, TYPE_CHECKING
|
|
22
23
|
from collections.abc import Awaitable, Callable, MutableMapping
|
|
23
24
|
from urllib.parse import urlparse, unquote, urljoin, urlsplit, urlunsplit, parse_qs, unquote_plus
|
|
@@ -102,6 +103,11 @@ class CaptureResponse(TypedDict, total=False):
|
|
|
102
103
|
# in the meantime, we need a workaround: https://github.com/Lookyloo/PlaywrightCapture/issues/45
|
|
103
104
|
potential_favicons: set[bytes] | None
|
|
104
105
|
|
|
106
|
+
# If enabled, the capture will store videos and return them
|
|
107
|
+
# In a multi-page context, we might have more than one video. In that case, we do the same as for downloads: an archive
|
|
108
|
+
video_filename: str | None
|
|
109
|
+
video_file: bytes | None
|
|
110
|
+
|
|
105
111
|
|
|
106
112
|
class PlaywrightCaptureLogAdapter(LoggerAdapter): # type: ignore[type-arg]
|
|
107
113
|
"""
|
|
@@ -204,6 +210,7 @@ class Capture():
|
|
|
204
210
|
self.allow_tracking = capture_settings.allow_tracking
|
|
205
211
|
self.rendered_hostname_only = capture_settings.rendered_hostname_only
|
|
206
212
|
self.with_screenshot = capture_settings.with_screenshot
|
|
213
|
+
self.with_video = capture_settings.with_video
|
|
207
214
|
self.with_favicon = capture_settings.with_favicon
|
|
208
215
|
self.with_trusted_timestamps = capture_settings.with_trusted_timestamps
|
|
209
216
|
self.capture_depth = capture_settings.depth
|
|
@@ -303,6 +310,11 @@ class Capture():
|
|
|
303
310
|
# Create the temporary file to store the HAR content.
|
|
304
311
|
self._temp_harfile = NamedTemporaryFile(delete=False, prefix="playwright_capture_har", suffix=".json")
|
|
305
312
|
|
|
313
|
+
# if the capture is triggered with video, create the directory
|
|
314
|
+
self._temp_video_dir: TemporaryDirectory[str] | None = None
|
|
315
|
+
if self.with_video:
|
|
316
|
+
self._temp_video_dir = TemporaryDirectory(delete=False, prefix="playwright_capture_video", ignore_cleanup_errors=True)
|
|
317
|
+
|
|
306
318
|
# all the errors gathered during the capture
|
|
307
319
|
self.errors: list[str] = []
|
|
308
320
|
|
|
@@ -325,11 +337,16 @@ class Capture():
|
|
|
325
337
|
# this should't happen, but just in case it does...
|
|
326
338
|
self.logger.info(f'Unable to stop playwright: {e}')
|
|
327
339
|
|
|
328
|
-
|
|
340
|
+
try:
|
|
341
|
+
os.unlink(self._temp_harfile.name)
|
|
342
|
+
except Exception as e:
|
|
343
|
+
self.logger.warning(f'Unable to remove temp HAR file {self._temp_harfile.name}: {e}')
|
|
344
|
+
|
|
345
|
+
if self.with_video and self._temp_video_dir:
|
|
329
346
|
try:
|
|
330
|
-
|
|
347
|
+
self._temp_video_dir.cleanup()
|
|
331
348
|
except Exception as e:
|
|
332
|
-
self.logger.warning(f'Unable to remove temp
|
|
349
|
+
self.logger.warning(f'Unable to remove temp video directory {self._temp_video_dir.name}: {e}')
|
|
333
350
|
|
|
334
351
|
if exc_type:
|
|
335
352
|
self.logger.warning(f'An exception occured during the capture: {exc_value}')
|
|
@@ -514,6 +531,18 @@ class Capture():
|
|
|
514
531
|
continue
|
|
515
532
|
self._headers[name] = value
|
|
516
533
|
|
|
534
|
+
def __video_size(self, viewport: dict[str, int] | None) -> dict[str, int]:
|
|
535
|
+
if viewport is None:
|
|
536
|
+
viewport = self._default_viewport
|
|
537
|
+
pref_width = 1000
|
|
538
|
+
pref_height = 1000
|
|
539
|
+
pref_ratio = pref_width / pref_height
|
|
540
|
+
ratio = viewport['width'] / viewport['height']
|
|
541
|
+
if ratio > pref_ratio:
|
|
542
|
+
return {'width': pref_width, 'height': int(viewport['height'] * (pref_width / viewport['width']))}
|
|
543
|
+
else:
|
|
544
|
+
return {'width': int(viewport['width'] * (pref_height / viewport['height'])), 'height': pref_height}
|
|
545
|
+
|
|
517
546
|
async def initialize_context(self) -> None:
|
|
518
547
|
device_context_settings = {}
|
|
519
548
|
vp: dict[str, int] | None = None
|
|
@@ -550,8 +579,8 @@ class Capture():
|
|
|
550
579
|
color_scheme=self._color_scheme,
|
|
551
580
|
viewport=vp if vp else self._default_viewport, # type: ignore[arg-type]
|
|
552
581
|
storage_state=self._storage, # type: ignore[arg-type]
|
|
553
|
-
|
|
554
|
-
#
|
|
582
|
+
record_video_dir=self._temp_video_dir.name if self._temp_video_dir else None,
|
|
583
|
+
record_video_size=self.__video_size(vp) if self._temp_video_dir else None, # type: ignore[arg-type]
|
|
555
584
|
**device_context_settings
|
|
556
585
|
)
|
|
557
586
|
self.context.set_default_timeout(self._capture_timeout * 1000)
|
|
@@ -1175,6 +1204,24 @@ class Capture():
|
|
|
1175
1204
|
to_return['har'] = orjson.loads(_har.read())
|
|
1176
1205
|
self.logger.debug('Got HAR.')
|
|
1177
1206
|
|
|
1207
|
+
if self.with_video and self._temp_video_dir:
|
|
1208
|
+
videos: list[tuple[str, bytes]] = []
|
|
1209
|
+
for video_file in Path(self._temp_video_dir.name).iterdir():
|
|
1210
|
+
with video_file.open('rb') as _vf:
|
|
1211
|
+
videos.append((os.path.basename(video_file), _vf.read()))
|
|
1212
|
+
|
|
1213
|
+
if len(videos) == 1:
|
|
1214
|
+
to_return["video_filename"] = videos[0][0]
|
|
1215
|
+
to_return["video_file"] = videos[0][1]
|
|
1216
|
+
else:
|
|
1217
|
+
mem_zip = BytesIO()
|
|
1218
|
+
to_return["video_filename"] = f'{self.uuid}_multiple_videos.zip'
|
|
1219
|
+
with ZipFile(mem_zip, 'w') as z:
|
|
1220
|
+
for i, f_details in enumerate(videos):
|
|
1221
|
+
filename, file_content = f_details
|
|
1222
|
+
z.writestr(f'{i}_{filename}', file_content)
|
|
1223
|
+
to_return["video_file"] = mem_zip.getvalue()
|
|
1224
|
+
|
|
1178
1225
|
# When using a socks5 proxy, post-process the HAR to resolve IPs via
|
|
1179
1226
|
# the proxy so the stored HAR contains addresses consistent with what
|
|
1180
1227
|
# the proxy saw.
|
|
@@ -1588,6 +1635,10 @@ class Capture():
|
|
|
1588
1635
|
to_timestamp['downloaded_filename'] = TimestampRequestBuilder().data(downloaded_filename.encode())
|
|
1589
1636
|
if downloaded_file := capture_response.get('downloaded_file'):
|
|
1590
1637
|
to_timestamp['downloaded_file'] = TimestampRequestBuilder().data(downloaded_file)
|
|
1638
|
+
if video_filename := capture_response.get('video_filename'):
|
|
1639
|
+
to_timestamp['video_filename'] = TimestampRequestBuilder().data(video_filename.encode())
|
|
1640
|
+
if video_file := capture_response.get('video_file'):
|
|
1641
|
+
to_timestamp['video_file'] = TimestampRequestBuilder().data(video_file)
|
|
1591
1642
|
# if potential_favicons := capture_response.get('potential_favicons'):
|
|
1592
1643
|
# to_timestamp['potential_favicons'] = TimestampRequestBuilder().data(potential_favicons)
|
|
1593
1644
|
|
|
@@ -1946,6 +1997,7 @@ class Capture():
|
|
|
1946
1997
|
'NS_ERROR_NET_ERROR_RESPONSE',
|
|
1947
1998
|
'NS_ERROR_NET_PARTIAL_TRANSFER',
|
|
1948
1999
|
'NS_ERROR_PARSED_DATA_CACHED',
|
|
2000
|
+
'net::ERR_HTTP2_PROTOCOL_ERROR',
|
|
1949
2001
|
'net::ERR_CONNECTION_RESET',
|
|
1950
2002
|
'net::ERR_EMPTY_RESPONSE',
|
|
1951
2003
|
'net::ERR_INVALID_RESPONSE',
|
|
@@ -1981,7 +2033,6 @@ class Capture():
|
|
|
1981
2033
|
'net::ERR_CONNECTION_REFUSED',
|
|
1982
2034
|
'net::ERR_CONNECTION_TIMED_OUT',
|
|
1983
2035
|
'net::ERR_HTTP_RESPONSE_CODE_FAILURE',
|
|
1984
|
-
'net::ERR_HTTP2_PROTOCOL_ERROR',
|
|
1985
2036
|
'net::ERR_INVALID_HTTP_RESPONSE',
|
|
1986
2037
|
'net::ERR_INVALID_REDIRECT',
|
|
1987
2038
|
'net::ERR_NAME_NOT_RESOLVED',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "PlaywrightCapture"
|
|
3
|
-
version = "1.41.
|
|
3
|
+
version = "1.41.7"
|
|
4
4
|
description = "A simple library to capture websites using playwright"
|
|
5
5
|
authors = [
|
|
6
6
|
{name="Raphaël Vinot", email= "raphael.vinot@circl.lu"}
|
|
@@ -24,8 +24,8 @@ dependencies = [
|
|
|
24
24
|
"python-socks (>=3.0.0,<4.0.0)",
|
|
25
25
|
"rfc3161-client (>=1.0.4,<2.0.0)",
|
|
26
26
|
"orjson (>=3.12,<4.0.0)",
|
|
27
|
-
"pure-magic-rs (>=0.5)",
|
|
28
|
-
"lookyloo-models (>=0.4.
|
|
27
|
+
"pure-magic-rs (>=0.5.1)",
|
|
28
|
+
"lookyloo-models (>=0.4.6)",
|
|
29
29
|
"charset-normalizer (>=3.4.6,<4.0.0)",
|
|
30
30
|
"pyfaup-rs (>=0.4.6,<0.5.0)"
|
|
31
31
|
]
|
|
@@ -54,7 +54,7 @@ types-beautifulsoup4 = "^4.12.0.20250516"
|
|
|
54
54
|
pytest = "^9.1.1"
|
|
55
55
|
mypy = "^2.3.1"
|
|
56
56
|
types-dateparser = "^1.4.3.20260907"
|
|
57
|
-
types-pytz = "^2026.
|
|
57
|
+
types-pytz = "^2026.4.0.20260926"
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
[build-system]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|