PlaywrightCapture 1.27.6__py3-none-any.whl → 1.27.8__py3-none-any.whl
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/capture.py +146 -138
- {playwrightcapture-1.27.6.dist-info → playwrightcapture-1.27.8.dist-info}/METADATA +8 -6
- {playwrightcapture-1.27.6.dist-info → playwrightcapture-1.27.8.dist-info}/RECORD +5 -5
- {playwrightcapture-1.27.6.dist-info → playwrightcapture-1.27.8.dist-info}/LICENSE +0 -0
- {playwrightcapture-1.27.6.dist-info → playwrightcapture-1.27.8.dist-info}/WHEEL +0 -0
playwrightcapture/capture.py
CHANGED
@@ -138,8 +138,8 @@ class Capture():
|
|
138
138
|
|
139
139
|
def __init__(self, browser: BROWSER | None=None, device_name: str | None=None,
|
140
140
|
proxy: str | dict[str, str] | None=None,
|
141
|
-
general_timeout_in_sec: int | None
|
142
|
-
uuid: str | None=None):
|
141
|
+
general_timeout_in_sec: int | None=None, loglevel: str | int='INFO',
|
142
|
+
uuid: str | None=None, headless: bool=True):
|
143
143
|
"""Captures a page with Playwright.
|
144
144
|
|
145
145
|
:param browser: The browser to use for the capture.
|
@@ -148,6 +148,7 @@ class Capture():
|
|
148
148
|
:param general_timeout_in_sec: The general timeout for the capture, including children.
|
149
149
|
:param loglevel: Python loglevel
|
150
150
|
:param uuid: The UUID of the capture.
|
151
|
+
:param headless: Whether to run the browser in headless mode. WARNING: requires to run in a graphical environment.
|
151
152
|
"""
|
152
153
|
master_logger = logging.getLogger('playwrightcapture')
|
153
154
|
master_logger.setLevel(loglevel)
|
@@ -167,6 +168,7 @@ class Capture():
|
|
167
168
|
self._capture_timeout = self._minimal_timeout
|
168
169
|
|
169
170
|
self.device_name: str | None = device_name
|
171
|
+
self.headless: bool = headless
|
170
172
|
self.proxy: ProxySettings = {}
|
171
173
|
if proxy:
|
172
174
|
if isinstance(proxy, str):
|
@@ -224,7 +226,7 @@ class Capture():
|
|
224
226
|
self.browser = await self.playwright[self.browser_name].launch(
|
225
227
|
proxy=self.proxy if self.proxy else None,
|
226
228
|
channel="chromium" if self.browser_name == "chromium" else None,
|
227
|
-
|
229
|
+
headless=self.headless
|
228
230
|
)
|
229
231
|
|
230
232
|
# Set of URLs that were captured in that context
|
@@ -756,6 +758,142 @@ class Capture():
|
|
756
758
|
except (TimeoutError, asyncio.TimeoutError):
|
757
759
|
self.logger.info('Unable to move time forward.')
|
758
760
|
|
761
|
+
async def __instrumentation(self, page: Page, url: str, allow_tracking: bool, clock_set: bool) -> None:
|
762
|
+
# page instrumentation
|
763
|
+
await self._wait_for_random_timeout(page, 5) # Wait 5 sec after document loaded
|
764
|
+
self.logger.debug('Start instrumentation.')
|
765
|
+
|
766
|
+
# check if we have anything on the page. If we don't, the page is not working properly.
|
767
|
+
if await self._failsafe_get_content(page):
|
768
|
+
self.logger.debug('Got rendered content')
|
769
|
+
|
770
|
+
# ==== recaptcha
|
771
|
+
# Same technique as: https://github.com/NikolaiT/uncaptcha3
|
772
|
+
if CAN_SOLVE_CAPTCHA:
|
773
|
+
try:
|
774
|
+
if (await page.locator("//iframe[@title='reCAPTCHA']").first.is_visible(timeout=3000)
|
775
|
+
and await page.locator("//iframe[@title='reCAPTCHA']").first.is_enabled(timeout=2000)):
|
776
|
+
self.logger.info('Found a captcha')
|
777
|
+
await self._recaptcha_solver(page)
|
778
|
+
except PlaywrightTimeoutError as e:
|
779
|
+
self.logger.info(f'Captcha on {url} is not ready: {e}')
|
780
|
+
except TargetClosedError as e:
|
781
|
+
self.logger.warning(f'Target closed while resolving captcha on {url}: {e}')
|
782
|
+
except Error as e:
|
783
|
+
self.logger.warning(f'Error while resolving captcha on {url}: {e}')
|
784
|
+
except (TimeoutError, asyncio.TimeoutError) as e:
|
785
|
+
self.logger.warning(f'[Timeout] Error while resolving captcha on {url}: {e}')
|
786
|
+
except Exception as e:
|
787
|
+
self.logger.exception(f'General error with captcha solving on {url}: {e}')
|
788
|
+
# ======
|
789
|
+
# NOTE: testing
|
790
|
+
# await self.__cloudflare_bypass_attempt(page)
|
791
|
+
self.logger.debug('Done with captcha.')
|
792
|
+
|
793
|
+
# move mouse
|
794
|
+
try:
|
795
|
+
async with timeout(5):
|
796
|
+
await page.mouse.move(x=random.uniform(300, 800), y=random.uniform(200, 500))
|
797
|
+
self.logger.debug('Moved mouse.')
|
798
|
+
except (asyncio.TimeoutError, TimeoutError):
|
799
|
+
self.logger.debug('Moving the mouse caused a timeout.')
|
800
|
+
|
801
|
+
await self._wait_for_random_timeout(page, 5)
|
802
|
+
self.logger.debug('Keep going after moving mouse.')
|
803
|
+
|
804
|
+
if allow_tracking:
|
805
|
+
await self._wait_for_random_timeout(page, 5)
|
806
|
+
# This event is required trigger the add_locator_handler
|
807
|
+
try:
|
808
|
+
if await page.locator("body").first.is_visible():
|
809
|
+
self.logger.debug('Got body.')
|
810
|
+
await page.locator("body").first.click(button="right",
|
811
|
+
timeout=5000,
|
812
|
+
delay=50)
|
813
|
+
self.logger.debug('Clicked on body.')
|
814
|
+
except Exception as e:
|
815
|
+
self.logger.warning(f'Could not find body: {e}')
|
816
|
+
|
817
|
+
await self._wait_for_random_timeout(page, 5)
|
818
|
+
# triggering clicks on very generic frames is sometimes impossible, using button and common language.
|
819
|
+
self.logger.debug('Check other frames for button')
|
820
|
+
for frame in page.frames:
|
821
|
+
if await self.__frame_consent(frame):
|
822
|
+
await self._wait_for_random_timeout(page, 10) # Wait 10 sec after click
|
823
|
+
self.logger.debug('Done with frames.')
|
824
|
+
|
825
|
+
self.logger.debug('Check main frame for button')
|
826
|
+
if await self.__frame_consent(page.main_frame):
|
827
|
+
self.logger.debug('Got button on main frame')
|
828
|
+
await self._wait_for_random_timeout(page, 10) # Wait 10 sec after click
|
829
|
+
|
830
|
+
if clock_set:
|
831
|
+
await self._move_time_forward(page, 10)
|
832
|
+
|
833
|
+
# Parse the URL. If there is a fragment, we need to scroll to it manually
|
834
|
+
parsed_url = urlparse(url, allow_fragments=True)
|
835
|
+
|
836
|
+
if parsed_url.fragment:
|
837
|
+
# We got a fragment, make sure we go to it and scroll only a little bit.
|
838
|
+
fragment = unquote(parsed_url.fragment)
|
839
|
+
try:
|
840
|
+
await page.locator(f'id={fragment}').first.scroll_into_view_if_needed(timeout=3000)
|
841
|
+
await self._wait_for_random_timeout(page, 2)
|
842
|
+
async with timeout(5):
|
843
|
+
await page.mouse.wheel(delta_y=random.uniform(150, 300), delta_x=0)
|
844
|
+
self.logger.debug('Jumped to fragment.')
|
845
|
+
except PlaywrightTimeoutError as e:
|
846
|
+
self.logger.info(f'Unable to go to fragment "{fragment}" (timeout): {e}')
|
847
|
+
except TargetClosedError as e:
|
848
|
+
self.logger.warning(f'Target closed, unable to go to fragment "{fragment}": {e}')
|
849
|
+
except Error as e:
|
850
|
+
self.logger.exception(f'Unable to go to fragment "{fragment}": {e}')
|
851
|
+
except (asyncio.TimeoutError, TimeoutError):
|
852
|
+
self.logger.debug('Unable to scroll due to timeout')
|
853
|
+
except (asyncio.CancelledError):
|
854
|
+
self.logger.debug('Unable to scroll due to timeout, call canceled')
|
855
|
+
else:
|
856
|
+
# scroll more
|
857
|
+
try:
|
858
|
+
# NOTE using page.mouse.wheel causes the instrumentation to fail, sometimes.
|
859
|
+
# 2024-07-08: Also, it sometimes get stuck.
|
860
|
+
async with timeout(5):
|
861
|
+
await page.mouse.wheel(delta_y=random.uniform(1500, 3000), delta_x=0)
|
862
|
+
self.logger.debug('Scrolled down.')
|
863
|
+
except Error as e:
|
864
|
+
self.logger.debug(f'Unable to scroll: {e}')
|
865
|
+
except (TimeoutError, asyncio.TimeoutError):
|
866
|
+
self.logger.debug('Unable to scroll due to timeout')
|
867
|
+
except (asyncio.CancelledError):
|
868
|
+
self.logger.debug('Unable to scroll due to timeout, call canceled')
|
869
|
+
|
870
|
+
await self._wait_for_random_timeout(page, 3)
|
871
|
+
self.logger.debug('Keep going after moving on page.')
|
872
|
+
|
873
|
+
try:
|
874
|
+
async with timeout(5):
|
875
|
+
await page.keyboard.press('PageUp')
|
876
|
+
self.logger.debug('PageUp on keyboard')
|
877
|
+
await self._wait_for_random_timeout(page, 3)
|
878
|
+
await page.keyboard.press('PageDown')
|
879
|
+
self.logger.debug('PageDown on keyboard')
|
880
|
+
except (asyncio.TimeoutError, TimeoutError):
|
881
|
+
self.logger.debug('Using keyboard caused a timeout.')
|
882
|
+
except Error as e:
|
883
|
+
self.logger.debug(f'Unable to use keyboard: {e}')
|
884
|
+
if self.wait_for_download > 0:
|
885
|
+
self.logger.info('Waiting for download to finish...')
|
886
|
+
await self._safe_wait(page, 20)
|
887
|
+
|
888
|
+
if clock_set:
|
889
|
+
# fast forward ~30s
|
890
|
+
await self._move_time_forward(page, 30)
|
891
|
+
|
892
|
+
self.logger.debug('Done with instrumentation, waiting for network idle.')
|
893
|
+
await self._wait_for_random_timeout(page, 5) # Wait 5 sec after instrumentation
|
894
|
+
await self._safe_wait(page)
|
895
|
+
self.logger.debug('Done with instrumentation, done with waiting.')
|
896
|
+
|
759
897
|
async def capture_page(self, url: str, *, max_depth_capture_time: int,
|
760
898
|
referer: str | None=None,
|
761
899
|
page: Page | None=None, depth: int=0,
|
@@ -858,9 +996,6 @@ class Capture():
|
|
858
996
|
page.on("dialog", lambda dialog: dialog.accept())
|
859
997
|
|
860
998
|
try:
|
861
|
-
# Parse the URL. If there is a fragment, we need to scroll to it manually
|
862
|
-
parsed_url = urlparse(url, allow_fragments=True)
|
863
|
-
|
864
999
|
try:
|
865
1000
|
await page.goto(url, wait_until='domcontentloaded', referer=referer if referer else '')
|
866
1001
|
page.on("download", handle_download)
|
@@ -906,128 +1041,11 @@ class Capture():
|
|
906
1041
|
except Error as e:
|
907
1042
|
self.logger.warning(f'Unable to bring the page to the front: {e}.')
|
908
1043
|
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
if await self._failsafe_get_content(page):
|
915
|
-
self.logger.debug('Got rendered content')
|
916
|
-
|
917
|
-
# ==== recaptcha
|
918
|
-
# Same technique as: https://github.com/NikolaiT/uncaptcha3
|
919
|
-
if CAN_SOLVE_CAPTCHA:
|
920
|
-
try:
|
921
|
-
if (await page.locator("//iframe[@title='reCAPTCHA']").first.is_visible(timeout=3000)
|
922
|
-
and await page.locator("//iframe[@title='reCAPTCHA']").first.is_enabled(timeout=2000)):
|
923
|
-
self.logger.info('Found a captcha')
|
924
|
-
await self._recaptcha_solver(page)
|
925
|
-
except PlaywrightTimeoutError as e:
|
926
|
-
self.logger.info(f'Captcha on {url} is not ready: {e}')
|
927
|
-
except TargetClosedError as e:
|
928
|
-
self.logger.warning(f'Target closed while resolving captcha on {url}: {e}')
|
929
|
-
except Error as e:
|
930
|
-
self.logger.warning(f'Error while resolving captcha on {url}: {e}')
|
931
|
-
except (TimeoutError, asyncio.TimeoutError) as e:
|
932
|
-
self.logger.warning(f'[Timeout] Error while resolving captcha on {url}: {e}')
|
933
|
-
except Exception as e:
|
934
|
-
self.logger.exception(f'General error with captcha solving on {url}: {e}')
|
935
|
-
# ======
|
936
|
-
# NOTE: testing
|
937
|
-
# await self.__cloudflare_bypass_attempt(page)
|
938
|
-
self.logger.debug('Done with captcha.')
|
939
|
-
|
940
|
-
# move mouse
|
941
|
-
try:
|
942
|
-
async with timeout(5):
|
943
|
-
await page.mouse.move(x=random.uniform(300, 800), y=random.uniform(200, 500))
|
944
|
-
self.logger.debug('Moved mouse.')
|
945
|
-
except (asyncio.TimeoutError, TimeoutError):
|
946
|
-
self.logger.debug('Moving the mouse caused a timeout.')
|
947
|
-
|
948
|
-
await self._wait_for_random_timeout(page, 5)
|
949
|
-
self.logger.debug('Keep going after moving mouse.')
|
950
|
-
|
951
|
-
if allow_tracking:
|
952
|
-
await self._wait_for_random_timeout(page, 5)
|
953
|
-
# This event is required trigger the add_locator_handler
|
954
|
-
try:
|
955
|
-
if await page.locator("body").first.is_visible():
|
956
|
-
self.logger.debug('Got body.')
|
957
|
-
await page.locator("body").first.click(button="right",
|
958
|
-
timeout=5000,
|
959
|
-
delay=50)
|
960
|
-
self.logger.debug('Clicked on body.')
|
961
|
-
except Exception as e:
|
962
|
-
self.logger.warning(f'Could not find body: {e}')
|
963
|
-
|
964
|
-
await self._wait_for_random_timeout(page, 5)
|
965
|
-
# triggering clicks on very generic frames is sometimes impossible, using button and common language.
|
966
|
-
self.logger.debug('Check other frames for button')
|
967
|
-
for frame in page.frames:
|
968
|
-
if await self.__frame_consent(frame):
|
969
|
-
await self._wait_for_random_timeout(page, 10) # Wait 10 sec after click
|
970
|
-
self.logger.debug('Done with frames.')
|
971
|
-
|
972
|
-
self.logger.debug('Check main frame for button')
|
973
|
-
if await self.__frame_consent(page.main_frame):
|
974
|
-
self.logger.debug('Got button on main frame')
|
975
|
-
await self._wait_for_random_timeout(page, 10) # Wait 10 sec after click
|
976
|
-
|
977
|
-
if clock_set:
|
978
|
-
await self._move_time_forward(page, 10)
|
979
|
-
|
980
|
-
if parsed_url.fragment:
|
981
|
-
# We got a fragment, make sure we go to it and scroll only a little bit.
|
982
|
-
fragment = unquote(parsed_url.fragment)
|
983
|
-
try:
|
984
|
-
await page.locator(f'id={fragment}').first.scroll_into_view_if_needed(timeout=3000)
|
985
|
-
await self._wait_for_random_timeout(page, 2)
|
986
|
-
async with timeout(5):
|
987
|
-
await page.mouse.wheel(delta_y=random.uniform(150, 300), delta_x=0)
|
988
|
-
self.logger.debug('Jumped to fragment.')
|
989
|
-
except PlaywrightTimeoutError as e:
|
990
|
-
self.logger.info(f'Unable to go to fragment "{fragment}" (timeout): {e}')
|
991
|
-
except TargetClosedError as e:
|
992
|
-
self.logger.warning(f'Target closed, unable to go to fragment "{fragment}": {e}')
|
993
|
-
except Error as e:
|
994
|
-
self.logger.exception(f'Unable to go to fragment "{fragment}": {e}')
|
995
|
-
except (asyncio.TimeoutError, TimeoutError):
|
996
|
-
self.logger.debug('Unable to scroll due to timeout')
|
997
|
-
except (asyncio.CancelledError):
|
998
|
-
self.logger.debug('Unable to scroll due to timeout, call canceled')
|
999
|
-
else:
|
1000
|
-
# scroll more
|
1001
|
-
try:
|
1002
|
-
# NOTE using page.mouse.wheel causes the instrumentation to fail, sometimes.
|
1003
|
-
# 2024-07-08: Also, it sometimes get stuck.
|
1004
|
-
async with timeout(5):
|
1005
|
-
await page.mouse.wheel(delta_y=random.uniform(1500, 3000), delta_x=0)
|
1006
|
-
self.logger.debug('Scrolled down.')
|
1007
|
-
except Error as e:
|
1008
|
-
self.logger.debug(f'Unable to scroll: {e}')
|
1009
|
-
except (TimeoutError, asyncio.TimeoutError):
|
1010
|
-
self.logger.debug('Unable to scroll due to timeout')
|
1011
|
-
except (asyncio.CancelledError):
|
1012
|
-
self.logger.debug('Unable to scroll due to timeout, call canceled')
|
1013
|
-
|
1014
|
-
await self._wait_for_random_timeout(page, 3)
|
1015
|
-
self.logger.debug('Keep going after moving on page.')
|
1016
|
-
|
1017
|
-
try:
|
1018
|
-
async with timeout(5):
|
1019
|
-
await page.keyboard.press('PageUp')
|
1020
|
-
self.logger.debug('PageUp on keyboard')
|
1021
|
-
await self._wait_for_random_timeout(page, 3)
|
1022
|
-
await page.keyboard.press('PageDown')
|
1023
|
-
self.logger.debug('PageDown on keyboard')
|
1024
|
-
except (asyncio.TimeoutError, TimeoutError):
|
1025
|
-
self.logger.debug('Using keyboard caused a timeout.')
|
1026
|
-
except Error as e:
|
1027
|
-
self.logger.debug(f'Unable to use keyboard: {e}')
|
1028
|
-
if self.wait_for_download > 0:
|
1029
|
-
self.logger.info('Waiting for download to finish...')
|
1030
|
-
await self._safe_wait(page, 20)
|
1044
|
+
if self.headless:
|
1045
|
+
await self.__instrumentation(page, url, allow_tracking, clock_set)
|
1046
|
+
else:
|
1047
|
+
self.logger.debug('Headed mode, skipping instrumentation.')
|
1048
|
+
await self._wait_for_random_timeout(page, self._capture_timeout - 5)
|
1031
1049
|
|
1032
1050
|
if multiple_downloads:
|
1033
1051
|
if len(multiple_downloads) == 1:
|
@@ -1043,16 +1061,6 @@ class Capture():
|
|
1043
1061
|
z.writestr(f'{i}_{filename}', file_content)
|
1044
1062
|
to_return["downloaded_file"] = mem_zip.getvalue()
|
1045
1063
|
|
1046
|
-
if clock_set:
|
1047
|
-
# fast forward ~30s
|
1048
|
-
await self._move_time_forward(page, 30)
|
1049
|
-
|
1050
|
-
self.logger.debug('Done with instrumentation, waiting for network idle.')
|
1051
|
-
await self._wait_for_random_timeout(page, 5) # Wait 5 sec after instrumentation
|
1052
|
-
await self._safe_wait(page)
|
1053
|
-
|
1054
|
-
self.logger.debug('Done with instrumentation, done with waiting.')
|
1055
|
-
|
1056
1064
|
if content := await self._failsafe_get_content(page):
|
1057
1065
|
to_return['html'] = content
|
1058
1066
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: PlaywrightCapture
|
3
|
-
Version: 1.27.
|
3
|
+
Version: 1.27.8
|
4
4
|
Summary: A simple library to capture websites using playwright
|
5
5
|
License: BSD-3-Clause
|
6
6
|
Author: Raphaël Vinot
|
@@ -20,17 +20,19 @@ Classifier: Topic :: Security
|
|
20
20
|
Provides-Extra: recaptcha
|
21
21
|
Requires-Dist: SpeechRecognition (>=3.14.1) ; extra == "recaptcha"
|
22
22
|
Requires-Dist: aiohttp-socks (>=0.10.1)
|
23
|
-
Requires-Dist: aiohttp[speedups] (>=3.11.
|
23
|
+
Requires-Dist: aiohttp[speedups] (>=3.11.12)
|
24
24
|
Requires-Dist: async-timeout (>=5.0.1) ; python_version < "3.11"
|
25
|
-
Requires-Dist: beautifulsoup4[charset-normalizer,lxml] (>=4.
|
26
|
-
Requires-Dist: dateparser (>=1.2.
|
27
|
-
Requires-Dist: playwright (>=1.
|
25
|
+
Requires-Dist: beautifulsoup4[charset-normalizer,lxml] (>=4.13.3)
|
26
|
+
Requires-Dist: dateparser (>=1.2.1)
|
27
|
+
Requires-Dist: playwright (>=1.50.0)
|
28
28
|
Requires-Dist: playwright-stealth (>=1.0.6)
|
29
29
|
Requires-Dist: puremagic (>=1.28)
|
30
30
|
Requires-Dist: pydub (>=0.25.1) ; extra == "recaptcha"
|
31
31
|
Requires-Dist: setuptools (>=75.8.0)
|
32
32
|
Requires-Dist: tzdata (>=2025.1)
|
33
|
-
Requires-Dist: w3lib (>=2.
|
33
|
+
Requires-Dist: w3lib (>=2.3.1)
|
34
|
+
Project-URL: Issues, https://github.com/Lookyloo/PlaywrightCapture/issues
|
35
|
+
Project-URL: Repository, https://github.com/Lookyloo/PlaywrightCapture
|
34
36
|
Description-Content-Type: text/markdown
|
35
37
|
|
36
38
|
# Playwright Capture
|
@@ -1,9 +1,9 @@
|
|
1
1
|
playwrightcapture/__init__.py,sha256=F90Y8wYS13tDjgsfjuFrCfmzQfdnH44G-ovuilJfLEE,511
|
2
|
-
playwrightcapture/capture.py,sha256=
|
2
|
+
playwrightcapture/capture.py,sha256=vz1XjydsMaN6jlZNVlSxJsYnyqsNDWFlz5ppBEa2Ylk,80519
|
3
3
|
playwrightcapture/exceptions.py,sha256=LhGJQCGHzEu7Sx2Dfl28OFeDg1OmrwufFjAWXlxQnEA,366
|
4
4
|
playwrightcapture/helpers.py,sha256=SXQLEuxMs8-bcWykMiUVosHzzxBKuS-QC0gBV3OmKmo,1764
|
5
5
|
playwrightcapture/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
-
playwrightcapture-1.27.
|
7
|
-
playwrightcapture-1.27.
|
8
|
-
playwrightcapture-1.27.
|
9
|
-
playwrightcapture-1.27.
|
6
|
+
playwrightcapture-1.27.8.dist-info/LICENSE,sha256=uwFc39fTLacBUG-XTuxX6IQKTKhg4z14gWOLt3ex4Ho,1775
|
7
|
+
playwrightcapture-1.27.8.dist-info/METADATA,sha256=BejRdCuS5X4I_k8c0XlDMiHXHGvGITahyzv9aniN8tY,2998
|
8
|
+
playwrightcapture-1.27.8.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
9
|
+
playwrightcapture-1.27.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|