PlaywrightCapture 1.41.6__tar.gz → 1.41.8__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: PlaywrightCapture
3
- Version: 1.41.6
3
+ Version: 1.41.8
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.3)
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 tempfile import NamedTemporaryFile
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(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
- if hasattr(self, '_temp_harfile'):
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
- os.unlink(self._temp_harfile.name)
347
+ self._temp_video_dir.cleanup()
331
348
  except Exception as e:
332
- self.logger.warning(f'Unable to remove temp HAR file {self._temp_harfile.name}: {e}')
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
- # For debug only
554
- # record_video_dir='./videos/',
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.6"
3
+ version = "1.41.8"
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.3)",
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.3.1.20260727"
57
+ types-pytz = "^2026.4.0.20260926"
58
58
 
59
59
 
60
60
  [build-system]