diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..3ab78cc
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+*.js linguist-language=python
+*.css linguist-language=python
+*.html linguist-language=python
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d1e4bea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,170 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+data
+browser_data
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+*.xml
+*.iml
+.idea
+/temp_image/
+/browser_data/
+/data/
+/cache
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cf1fcf1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,115 @@
+> **免责声明:**
+
+>本仓库的所有内容仅供学习和参考之用,禁止用于商业用途。任何人或组织不得将本仓库的内容用于非法用途或侵犯他人合法权益。本仓库所涉及的爬虫技术仅用于学习和研究,不得用于对其他平台进行大规模爬虫或其他非法行为。对于因使用本仓库内容而引起的任何法律责任,本仓库不承担任何责任。使用本仓库的内容即表示您同意本免责声明的所有条款和条件。
+
+**本仓库作为开源抖音,小红书,快手,B站,微博的爬虫的备份仓库,不接受资助,不承担任何商业后果!!!**
+
+遵循robot.txt协议
+
+# 仓库描述
+
+**小红书爬虫**,**抖音爬虫**, **快手爬虫**, **B站爬虫**, **微博爬虫**...。
+目前能抓取小红书、抖音、快手、B站、微博的视频、图片、评论、点赞、转发等信息。
+
+原理:利用[playwright](https://playwright.dev/)搭桥,保留登录成功后的上下文浏览器环境,通过执行JS表达式获取一些加密参数
+通过使用此方式,免去了复现核心加密JS代码,逆向难度大大降低
+
+爬虫技术交流群:[949715256](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=NFz-oY7Pek3gpG5zbLJFHARlB8lKL94f&authKey=FlxIQK99Uu90wddNV5W%2FBga6T6lXU5BRqyTTc26f2P2ZK5OW%2BDhHp7MwviX%2BbrPa&noverify=0&group_code=949715256),同时欢迎大家贡献代码提交PR
+
+视频配置教程:[MediaCrawler视频入门教程](https://space.bilibili.com/434377496/channel/series)
+
+
+## 功能列表
+| 平台 | Cookie 登录 | 二维码登录 | 手机号登录 | 关键词搜索 | 指定视频/帖子 ID 爬取 | 登录状态缓存 | 数据保存 | IP 代理池 | 滑块验证码 |
+|:---:|:---------:|:-----:|:-----:|:-----:|:-------------:|:------:|:----:|:------:|:-----:|
+| 小红书 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✕ |
+| 抖音 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| 快手 | ✅ | ✅ | ✕ | ✅ | ✅ | ✅ | ✅ | ✅ | ✕ |
+| B 站 | ✅ | ✅ | ✕ | ✅ | ✅ | ✅ | ✅ | ✅ | ✕ |
+| 微博 | ✅ | ✅ | ✕ | ✅ | ✅ | ✅ | ✅ | ✅ | ✕ |
+
+
+## 使用方法
+
+### 创建并激活 python 虚拟环境
+ ```shell
+ # 进入项目根目录
+ cd MediaCrawler
+
+ # 创建虚拟环境
+ python -m venv venv
+
+ # macos & linux 激活虚拟环境
+ source venv/bin/activate
+
+ # windows 激活虚拟环境
+ venv\Scripts\activate
+
+ ```
+
+### 安装依赖库
+
+ ```shell
+ pip3 install -r requirements.txt
+ ```
+
+### 安装 playwright浏览器驱动
+
+ ```shell
+ playwright install
+ ```
+
+### 运行爬虫程序
+
+ ```shell
+ # 从配置文件中读取关键词搜索相关的帖子并爬去帖子信息与评论
+ python main.py --platform xhs --lt qrcode --type search
+
+ # 从配置文件中读取指定的帖子ID列表获取指定帖子的信息与评论信息
+ python main.py --platform xhs --lt qrcode --type detail
+
+ # 打开对应APP扫二维码登录
+
+ # 其他平台爬虫使用示例, 执行下面的命令查看
+ python main.py --help
+ ```
+
+
+### 数据保存
+- 支持保存到关系型数据库(Mysql、PgSQL等)
+- 支持保存到csv中(data/目录下)
+- 支持保存到json中(data/目录下)
+
+
+## 捐赠信息
+
+PS:如果打赏时请备注捐赠者,如有遗漏请联系我添加(有时候消息多可能会漏掉,十分抱歉)
+
+| 捐赠者 | 捐赠金额 | 捐赠日期 |
+|----------|-------|------------|
+
+
+## 运行报错常见问题Q&A
+> 遇到问题先自行搜索解决下,现在AI很火,用ChatGPT大多情况下能解决你的问题 [免费的ChatGPT](https://sider.ai/invited?c=8e03db1a973401fdf114ed9cf9f8c183)
+
+➡️➡️➡️ [常见问题](docs/常见问题.md)
+
+
+## 项目代码结构
+➡️➡️➡️ [项目代码结构说明](docs/项目代码结构.md)
+
+## 手机号登录说明
+➡️➡️➡️ [手机号登录说明](docs/手机号登录说明.md)
+
+
+
+## star
+- 如果该项目对你有帮助,star一下 ❤️❤️❤️
+
+
+## 参考
+
+- xhs客户端 [ReaJason的xhs仓库](https://github.com/ReaJason/xhs)
+- 短信转发 [参考仓库](https://github.com/pppscn/SmsForwarder)
+- 内网穿透工具 [ngrok](https://ngrok.com/docs/)
+
diff --git a/base/__init__.py b/base/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/base/base_crawler.py b/base/base_crawler.py
new file mode 100644
index 0000000..5a59a3d
--- /dev/null
+++ b/base/base_crawler.py
@@ -0,0 +1,57 @@
+from abc import ABC, abstractmethod
+from typing import Dict, Optional
+
+from playwright.async_api import BrowserContext, BrowserType
+
+
+class AbstractCrawler(ABC):
+ @abstractmethod
+ def init_config(self, platform: str, login_type: str, crawler_type: str):
+ pass
+
+ @abstractmethod
+ async def start(self):
+ pass
+
+ @abstractmethod
+ async def search(self):
+ pass
+
+ @abstractmethod
+ async def launch_browser(self, chromium: BrowserType, playwright_proxy: Optional[Dict], user_agent: Optional[str],
+ headless: bool = True) -> BrowserContext:
+ pass
+
+
+class AbstractLogin(ABC):
+ @abstractmethod
+ async def begin(self):
+ pass
+
+ @abstractmethod
+ async def login_by_qrcode(self):
+ pass
+
+ @abstractmethod
+ async def login_by_mobile(self):
+ pass
+
+ @abstractmethod
+ async def login_by_cookies(self):
+ pass
+
+
+class AbstractStore(ABC):
+ @abstractmethod
+ async def store_content(self, content_item: Dict):
+ pass
+
+ @abstractmethod
+ async def store_comment(self, comment_item: Dict):
+ pass
+
+ # TODO support all platform
+ # only xhs is supported, so @abstractmethod is commented
+ # @abstractmethod
+ async def store_creator(self, creator: Dict):
+ pass
diff --git a/config/__init__.py b/config/__init__.py
new file mode 100644
index 0000000..c2b8e48
--- /dev/null
+++ b/config/__init__.py
@@ -0,0 +1,2 @@
+from .base_config import *
+from .db_config import *
diff --git a/config/base_config.py b/config/base_config.py
new file mode 100644
index 0000000..bf4ecec
--- /dev/null
+++ b/config/base_config.py
@@ -0,0 +1,81 @@
+# 基础配置
+PLATFORM = "xhs"
+KEYWORDS = "python,golang"
+LOGIN_TYPE = "qrcode" # qrcode or phone or cookie
+COOKIES = ""
+SORT_TYPE="popularity_descending" # 具体值参见media_platform.xxx.field下的枚举值,展示只支持小红书
+CRAWLER_TYPE = "search"
+
+# 是否开启 IP 代理
+ENABLE_IP_PROXY = False
+
+# 代理IP池数量
+IP_PROXY_POOL_COUNT = 2
+
+# 设置为True不会打开浏览器(无头浏览器),设置False会打开一个浏览器(小红书如果一直扫码登录不通过,打开浏览器手动过一下滑动验证码)
+HEADLESS = False
+
+# 是否保存登录状态
+SAVE_LOGIN_STATE = True
+
+# 数据保存类型选项配置,支持三种类型:csv、db、json
+SAVE_DATA_OPTION = "json" # csv or db or json
+
+# 用户浏览器缓存的浏览器文件配置
+USER_DATA_DIR = "%s_user_data_dir" # %s will be replaced by platform name
+
+# 爬取视频/帖子的数量控制
+CRAWLER_MAX_NOTES_COUNT = 20
+
+# 并发爬虫数量控制
+MAX_CONCURRENCY_NUM = 4
+
+
+# 评论关键词筛选(只会留下包含关键词的评论,为空不限制)
+COMMENT_KEYWORDS = [
+ # "真棒"
+ # ........................
+]
+
+# 指定小红书需要爬虫的笔记ID列表
+XHS_SPECIFIED_ID_LIST = [
+ "6422c2750000000027000d88",
+ "64ca1b73000000000b028dd2",
+ "630d5b85000000001203ab41",
+ # ........................
+]
+
+# 指定抖音需要爬取的ID列表
+DY_SPECIFIED_ID_LIST = [
+ "7280854932641664319",
+ "7202432992642387233"
+ # ........................
+]
+
+# 指定快手平台需要爬取的ID列表
+KS_SPECIFIED_ID_LIST = [
+ "3xf8enb8dbj6uig",
+ "3x6zz972bchmvqe"
+]
+
+# 指定B站平台需要爬取的视频bvid列表
+BILI_SPECIFIED_ID_LIST = [
+ "BV1d54y1g7db",
+ "BV1Sz4y1U77N",
+ "BV14Q4y1n7jz",
+ # ........................
+]
+
+# 指定微博平台需要爬取的帖子列表
+WEIBO_SPECIFIED_ID_LIST = [
+ "4982041758140155",
+ # ........................
+]
+
+# 指定小红书创作者ID列表
+XHS_CREATOR_ID_LIST = [
+ "59d8cb33de5fb4696bf17217",
+ "61b87386000000001000b18b",
+ "5e8558100000000001005bc5",
+ # ........................
+]
\ No newline at end of file
diff --git a/config/db_config.py b/config/db_config.py
new file mode 100644
index 0000000..f7d9748
--- /dev/null
+++ b/config/db_config.py
@@ -0,0 +1,12 @@
+import os
+
+# redis config
+REDIS_DB_HOST = "127.0.0.1" # your redis host
+REDIS_DB_PWD = os.getenv("REDIS_DB_PWD", "123456") # your redis password
+
+# mysql config
+RELATION_DB_PWD = os.getenv("RELATION_DB_PWD", "123456") # your relation db password
+RELATION_DB_URL = f"mysql://root:{RELATION_DB_PWD}@localhost:3306/media_crawler"
+
+# sqlite3 config
+# RELATION_DB_URL = f"sqlite://data/media_crawler.sqlite"
\ No newline at end of file
diff --git a/db.py b/db.py
new file mode 100644
index 0000000..b1bc15e
--- /dev/null
+++ b/db.py
@@ -0,0 +1,31 @@
+from typing import List
+
+from tortoise import Tortoise, run_async
+
+from config.db_config import *
+from tools import utils
+
+
+def get_platform_models() -> List[str]:
+ models = ["store.xhs", "store.douyin", "store.bilibili", "store.kuaishou", "store.weibo"]
+ return models
+
+
+async def init_db(create_db: bool = False) -> None:
+ await Tortoise.init(
+ db_url=RELATION_DB_URL,
+ modules={'models': get_platform_models()},
+ _create_db=create_db
+ )
+
+async def close() -> None:
+ await Tortoise.close_connections()
+
+async def init():
+ await init_db(create_db=True)
+ await Tortoise.generate_schemas()
+ utils.logger.info("[db.init] Init DB Success!")
+
+
+if __name__ == '__main__':
+ run_async(init())
diff --git "a/docs/\344\273\243\347\220\206\344\275\277\347\224\250.md" "b/docs/\344\273\243\347\220\206\344\275\277\347\224\250.md"
new file mode 100644
index 0000000..528aeb2
--- /dev/null
+++ "b/docs/\344\273\243\347\220\206\344\275\277\347\224\250.md"
@@ -0,0 +1,34 @@
+## 代理 IP 使用说明
+> 还是得跟大家再次强调下,不要对一些自媒体平台进行大规模爬虫或其他非法行为,要踩缝纫机的哦🤣
+> 另外如果要是用代理功能,请安装Redis并设置一个密码, 从下面的流程图讲解了redis在这个缓存功能中起到的作用
+### 简易的流程图
+
+
+
+### 准备代理 IP 信息
+点击 极速HTTP代理 官网注册并实名认证(国内使用代理 IP 必须要实名,懂的都懂)
+
+### 获取 IP 提取链接
+> 每个人注册并实名认证后都会送一定的余额。(当然有些网站上也有一些免费的IP,但失效时间极快,也体验过一些免费的 IP 代理池,轮询去找一个可用IP都得半天)
+
+在IP提取页面点击生成 API 链接,这样就会生成一个跟你账号相关的IP提取的链接,其中我们只需要关注2个参数
+`key`、`crypto`,比如下面这张图中`key=w3q**********` `crypto=2f945*********`
+
+
+
+### 将提取密钥参数 key crypto 写入环境变量
+> 或者直接在代码中填写 `key` 和 `crypto` 的值
+
+
+
+
+### 将配置文件中的`ENABLE_IP_PROXY`置为 `True`
+> `IP_PROXY_POOL_COUNT` 池子中 IP 的数量
+
+
+### 其他说明
+> 代理IP池使用了redis来缓存IP和记录过期时间
+> 使用 chatgpt 快速询问如何安装 redis 并设置密码
+
+
+
diff --git "a/docs/\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/docs/\345\270\270\350\247\201\351\227\256\351\242\230.md"
new file mode 100644
index 0000000..b740839
--- /dev/null
+++ "b/docs/\345\270\270\350\247\201\351\227\256\351\242\230.md"
@@ -0,0 +1,19 @@
+## 常见程序运行出错问题
+
+Q: 爬取抖音报错: `execjs._exceptions.ProgramError: SyntaxError: 缺少 ';'`
+A: 该错误为缺少 nodejs 环境这个错误安装 nodejs 环境即可,版本为:`v16.8.0`
+
+Q: 可以指定关键词爬取吗?
+A: 在config/base_config.py 中 KEYWORDS 参数用于控制需要爬去的关键词
+
+Q: 可以指定帖子爬去吗?
+A:在config/base_config.py 中 XHS_SPECIFIED_ID_LIST 参数用于控制需要指定爬去的帖子ID列表
+
+Q: 刚开始能爬取数据,过一段时间就是失效了?
+A:出现这种情况多半是由于你的账号触发了平台风控机制了,❗️❗️请勿大规模对平台进行爬虫,影响平台。
+
+Q: 如何更换登录账号?
+A:删除项目根目录下的 brower_data/ 文件夹即可
+
+Q: 报错 `playwright._impl._api_types.TimeoutError: Timeout 30000ms exceeded.`
+A: 出现这种情况检查下开梯子没有`
diff --git "a/docs/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225\350\257\264\346\230\216.md" "b/docs/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..5a55dcc
--- /dev/null
+++ "b/docs/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225\350\257\264\346\230\216.md"
@@ -0,0 +1,20 @@
+## 关于手机号+验证码登录的说明
+
+当在浏览器模拟人为发起手机号登录请求时,使用短信转发软件将验证码发送至爬虫端回填,完成自动登录
+
+准备工作:
+
+- 安卓机1台(IOS没去研究,理论上监控短信也是可行的)
+- 安装短信转发软件 [参考仓库](https://github.com/pppscn/SmsForwarder)
+- 转发软件中配置WEBHOOK相关的信息,主要分为 消息模板(请查看本项目中的recv_sms_notification.py)、一个能push短信通知的API地址
+- push的API地址一般是需要绑定一个域名的(当然也可以是内网的IP地址),我用的是内网穿透方式,会有一个免费的域名绑定到内网的web
+ server,内网穿透工具 [ngrok](https://ngrok.com/docs/)
+- 安装redis并设置一个密码 [redis安装](https://www.cnblogs.com/hunanzp/p/12304622.html)
+- 执行 `python recv_sms_notification.py` 等待短信转发器发送HTTP通知
+- 执行手机号登录的爬虫程序 `python main.py --platform xhs --lt phone`
+
+备注:
+
+- 小红书这边一个手机号一天只能发10条短信(悠着点),目前在发验证码时还未触发滑块验证,估计多了之后也会有~
+- 短信转发软件会不会监控自己手机上其他短信内容?(理论上应该不会,因为[短信转发仓库](https://github.com/pppscn/SmsForwarder)
+star还是蛮多的)
\ No newline at end of file
diff --git "a/docs/\351\241\271\347\233\256\344\273\243\347\240\201\347\273\223\346\236\204.md" "b/docs/\351\241\271\347\233\256\344\273\243\347\240\201\347\273\223\346\236\204.md"
new file mode 100644
index 0000000..baaa71f
--- /dev/null
+++ "b/docs/\351\241\271\347\233\256\344\273\243\347\240\201\347\273\223\346\236\204.md"
@@ -0,0 +1,37 @@
+
+## 项目代码结构
+
+```
+MediaCrawler
+├── base
+│ └── base_crawler.py # 项目的抽象类
+├── browser_data # 换成用户的浏览器数据目录
+├── config
+│ ├── account_config.py # 账号代理池配置
+│ ├── base_config.py # 基础配置
+│ └── db_config.py # 数据库配置
+├── data # 数据保存目录
+├── libs
+│ ├── douyin.js # 抖音Sign函数
+│ └── stealth.min.js # 去除浏览器自动化特征的JS
+├── media_platform
+│ ├── douyin # 抖音crawler实现
+│ ├── xhs # 小红书crawler实现
+│ ├── bilibili # B站crawler实现
+│ └── kuaishou # 快手crawler实现
+├── modles
+│ ├── douyin.py # 抖音数据模型
+│ ├── xiaohongshu.py # 小红书数据模型
+│ ├── kuaishou.py # 快手数据模型
+│ └── bilibili.py # B站数据模型
+├── tools
+│ ├── utils.py # 暴露给外部的工具函数
+│ ├── crawler_util.py # 爬虫相关的工具函数
+│ ├── slider_util.py # 滑块相关的工具函数
+│ ├── time_util.py # 时间相关的工具函数
+│ └── easing.py # 模拟滑动轨迹相关的函数
+├── db.py # DB ORM
+├── main.py # 程序入口
+├── var.py # 上下文变量定义
+└── recv_sms_notification.py # 短信转发器的HTTP SERVER接口
+```
\ No newline at end of file
diff --git a/libs/douyin.js b/libs/douyin.js
new file mode 100644
index 0000000..efb6415
--- /dev/null
+++ b/libs/douyin.js
@@ -0,0 +1,578 @@
+var window = null;
+
+function _0x5cd844(e) {
+ var b = {
+ exports: {}
+ };
+ return e(b, b.exports), b.exports
+}
+
+jsvmp = function (e, b, a) {
+ function f(e, b, a) {
+ return (f = function () {
+ if ("undefined" == typeof Reflect || !Reflect.construct || Reflect.construct.sham) return !1;
+ if ("function" == typeof Proxy) return !0;
+ try {
+ return Date.prototype.toString.call(Reflect.construct(Date, [], function () {
+ })), !0
+ } catch (e) {
+ return !1
+ }
+ }() ? Reflect.construct : function (e, b, a) {
+ var f = [null];
+ f.push.apply(f, b);
+ var c = new (Function.bind.apply(e, f));
+ return a && function (e, b) {
+ (Object.setPrototypeOf || function (e, b) {
+ return e.__proto__ = b, e
+ })(e, b)
+ }(c, a.prototype), c
+ }).apply(null, arguments)
+ }
+
+ function c(e) {
+ return function (e) {
+ if (Array.isArray(e)) {
+ for (var b = 0, a = new Array(e.length); b < e.length; b++) a[b] = e[b];
+ return a
+ }
+ }(e) || function (e) {
+ if (Symbol.iterator in Object(e) || "[object Arguments]" === Object.prototype.toString.call(e)) return Array.from(e)
+ }(e) || function () {
+ throw new TypeError("Invalid attempt to spread non-iterable instance")
+ }()
+ }
+
+ for (var r = [], t = 0, d = [], i = 0, n = function (e, b) {
+ var a = e[b++],
+ f = e[b],
+ c = parseInt("" + a + f, 16);
+ if (c >> 7 == 0) return [1, c];
+ if (c >> 6 == 2) {
+ var r = parseInt("" + e[++b] + e[++b], 16);
+ return c &= 63, [2, r = (c <<= 8) + r]
+ }
+ if (c >> 6 == 3) {
+ var t = parseInt("" + e[++b] + e[++b], 16),
+ d = parseInt("" + e[++b] + e[++b], 16);
+ return c &= 63, [3, d = (c <<= 16) + (t <<= 8) + d]
+ }
+ }, s = function (e, b) {
+ var a = parseInt("" + e[b] + e[b + 1], 16);
+ return a > 127 ? -256 + a : a
+ }, o = function (e, b) {
+ var a = parseInt("" + e[b] + e[b + 1] + e[b + 2] + e[b + 3], 16);
+ return a > 32767 ? -65536 + a : a
+ }, l = function (e, b) {
+ var a = parseInt("" + e[b] + e[b + 1] + e[b + 2] + e[b + 3] + e[b + 4] + e[b + 5] + e[b + 6] + e[b + 7], 16);
+ return a > 2147483647 ? 0 + a : a
+ }, _ = function (e, b) {
+ return parseInt("" + e[b] + e[b + 1], 16)
+ }, x = function (e, b) {
+ return parseInt("" + e[b] + e[b + 1] + e[b + 2] + e[b + 3], 16)
+ }, u = u || this || window, h = (e.length, 0), p = "", y = h; y < h + 16; y++) {
+ var v = "" + e[y++] + e[y];
+ v = parseInt(v, 16), p += String.fromCharCode(v)
+ }
+ if ("HNOJ@?RC" != p) throw new Error("error magic number " + p);
+ parseInt("" + e[h += 16] + e[h + 1], 16), h += 8, t = 0;
+ for (var g = 0; g < 4; g++) {
+ var w = h + 2 * g,
+ A = parseInt("" + e[w++] + e[w], 16);
+ t += (3 & A) << 2 * g
+ }
+ h += 16;
+ var C = parseInt("" + e[h += 8] + e[h + 1] + e[h + 2] + e[h + 3] + e[h + 4] + e[h + 5] + e[h + 6] + e[h + 7], 16),
+ m = C,
+ S = h += 8,
+ z = x(e, h += C);
+ z[1], h += 4, r = {
+ p: [],
+ q: []
+ };
+ for (var B = 0; B < z; B++) {
+ for (var R = n(e, h), q = h += 2 * R[0], I = r.p.length, k = 0; k < R[1]; k++) {
+ var j = n(e, q);
+ r.p.push(j[1]), q += 2 * j[0]
+ }
+ h = q, r.q.push([I, r.p.length])
+ }
+ var O = {
+ 5: 1,
+ 6: 1,
+ 70: 1,
+ 22: 1,
+ 23: 1,
+ 37: 1,
+ 73: 1
+ },
+ U = {
+ 72: 1
+ },
+ D = {
+ 74: 1
+ },
+ N = {
+ 11: 1,
+ 12: 1,
+ 24: 1,
+ 26: 1,
+ 27: 1,
+ 31: 1
+ },
+ J = {
+ 10: 1
+ },
+ L = {
+ 2: 1,
+ 29: 1,
+ 30: 1,
+ 20: 1
+ },
+ T = [],
+ E = [];
+
+ function M(e, b, a) {
+ for (var f = b; f < b + a;) {
+ var c = _(e, f);
+ T[f] = c, f += 2, U[c] ? (E[f] = s(e, f), f += 2) : O[c] ? (E[f] = o(e, f), f += 4) : D[c] ? (E[f] = l(e, f), f += 8) : N[c] ? (E[f] = _(e, f), f += 2) : J[c] ? (E[f] = x(e, f), f += 4) : L[c] && (E[f] = x(e, f), f += 4)
+ }
+ }
+
+ return F(e, S, m / 2, [], b, a);
+
+ function P(e, b, a, n, h, p, y, v) {
+ null == p && (p = this);
+ var g, w, A, C, m = [],
+ S = 0;
+ y && (w = y);
+ var z, B, R = b,
+ q = R + 2 * a;
+ if (!v)
+ for (; R < q;) {
+ var I = parseInt("" + e[R] + e[R + 1], 16);
+ R += 2;
+ var j = 3 & (z = 13 * I % 241);
+ if (z >>= 2, j < 1)
+ if (j = 3 & z, z >>= 2, j < 1) {
+ if ((j = z) < 1) return [1, m[S--]];
+ j < 5 ? (w = m[S--], m[S] = m[S] * w) : j < 7 ? (w = m[S--], m[S] = m[S] != w) : j < 14 ? (A = m[S--], C = m[S--], (j = m[S--]).x === P ? j.y >= 1 ? m[++S] = F(e, j.c, j.l, A, j.z, C, null, 1) : (m[++S] = F(e, j.c, j.l, A, j.z, C, null, 0), j.y++) : m[++S] = j.apply(C, A)) : j < 16 && (B = o(e, R), (g = function b() {
+ var a = arguments;
+ return b.y > 0 || b.y++, F(e, b.c, b.l, a, b.z, this, null, 0)
+ }).c = R + 4, g.l = B - 2, g.x = P, g.y = 0, g.z = h, m[S] = g, R += 2 * B - 2)
+ } else if (j < 2) (j = z) > 8 ? (w = m[S--], m[S] = typeof w) : j > 4 ? m[S -= 1] = m[S][m[S + 1]] : j > 2 && (A = m[S--], (j = m[S]).x === P ? j.y >= 1 ? m[S] = F(e, j.c, j.l, [A], j.z, C, null, 1) : (m[S] = F(e, j.c, j.l, [A], j.z, C, null, 0), j.y++) : m[S] = j(A));
+ else if (j < 3) {
+ if ((j = z) < 9) {
+ for (w = m[S--], B = x(e, R), j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
+ R += 4, m[S--][j] = w
+ } else if (j < 13) throw m[S--]
+ } else (j = z) < 1 ? m[++S] = null : j < 3 ? (w = m[S--], m[S] = m[S] >= w) : j < 12 && (m[++S] = void 0);
+ else if (j < 2)
+ if (j = 3 & z, z >>= 2, j < 1)
+ if ((j = z) < 5) {
+ B = o(e, R);
+ try {
+ if (d[i][2] = 1, 1 == (w = P(e, R + 4, B - 3, [], h, p, null, 0))[0]) return w
+ } catch (b) {
+ if (d[i] && d[i][1] && 1 == (w = P(e, d[i][1][0], d[i][1][1], [], h, p, b, 0))[0]) return w
+ } finally {
+ if (d[i] && d[i][0] && 1 == (w = P(e, d[i][0][0], d[i][0][1], [], h, p, null, 0))[0]) return w;
+ d[i] = 0, i--
+ }
+ R += 2 * B - 2
+ } else j < 7 ? (B = _(e, R), R += 2, m[S -= B] = 0 === B ? new m[S] : f(m[S], c(m.slice(S + 1, S + B + 1)))) : j < 9 && (w = m[S--], m[S] = m[S] & w);
+ else if (j < 2)
+ if ((j = z) > 12) m[++S] = s(e, R), R += 2;
+ else if (j > 10) w = m[S--], m[S] = m[S] << w;
+ else if (j > 8) {
+ for (B = x(e, R), j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
+ R += 4, m[S] = m[S][j]
+ } else j > 6 && (A = m[S--], w = delete m[S--][A]);
+ else if (j < 3) (j = z) < 2 ? m[++S] = w : j < 11 ? (w = m[S -= 2][m[S + 1]] = m[S + 2], S--) : j < 13 && (w = m[S], m[++S] = w);
+ else if ((j = z) > 12) m[++S] = p;
+ else if (j > 5) w = m[S--], m[S] = m[S] !== w;
+ else if (j > 3) w = m[S--], m[S] = m[S] / w;
+ else if (j > 1) {
+ if ((B = o(e, R)) < 0) {
+ v = 1, M(e, b, 2 * a), R += 2 * B - 2;
+ break
+ }
+ R += 2 * B - 2
+ } else j > -1 && (m[S] = !m[S]);
+ else if (j < 3)
+ if (j = 3 & z, z >>= 2, j < 1) (j = z) > 13 ? (m[++S] = o(e, R), R += 4) : j > 11 ? (w = m[S--], m[S] = m[S] >> w) : j > 9 ? (B = _(e, R), R += 2, w = m[S--], h[B] = w) : j > 7 ? (B = x(e, R), R += 4, A = S + 1, m[S -= B - 1] = B ? m.slice(S, A) : []) : j > 0 && (w = m[S--], m[S] = m[S] > w);
+ else if (j < 2) (j = z) > 12 ? (w = m[S - 1], A = m[S], m[++S] = w, m[++S] = A) : j > 3 ? (w = m[S--], m[S] = m[S] == w) : j > 1 ? (w = m[S--], m[S] = m[S] + w) : j > -1 && (m[++S] = u);
+ else if (j < 3) {
+ if ((j = z) > 13) m[++S] = !1;
+ else if (j > 6) w = m[S--], m[S] = m[S] instanceof w;
+ else if (j > 4) w = m[S--], m[S] = m[S] % w;
+ else if (j > 2)
+ if (m[S--]) R += 4;
+ else {
+ if ((B = o(e, R)) < 0) {
+ v = 1, M(e, b, 2 * a), R += 2 * B - 2;
+ break
+ }
+ R += 2 * B - 2
+ }
+ else if (j > 0) {
+ for (B = x(e, R), w = "", k = r.q[B][0]; k < r.q[B][1]; k++) w += String.fromCharCode(t ^ r.p[k]);
+ m[++S] = w, R += 4
+ }
+ } else (j = z) > 7 ? (w = m[S--], m[S] = m[S] | w) : j > 5 ? (B = _(e, R), R += 2, m[++S] = h["$" + B]) : j > 3 && (B = o(e, R), d[i][0] && !d[i][2] ? d[i][1] = [R + 4, B - 3] : d[i++] = [0, [R + 4, B - 3], 0], R += 2 * B - 2);
+ else if (j = 3 & z, z >>= 2, j > 2) (j = z) > 13 ? (m[++S] = l(e, R), R += 8) : j > 11 ? (w = m[S--], m[S] = m[S] >>> w) : j > 9 ? m[++S] = !0 : j > 7 ? (B = _(e, R), R += 2, m[S] = m[S][B]) : j > 0 && (w = m[S--], m[S] = m[S] < w);
+ else if (j > 1) (j = z) > 10 ? (B = o(e, R), d[++i] = [
+ [R + 4, B - 3], 0, 0
+ ], R += 2 * B - 2) : j > 8 ? (w = m[S--], m[S] = m[S] ^ w) : j > 6 && (w = m[S--]);
+ else if (j > 0) {
+ if ((j = z) > 7) w = m[S--], m[S] = m[S] in w;
+ else if (j > 5) m[S] = ++m[S];
+ else if (j > 3) B = _(e, R), R += 2, w = h[B], m[++S] = w;
+ else if (j > 1) {
+ var O = 0,
+ U = m[S].length,
+ D = m[S];
+ m[++S] = function () {
+ var e = O < U;
+ if (e) {
+ var b = D[O++];
+ m[++S] = b
+ }
+ m[++S] = e
+ }
+ }
+ } else if ((j = z) > 13) w = m[S], m[S] = m[S - 1], m[S - 1] = w;
+ else if (j > 4) w = m[S--], m[S] = m[S] === w;
+ else if (j > 2) w = m[S--], m[S] = m[S] - w;
+ else if (j > 0) {
+ for (B = x(e, R), j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
+ j = +j, R += 4, m[++S] = j
+ }
+ }
+ if (v)
+ for (; R < q;)
+ if (I = T[R], R += 2, j = 3 & (z = 13 * I % 241), z >>= 2, j > 2)
+ if (j = 3 & z, z >>= 2, j > 2) (j = z) < 2 ? (w = m[S--], m[S] = m[S] < w) : j < 9 ? (B = E[R], R += 2, m[S] = m[S][B]) : j < 11 ? m[++S] = !0 : j < 13 ? (w = m[S--], m[S] = m[S] >>> w) : j < 15 && (m[++S] = E[R], R += 8);
+ else if (j > 1) (j = z) < 6 || (j < 8 ? w = m[S--] : j < 10 ? (w = m[S--], m[S] = m[S] ^ w) : j < 12 && (B = E[R], d[++i] = [
+ [R + 4, B - 3], 0, 0
+ ], R += 2 * B - 2));
+ else if (j > 0) (j = z) > 7 ? (w = m[S--], m[S] = m[S] in w) : j > 5 ? m[S] = ++m[S] : j > 3 ? (B = E[R], R += 2, w = h[B], m[++S] = w) : j > 1 && (O = 0, U = m[S].length, D = m[S], m[++S] = function () {
+ var e = O < U;
+ if (e) {
+ var b = D[O++];
+ m[++S] = b
+ }
+ m[++S] = e
+ });
+ else if ((j = z) < 2) {
+ for (B = E[R], j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
+ j = +j, R += 4, m[++S] = j
+ } else j < 4 ? (w = m[S--], m[S] = m[S] - w) : j < 6 ? (w = m[S--], m[S] = m[S] === w) : j < 15 && (w = m[S], m[S] = m[S - 1], m[S - 1] = w);
+ else if (j > 1)
+ if (j = 3 & z, z >>= 2, j < 1) (j = z) > 13 ? (m[++S] = E[R], R += 4) : j > 11 ? (w = m[S--], m[S] = m[S] >> w) : j > 9 ? (B = E[R], R += 2, w = m[S--], h[B] = w) : j > 7 ? (B = E[R], R += 4, A = S + 1, m[S -= B - 1] = B ? m.slice(S, A) : []) : j > 0 && (w = m[S--], m[S] = m[S] > w);
+ else if (j < 2) (j = z) < 1 ? m[++S] = u : j < 3 ? (w = m[S--], m[S] = m[S] + w) : j < 5 ? (w = m[S--], m[S] = m[S] == w) : j < 14 && (w = m[S - 1], A = m[S], m[++S] = w, m[++S] = A);
+ else if (j < 3) {
+ if ((j = z) > 13) m[++S] = !1;
+ else if (j > 6) w = m[S--], m[S] = m[S] instanceof w;
+ else if (j > 4) w = m[S--], m[S] = m[S] % w;
+ else if (j > 2) m[S--] ? R += 4 : R += 2 * (B = E[R]) - 2;
+ else if (j > 0) {
+ for (B = E[R], w = "", k = r.q[B][0]; k < r.q[B][1]; k++) w += String.fromCharCode(t ^ r.p[k]);
+ m[++S] = w, R += 4
+ }
+ } else (j = z) > 7 ? (w = m[S--], m[S] = m[S] | w) : j > 5 ? (B = E[R], R += 2, m[++S] = h["$" + B]) : j > 3 && (B = E[R], d[i][0] && !d[i][2] ? d[i][1] = [R + 4, B - 3] : d[i++] = [0, [R + 4, B - 3], 0], R += 2 * B - 2);
+ else if (j > 0)
+ if (j = 3 & z, z >>= 2, j < 1) {
+ if ((j = z) > 9) ;
+ else if (j > 7) w = m[S--], m[S] = m[S] & w;
+ else if (j > 5) B = E[R], R += 2, m[S -= B] = 0 === B ? new m[S] : f(m[S], c(m.slice(S + 1, S + B + 1)));
+ else if (j > 3) {
+ B = E[R];
+ try {
+ if (d[i][2] = 1, 1 == (w = P(e, R + 4, B - 3, [], h, p, null, 0))[0]) return w
+ } catch (b) {
+ if (d[i] && d[i][1] && 1 == (w = P(e, d[i][1][0], d[i][1][1], [], h, p, b, 0))[0]) return w
+ } finally {
+ if (d[i] && d[i][0] && 1 == (w = P(e, d[i][0][0], d[i][0][1], [], h, p, null, 0))[0]) return w;
+ d[i] = 0, i--
+ }
+ R += 2 * B - 2
+ }
+ } else if (j < 2)
+ if ((j = z) < 8) A = m[S--], w = delete m[S--][A];
+ else if (j < 10) {
+ for (B = E[R], j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
+ R += 4, m[S] = m[S][j]
+ } else j < 12 ? (w = m[S--], m[S] = m[S] << w) : j < 14 && (m[++S] = E[R], R += 2);
+ else j < 3 ? (j = z) < 2 ? m[++S] = w : j < 11 ? (w = m[S -= 2][m[S + 1]] = m[S + 2], S--) : j < 13 && (w = m[S], m[++S] = w) : (j = z) > 12 ? m[++S] = p : j > 5 ? (w = m[S--], m[S] = m[S] !== w) : j > 3 ? (w = m[S--], m[S] = m[S] / w) : j > 1 ? R += 2 * (B = E[R]) - 2 : j > -1 && (m[S] = !m[S]);
+ else if (j = 3 & z, z >>= 2, j < 1) {
+ if ((j = z) < 1) return [1, m[S--]];
+ j < 5 ? (w = m[S--], m[S] = m[S] * w) : j < 7 ? (w = m[S--], m[S] = m[S] != w) : j < 14 ? (A = m[S--], C = m[S--], (j = m[S--]).x === P ? j.y >= 1 ? m[++S] = F(e, j.c, j.l, A, j.z, C, null, 1) : (m[++S] = F(e, j.c, j.l, A, j.z, C, null, 0), j.y++) : m[++S] = j.apply(C, A)) : j < 16 && (B = E[R], (g = function b() {
+ var a = arguments;
+ return b.y > 0 || b.y++, F(e, b.c, b.l, a, b.z, this, null, 0)
+ }).c = R + 4, g.l = B - 2, g.x = P, g.y = 0, g.z = h, m[S] = g, R += 2 * B - 2)
+ } else if (j < 2) (j = z) > 8 ? (w = m[S--], m[S] = typeof w) : j > 4 ? m[S -= 1] = m[S][m[S + 1]] : j > 2 && (A = m[S--], (j = m[S]).x === P ? j.y >= 1 ? m[S] = F(e, j.c, j.l, [A], j.z, C, null, 1) : (m[S] = F(e, j.c, j.l, [A], j.z, C, null, 0), j.y++) : m[S] = j(A));
+ else if (j < 3) {
+ if ((j = z) < 9) {
+ for (w = m[S--], B = E[R], j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
+ R += 4, m[S--][j] = w
+ } else if (j < 13) throw m[S--]
+ } else (j = z) < 1 ? m[++S] = null : j < 3 ? (w = m[S--], m[S] = m[S] >= w) : j < 12 && (m[++S] = void 0);
+ return [0, null]
+ }
+
+ function F(e, b, a, f, c, r, t, d) {
+ null == r && (r = this), c && !c.d && (c.d = 0, c.$0 = c, c[1] = {});
+ var i, n, s = {},
+ o = s.d = c ? c.d + 1 : 0;
+ for (s["$" + o] = s, n = 0; n < o; n++) s[i = "$" + n] = c[i];
+ for (n = 0, o = s.length = f.length; n < o; n++) s[n] = f[n];
+ return d && !T[b] && M(e, b, 2 * a), T[b] ? P(e, b, a, 0, s, r, null, 1)[1] : P(e, b, a, 0, s, r, null, 0)[1]
+ }
+};
+var _0x397dc7 = "undefined" != typeof globalThis ? globalThis : void 0 !== window ? window : "undefined" != typeof global ? global : "undefined" != typeof self ? self : {},
+ _0x124d1a = _0x5cd844(function (_0x770f81) {
+ !function () {
+ var _0x250d36 = "input is invalid type",
+ _0x4cfaee = !1,
+ _0x1702f9 = {},
+ _0x5ccbb3 = !_0x4cfaee && "object" == typeof self,
+ _0x54d876 = !_0x1702f9.JS_MD5_NO_NODE_JS && "object" == typeof process && process.versions && process.versions.node,
+ _0x185caf;
+ _0x54d876 ? _0x1702f9 = _0x397dc7 : _0x5ccbb3 && (_0x1702f9 = self);
+ var _0x17dcbf = !_0x1702f9.JS_MD5_NO_COMMON_JS && _0x770f81.exports,
+ _0x554fed = !1,
+ _0x2de28f = !_0x1702f9.JS_MD5_NO_ARRAY_BUFFER && "undefined" != typeof ArrayBuffer,
+ _0x3a9a1b = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"],
+ _0x465562 = [128, 32768, 8388608, -2147483648],
+ _0x20b37e = [0, 8, 16, 24],
+ _0x323604 = ["hex", "array", "digest", "buffer", "arrayBuffer", "base64"],
+ _0x2c185e = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"],
+ _0x4b59e0 = [];
+ if (_0x2de28f) {
+ var _0x395837 = new ArrayBuffer(68);
+ _0x185caf = new Uint8Array(_0x395837), _0x4b59e0 = new Uint32Array(_0x395837)
+ }
+ !_0x1702f9.JS_MD5_NO_NODE_JS && Array.isArray || (Array.isArray = function (e) {
+ return "[object Array]" === Object.prototype.toString.call(e)
+ }), _0x2de28f && (_0x1702f9.JS_MD5_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView) && (ArrayBuffer.isView = function (e) {
+ return "object" == typeof e && e.buffer && e.buffer.constructor === ArrayBuffer
+ });
+ var _0x4e9930 = function (e) {
+ return function (b) {
+ return new _0x5887c8(!0).update(b)[e]()
+ }
+ },
+ _0x38ba77 = function () {
+ var e = _0x4e9930("hex");
+ _0x54d876 && (e = _0x474989(e)), e.create = function () {
+ return new _0x5887c8
+ }, e.update = function (b) {
+ return e.create().update(b)
+ };
+ for (var b = 0; b < _0x323604.length; ++b) {
+ var a = _0x323604[b];
+ e[a] = _0x4e9930(a)
+ }
+ return e
+ },
+ _0x474989 = function (_0x57eeaa) {
+ var _0x114910, _0x226465 = eval("require('crypto');"),
+ _0x1f6ae0 = eval("require('buffer')['Buffer'];");
+ return function (e) {
+ if ("string" == typeof e) return _0x226465.createHash("md5").update(e, "utf8").digest("hex");
+ if (null == e) throw _0x250d36;
+ return e.constructor === ArrayBuffer && (e = new Uint8Array(e)), Array.isArray(e) || ArrayBuffer.isView(e) || e.constructor === _0x1f6ae0 ? _0x226465.createHash("md5").update(new _0x1f6ae0.from(e)).digest("hex") : _0x57eeaa(e)
+ }
+ };
+
+ function _0x5887c8(e) {
+ if (e) _0x4b59e0[0] = _0x4b59e0[16] = _0x4b59e0[1] = _0x4b59e0[2] = _0x4b59e0[3] = _0x4b59e0[4] = _0x4b59e0[5] = _0x4b59e0[6] = _0x4b59e0[7] = _0x4b59e0[8] = _0x4b59e0[9] = _0x4b59e0[10] = _0x4b59e0[11] = _0x4b59e0[12] = _0x4b59e0[13] = _0x4b59e0[14] = _0x4b59e0[15] = 0, this.blocks = _0x4b59e0, this.buffer8 = _0x185caf;
+ else if (_0x2de28f) {
+ var b = new ArrayBuffer(68);
+ this.buffer8 = new Uint8Array(b), this.blocks = new Uint32Array(b)
+ } else this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ this.h0 = this.h1 = this.h2 = this.h3 = this.start = this.bytes = this.hBytes = 0, this.finalized = this.hashed = !1, this.first = !0
+ }
+
+ _0x5887c8.prototype.update = function (e) {
+ if (!this.finalized) {
+ var b, a = typeof e;
+ if ("string" !== a) {
+ if ("object" !== a || null === e) throw _0x250d36;
+ if (_0x2de28f && e.constructor === ArrayBuffer) e = new Uint8Array(e);
+ else if (!(Array.isArray(e) || _0x2de28f && ArrayBuffer.isView(e))) throw _0x250d36;
+ b = !0
+ }
+ for (var f, c, r = 0, t = e.length, d = this.blocks, i = this.buffer8; r < t;) {
+ if (this.hashed && (this.hashed = !1, d[0] = d[16], d[16] = d[1] = d[2] = d[3] = d[4] = d[5] = d[6] = d[7] = d[8] = d[9] = d[10] = d[11] = d[12] = d[13] = d[14] = d[15] = 0), b)
+ if (_0x2de28f)
+ for (c = this.start; r < t && c < 64; ++r) i[c++] = e[r];
+ else
+ for (c = this.start; r < t && c < 64; ++r) d[c >> 2] |= e[r] << _0x20b37e[3 & c++];
+ else if (_0x2de28f)
+ for (c = this.start; r < t && c < 64; ++r) (f = e.charCodeAt(r)) < 128 ? i[c++] = f : f < 2048 ? (i[c++] = 192 | f >> 6, i[c++] = 128 | 63 & f) : f < 55296 || f >= 57344 ? (i[c++] = 224 | f >> 12, i[c++] = 128 | f >> 6 & 63, i[c++] = 128 | 63 & f) : (f = 65536 + ((1023 & f) << 10 | 1023 & e.charCodeAt(++r)), i[c++] = 240 | f >> 18, i[c++] = 128 | f >> 12 & 63, i[c++] = 128 | f >> 6 & 63, i[c++] = 128 | 63 & f);
+ else
+ for (c = this.start; r < t && c < 64; ++r) (f = e.charCodeAt(r)) < 128 ? d[c >> 2] |= f << _0x20b37e[3 & c++] : f < 2048 ? (d[c >> 2] |= (192 | f >> 6) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]) : f < 55296 || f >= 57344 ? (d[c >> 2] |= (224 | f >> 12) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 6 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]) : (f = 65536 + ((1023 & f) << 10 | 1023 & e.charCodeAt(++r)), d[c >> 2] |= (240 | f >> 18) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 12 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 6 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]);
+ this.lastByteIndex = c, this.bytes += c - this.start, c >= 64 ? (this.start = c - 64, this.hash(), this.hashed = !0) : this.start = c
+ }
+ return this.bytes > 4294967295 && (this.hBytes += this.bytes / 4294967296 << 0, this.bytes = this.bytes % 4294967296), this
+ }
+ }, _0x5887c8.prototype.finalize = function () {
+ if (!this.finalized) {
+ this.finalized = !0;
+ var e = this.blocks,
+ b = this.lastByteIndex;
+ e[b >> 2] |= _0x465562[3 & b], b >= 56 && (this.hashed || this.hash(), e[0] = e[16], e[16] = e[1] = e[2] = e[3] = e[4] = e[5] = e[6] = e[7] = e[8] = e[9] = e[10] = e[11] = e[12] = e[13] = e[14] = e[15] = 0), e[14] = this.bytes << 3, e[15] = this.hBytes << 3 | this.bytes >>> 29, this.hash()
+ }
+ }, _0x5887c8.prototype.hash = function () {
+ var e, b, a, f, c, r, t = this.blocks;
+ this.first ? b = ((b = ((e = ((e = t[0] - 680876937) << 7 | e >>> 25) - 271733879 << 0) ^ (a = ((a = (-271733879 ^ (f = ((f = (-1732584194 ^ 2004318071 & e) + t[1] - 117830708) << 12 | f >>> 20) + e << 0) & (-271733879 ^ e)) + t[2] - 1126478375) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[3] - 1316259209) << 22 | b >>> 10) + a << 0 : (e = this.h0, b = this.h1, a = this.h2, b = ((b += ((e = ((e += ((f = this.h3) ^ b & (a ^ f)) + t[0] - 680876936) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[1] - 389564586) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[2] + 606105819) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[3] - 1044525330) << 22 | b >>> 10) + a << 0), b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[4] - 176418897) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[5] + 1200080426) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[6] - 1473231341) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[7] - 45705983) << 22 | b >>> 10) + a << 0, b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[8] + 1770035416) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[9] - 1958414417) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[10] - 42063) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[11] - 1990404162) << 22 | b >>> 10) + a << 0, b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[12] + 1804603682) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[13] - 40341101) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[14] - 1502002290) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[15] + 1236535329) << 22 | b >>> 10) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[1] - 165796510) << 5 | e >>> 27) + b << 0) ^ b)) + t[6] - 1069501632) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[11] + 643717713) << 14 | a >>> 18) + f << 0) ^ f)) + t[0] - 373897302) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[5] - 701558691) << 5 | e >>> 27) + b << 0) ^ b)) + t[10] + 38016083) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[15] - 660478335) << 14 | a >>> 18) + f << 0) ^ f)) + t[4] - 405537848) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[9] + 568446438) << 5 | e >>> 27) + b << 0) ^ b)) + t[14] - 1019803690) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[3] - 187363961) << 14 | a >>> 18) + f << 0) ^ f)) + t[8] + 1163531501) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[13] - 1444681467) << 5 | e >>> 27) + b << 0) ^ b)) + t[2] - 51403784) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[7] + 1735328473) << 14 | a >>> 18) + f << 0) ^ f)) + t[12] - 1926607734) << 20 | b >>> 12) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[5] - 378558) << 4 | e >>> 28) + b << 0)) + t[8] - 2022574463) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[11] + 1839030562) << 16 | a >>> 16) + f << 0)) + t[14] - 35309556) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[1] - 1530992060) << 4 | e >>> 28) + b << 0)) + t[4] + 1272893353) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[7] - 155497632) << 16 | a >>> 16) + f << 0)) + t[10] - 1094730640) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[13] + 681279174) << 4 | e >>> 28) + b << 0)) + t[0] - 358537222) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[3] - 722521979) << 16 | a >>> 16) + f << 0)) + t[6] + 76029189) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[9] - 640364487) << 4 | e >>> 28) + b << 0)) + t[12] - 421815835) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[15] + 530742520) << 16 | a >>> 16) + f << 0)) + t[2] - 995338651) << 23 | b >>> 9) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[0] - 198630844) << 6 | e >>> 26) + b << 0) | ~a)) + t[7] + 1126891415) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[14] - 1416354905) << 15 | a >>> 17) + f << 0) | ~e)) + t[5] - 57434055) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[12] + 1700485571) << 6 | e >>> 26) + b << 0) | ~a)) + t[3] - 1894986606) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[10] - 1051523) << 15 | a >>> 17) + f << 0) | ~e)) + t[1] - 2054922799) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[8] + 1873313359) << 6 | e >>> 26) + b << 0) | ~a)) + t[15] - 30611744) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[6] - 1560198380) << 15 | a >>> 17) + f << 0) | ~e)) + t[13] + 1309151649) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[4] - 145523070) << 6 | e >>> 26) + b << 0) | ~a)) + t[11] - 1120210379) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[2] + 718787259) << 15 | a >>> 17) + f << 0) | ~e)) + t[9] - 343485551) << 21 | b >>> 11) + a << 0, this.first ? (this.h0 = e + 1732584193 << 0, this.h1 = b - 271733879 << 0, this.h2 = a - 1732584194 << 0, this.h3 = f + 271733878 << 0, this.first = !1) : (this.h0 = this.h0 + e << 0, this.h1 = this.h1 + b << 0, this.h2 = this.h2 + a << 0, this.h3 = this.h3 + f << 0)
+ }, _0x5887c8.prototype.hex = function () {
+ this.finalize();
+ var e = this.h0,
+ b = this.h1,
+ a = this.h2,
+ f = this.h3;
+ return _0x3a9a1b[e >> 4 & 15] + _0x3a9a1b[15 & e] + _0x3a9a1b[e >> 12 & 15] + _0x3a9a1b[e >> 8 & 15] + _0x3a9a1b[e >> 20 & 15] + _0x3a9a1b[e >> 16 & 15] + _0x3a9a1b[e >> 28 & 15] + _0x3a9a1b[e >> 24 & 15] + _0x3a9a1b[b >> 4 & 15] + _0x3a9a1b[15 & b] + _0x3a9a1b[b >> 12 & 15] + _0x3a9a1b[b >> 8 & 15] + _0x3a9a1b[b >> 20 & 15] + _0x3a9a1b[b >> 16 & 15] + _0x3a9a1b[b >> 28 & 15] + _0x3a9a1b[b >> 24 & 15] + _0x3a9a1b[a >> 4 & 15] + _0x3a9a1b[15 & a] + _0x3a9a1b[a >> 12 & 15] + _0x3a9a1b[a >> 8 & 15] + _0x3a9a1b[a >> 20 & 15] + _0x3a9a1b[a >> 16 & 15] + _0x3a9a1b[a >> 28 & 15] + _0x3a9a1b[a >> 24 & 15] + _0x3a9a1b[f >> 4 & 15] + _0x3a9a1b[15 & f] + _0x3a9a1b[f >> 12 & 15] + _0x3a9a1b[f >> 8 & 15] + _0x3a9a1b[f >> 20 & 15] + _0x3a9a1b[f >> 16 & 15] + _0x3a9a1b[f >> 28 & 15] + _0x3a9a1b[f >> 24 & 15]
+ }, _0x5887c8.prototype.toString = _0x5887c8.prototype.hex, _0x5887c8.prototype.digest = function () {
+ this.finalize();
+ var e = this.h0,
+ b = this.h1,
+ a = this.h2,
+ f = this.h3;
+ return [255 & e, e >> 8 & 255, e >> 16 & 255, e >> 24 & 255, 255 & b, b >> 8 & 255, b >> 16 & 255, b >> 24 & 255, 255 & a, a >> 8 & 255, a >> 16 & 255, a >> 24 & 255, 255 & f, f >> 8 & 255, f >> 16 & 255, f >> 24 & 255]
+ }, _0x5887c8.prototype.array = _0x5887c8.prototype.digest, _0x5887c8.prototype.arrayBuffer = function () {
+ this.finalize();
+ var e = new ArrayBuffer(16),
+ b = new Uint32Array(e);
+ return b[0] = this.h0, b[1] = this.h1, b[2] = this.h2, b[3] = this.h3, e
+ }, _0x5887c8.prototype.buffer = _0x5887c8.prototype.arrayBuffer, _0x5887c8.prototype.base64 = function () {
+ for (var e, b, a, f = "", c = this.array(), r = 0; r < 15;) e = c[r++], b = c[r++], a = c[r++], f += _0x2c185e[e >>> 2] + _0x2c185e[63 & (e << 4 | b >>> 4)] + _0x2c185e[63 & (b << 2 | a >>> 6)] + _0x2c185e[63 & a];
+ return f + (_0x2c185e[(e = c[r]) >>> 2] + _0x2c185e[e << 4 & 63] + "==")
+ };
+ var _0x4dd781 = _0x38ba77();
+ _0x17dcbf ? _0x770f81.exports = _0x4dd781 : (_0x1702f9.md5 = _0x4dd781, _0x554fed && (void 0)(function () {
+ return _0x4dd781
+ }))
+ }()
+ });
+
+function _0x178cef(e) {
+ return jsvmp("484e4f4a403f52430038001eab0015840e8ee21a00000000000000621b000200001d000146000306000e271f001b000200021d00010500121b001b000b021b000b04041d0001071b000b0500000003000126207575757575757575757575757575757575757575757575757575757575757575", [, , void 0 !== _0x124d1a ? _0x124d1a : void 0, _0x178cef, e])
+}
+
+for (var _0xb55f3e = {
+ boe: !1,
+ aid: 0,
+ dfp: !1,
+ sdi: !1,
+ enablePathList: [],
+ _enablePathListRegex: [],
+ urlRewriteRules: [],
+ _urlRewriteRules: [],
+ initialized: !1,
+ enableTrack: !1,
+ track: {
+ unitTime: 0,
+ unitAmount: 0,
+ fre: 0
+ },
+ triggerUnload: !1,
+ region: "",
+ regionConf: {},
+ umode: 0,
+ v: !1,
+ perf: !1,
+ xxbg: !0
+}, _0x3eaf64 = {
+ debug: function (e, b) {
+ let a = !1;
+ a = !1
+ }
+}, _0x233455 = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"], _0x2e9f6d = [], _0x511f86 = [], _0x3d35de = 0; _0x3d35de < 256; _0x3d35de++) _0x2e9f6d[_0x3d35de] = _0x233455[_0x3d35de >> 4 & 15] + _0x233455[15 & _0x3d35de], _0x3d35de < 16 && (_0x3d35de < 10 ? _0x511f86[48 + _0x3d35de] = _0x3d35de : _0x511f86[87 + _0x3d35de] = _0x3d35de);
+var _0x2ce54d = function (e) {
+ for (var b = e.length, a = "", f = 0; f < b;) a += _0x2e9f6d[e[f++]];
+ return a
+ },
+ _0x5960a2 = function (e) {
+ for (var b = e.length >> 1, a = b << 1, f = new Uint8Array(b), c = 0, r = 0; r < a;) f[c++] = _0x511f86[e.charCodeAt(r++)] << 4 | _0x511f86[e.charCodeAt(r++)];
+ return f
+ },
+ _0x4e46b6 = {
+ encode: _0x2ce54d,
+ decode: _0x5960a2
+ };
+
+function sign(e, b) {
+ return jsvmp("484e4f4a403f5243001f240fbf2031ccf317480300000000000007181b0002012e1d00921b000b171b000b02402217000a1c1b000b1726402217000c1c1b000b170200004017002646000306000e271f001b000200021d00920500121b001b000b031b000b17041d0092071b000b041e012f17000d1b000b05260a0000101c1b000b06260a0000101c1b001b000b071e01301d00931b001b000b081e00081d00941b0048021d00951b001b000b1b1d00961b0048401d009e1b001b000b031b000b16041d009f1b001b000b09221e0131241b000b031b000b09221e0131241b000b1e0a000110040a0001101d00d51b001b000b09221e0131241b000b031b000b09221e0131241b000b180a000110040a0001101d00d71b001b000b0a1e00101d00d91b001b000b0b261b000b1a1b000b190a0002101d00db1b001b000b0c261b000b221b000b210a0002101d00dc1b001b000b0d261b000b230200200a0002101d00dd1b001b000b09221e0131241b000b031b000b24040a0001101d00df1b001b000b0e1a00221e00de240a0000104903e82b1d00e31b001b000b0f260a0000101d00e41b001b000b1d1d00e71b001b000b1a4901002b1d00e81b001b000b1a4901002c1d00ea1b001b000b191d00f21b001b000b1f480e191d00f81b001b000b1f480f191d00f91b001b000b20480e191d00fb1b001b000b20480f191d00fe1b001b000b25480e191d01001b001b000b25480f191d01011b001b000b264818344900ff2f1d01031b001b000b264810344900ff2f1d01321b001b000b264808344900ff2f1d01331b001b000b264800344900ff2f1d01341b001b000b274818344900ff2f1d01351b001b000b274810344900ff2f1d01361b001b000b274808344900ff2f1d01371b001b000b274800344900ff2f1d01381b001b000b281b000b29311b000b2a311b000b2b311b000b2c311b000b2d311b000b2e311b000b2f311b000b30311b000b31311b000b32311b000b33311b000b34311b000b35311b000b36311b000b37311b000b38311b000b39311d01391b004900ff1d013a1b001b000b10261b000b281b000b2a1b000b2c1b000b2e1b000b301b000b321b000b341b000b361b000b381b000b3a1b000b291b000b2b1b000b2d1b000b2f1b000b311b000b331b000b351b000b371b000b390a0013101d013b1b001b000b0c261b000b111b000b3b041b000b3c0a0002101d013c1b001b000b12261b000b1c1b000b3b1b000b3d0a0003101d013d1b001b000b13261b000b3e0200240a0002101d013e1b000b3f0000013f000126207575757575757575757575757575757575757575757575757575757575757575012b0e7776757a7d7643617c637661676a027a77065c717976706708777671667474766107767d65707c77760374766707707c7d607c7f7607757a61767166740a7c66677661447a77677b0a7a7d7d7661447a77677b0b7c666776615b767a747b670b7a7d7d76615b767a747b6709666076615274767d670b677c5f7c64766150726076077a7d77766b5c7508767f767067617c7d09667d7776757a7d76770963617c677c676a637608677c4067617a7d740470727f7f0763617c7076606010487c71797670673363617c707660604e067c717976706705677a677f76047d7c7776012e0125012402602341525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a383c2e0260224157787763747b2749586042512b233c5e75656420254b5a22412126384446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e0260214157787763747b2749586042512b233c5e75656420254b5a224121263e4446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e02602041525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e4c2e012a022222067f767d74677b0a707b7261507c7776526702222306707b726152670f487c717976706733447a7d777c644e08577c70667e767d6712487c7179767067335d72657a7472677c614e057960777c7e10487c7179767067335b7a60677c616a4e07637f66747a7d60084c637b727d677c7e0b70727f7f437b727d677c7e0b4c4c7d7a747b677e726176055266777a7c1850727d65726041767d7776617a7d74507c7d67766b6721570964767177617a657661137476675c647d43617c637661676a5d727e7660097f727d74667274766006707b617c7e760761667d677a7e7607707c7d7d767067144c4c64767177617a6576614c7665727f66726776134c4c60767f767d7a667e4c7665727f667267761b4c4c64767177617a6576614c6070617a63674c75667d70677a7c7d174c4c64767177617a6576614c6070617a63674c75667d70154c4c64767177617a6576614c6070617a63674c757d134c4c756b77617a6576614c7665727f66726776124c4c77617a6576614c667d64617263637677154c4c64767177617a6576614c667d64617263637677114c4c77617a6576614c7665727f66726776144c4c60767f767d7a667e4c667d64617263637677144c4c756b77617a6576614c667d64617263637677094c60767f767d7a667e0c70727f7f40767f767d7a667e164c40767f767d7a667e4c5a57564c4176707c6177766108777c70667e767d670478766a60057e7267707b06417674566b630a4f3748723e694e77704c067072707b764c04607c7e7608707675407b72616308507675407b72616305767c72637a16767c44767151617c64607661577a60637267707b76610f717a7d775c717976706752606a7d700e7a60565c44767151617c646076610120047c63767d0467766067097a7d707c747d7a677c077c7d7661617c6104707c77761242465c47524c564b5056565756574c5641410e607660607a7c7d40677c61727476076076675a67767e10607c7e7658766a5b766176516a6776770a61767e7c65765a67767e097a7d77766b767757510c437c7a7d6776615665767d670e5e40437c7a7d6776615665767d670d706176726776567f767e767d670670727d65726009677c5772677246415f076176637f727076034f603901740a7d72677a6576707c777614487c717976706733437f66747a7d526161726a4e4a4d7b676763602c294f3c4f3c3b48233e2a4e68223f206e3b4f3d48233e2a4e68223f206e3a68206e6f48723e75233e2a4e68223f276e3b2948723e75233e2a4e68223f276e3a68246e3a0127087f7c7072677a7c7d047b61767504757a7f76107b676763293c3c7f7c70727f7b7c606708637f7267757c617e02222102222007647a7d777c646002222703647a7d02222607727d77617c7a77022225057f7a7d666b022224067a637b7c7d7602222b047a63727702222a047a637c77022123037e7270022122097e72707a7d677c607b0c7e72704c637c64766163703a0470617c60036b22220570617a7c6005756b7a7c6004637a787602212102212002212702212602212502212402212b08757a6176757c6b3c067c637661723c05337c63613c05337c63673c07707b617c7e763c0867617a77767d673c047e607a7602212a0220230665767d777c6106547c7c747f760e4c637261727e40647a67707b5c7d0a777a61767067407a747d0a707c7d607a6067767d670660647a67707b03777c7e07637b727d677c7e047b7c7c7840525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e3d03727a77017d01750161096067726167477a7e7601670972717a7f7a677a76600a677a7e766067727e6322137b72617764726176507c7d70666161767d706a0c7776657a70765e767e7c616a087f727d74667274760a6176607c7f66677a7c7d0f7265727a7f4176607c7f66677a7c7d0960706176767d477c630a60706176767d5f767567107776657a7076437a6b767f4172677a7c0a63617c77667067406671077172676776616a016309677c66707b5a7d757c08677a7e76697c7d760a677a7e766067727e6321077463665a7d757c0b7960557c7d67605f7a60670b637f66747a7d605f7a60670a677a7e766067727e63200a76657661507c7c787a760767674c60707a77017e0b606a7d67726b5661617c610c7d72677a65765f767d74677b056167705a43097563457661607a7c7d0b4c4c657661607a7c7d4c4c08707f7a767d675a770a677a7e766067727e63270b766b67767d77557a767f77046366607b03727f7f04677b767d097172607625274c707b0c75617c7e507b7261507c7776067125274c2023022022087172607625274c23022021087172607625274c22022020087172607625274c2102202702202602202507747667477a7e760220240b777c7e5d7c6745727f7a77096066716067617a7d740863617c677c707c7f02202b02202a01230e222323232323232322222323232302272302272207757c616176727f02272104717c776a096067617a7d747a756a02686e0b717c776a45727f216067610a717c776a4c7b72607b2e01350366617f02272005626676616a0a72607c7f774c607a747d096372677b7d727e762e0967674c6476717a772e063566667a772e0227270227260e4c716a6776774c6076704c777a770227250a27212a272a2524212a25097576457661607a7c7d0227240e4c232151274925647c232323232202272b02272a05607f7a7076022623074056505a5d555c037d7c6409677a7e766067727e6305757f7c7c610661727d777c7e0f7476674747447671507c7c787a7660056767647a770867674c6476717a770767674476715a770b67674c6476717a774c65210967674476717a7745210761667d7d7a7d7405757f66607b087e7c65765f7a60670660637f7a70760671765e7c657609707f7a70785f7a6067077176507f7a70780c78766a717c7261775f7a60670a717658766a717c7261770b7270677a657640677267760b647a7d777c6440677267760360477e05676172707808667d7a67477a7e76037270700a667d7a67527e7c667d670871767b72657a7c61077e6074476a637603645a5707727a775f7a60670b63617a6572706a5e7c777606706660677c7e067260607a747d0f4456514c5756455a50564c5a5d555c0479607c7d0a6176747a7c7d507c7d75096176637c616746617f04766b7a67094b3e5e403e404746510c4b3e5e403e43524a5f5c525720232323232323232323232323232323232323232323232323232323232323232320772722772b70772a2b75232371212327762a2b23232a2a2b7670752b272124760165066671707c7776067776707c777602262202262102262002262702262602262502262402262b02262a022523022522022521022520", [, , void 0, void 0 !== _0x178cef ? _0x178cef : void 0, {
+ boe: !1,
+ aid: 0,
+ dfp: !1,
+ sdi: !1,
+ enablePathList: [],
+ _enablePathListRegex: [/\/web\/report/],
+ urlRewriteRules: [],
+ _urlRewriteRules: [],
+ initialized: !1,
+ enableTrack: !1,
+ track: {
+ unitTime: 0,
+ unitAmount: 0,
+ fre: 0
+ },
+ triggerUnload: !1,
+ region: "",
+ regionConf: {},
+ umode: 0,
+ v: !1,
+ perf: !1,
+ xxbg: !0
+ }, () => 0, () => "03v", {
+ ubcode: 0
+ }, {
+ bogusIndex: 0,
+ msNewTokenList: [],
+ moveList: [],
+ clickList: [],
+ keyboardList: [],
+ activeState: [],
+ aidList: [],
+ envcode: 0,
+ msToken: "",
+ msStatus: 0,
+ __ac_testid: "",
+ ttwid: "",
+ tt_webid: "",
+ tt_webid_v2: ""
+ }, void 0 !== _0x4e46b6 ? _0x4e46b6 : void 0, {
+ userAgent: b
+ }, (e, b) => {
+ let a = new Uint8Array(3);
+ return a[0] = e / 256, a[1] = e % 256, a[2] = b % 256, String.fromCharCode.apply(null, a)
+ }, (e, b) => {
+ let a, f = [],
+ c = 0,
+ r = "";
+ for (let e = 0; e < 256; e++) f[e] = e;
+ for (let b = 0; b < 256; b++) c = (c + f[b] + e.charCodeAt(b % e.length)) % 256, a = f[b], f[b] = f[c], f[c] = a;
+ let t = 0;
+ c = 0;
+ for (let e = 0; e < b.length; e++) c = (c + f[t = (t + 1) % 256]) % 256, a = f[t], f[t] = f[c], f[c] = a, r += String.fromCharCode(b.charCodeAt(e) ^ f[(f[t] + f[c]) % 256]);
+ return r
+ }, (e, b) => jsvmp("484e4f4a403f524300281018f7b851f02d296e5b00000000000004a21b0002001d1d001e1b00131e00061a001d001f1b000b070200200200210d1b000b070200220200230d1b000b070200240200250d1b000b070200260200270d1b001b000b071b000b05191d00031b000200001d00281b0048001d00291b000b041e002a1b000b0b4803283b1700f11b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f480833301b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b08221e002d241b000b0a490fc02f4806340a000110281d00281b00220b091b000b08221e002d241b000b0a483f2f0a000110281d002816ff031b000b041e002a1b000b0b294800391700e01b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b041e002a1b000b0b3917001e1b000b04221e002b241b000b0b0a0001104900ff2f4808331600054800301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b041e002a1b000b0b3917001e1b000b08221e002d241b000b0a490fc02f4806340a0001101600071b000b06281d00281b00220b091b000b06281d00281b000b090000002e000126207575757575757575757575757575757575757575757575757575757575757575012b0e7776757a7d7643617c637661676a027a77065c717976706708777671667474766107767d65707c77760374766707707c7d607c7f7607757a61767166740a7c66677661447a77677b0a7a7d7d7661447a77677b0b7c666776615b767a747b670b7a7d7d76615b767a747b6709666076615274767d670b677c5f7c64766150726076077a7d77766b5c7508767f767067617c7d09667d7776757a7d76770963617c677c676a637608677c4067617a7d740470727f7f0763617c7076606010487c71797670673363617c707660604e067c717976706705677a677f76047d7c7776012e0125012402602341525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a383c2e0260224157787763747b2749586042512b233c5e75656420254b5a22412126384446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e0260214157787763747b2749586042512b233c5e75656420254b5a224121263e4446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e02602041525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e4c2e012a022222067f767d74677b0a707b7261507c7776526702222306707b72615267", [, , , , e, b]), "undefined" != typeof Date ? Date : void 0, () => 0, (e, b, a, f, c, r, t, d, i, n, s, o, l, _, x, u, h, p, y) => {
+ let v = new Uint8Array(19);
+ return v[0] = e, v[1] = s, v[2] = b, v[3] = o, v[4] = a, v[5] = l, v[6] = f, v[7] = _, v[8] = c, v[9] = x, v[10] = r, v[11] = u, v[12] = t, v[13] = h, v[14] = d, v[15] = p, v[16] = i, v[17] = y, v[18] = n, String.fromCharCode.apply(null, v)
+ }, e => String.fromCharCode(e), (e, b, a) => String.fromCharCode(e) + String.fromCharCode(b) + a, (e, b) => jsvmp("484e4f4a403f524300281018f7b851f02d296e5b00000000000004a21b0002001d1d001e1b00131e00061a001d001f1b000b070200200200210d1b000b070200220200230d1b000b070200240200250d1b000b070200260200270d1b001b000b071b000b05191d00031b000200001d00281b0048001d00291b000b041e002a1b000b0b4803283b1700f11b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f480833301b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b08221e002d241b000b0a490fc02f4806340a000110281d00281b00220b091b000b08221e002d241b000b0a483f2f0a000110281d002816ff031b000b041e002a1b000b0b294800391700e01b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b041e002a1b000b0b3917001e1b000b04221e002b241b000b0b0a0001104900ff2f4808331600054800301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b041e002a1b000b0b3917001e1b000b08221e002d241b000b0a490fc02f4806340a0001101600071b000b06281d00281b00220b091b000b06281d00281b000b090000002e000126207575757575757575757575757575757575757575757575757575757575757575012b0e7776757a7d7643617c637661676a027a77065c717976706708777671667474766107767d65707c77760374766707707c7d607c7f7607757a61767166740a7c66677661447a77677b0a7a7d7d7661447a77677b0b7c666776615b767a747b670b7a7d7d76615b767a747b6709666076615274767d670b677c5f7c64766150726076077a7d77766b5c7508767f767067617c7d09667d7776757a7d76770963617c677c676a637608677c4067617a7d740470727f7f0763617c7076606010487c71797670673363617c707660604e067c717976706705677a677f76047d7c7776012e0125012402602341525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a383c2e0260224157787763747b2749586042512b233c5e75656420254b5a22412126384446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e0260214157787763747b2749586042512b233c5e75656420254b5a224121263e4446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e02602041525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e4c2e012a022222067f767d74677b0a707b7261507c7776526702222306707b72615267", [, , , , e, b]), , sign, e, void 0])
+}
+
+function get_web_id() {
+ return function e(t) {
+ return t ? (t ^ 16 * Math.random() >> t / 4).toString(10) : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, e)
+ }().replace(/-/g, "").slice(0, 19)
+}
+
+module.exports = {
+ sign,
+ get_web_id
+};
diff --git a/libs/stealth.min.js b/libs/stealth.min.js
new file mode 100644
index 0000000..b4ca68f
--- /dev/null
+++ b/libs/stealth.min.js
@@ -0,0 +1,7 @@
+/*!
+ * Note: Auto-generated, do not update manually.
+ * Generated by: https://github.com/berstend/puppeteer-extra/tree/master/packages/extract-stealth-evasions
+ * Generated on: Mon, 05 Jun 2023 06:17:57 GMT
+ * License: MIT
+ */
+(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:'utils => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via `Object.getOwnPropertyDescriptor(window, \'chrome\')`\n Object.defineProperty(window, \'chrome\', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We\'ll extend that later\n })\n }\n\n // That means we\'re running headful and don\'t need to mock anything\n if (\'app\' in window.chrome) {\n return // Nothing to do here\n }\n\n const makeError = {\n ErrorInInvocation: fn => {\n const err = new TypeError(`Error in invocation of app.${fn}()`)\n return utils.stripErrorWithAnchor(\n err,\n `at ${fn} (eval at `\n )\n }\n }\n\n // There\'s a some static data in that property which doesn\'t seem to change,\n // we should periodically check for updates: `JSON.stringify(window.app, null, 2)`\n const STATIC_DATA = JSON.parse(\n `\n{\n "isInstalled": false,\n "InstallState": {\n "DISABLED": "disabled",\n "INSTALLED": "installed",\n "NOT_INSTALLED": "not_installed"\n },\n "RunningState": {\n "CANNOT_RUN": "cannot_run",\n "READY_TO_RUN": "ready_to_run",\n "RUNNING": "running"\n }\n}\n `.trim()\n )\n\n window.chrome.app = {\n ...STATIC_DATA,\n\n get isInstalled() {\n return false\n },\n\n getDetails: function getDetails() {\n if (arguments.length) {\n throw makeError.ErrorInInvocation(`getDetails`)\n }\n return null\n },\n getIsInstalled: function getDetails() {\n if (arguments.length) {\n throw makeError.ErrorInInvocation(`getIsInstalled`)\n }\n return false\n },\n runningState: function getDetails() {\n if (arguments.length) {\n throw makeError.ErrorInInvocation(`runningState`)\n }\n return \'cannot_run\'\n }\n }\n utils.patchToStringNested(window.chrome.app)\n }',_args:[]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"utils => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`\n Object.defineProperty(window, 'chrome', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We'll extend that later\n })\n }\n\n // That means we're running headful and don't need to mock anything\n if ('csi' in window.chrome) {\n return // Nothing to do here\n }\n\n // Check that the Navigation Timing API v1 is available, we need that\n if (!window.performance || !window.performance.timing) {\n return\n }\n\n const { timing } = window.performance\n\n window.chrome.csi = function() {\n return {\n onloadT: timing.domContentLoadedEventEnd,\n startE: timing.navigationStart,\n pageT: Date.now() - timing.navigationStart,\n tran: 15 // Transition type or something\n }\n }\n utils.patchToString(window.chrome.csi)\n }",_args:[]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { opts }) => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`\n Object.defineProperty(window, 'chrome', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We'll extend that later\n })\n }\n\n // That means we're running headful and don't need to mock anything\n if ('loadTimes' in window.chrome) {\n return // Nothing to do here\n }\n\n // Check that the Navigation Timing API v1 + v2 is available, we need that\n if (\n !window.performance ||\n !window.performance.timing ||\n !window.PerformancePaintTiming\n ) {\n return\n }\n\n const { performance } = window\n\n // Some stuff is not available on about:blank as it requires a navigation to occur,\n // let's harden the code to not fail then:\n const ntEntryFallback = {\n nextHopProtocol: 'h2',\n type: 'other'\n }\n\n // The API exposes some funky info regarding the connection\n const protocolInfo = {\n get connectionInfo() {\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ntEntry.nextHopProtocol\n },\n get npnNegotiatedProtocol() {\n // NPN is deprecated in favor of ALPN, but this implementation returns the\n // HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)\n ? ntEntry.nextHopProtocol\n : 'unknown'\n },\n get navigationType() {\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ntEntry.type\n },\n get wasAlternateProtocolAvailable() {\n // The Alternate-Protocol header is deprecated in favor of Alt-Svc\n // (https://www.mnot.net/blog/2016/03/09/alt-svc), so technically this\n // should always return false.\n return false\n },\n get wasFetchedViaSpdy() {\n // SPDY is deprecated in favor of HTTP/2, but this implementation returns\n // true for HTTP/2 or HTTP2+QUIC/39 as well.\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)\n },\n get wasNpnNegotiated() {\n // NPN is deprecated in favor of ALPN, but this implementation returns true\n // for HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)\n }\n }\n\n const { timing } = window.performance\n\n // Truncate number to specific number of decimals, most of the `loadTimes` stuff has 3\n function toFixed(num, fixed) {\n var re = new RegExp('^-?\\\\d+(?:.\\\\d{0,' + (fixed || -1) + '})?')\n return num.toString().match(re)[0]\n }\n\n const timingInfo = {\n get firstPaintAfterLoadTime() {\n // This was never actually implemented and always returns 0.\n return 0\n },\n get requestTime() {\n return timing.navigationStart / 1000\n },\n get startLoadTime() {\n return timing.navigationStart / 1000\n },\n get commitLoadTime() {\n return timing.responseStart / 1000\n },\n get finishDocumentLoadTime() {\n return timing.domContentLoadedEventEnd / 1000\n },\n get finishLoadTime() {\n return timing.loadEventEnd / 1000\n },\n get firstPaintTime() {\n const fpEntry = performance.getEntriesByType('paint')[0] || {\n startTime: timing.loadEventEnd / 1000 // Fallback if no navigation occured (`about:blank`)\n }\n return toFixed(\n (fpEntry.startTime + performance.timeOrigin) / 1000,\n 3\n )\n }\n }\n\n window.chrome.loadTimes = function() {\n return {\n ...protocolInfo,\n ...timingInfo\n }\n }\n utils.patchToString(window.chrome.loadTimes)\n }",_args:[{opts:{}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { opts, STATIC_DATA }) => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`\n Object.defineProperty(window, 'chrome', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We'll extend that later\n })\n }\n\n // That means we're running headful and don't need to mock anything\n const existsAlready = 'runtime' in window.chrome\n // `chrome.runtime` is only exposed on secure origins\n const isNotSecure = !window.location.protocol.startsWith('https')\n if (existsAlready || (isNotSecure && !opts.runOnInsecureOrigins)) {\n return // Nothing to do here\n }\n\n window.chrome.runtime = {\n // There's a bunch of static data in that property which doesn't seem to change,\n // we should periodically check for updates: `JSON.stringify(window.chrome.runtime, null, 2)`\n ...STATIC_DATA,\n // `chrome.runtime.id` is extension related and returns undefined in Chrome\n get id() {\n return undefined\n },\n // These two require more sophisticated mocks\n connect: null,\n sendMessage: null\n }\n\n const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({\n NoMatchingSignature: new TypeError(\n preamble + `No matching signature.`\n ),\n MustSpecifyExtensionID: new TypeError(\n preamble +\n `${method} called from a webpage must specify an Extension ID (string) for its first argument.`\n ),\n InvalidExtensionID: new TypeError(\n preamble + `Invalid extension id: '${extensionId}'`\n )\n })\n\n // Valid Extension IDs are 32 characters in length and use the letter `a` to `p`:\n // https://source.chromium.org/chromium/chromium/src/+/master:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90\n const isValidExtensionID = str =>\n str.length === 32 && str.toLowerCase().match(/^[a-p]+$/)\n\n /** Mock `chrome.runtime.sendMessage` */\n const sendMessageHandler = {\n apply: function(target, ctx, args) {\n const [extensionId, options, responseCallback] = args || []\n\n // Define custom errors\n const errorPreamble = `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): `\n const Errors = makeCustomRuntimeErrors(\n errorPreamble,\n `chrome.runtime.sendMessage()`,\n extensionId\n )\n\n // Check if the call signature looks ok\n const noArguments = args.length === 0\n const tooManyArguments = args.length > 4\n const incorrectOptions = options && typeof options !== 'object'\n const incorrectResponseCallback =\n responseCallback && typeof responseCallback !== 'function'\n if (\n noArguments ||\n tooManyArguments ||\n incorrectOptions ||\n incorrectResponseCallback\n ) {\n throw Errors.NoMatchingSignature\n }\n\n // At least 2 arguments are required before we even validate the extension ID\n if (args.length < 2) {\n throw Errors.MustSpecifyExtensionID\n }\n\n // Now let's make sure we got a string as extension ID\n if (typeof extensionId !== 'string') {\n throw Errors.NoMatchingSignature\n }\n\n if (!isValidExtensionID(extensionId)) {\n throw Errors.InvalidExtensionID\n }\n\n return undefined // Normal behavior\n }\n }\n utils.mockWithProxy(\n window.chrome.runtime,\n 'sendMessage',\n function sendMessage() {},\n sendMessageHandler\n )\n\n /**\n * Mock `chrome.runtime.connect`\n *\n * @see https://developer.chrome.com/apps/runtime#method-connect\n */\n const connectHandler = {\n apply: function(target, ctx, args) {\n const [extensionId, connectInfo] = args || []\n\n // Define custom errors\n const errorPreamble = `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): `\n const Errors = makeCustomRuntimeErrors(\n errorPreamble,\n `chrome.runtime.connect()`,\n extensionId\n )\n\n // Behavior differs a bit from sendMessage:\n const noArguments = args.length === 0\n const emptyStringArgument = args.length === 1 && extensionId === ''\n if (noArguments || emptyStringArgument) {\n throw Errors.MustSpecifyExtensionID\n }\n\n const tooManyArguments = args.length > 2\n const incorrectConnectInfoType =\n connectInfo && typeof connectInfo !== 'object'\n\n if (tooManyArguments || incorrectConnectInfoType) {\n throw Errors.NoMatchingSignature\n }\n\n const extensionIdIsString = typeof extensionId === 'string'\n if (extensionIdIsString && extensionId === '') {\n throw Errors.MustSpecifyExtensionID\n }\n if (extensionIdIsString && !isValidExtensionID(extensionId)) {\n throw Errors.InvalidExtensionID\n }\n\n // There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate\n const validateConnectInfo = ci => {\n // More than a first param connectInfo as been provided\n if (args.length > 1) {\n throw Errors.NoMatchingSignature\n }\n // An empty connectInfo has been provided\n if (Object.keys(ci).length === 0) {\n throw Errors.MustSpecifyExtensionID\n }\n // Loop over all connectInfo props an check them\n Object.entries(ci).forEach(([k, v]) => {\n const isExpected = ['name', 'includeTlsChannelId'].includes(k)\n if (!isExpected) {\n throw new TypeError(\n errorPreamble + `Unexpected property: '${k}'.`\n )\n }\n const MismatchError = (propName, expected, found) =>\n TypeError(\n errorPreamble +\n `Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.`\n )\n if (k === 'name' && typeof v !== 'string') {\n throw MismatchError(k, 'string', typeof v)\n }\n if (k === 'includeTlsChannelId' && typeof v !== 'boolean') {\n throw MismatchError(k, 'boolean', typeof v)\n }\n })\n }\n if (typeof extensionId === 'object') {\n validateConnectInfo(extensionId)\n throw Errors.MustSpecifyExtensionID\n }\n\n // Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well\n return utils.patchToStringNested(makeConnectResponse())\n }\n }\n utils.mockWithProxy(\n window.chrome.runtime,\n 'connect',\n function connect() {},\n connectHandler\n )\n\n function makeConnectResponse() {\n const onSomething = () => ({\n addListener: function addListener() {},\n dispatch: function dispatch() {},\n hasListener: function hasListener() {},\n hasListeners: function hasListeners() {\n return false\n },\n removeListener: function removeListener() {}\n })\n\n const response = {\n name: '',\n sender: undefined,\n disconnect: function disconnect() {},\n onDisconnect: onSomething(),\n onMessage: onSomething(),\n postMessage: function postMessage() {\n if (!arguments.length) {\n throw new TypeError(`Insufficient number of arguments.`)\n }\n throw new Error(`Attempting to use a disconnected port object`)\n }\n }\n return response\n }\n }",_args:[{opts:{runOnInsecureOrigins:!1},STATIC_DATA:{OnInstalledReason:{CHROME_UPDATE:"chrome_update",INSTALL:"install",SHARED_MODULE_UPDATE:"shared_module_update",UPDATE:"update"},OnRestartRequiredReason:{APP_UPDATE:"app_update",OS_UPDATE:"os_update",PERIODIC:"periodic"},PlatformArch:{ARM:"arm",ARM64:"arm64",MIPS:"mips",MIPS64:"mips64",X86_32:"x86-32",X86_64:"x86-64"},PlatformNaclArch:{ARM:"arm",MIPS:"mips",MIPS64:"mips64",X86_32:"x86-32",X86_64:"x86-64"},PlatformOs:{ANDROID:"android",CROS:"cros",LINUX:"linux",MAC:"mac",OPENBSD:"openbsd",WIN:"win"},RequestUpdateCheckStatus:{NO_UPDATE:"no_update",THROTTLED:"throttled",UPDATE_AVAILABLE:"update_available"}}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"utils => {\n /**\n * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.\n *\n * @example\n * video/webm; codecs=\"vp8, vorbis\"\n * video/mp4; codecs=\"avc1.42E01E\"\n * audio/x-m4a;\n * audio/ogg; codecs=\"vorbis\"\n * @param {String} arg\n */\n const parseInput = arg => {\n const [mime, codecStr] = arg.trim().split(';')\n let codecs = []\n if (codecStr && codecStr.includes('codecs=\"')) {\n codecs = codecStr\n .trim()\n .replace(`codecs=\"`, '')\n .replace(`\"`, '')\n .trim()\n .split(',')\n .filter(x => !!x)\n .map(x => x.trim())\n }\n return {\n mime,\n codecStr,\n codecs\n }\n }\n\n const canPlayType = {\n // Intercept certain requests\n apply: function(target, ctx, args) {\n if (!args || !args.length) {\n return target.apply(ctx, args)\n }\n const { mime, codecs } = parseInput(args[0])\n // This specific mp4 codec is missing in Chromium\n if (mime === 'video/mp4') {\n if (codecs.includes('avc1.42E01E')) {\n return 'probably'\n }\n }\n // This mimetype is only supported if no codecs are specified\n if (mime === 'audio/x-m4a' && !codecs.length) {\n return 'maybe'\n }\n\n // This mimetype is only supported if no codecs are specified\n if (mime === 'audio/aac' && !codecs.length) {\n return 'probably'\n }\n // Everything else as usual\n return target.apply(ctx, args)\n }\n }\n\n /* global HTMLMediaElement */\n utils.replaceWithProxy(\n HTMLMediaElement.prototype,\n 'canPlayType',\n canPlayType\n )\n }",_args:[]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { opts }) => {\n utils.replaceGetterWithProxy(\n Object.getPrototypeOf(navigator),\n 'hardwareConcurrency',\n utils.makeHandler().getterValue(opts.hardwareConcurrency)\n )\n }",_args:[{opts:{hardwareConcurrency:4}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { opts }) => {\n const languages = opts.languages.length\n ? opts.languages\n : ['en-US', 'en']\n utils.replaceGetterWithProxy(\n Object.getPrototypeOf(navigator),\n 'languages',\n utils.makeHandler().getterValue(Object.freeze([...languages]))\n )\n }",_args:[{opts:{languages:[]}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, opts) => {\n const isSecure = document.location.protocol.startsWith('https')\n\n // In headful on secure origins the permission should be \"default\", not \"denied\"\n if (isSecure) {\n utils.replaceGetterWithProxy(Notification, 'permission', {\n apply() {\n return 'default'\n }\n })\n }\n\n // Another weird behavior:\n // On insecure origins in headful the state is \"denied\",\n // whereas in headless it's \"prompt\"\n if (!isSecure) {\n const handler = {\n apply(target, ctx, args) {\n const param = (args || [])[0]\n\n const isNotifications =\n param && param.name && param.name === 'notifications'\n if (!isNotifications) {\n return utils.cache.Reflect.apply(...arguments)\n }\n\n return Promise.resolve(\n Object.setPrototypeOf(\n {\n state: 'denied',\n onchange: null\n },\n PermissionStatus.prototype\n )\n )\n }\n }\n // Note: Don't use `Object.getPrototypeOf` here\n utils.replaceWithProxy(Permissions.prototype, 'query', handler)\n }\n }",_args:[{}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { fns, data }) => {\n fns = utils.materializeFns(fns)\n\n // That means we're running headful\n const hasPlugins = 'plugins' in navigator && navigator.plugins.length\n if (hasPlugins) {\n return // nothing to do here\n }\n\n const mimeTypes = fns.generateMimeTypeArray(utils, fns)(data.mimeTypes)\n const plugins = fns.generatePluginArray(utils, fns)(data.plugins)\n\n // Plugin and MimeType cross-reference each other, let's do that now\n // Note: We're looping through `data.plugins` here, not the generated `plugins`\n for (const pluginData of data.plugins) {\n pluginData.__mimeTypes.forEach((type, index) => {\n plugins[pluginData.name][index] = mimeTypes[type]\n\n Object.defineProperty(plugins[pluginData.name], type, {\n value: mimeTypes[type],\n writable: false,\n enumerable: false, // Not enumerable\n configurable: true\n })\n Object.defineProperty(mimeTypes[type], 'enabledPlugin', {\n value:\n type === 'application/x-pnacl'\n ? mimeTypes['application/x-nacl'].enabledPlugin // these reference the same plugin, so we need to re-use the Proxy in order to avoid leaks\n : new Proxy(plugins[pluginData.name], {}), // Prevent circular references\n writable: false,\n enumerable: false, // Important: `JSON.stringify(navigator.plugins)`\n configurable: true\n })\n })\n }\n\n const patchNavigator = (name, value) =>\n utils.replaceProperty(Object.getPrototypeOf(navigator), name, {\n get() {\n return value\n }\n })\n\n patchNavigator('mimeTypes', mimeTypes)\n patchNavigator('plugins', plugins)\n\n // All done\n }",_args:[{fns:{generateMimeTypeArray:"(utils, fns) => mimeTypesData => {\n return fns.generateMagicArray(utils, fns)(\n mimeTypesData,\n MimeTypeArray.prototype,\n MimeType.prototype,\n 'type'\n )\n}",generatePluginArray:"(utils, fns) => pluginsData => {\n return fns.generateMagicArray(utils, fns)(\n pluginsData,\n PluginArray.prototype,\n Plugin.prototype,\n 'name'\n )\n}",generateMagicArray:"(utils, fns) =>\n function(\n dataArray = [],\n proto = MimeTypeArray.prototype,\n itemProto = MimeType.prototype,\n itemMainProp = 'type'\n ) {\n // Quick helper to set props with the same descriptors vanilla is using\n const defineProp = (obj, prop, value) =>\n Object.defineProperty(obj, prop, {\n value,\n writable: false,\n enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)`\n configurable: true\n })\n\n // Loop over our fake data and construct items\n const makeItem = data => {\n const item = {}\n for (const prop of Object.keys(data)) {\n if (prop.startsWith('__')) {\n continue\n }\n defineProp(item, prop, data[prop])\n }\n return patchItem(item, data)\n }\n\n const patchItem = (item, data) => {\n let descriptor = Object.getOwnPropertyDescriptors(item)\n\n // Special case: Plugins have a magic length property which is not enumerable\n // e.g. `navigator.plugins[i].length` should always be the length of the assigned mimeTypes\n if (itemProto === Plugin.prototype) {\n descriptor = {\n ...descriptor,\n length: {\n value: data.__mimeTypes.length,\n writable: false,\n enumerable: false,\n configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`\n }\n }\n }\n\n // We need to spoof a specific `MimeType` or `Plugin` object\n const obj = Object.create(itemProto, descriptor)\n\n // Virtually all property keys are not enumerable in vanilla\n const blacklist = [...Object.keys(data), 'length', 'enabledPlugin']\n return new Proxy(obj, {\n ownKeys(target) {\n return Reflect.ownKeys(target).filter(k => !blacklist.includes(k))\n },\n getOwnPropertyDescriptor(target, prop) {\n if (blacklist.includes(prop)) {\n return undefined\n }\n return Reflect.getOwnPropertyDescriptor(target, prop)\n }\n })\n }\n\n const magicArray = []\n\n // Loop through our fake data and use that to create convincing entities\n dataArray.forEach(data => {\n magicArray.push(makeItem(data))\n })\n\n // Add direct property access based on types (e.g. `obj['application/pdf']`) afterwards\n magicArray.forEach(entry => {\n defineProp(magicArray, entry[itemMainProp], entry)\n })\n\n // This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)`\n const magicArrayObj = Object.create(proto, {\n ...Object.getOwnPropertyDescriptors(magicArray),\n\n // There's one ugly quirk we unfortunately need to take care of:\n // The `MimeTypeArray` prototype has an enumerable `length` property,\n // but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`.\n // To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap.\n length: {\n value: magicArray.length,\n writable: false,\n enumerable: false,\n configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`\n }\n })\n\n // Generate our functional function mocks :-)\n const functionMocks = fns.generateFunctionMocks(utils)(\n proto,\n itemMainProp,\n magicArray\n )\n\n // We need to overlay our custom object with a JS Proxy\n const magicArrayObjProxy = new Proxy(magicArrayObj, {\n get(target, key = '') {\n // Redirect function calls to our custom proxied versions mocking the vanilla behavior\n if (key === 'item') {\n return functionMocks.item\n }\n if (key === 'namedItem') {\n return functionMocks.namedItem\n }\n if (proto === PluginArray.prototype && key === 'refresh') {\n return functionMocks.refresh\n }\n // Everything else can pass through as normal\n return utils.cache.Reflect.get(...arguments)\n },\n ownKeys(target) {\n // There are a couple of quirks where the original property demonstrates \"magical\" behavior that makes no sense\n // This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length`\n // My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly\n // For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing\n // Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing\n const keys = []\n const typeProps = magicArray.map(mt => mt[itemMainProp])\n typeProps.forEach((_, i) => keys.push(`${i}`))\n typeProps.forEach(propName => keys.push(propName))\n return keys\n },\n getOwnPropertyDescriptor(target, prop) {\n if (prop === 'length') {\n return undefined\n }\n return Reflect.getOwnPropertyDescriptor(target, prop)\n }\n })\n\n return magicArrayObjProxy\n }",generateFunctionMocks:"utils => (\n proto,\n itemMainProp,\n dataArray\n) => ({\n /** Returns the MimeType object with the specified index. */\n item: utils.createProxy(proto.item, {\n apply(target, ctx, args) {\n if (!args.length) {\n throw new TypeError(\n `Failed to execute 'item' on '${\n proto[Symbol.toStringTag]\n }': 1 argument required, but only 0 present.`\n )\n }\n // Special behavior alert:\n // - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup\n // - If anything else than an integer (including as string) is provided it will return the first entry\n const isInteger = args[0] && Number.isInteger(Number(args[0])) // Cast potential string to number first, then check for integer\n // Note: Vanilla never returns `undefined`\n return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null\n }\n }),\n /** Returns the MimeType object with the specified name. */\n namedItem: utils.createProxy(proto.namedItem, {\n apply(target, ctx, args) {\n if (!args.length) {\n throw new TypeError(\n `Failed to execute 'namedItem' on '${\n proto[Symbol.toStringTag]\n }': 1 argument required, but only 0 present.`\n )\n }\n return dataArray.find(mt => mt[itemMainProp] === args[0]) || null // Not `undefined`!\n }\n }),\n /** Does nothing and shall return nothing */\n refresh: proto.refresh\n ? utils.createProxy(proto.refresh, {\n apply(target, ctx, args) {\n return undefined\n }\n })\n : undefined\n})"},data:{mimeTypes:[{type:"application/pdf",suffixes:"pdf",description:"",__pluginName:"Chrome PDF Viewer"},{type:"application/x-google-chrome-pdf",suffixes:"pdf",description:"Portable Document Format",__pluginName:"Chrome PDF Plugin"},{type:"application/x-nacl",suffixes:"",description:"Native Client Executable",__pluginName:"Native Client"},{type:"application/x-pnacl",suffixes:"",description:"Portable Native Client Executable",__pluginName:"Native Client"}],plugins:[{name:"Chrome PDF Plugin",filename:"internal-pdf-viewer",description:"Portable Document Format",__mimeTypes:["application/x-google-chrome-pdf"]},{name:"Chrome PDF Viewer",filename:"mhjfbmdgcfjbbpaeojofohoefgiehjai",description:"",__mimeTypes:["application/pdf"]},{name:"Native Client",filename:"internal-nacl-plugin",description:"",__mimeTypes:["application/x-nacl","application/x-pnacl"]}]}}]}),!1===navigator.webdriver||void 0===navigator.webdriver||delete Object.getPrototypeOf(navigator).webdriver,(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, opts) => {\n const getParameterProxyHandler = {\n apply: function(target, ctx, args) {\n const param = (args || [])[0]\n const result = utils.cache.Reflect.apply(target, ctx, args)\n // UNMASKED_VENDOR_WEBGL\n if (param === 37445) {\n return opts.vendor || 'Intel Inc.' // default in headless: Google Inc.\n }\n // UNMASKED_RENDERER_WEBGL\n if (param === 37446) {\n return opts.renderer || 'Intel Iris OpenGL Engine' // default in headless: Google SwiftShader\n }\n return result\n }\n }\n\n // There's more than one WebGL rendering context\n // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext#Browser_compatibility\n // To find out the original values here: Object.getOwnPropertyDescriptors(WebGLRenderingContext.prototype.getParameter)\n const addProxy = (obj, propName) => {\n utils.replaceWithProxy(obj, propName, getParameterProxyHandler)\n }\n // For whatever weird reason loops don't play nice with Object.defineProperty, here's the next best thing:\n addProxy(WebGLRenderingContext.prototype, 'getParameter')\n addProxy(WebGL2RenderingContext.prototype, 'getParameter')\n }",_args:[{}]}),(()=>{try{if(window.outerWidth&&window.outerHeight)return;const n=85;window.outerWidth=window.innerWidth,window.outerHeight=window.innerHeight+n}catch(n){}})(),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(`at `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply\n `at Object.${trap} `, // e.g. Object.get or Object.apply\n `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in `makeNativeString`\n nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // `toString` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + \"\"`\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // `toString` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> `canPlayType`\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple `navigator` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like `navigator.__proto__.vendor` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, opts) => {\n try {\n // Adds a contentWindow proxy to the provided iframe element\n const addContentWindowProxy = iframe => {\n const contentWindowProxy = {\n get(target, key) {\n // Now to the interesting part:\n // We actually make this thing behave like a regular iframe window,\n // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :)\n // That makes it possible for these assertions to be correct:\n // iframe.contentWindow.self === window.top // must be false\n if (key === 'self') {\n return this\n }\n // iframe.contentWindow.frameElement === iframe // must be true\n if (key === 'frameElement') {\n return iframe\n }\n // Intercept iframe.contentWindow[0] to hide the property 0 added by the proxy.\n if (key === '0') {\n return undefined\n }\n return Reflect.get(target, key)\n }\n }\n\n if (!iframe.contentWindow) {\n const proxy = new Proxy(window, contentWindowProxy)\n Object.defineProperty(iframe, 'contentWindow', {\n get() {\n return proxy\n },\n set(newValue) {\n return newValue // contentWindow is immutable\n },\n enumerable: true,\n configurable: false\n })\n }\n }\n\n // Handles iframe element creation, augments `srcdoc` property so we can intercept further\n const handleIframeCreation = (target, thisArg, args) => {\n const iframe = target.apply(thisArg, args)\n\n // We need to keep the originals around\n const _iframe = iframe\n const _srcdoc = _iframe.srcdoc\n\n // Add hook for the srcdoc property\n // We need to be very surgical here to not break other iframes by accident\n Object.defineProperty(iframe, 'srcdoc', {\n configurable: true, // Important, so we can reset this later\n get: function() {\n return _srcdoc\n },\n set: function(newValue) {\n addContentWindowProxy(this)\n // Reset property, the hook is only needed once\n Object.defineProperty(iframe, 'srcdoc', {\n configurable: false,\n writable: false,\n value: _srcdoc\n })\n _iframe.srcdoc = newValue\n }\n })\n return iframe\n }\n\n // Adds a hook to intercept iframe creation events\n const addIframeCreationSniffer = () => {\n /* global document */\n const createElementHandler = {\n // Make toString() native\n get(target, key) {\n return Reflect.get(target, key)\n },\n apply: function(target, thisArg, args) {\n const isIframe =\n args && args.length && `${args[0]}`.toLowerCase() === 'iframe'\n if (!isIframe) {\n // Everything as usual\n return target.apply(thisArg, args)\n } else {\n return handleIframeCreation(target, thisArg, args)\n }\n }\n }\n // All this just due to iframes with srcdoc bug\n utils.replaceWithProxy(\n document,\n 'createElement',\n createElementHandler\n )\n }\n\n // Let's go\n addIframeCreationSniffer()\n } catch (err) {\n // console.warn(err)\n }\n }",_args:[]});
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..7c5902a
--- /dev/null
+++ b/main.py
@@ -0,0 +1,64 @@
+import argparse
+import asyncio
+import sys
+
+import config
+import db
+from base.base_crawler import AbstractCrawler
+from media_platform.bilibili import BilibiliCrawler
+from media_platform.douyin import DouYinCrawler
+from media_platform.kuaishou import KuaishouCrawler
+from media_platform.weibo import WeiboCrawler
+from media_platform.xhs import XiaoHongShuCrawler
+
+
+class CrawlerFactory:
+ CRAWLERS = {
+ "xhs": XiaoHongShuCrawler,
+ "dy": DouYinCrawler,
+ "ks": KuaishouCrawler,
+ "bili": BilibiliCrawler,
+ "wb": WeiboCrawler
+ }
+
+ @staticmethod
+ def create_crawler(platform: str) -> AbstractCrawler:
+ crawler_class = CrawlerFactory.CRAWLERS.get(platform)
+ if not crawler_class:
+ raise ValueError("Invalid Media Platform Currently only supported xhs or dy or ks or bili ...")
+ return crawler_class()
+
+
+async def main():
+ # define command line params ...
+ parser = argparse.ArgumentParser(description='Media crawler program.')
+ parser.add_argument('--platform', type=str, help='Media platform select (xhs | dy | ks | bili | wb)',
+ choices=["xhs", "dy", "ks", "bili", "wb"], default=config.PLATFORM)
+ parser.add_argument('--lt', type=str, help='Login type (qrcode | phone | cookie)',
+ choices=["qrcode", "phone", "cookie"], default=config.LOGIN_TYPE)
+ parser.add_argument('--type', type=str, help='crawler type (search | detail | creator)',
+ choices=["search", "detail", "creator"], default=config.CRAWLER_TYPE)
+
+ # init db
+ if config.SAVE_DATA_OPTION == "db":
+ await db.init_db()
+
+ args = parser.parse_args()
+ crawler = CrawlerFactory.create_crawler(platform=args.platform)
+ crawler.init_config(
+ platform=args.platform,
+ login_type=args.lt,
+ crawler_type=args.type
+ )
+ await crawler.start()
+
+ if config.SAVE_DATA_OPTION == "db":
+ await db.close()
+
+
+if __name__ == '__main__':
+ try:
+ # asyncio.run(main())
+ asyncio.get_event_loop().run_until_complete(main())
+ except KeyboardInterrupt:
+ sys.exit()
diff --git a/media_platform/__init__.py b/media_platform/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/media_platform/bilibili/__init__.py b/media_platform/bilibili/__init__.py
new file mode 100644
index 0000000..fc0c8ee
--- /dev/null
+++ b/media_platform/bilibili/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/2 18:36
+# @Desc :
+
+from .core import *
\ No newline at end of file
diff --git a/media_platform/bilibili/client.py b/media_platform/bilibili/client.py
new file mode 100644
index 0000000..372d26d
--- /dev/null
+++ b/media_platform/bilibili/client.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/2 18:44
+# @Desc : bilibili 请求客户端
+import asyncio
+import json
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+from urllib.parse import urlencode
+
+import httpx
+from playwright.async_api import BrowserContext, Page
+
+from tools import utils
+
+from .exception import DataFetchError
+from .field import CommentOrderType, SearchOrderType
+from .help import BilibiliSign
+
+
+class BilibiliClient:
+ def __init__(
+ self,
+ timeout=10,
+ proxies=None,
+ *,
+ headers: Dict[str, str],
+ playwright_page: Page,
+ cookie_dict: Dict[str, str],
+ ):
+ self.proxies = proxies
+ self.timeout = timeout
+ self.headers = headers
+ self._host = "https://api.bilibili.com"
+ self.playwright_page = playwright_page
+ self.cookie_dict = cookie_dict
+
+ async def request(self, method, url, **kwargs) -> Any:
+ async with httpx.AsyncClient(proxies=self.proxies) as client:
+ response = await client.request(
+ method, url, timeout=self.timeout,
+ **kwargs
+ )
+ data: Dict = response.json()
+ if data.get("code") != 0:
+ raise DataFetchError(data.get("message", "unkonw error"))
+ else:
+ return data.get("data", {})
+
+ async def pre_request_data(self, req_data: Dict) -> Dict:
+ """
+ 发送请求进行请求参数签名
+ 需要从 localStorage 拿 wbi_img_urls 这参数,值如下:
+ https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png-https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png
+ :param req_data:
+ :return:
+ """
+ if not req_data:
+ return {}
+ img_key, sub_key = await self.get_wbi_keys()
+ return BilibiliSign(img_key, sub_key).sign(req_data)
+
+ async def get_wbi_keys(self) -> Tuple[str, str]:
+ """
+ 获取最新的 img_key 和 sub_key
+ :return:
+ """
+ local_storage = await self.playwright_page.evaluate("() => window.localStorage")
+ wbi_img_urls = local_storage.get("wbi_img_urls", "") or local_storage.get(
+ "wbi_img_url") + "-" + local_storage.get("wbi_sub_url")
+ if wbi_img_urls and "-" in wbi_img_urls:
+ img_url, sub_url = wbi_img_urls.split("-")
+ else:
+ resp = await self.request(method="GET", url=self._host + "/x/web-interface/nav")
+ img_url: str = resp['wbi_img']['img_url']
+ sub_url: str = resp['wbi_img']['sub_url']
+ img_key = img_url.rsplit('/', 1)[1].split('.')[0]
+ sub_key = sub_url.rsplit('/', 1)[1].split('.')[0]
+ return img_key, sub_key
+
+ async def get(self, uri: str, params=None, enable_params_sign: bool = True) -> Dict:
+ final_uri = uri
+ if enable_params_sign:
+ params = await self.pre_request_data(params)
+ if isinstance(params, dict):
+ final_uri = (f"{uri}?"
+ f"{urlencode(params)}")
+ return await self.request(method="GET", url=f"{self._host}{final_uri}", headers=self.headers)
+
+ async def post(self, uri: str, data: dict) -> Dict:
+ data = await self.pre_request_data(data)
+ json_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
+ return await self.request(method="POST", url=f"{self._host}{uri}",
+ data=json_str, headers=self.headers)
+
+ async def pong(self) -> bool:
+ """get a note to check if login state is ok"""
+ utils.logger.info("[BilibiliClient.pong] Begin pong bilibili...")
+ ping_flag = False
+ try:
+ check_login_uri = "/x/web-interface/nav"
+ response = await self.get(check_login_uri)
+ if response.get("isLogin"):
+ utils.logger.info("[BilibiliClient.pong] Use cache login state get web interface successfull!")
+ ping_flag = True
+ except Exception as e:
+ utils.logger.error(f"[BilibiliClient.pong] Pong bilibili failed: {e}, and try to login again...")
+ ping_flag = False
+ return ping_flag
+
+ async def update_cookies(self, browser_context: BrowserContext):
+ cookie_str, cookie_dict = utils.convert_cookies(await browser_context.cookies())
+ self.headers["Cookie"] = cookie_str
+ self.cookie_dict = cookie_dict
+
+ async def search_video_by_keyword(self, keyword: str, page: int = 1, page_size: int = 20,
+ order: SearchOrderType = SearchOrderType.DEFAULT):
+ """
+ KuaiShou web search api
+ :param keyword: 搜索关键词
+ :param page: 分页参数具体第几页
+ :param page_size: 每一页参数的数量
+ :param order: 搜索结果排序,默认位综合排序
+ :return:
+ """
+ uri = "/x/web-interface/wbi/search/type"
+ post_data = {
+ "search_type": "video",
+ "keyword": keyword,
+ "page": page,
+ "page_size": page_size,
+ "order": order.value
+ }
+ return await self.get(uri, post_data)
+
+ async def get_video_info(self, aid: Union[int, None] = None, bvid: Union[str, None] = None) -> Dict:
+ """
+ Bilibli web video detail api, aid 和 bvid任选一个参数
+ :param aid: 稿件avid
+ :param bvid: 稿件bvid
+ :return:
+ """
+ if not aid and not bvid:
+ raise ValueError("请提供 aid 或 bvid 中的至少一个参数")
+
+ uri = "/x/web-interface/view/detail"
+ params = dict()
+ if aid:
+ params.update({"aid": aid})
+ else:
+ params.update({"bvid": bvid})
+ return await self.get(uri, params, enable_params_sign=False)
+
+ async def get_video_comments(self,
+ video_id: str,
+ order_mode: CommentOrderType = CommentOrderType.DEFAULT,
+ next: int = 0
+ ) -> Dict:
+ """get video comments
+ :param video_id: 视频 ID
+ :param order_mode: 排序方式
+ :param next: 评论页选择
+ :return:
+ """
+ uri = "/x/v2/reply/wbi/main"
+ post_data = {
+ "oid": video_id,
+ "mode": order_mode.value,
+ "type": 1,
+ "ps": 20,
+ "next": next
+ }
+ return await self.get(uri, post_data)
+
+ async def get_video_all_comments(self, video_id: str, crawl_interval: float = 1.0, is_fetch_sub_comments=False,
+ callback: Optional[Callable] = None, ):
+ """
+ get video all comments include sub comments
+ :param video_id:
+ :param crawl_interval:
+ :param is_fetch_sub_comments:
+ :param callback:
+ :return:
+ """
+
+ result = []
+ is_end = False
+ next_page =0
+ while not is_end:
+ comments_res = await self.get_video_comments(video_id, CommentOrderType.DEFAULT, next_page)
+ curson_info: Dict = comments_res.get("cursor")
+ comment_list: List[Dict] = comments_res.get("replies", [])
+ is_end = curson_info.get("is_end")
+ next_page = curson_info.get("next")
+ if callback: # 如果有回调函数,就执行回调函数
+ await callback(video_id, comment_list)
+ await asyncio.sleep(crawl_interval)
+ if not is_fetch_sub_comments:
+ result.extend(comment_list)
+ continue
+ # todo handle get sub comments
+ return result
diff --git a/media_platform/bilibili/core.py b/media_platform/bilibili/core.py
new file mode 100644
index 0000000..0a7a0ab
--- /dev/null
+++ b/media_platform/bilibili/core.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/2 18:44
+# @Desc : B站爬虫
+
+import asyncio
+import os
+import random
+from asyncio import Task
+from typing import Dict, List, Optional, Tuple
+
+from playwright.async_api import (BrowserContext, BrowserType, Page,
+ async_playwright)
+
+import config
+from base.base_crawler import AbstractCrawler
+from proxy.proxy_ip_pool import IpInfoModel, create_ip_pool
+from store import bilibili as bilibili_store
+from tools import utils
+from var import crawler_type_var
+
+from .client import BilibiliClient
+from .exception import DataFetchError
+from .field import SearchOrderType
+from .login import BilibiliLogin
+
+
+class BilibiliCrawler(AbstractCrawler):
+ platform: str
+ login_type: str
+ crawler_type: str
+ context_page: Page
+ bili_client: BilibiliClient
+ browser_context: BrowserContext
+
+ def __init__(self):
+ self.index_url = "https://www.bilibili.com"
+ self.user_agent = utils.get_user_agent()
+
+ def init_config(self, platform: str, login_type: str, crawler_type: str):
+ self.platform = platform
+ self.login_type = login_type
+ self.crawler_type = crawler_type
+
+ async def start(self):
+ playwright_proxy_format, httpx_proxy_format = None, None
+ if config.ENABLE_IP_PROXY:
+ ip_proxy_pool = await create_ip_pool(config.IP_PROXY_POOL_COUNT, enable_validate_ip=True)
+ ip_proxy_info: IpInfoModel = await ip_proxy_pool.get_proxy()
+ playwright_proxy_format, httpx_proxy_format = self.format_proxy_info(ip_proxy_info)
+
+ async with async_playwright() as playwright:
+ # Launch a browser context.
+ chromium = playwright.chromium
+ self.browser_context = await self.launch_browser(
+ chromium,
+ None,
+ self.user_agent,
+ headless=config.HEADLESS
+ )
+ # stealth.min.js is a js script to prevent the website from detecting the crawler.
+ await self.browser_context.add_init_script(path="libs/stealth.min.js")
+ self.context_page = await self.browser_context.new_page()
+ await self.context_page.goto(self.index_url)
+
+ # Create a client to interact with the xiaohongshu website.
+ self.bili_client = await self.create_bilibili_client(httpx_proxy_format)
+ if not await self.bili_client.pong():
+ login_obj = BilibiliLogin(
+ login_type=self.login_type,
+ login_phone="", # your phone number
+ browser_context=self.browser_context,
+ context_page=self.context_page,
+ cookie_str=config.COOKIES
+ )
+ await login_obj.begin()
+ await self.bili_client.update_cookies(browser_context=self.browser_context)
+
+ crawler_type_var.set(self.crawler_type)
+ if self.crawler_type == "search":
+ # Search for video and retrieve their comment information.
+ await self.search()
+ elif self.crawler_type == "detail":
+ # Get the information and comments of the specified post
+ await self.get_specified_videos()
+ else:
+ pass
+ utils.logger.info("[BilibiliCrawler.start] Bilibili Crawler finished ...")
+
+ async def search(self):
+ """
+ search bilibili video with keywords
+ :return:
+ """
+ utils.logger.info("[BilibiliCrawler.search] Begin search bilibli keywords")
+ bili_limit_count = 20 # bilibili limit page fixed value
+ for keyword in config.KEYWORDS.split(","):
+ utils.logger.info(f"[BilibiliCrawler.search] Current search keyword: {keyword}")
+ page = 1
+ while page * bili_limit_count <= config.CRAWLER_MAX_NOTES_COUNT:
+ video_id_list: List[str] = []
+ videos_res = await self.bili_client.search_video_by_keyword(
+ keyword=keyword,
+ page=page,
+ page_size=bili_limit_count,
+ order=SearchOrderType.DEFAULT,
+ )
+ video_list: List[Dict] = videos_res.get("result")
+
+ semaphore = asyncio.Semaphore(config.MAX_CONCURRENCY_NUM)
+ task_list = [
+ self.get_video_info_task(aid=video_item.get("aid"), bvid="", semaphore=semaphore)
+ for video_item in video_list
+ ]
+ video_items = await asyncio.gather(*task_list)
+ for video_item in video_items:
+ if video_item:
+ video_id_list.append(video_item.get("View").get("aid"))
+ await bilibili_store.update_bilibili_video(video_item)
+
+ page += 1
+ await self.batch_get_video_comments(video_id_list)
+
+ async def batch_get_video_comments(self, video_id_list: List[str]):
+ """
+ batch get video comments
+ :param video_id_list:
+ :return:
+ """
+ utils.logger.info(f"[BilibiliCrawler.batch_get_video_comments] video ids:{video_id_list}")
+ semaphore = asyncio.Semaphore(config.MAX_CONCURRENCY_NUM)
+ task_list: List[Task] = []
+ for video_id in video_id_list:
+ task = asyncio.create_task(self.get_comments(video_id, semaphore), name=video_id)
+ task_list.append(task)
+ await asyncio.gather(*task_list)
+
+ async def get_comments(self, video_id: str, semaphore: asyncio.Semaphore):
+ """
+ get comment for video id
+ :param video_id:
+ :param semaphore:
+ :return:
+ """
+ async with semaphore:
+ try:
+ utils.logger.info(f"[BilibiliCrawler.get_comments] begin get video_id: {video_id} comments ...")
+ await self.bili_client.get_video_all_comments(
+ video_id=video_id,
+ crawl_interval=random.random(),
+ callback=bilibili_store.batch_update_bilibili_video_comments
+ )
+
+ except DataFetchError as ex:
+ utils.logger.error(f"[BilibiliCrawler.get_comments] get video_id: {video_id} comment error: {ex}")
+ except Exception as e:
+ utils.logger.error(f"[BilibiliCrawler.get_comments] may be been blocked, err:{e}")
+
+ async def get_specified_videos(self):
+ """
+ get specified videos info
+ :return:
+ """
+ semaphore = asyncio.Semaphore(config.MAX_CONCURRENCY_NUM)
+ task_list = [
+ self.get_video_info_task(aid=0, bvid=video_id, semaphore=semaphore) for video_id in
+ config.BILI_SPECIFIED_ID_LIST
+ ]
+ video_details = await asyncio.gather(*task_list)
+ video_aids_list = []
+ for video_detail in video_details:
+ if video_detail is not None:
+ video_item_view: Dict = video_detail.get("View")
+ video_aid: str = video_item_view.get("aid")
+ if video_aid:
+ video_aids_list.append(video_aid)
+ await bilibili_store.update_bilibili_video(video_detail)
+ await self.batch_get_video_comments(video_aids_list)
+
+ async def get_video_info_task(self, aid: int, bvid: str, semaphore: asyncio.Semaphore) -> Optional[Dict]:
+ """
+ Get video detail task
+ :param aid:
+ :param bvid:
+ :param semaphore:
+ :return:
+ """
+ async with semaphore:
+ try:
+ result = await self.bili_client.get_video_info(aid=aid, bvid=bvid)
+ return result
+ except DataFetchError as ex:
+ utils.logger.error(f"[BilibiliCrawler.get_video_info_task] Get video detail error: {ex}")
+ return None
+ except KeyError as ex:
+ utils.logger.error(
+ f"[BilibiliCrawler.get_video_info_task] have not fund note detail video_id:{bvid}, err: {ex}")
+ return None
+
+ async def create_bilibili_client(self, httpx_proxy: Optional[str]) -> BilibiliClient:
+ """Create xhs client"""
+ utils.logger.info("[BilibiliCrawler.create_bilibili_client] Begin create bilibili API client ...")
+ cookie_str, cookie_dict = utils.convert_cookies(await self.browser_context.cookies())
+ bilibili_client_obj = BilibiliClient(
+ proxies=httpx_proxy,
+ headers={
+ "User-Agent": self.user_agent,
+ "Cookie": cookie_str,
+ "Origin": "https://www.bilibili.com",
+ "Referer": "https://www.bilibili.com",
+ "Content-Type": "application/json;charset=UTF-8"
+ },
+ playwright_page=self.context_page,
+ cookie_dict=cookie_dict,
+ )
+ return bilibili_client_obj
+
+ @staticmethod
+ def format_proxy_info(ip_proxy_info: IpInfoModel) -> Tuple[Optional[Dict], Optional[Dict]]:
+ """format proxy info for playwright and httpx"""
+ playwright_proxy = {
+ "server": f"{ip_proxy_info.protocol}{ip_proxy_info.ip}:{ip_proxy_info.port}",
+ "username": ip_proxy_info.user,
+ "password": ip_proxy_info.password,
+ }
+ httpx_proxy = {
+ f"{ip_proxy_info.protocol}": f"http://{ip_proxy_info.user}:{ip_proxy_info.password}@{ip_proxy_info.ip}:{ip_proxy_info.port}"
+ }
+ return playwright_proxy, httpx_proxy
+
+ async def launch_browser(
+ self,
+ chromium: BrowserType,
+ playwright_proxy: Optional[Dict],
+ user_agent: Optional[str],
+ headless: bool = True
+ ) -> BrowserContext:
+ """Launch browser and create browser context"""
+ utils.logger.info("[BilibiliCrawler.launch_browser] Begin create browser context ...")
+ if config.SAVE_LOGIN_STATE:
+ # feat issue #14
+ # we will save login state to avoid login every time
+ user_data_dir = os.path.join(os.getcwd(), "browser_data",
+ config.USER_DATA_DIR % self.platform) # type: ignore
+ browser_context = await chromium.launch_persistent_context(
+ user_data_dir=user_data_dir,
+ accept_downloads=True,
+ headless=headless,
+ proxy=playwright_proxy, # type: ignore
+ viewport={"width": 1920, "height": 1080},
+ user_agent=user_agent
+ )
+ return browser_context
+ else:
+ browser = await chromium.launch(headless=headless, proxy=playwright_proxy) # type: ignore
+ browser_context = await browser.new_context(
+ viewport={"width": 1920, "height": 1080},
+ user_agent=user_agent
+ )
+ return browser_context
diff --git a/media_platform/bilibili/exception.py b/media_platform/bilibili/exception.py
new file mode 100644
index 0000000..9aecdf4
--- /dev/null
+++ b/media_platform/bilibili/exception.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/2 18:44
+# @Desc :
+
+from httpx import RequestError
+
+
+class DataFetchError(RequestError):
+ """something error when fetch"""
+
+
+class IPBlockError(RequestError):
+ """fetch so fast that the server block us ip"""
diff --git a/media_platform/bilibili/field.py b/media_platform/bilibili/field.py
new file mode 100644
index 0000000..20b55f7
--- /dev/null
+++ b/media_platform/bilibili/field.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/3 16:20
+# @Desc :
+
+from enum import Enum
+
+
+class SearchOrderType(Enum):
+ # 综合排序
+ DEFAULT = ""
+
+ # 最多点击
+ MOST_CLICK = "click"
+
+ # 最新发布
+ LAST_PUBLISH = "pubdate"
+
+ # 最多弹幕
+ MOST_DANMU = "dm"
+
+ # 最多收藏
+ MOST_MARK = "stow"
+
+
+class CommentOrderType(Enum):
+ # 仅按热度
+ DEFAULT = 0
+
+ # 按热度+按时间
+ MIXED = 1
+
+ # 按时间
+ TIME = 2
diff --git a/media_platform/bilibili/help.py b/media_platform/bilibili/help.py
new file mode 100644
index 0000000..c5c52e6
--- /dev/null
+++ b/media_platform/bilibili/help.py
@@ -0,0 +1,70 @@
+ # -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/2 23:26
+# @Desc : bilibili 请求参数签名
+# 逆向实现参考:https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/sign/wbi.html#wbi%E7%AD%BE%E5%90%8D%E7%AE%97%E6%B3%95
+import urllib.parse
+from hashlib import md5
+from typing import Dict
+
+from tools import utils
+
+
+class BilibiliSign:
+ def __init__(self, img_key: str, sub_key: str):
+ self.img_key = img_key
+ self.sub_key = sub_key
+ self.map_table = [
+ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
+ 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
+ 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
+ 36, 20, 34, 44, 52
+ ]
+
+ def get_salt(self) -> str:
+ """
+ 获取加盐的 key
+ :return:
+ """
+ salt = ""
+ mixin_key = self.img_key + self.sub_key
+ for mt in self.map_table:
+ salt += mixin_key[mt]
+ return salt[:32]
+
+ def sign(self, req_data: Dict) -> Dict:
+ """
+ 请求参数中加上当前时间戳对请求参数中的key进行字典序排序
+ 再将请求参数进行 url 编码集合 salt 进行 md5 就可以生成w_rid参数了
+ :param req_data:
+ :return:
+ """
+ current_ts = utils.get_unix_timestamp()
+ req_data.update({"wts": current_ts})
+ req_data = dict(sorted(req_data.items()))
+ req_data = {
+ # 过滤 value 中的 "!'()*" 字符
+ k: ''.join(filter(lambda ch: ch not in "!'()*", str(v)))
+ for k, v
+ in req_data.items()
+ }
+ query = urllib.parse.urlencode(req_data)
+ salt = self.get_salt()
+ wbi_sign = md5((query + salt).encode()).hexdigest() # 计算 w_rid
+ req_data['w_rid'] = wbi_sign
+ return req_data
+
+
+if __name__ == '__main__':
+ _img_key = "7cd084941338484aae1ad9425b84077c"
+ _sub_key = "4932caff0ff746eab6f01bf08b70ac45"
+ _search_url = "__refresh__=true&_extra=&ad_resource=5654&category_id=&context=&dynamic_offset=0&from_source=&from_spmid=333.337&gaia_vtoken=&highlight=1&keyword=python&order=click&page=1&page_size=20&platform=pc&qv_id=OQ8f2qtgYdBV1UoEnqXUNUl8LEDAdzsD&search_type=video&single_column=0&source_tag=3&web_location=1430654"
+ _req_data = dict()
+ for params in _search_url.split("&"):
+ kvalues = params.split("=")
+ key = kvalues[0]
+ value = kvalues[1]
+ _req_data[key] = value
+ print("pre req_data", _req_data)
+ _req_data = BilibiliSign(img_key=_img_key, sub_key=_sub_key).sign(req_data={"aid":170001})
+ print(_req_data)
diff --git a/media_platform/bilibili/login.py b/media_platform/bilibili/login.py
new file mode 100644
index 0000000..ddf11bf
--- /dev/null
+++ b/media_platform/bilibili/login.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/2 18:44
+# @Desc : bilibli登录实现类
+
+import asyncio
+import functools
+import sys
+from typing import Optional
+
+from playwright.async_api import BrowserContext, Page
+from tenacity import (RetryError, retry, retry_if_result, stop_after_attempt,
+ wait_fixed)
+
+from base.base_crawler import AbstractLogin
+from tools import utils
+
+
+class BilibiliLogin(AbstractLogin):
+ def __init__(self,
+ login_type: str,
+ browser_context: BrowserContext,
+ context_page: Page,
+ login_phone: Optional[str] = "",
+ cookie_str: str = ""
+ ):
+ self.login_type = login_type
+ self.browser_context = browser_context
+ self.context_page = context_page
+ self.login_phone = login_phone
+ self.cookie_str = cookie_str
+
+ async def begin(self):
+ """Start login bilibili"""
+ utils.logger.info("[BilibiliLogin.begin] Begin login Bilibili ...")
+ if self.login_type == "qrcode":
+ await self.login_by_qrcode()
+ elif self.login_type == "phone":
+ await self.login_by_mobile()
+ elif self.login_type == "cookie":
+ await self.login_by_cookies()
+ else:
+ raise ValueError(
+ "[BilibiliLogin.begin] Invalid Login Type Currently only supported qrcode or phone or cookie ...")
+
+ @retry(stop=stop_after_attempt(20), wait=wait_fixed(1), retry=retry_if_result(lambda value: value is False))
+ async def check_login_state(self) -> bool:
+ """
+ Check if the current login status is successful and return True otherwise return False
+ retry decorator will retry 20 times if the return value is False, and the retry interval is 1 second
+ if max retry times reached, raise RetryError
+ """
+ current_cookie = await self.browser_context.cookies()
+ _, cookie_dict = utils.convert_cookies(current_cookie)
+ if cookie_dict.get("SESSDATA", "") or cookie_dict.get("DedeUserID"):
+ return True
+ return False
+
+ async def login_by_qrcode(self):
+ """login bilibili website and keep webdriver login state"""
+ utils.logger.info("[BilibiliLogin.login_by_qrcode] Begin login bilibili by qrcode ...")
+
+ # click login button
+ login_button_ele = self.context_page.locator(
+ "xpath=//div[@class='right-entry__outside go-login-btn']//div"
+ )
+ await login_button_ele.click()
+
+ # find login qrcode
+ qrcode_img_selector = "//div[@class='login-scan-box']//img"
+ base64_qrcode_img = await utils.find_login_qrcode(
+ self.context_page,
+ selector=qrcode_img_selector
+ )
+ if not base64_qrcode_img:
+ utils.logger.info("[BilibiliLogin.login_by_qrcode] login failed , have not found qrcode please check ....")
+ sys.exit()
+
+ # show login qrcode
+ partial_show_qrcode = functools.partial(utils.show_qrcode, base64_qrcode_img)
+ asyncio.get_running_loop().run_in_executor(executor=None, func=partial_show_qrcode)
+
+ utils.logger.info(f"[BilibiliLogin.login_by_qrcode] Waiting for scan code login, remaining time is 20s")
+ try:
+ await self.check_login_state()
+ except RetryError:
+ utils.logger.info("[BilibiliLogin.login_by_qrcode] Login bilibili failed by qrcode login method ...")
+ sys.exit()
+
+ wait_redirect_seconds = 5
+ utils.logger.info(
+ f"[BilibiliLogin.login_by_qrcode] Login successful then wait for {wait_redirect_seconds} seconds redirect ...")
+ await asyncio.sleep(wait_redirect_seconds)
+
+ async def login_by_mobile(self):
+ pass
+
+ async def login_by_cookies(self):
+ utils.logger.info("[BilibiliLogin.login_by_qrcode] Begin login bilibili by cookie ...")
+ for key, value in utils.convert_str_cookie_to_dict(self.cookie_str).items():
+ await self.browser_context.add_cookies([{
+ 'name': key,
+ 'value': value,
+ 'domain': ".bilibili.com",
+ 'path': "/"
+ }])
diff --git a/media_platform/douyin/__init__.py b/media_platform/douyin/__init__.py
new file mode 100644
index 0000000..636dace
--- /dev/null
+++ b/media_platform/douyin/__init__.py
@@ -0,0 +1 @@
+from .core import DouYinCrawler
diff --git a/media_platform/douyin/client.py b/media_platform/douyin/client.py
new file mode 100644
index 0000000..bca4f95
--- /dev/null
+++ b/media_platform/douyin/client.py
@@ -0,0 +1,198 @@
+import asyncio
+import copy
+import urllib.parse
+from typing import Any, Callable, Dict, List, Optional
+
+import execjs
+import httpx
+from playwright.async_api import BrowserContext, Page
+
+from tools import utils
+from var import request_keyword_var
+
+from .exception import *
+from .field import *
+
+
+class DOUYINClient:
+ def __init__(
+ self,
+ timeout=30,
+ proxies=None,
+ *,
+ headers: Dict,
+ playwright_page: Optional[Page],
+ cookie_dict: Dict
+ ):
+ self.proxies = proxies
+ self.timeout = timeout
+ self.headers = headers
+ self._host = "https://www.douyin.com"
+ self.playwright_page = playwright_page
+ self.cookie_dict = cookie_dict
+
+ async def __process_req_params(self, params: Optional[Dict] = None, headers: Optional[Dict] = None):
+ if not params:
+ return
+ headers = headers or self.headers
+ local_storage: Dict = await self.playwright_page.evaluate("() => window.localStorage") # type: ignore
+ douyin_js_obj = execjs.compile(open('libs/douyin.js').read())
+ common_params = {
+ "device_platform": "webapp",
+ "aid": "6383",
+ "channel": "channel_pc_web",
+ "cookie_enabled": "true",
+ "browser_language": "zh-CN",
+ "browser_platform": "Win32",
+ "browser_name": "Firefox",
+ "browser_version": "110.0",
+ "browser_online": "true",
+ "engine_name": "Gecko",
+ "os_name": "Windows",
+ "os_version": "10",
+ "engine_version": "109.0",
+ "platform": "PC",
+ "screen_width": "1920",
+ "screen_height": "1200",
+ # " webid": douyin_js_obj.call("get_web_id"),
+ # "msToken": local_storage.get("xmst"),
+ # "msToken": "abL8SeUTPa9-EToD8qfC7toScSADxpg6yLh2dbNcpWHzE0bT04txM_4UwquIcRvkRb9IU8sifwgM1Kwf1Lsld81o9Irt2_yNyUbbQPSUO8EfVlZJ_78FckDFnwVBVUVK",
+ }
+ params.update(common_params)
+ query = '&'.join([f'{k}={v}' for k, v in params.items()])
+ x_bogus = douyin_js_obj.call('sign', query, headers["User-Agent"])
+ params["X-Bogus"] = x_bogus
+ # print(x_bogus, query)
+
+ async def request(self, method, url, **kwargs):
+ async with httpx.AsyncClient(proxies=self.proxies) as client:
+ response = await client.request(
+ method, url, timeout=self.timeout,
+ **kwargs
+ )
+ try:
+ return response.json()
+ except Exception as e:
+ raise DataFetchError(f"{e}, {response.text}")
+
+ async def get(self, uri: str, params: Optional[Dict] = None, headers: Optional[Dict] = None):
+ await self.__process_req_params(params, headers)
+ headers = headers or self.headers
+ return await self.request(method="GET", url=f"{self._host}{uri}", params=params, headers=headers)
+
+ async def post(self, uri: str, data: dict, headers: Optional[Dict] = None):
+ await self.__process_req_params(data, headers)
+ headers = headers or self.headers
+ return await self.request(method="POST", url=f"{self._host}{uri}", data=data, headers=headers)
+
+ @staticmethod
+ async def pong(browser_context: BrowserContext) -> bool:
+ _, cookie_dict = utils.convert_cookies(await browser_context.cookies())
+ # todo send some api to test login status
+ return cookie_dict.get("LOGIN_STATUS") == "1"
+
+ async def update_cookies(self, browser_context: BrowserContext):
+ cookie_str, cookie_dict = utils.convert_cookies(await browser_context.cookies())
+ self.headers["Cookie"] = cookie_str
+ self.cookie_dict = cookie_dict
+
+ async def search_info_by_keyword(
+ self,
+ keyword: str,
+ offset: int = 0,
+ search_channel: SearchChannelType = SearchChannelType.GENERAL,
+ sort_type: SearchSortType = SearchSortType.GENERAL,
+ publish_time: PublishTimeType = PublishTimeType.UNLIMITED
+ ):
+ """
+ DouYin Web Search API
+ :param keyword:
+ :param offset:
+ :param search_channel:
+ :param sort_type:
+ :param publish_time: ·
+ :return:
+ """
+ params = {
+ "keyword": urllib.parse.quote(keyword),
+ "search_channel": search_channel.value,
+ "sort_type": sort_type.value,
+ "publish_time": publish_time.value,
+ "search_source": "normal_search",
+ "query_correct_type": "1",
+ "is_filter_search": "0",
+ "offset": offset,
+ "count": 10 # must be set to 10
+ }
+ referer_url = "https://www.douyin.com/search/" + keyword
+ referer_url += f"?publish_time={publish_time.value}&sort_type={sort_type.value}&type=general"
+ headers = copy.copy(self.headers)
+ headers["Referer"] = urllib.parse.quote(referer_url, safe=':/')
+ return await self.get("/aweme/v1/web/general/search/single/", params, headers=headers)
+
+ async def get_video_by_id(self, aweme_id: str) -> Any:
+ """
+ DouYin Video Detail API
+ :param aweme_id:
+ :return:
+ """
+ params = {
+ "aweme_id": aweme_id
+ }
+ headers = copy.copy(self.headers)
+ # headers["Cookie"] = "s_v_web_id=verify_lol4a8dv_wpQ1QMyP_xemd_4wON_8Yzr_FJa8DN1vdY2m;"
+ del headers["Origin"]
+ res = await self.get("/aweme/v1/web/aweme/detail/", params, headers)
+ return res.get("aweme_detail", {})
+
+ async def get_aweme_comments(self, aweme_id: str, cursor: int = 0):
+ """get note comments
+
+ """
+ uri = "/aweme/v1/web/comment/list/"
+ params = {
+ "aweme_id": aweme_id,
+ "cursor": cursor,
+ "count": 20,
+ "item_type": 0
+ }
+ keywords = request_keyword_var.get()
+ referer_url = "https://www.douyin.com/search/" + keywords + '?aid=3a3cec5a-9e27-4040-b6aa-ef548c2c1138&publish_time=0&sort_type=0&source=search_history&type=general'
+ headers = copy.copy(self.headers)
+ headers["Referer"] = urllib.parse.quote(referer_url, safe=':/')
+ return await self.get(uri, params)
+
+ async def get_aweme_all_comments(
+ self,
+ aweme_id: str,
+ crawl_interval: float = 1.0,
+ is_fetch_sub_comments=False,
+ callback: Optional[Callable] = None,
+ ):
+ """
+ 获取帖子的所有评论,包括子评论
+ :param aweme_id: 帖子ID
+ :param crawl_interval: 抓取间隔
+ :param is_fetch_sub_comments: 是否抓取子评论
+ :param callback: 回调函数,用于处理抓取到的评论
+ :return: 评论列表
+ """
+ result = []
+ comments_has_more = 1
+ comments_cursor = 0
+ while comments_has_more:
+ comments_res = await self.get_aweme_comments(aweme_id, comments_cursor)
+ comments_has_more = comments_res.get("has_more", 0)
+ comments_cursor = comments_res.get("cursor", 0)
+ comments = comments_res.get("comments", [])
+ if not comments:
+ continue
+ result.extend(comments)
+ if callback: # 如果有回调函数,就执行回调函数
+ await callback(aweme_id, comments)
+
+ await asyncio.sleep(crawl_interval)
+ if not is_fetch_sub_comments:
+ continue
+ # todo fetch sub comments
+ return result
diff --git a/media_platform/douyin/core.py b/media_platform/douyin/core.py
new file mode 100644
index 0000000..1eacf89
--- /dev/null
+++ b/media_platform/douyin/core.py
@@ -0,0 +1,220 @@
+import asyncio
+import os
+import random
+from asyncio import Task
+from typing import Any, Dict, List, Optional, Tuple
+
+from playwright.async_api import (BrowserContext, BrowserType, Page,
+ async_playwright)
+
+import config
+from base.base_crawler import AbstractCrawler
+from proxy.proxy_ip_pool import IpInfoModel, create_ip_pool
+from store import douyin as douyin_store
+from tools import utils
+from var import crawler_type_var
+
+from .client import DOUYINClient
+from .exception import DataFetchError
+from .login import DouYinLogin
+from .field import PublishTimeType
+
+
+class DouYinCrawler(AbstractCrawler):
+ platform: str
+ login_type: str
+ crawler_type: str
+ context_page: Page
+ dy_client: DOUYINClient
+ browser_context: BrowserContext
+
+ def __init__(self) -> None:
+ self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36" # fixed
+ self.index_url = "https://www.douyin.com"
+
+ def init_config(self, platform: str, login_type: str, crawler_type: str) -> None:
+ self.platform = platform
+ self.login_type = login_type
+ self.crawler_type = crawler_type
+
+ async def start(self) -> None:
+ playwright_proxy_format, httpx_proxy_format = None, None
+ if config.ENABLE_IP_PROXY:
+ ip_proxy_pool = await create_ip_pool(config.IP_PROXY_POOL_COUNT, enable_validate_ip=True)
+ ip_proxy_info: IpInfoModel = await ip_proxy_pool.get_proxy()
+ playwright_proxy_format, httpx_proxy_format = self.format_proxy_info(ip_proxy_info)
+
+ async with async_playwright() as playwright:
+ # Launch a browser context.
+ chromium = playwright.chromium
+ self.browser_context = await self.launch_browser(
+ chromium,
+ None,
+ self.user_agent,
+ headless=config.HEADLESS
+ )
+ # stealth.min.js is a js script to prevent the website from detecting the crawler.
+ await self.browser_context.add_init_script(path="libs/stealth.min.js")
+ self.context_page = await self.browser_context.new_page()
+ await self.context_page.goto(self.index_url)
+
+ self.dy_client = await self.create_douyin_client(httpx_proxy_format)
+ if not await self.dy_client.pong(browser_context=self.browser_context):
+ login_obj = DouYinLogin(
+ login_type=self.login_type,
+ login_phone="", # you phone number
+ browser_context=self.browser_context,
+ context_page=self.context_page,
+ cookie_str=config.COOKIES
+ )
+ await login_obj.begin()
+ await self.dy_client.update_cookies(browser_context=self.browser_context)
+ crawler_type_var.set(self.crawler_type)
+ if self.crawler_type == "search":
+ # Search for notes and retrieve their comment information.
+ await self.search()
+ elif self.crawler_type == "detail":
+ # Get the information and comments of the specified post
+ await self.get_specified_awemes()
+
+ utils.logger.info("[DouYinCrawler.start] Douyin Crawler finished ...")
+
+ async def search(self) -> None:
+ utils.logger.info("[DouYinCrawler.search] Begin search douyin keywords")
+ for keyword in config.KEYWORDS.split(","):
+ utils.logger.info(f"[DouYinCrawler.search] Current keyword: {keyword}")
+ aweme_list: List[str] = []
+ dy_limit_count = 10
+ page = 0
+ while (page + 1) * dy_limit_count <= config.CRAWLER_MAX_NOTES_COUNT:
+ try:
+ posts_res = await self.dy_client.search_info_by_keyword(keyword=keyword,
+ offset=page * dy_limit_count,
+ publish_time=PublishTimeType.UNLIMITED
+ )
+ except DataFetchError:
+ utils.logger.error(f"[DouYinCrawler.search] search douyin keyword: {keyword} failed")
+ break
+ page += 1
+ for post_item in posts_res.get("data"):
+ try:
+ aweme_info: Dict = post_item.get("aweme_info") or \
+ post_item.get("aweme_mix_info", {}).get("mix_items")[0]
+ except TypeError:
+ continue
+ aweme_list.append(aweme_info.get("aweme_id", ""))
+ await douyin_store.update_douyin_aweme(aweme_item=aweme_info)
+ utils.logger.info(f"[DouYinCrawler.search] keyword:{keyword}, aweme_list:{aweme_list}")
+ await self.batch_get_note_comments(aweme_list)
+
+ async def get_specified_awemes(self):
+ """Get the information and comments of the specified post"""
+ semaphore = asyncio.Semaphore(config.MAX_CONCURRENCY_NUM)
+ task_list = [
+ self.get_aweme_detail(aweme_id=aweme_id, semaphore=semaphore) for aweme_id in config.DY_SPECIFIED_ID_LIST
+ ]
+ aweme_details = await asyncio.gather(*task_list)
+ for aweme_detail in aweme_details:
+ if aweme_detail is not None:
+ await douyin_store.update_douyin_aweme(aweme_detail)
+ await self.batch_get_note_comments(config.DY_SPECIFIED_ID_LIST)
+
+ async def get_aweme_detail(self, aweme_id: str, semaphore: asyncio.Semaphore) -> Any:
+ """Get note detail"""
+ async with semaphore:
+ try:
+ return await self.dy_client.get_video_by_id(aweme_id)
+ except DataFetchError as ex:
+ utils.logger.error(f"[DouYinCrawler.get_aweme_detail] Get aweme detail error: {ex}")
+ return None
+ except KeyError as ex:
+ utils.logger.error(f"[DouYinCrawler.get_aweme_detail] have not fund note detail aweme_id:{aweme_id}, err: {ex}")
+ return None
+
+ async def batch_get_note_comments(self, aweme_list: List[str]) -> None:
+ task_list: List[Task] = []
+ semaphore = asyncio.Semaphore(config.MAX_CONCURRENCY_NUM)
+ for aweme_id in aweme_list:
+ task = asyncio.create_task(
+ self.get_comments(aweme_id, semaphore), name=aweme_id)
+ task_list.append(task)
+ if len(task_list) > 0 :
+ await asyncio.wait(task_list)
+
+ async def get_comments(self, aweme_id: str, semaphore: asyncio.Semaphore) -> None:
+ async with semaphore:
+ try:
+ # 将关键词列表传递给 get_aweme_all_comments 方法
+ comments = await self.dy_client.get_aweme_all_comments(
+ aweme_id=aweme_id,
+ crawl_interval=random.random(),
+ callback=douyin_store.batch_update_dy_aweme_comments
+
+ )
+ utils.logger.info(f"[DouYinCrawler.get_comments] aweme_id: {aweme_id} comments have all been obtained and filtered ...")
+ except DataFetchError as e:
+ utils.logger.error(f"[DouYinCrawler.get_comments] aweme_id: {aweme_id} get comments failed, error: {e}")
+
+ @staticmethod
+ def format_proxy_info(ip_proxy_info: IpInfoModel) -> Tuple[Optional[Dict], Optional[Dict]]:
+ """format proxy info for playwright and httpx"""
+ playwright_proxy = {
+ "server": f"{ip_proxy_info.protocol}{ip_proxy_info.ip}:{ip_proxy_info.port}",
+ "username": ip_proxy_info.user,
+ "password": ip_proxy_info.password,
+ }
+ httpx_proxy = {
+ f"{ip_proxy_info.protocol}": f"http://{ip_proxy_info.user}:{ip_proxy_info.password}@{ip_proxy_info.ip}:{ip_proxy_info.port}"
+ }
+ return playwright_proxy, httpx_proxy
+
+ async def create_douyin_client(self, httpx_proxy: Optional[str]) -> DOUYINClient:
+ """Create douyin client"""
+ cookie_str, cookie_dict = utils.convert_cookies(await self.browser_context.cookies()) # type: ignore
+ douyin_client = DOUYINClient(
+ proxies=httpx_proxy,
+ headers={
+ "User-Agent": self.user_agent,
+ "Cookie": cookie_str,
+ "Host": "www.douyin.com",
+ "Origin": "https://www.douyin.com/",
+ "Referer": "https://www.douyin.com/",
+ "Content-Type": "application/json;charset=UTF-8"
+ },
+ playwright_page=self.context_page,
+ cookie_dict=cookie_dict,
+ )
+ return douyin_client
+
+ async def launch_browser(
+ self,
+ chromium: BrowserType,
+ playwright_proxy: Optional[Dict],
+ user_agent: Optional[str],
+ headless: bool = True
+ ) -> BrowserContext:
+ """Launch browser and create browser context"""
+ if config.SAVE_LOGIN_STATE:
+ user_data_dir = os.path.join(os.getcwd(), "browser_data",
+ config.USER_DATA_DIR % self.platform) # type: ignore
+ browser_context = await chromium.launch_persistent_context(
+ user_data_dir=user_data_dir,
+ accept_downloads=True,
+ headless=headless,
+ proxy=playwright_proxy, # type: ignore
+ viewport={"width": 1920, "height": 1080},
+ user_agent=user_agent
+ ) # type: ignore
+ return browser_context
+ else:
+ browser = await chromium.launch(headless=headless, proxy=playwright_proxy) # type: ignore
+ browser_context = await browser.new_context(
+ viewport={"width": 1920, "height": 1080},
+ user_agent=user_agent
+ )
+ return browser_context
+
+ async def close(self) -> None:
+ """Close browser context"""
+ await self.browser_context.close()
+ utils.logger.info("[DouYinCrawler.close] Browser context closed ...")
diff --git a/media_platform/douyin/exception.py b/media_platform/douyin/exception.py
new file mode 100644
index 0000000..1a8642e
--- /dev/null
+++ b/media_platform/douyin/exception.py
@@ -0,0 +1,9 @@
+from httpx import RequestError
+
+
+class DataFetchError(RequestError):
+ """something error when fetch"""
+
+
+class IPBlockError(RequestError):
+ """fetch so fast that the server block us ip"""
diff --git a/media_platform/douyin/field.py b/media_platform/douyin/field.py
new file mode 100644
index 0000000..2ac4d62
--- /dev/null
+++ b/media_platform/douyin/field.py
@@ -0,0 +1,24 @@
+from enum import Enum
+
+
+class SearchChannelType(Enum):
+ """search channel type"""
+ GENERAL = "aweme_general" # 综合
+ VIDEO = "aweme_video_web" # 视频
+ USER = "aweme_user_web" # 用户
+ LIVE = "aweme_live" # 直播
+
+
+class SearchSortType(Enum):
+ """search sort type"""
+ GENERAL = 0 # 综合排序
+ LATEST = 1 # 最新发布
+ MOST_LIKE = 2 # 最多点赞
+
+
+class PublishTimeType(Enum):
+ """publish time type"""
+ UNLIMITED = 0 # 不限
+ ONE_DAY = 1 # 一天内
+ ONE_WEEK = 2 # 一周内
+ SIX_MONTH = 3 # 半年内
diff --git a/media_platform/douyin/login.py b/media_platform/douyin/login.py
new file mode 100644
index 0000000..77f9b6e
--- /dev/null
+++ b/media_platform/douyin/login.py
@@ -0,0 +1,246 @@
+import asyncio
+import functools
+import sys
+from typing import Optional
+
+import redis
+from playwright.async_api import BrowserContext, Page
+from playwright.async_api import TimeoutError as PlaywrightTimeoutError
+from tenacity import (RetryError, retry, retry_if_result, stop_after_attempt,
+ wait_fixed)
+
+import config
+from base.base_crawler import AbstractLogin
+from tools import utils
+
+
+class DouYinLogin(AbstractLogin):
+
+ def __init__(self,
+ login_type: str,
+ browser_context: BrowserContext, # type: ignore
+ context_page: Page, # type: ignore
+ login_phone: Optional[str] = "",
+ cookie_str: Optional[str] = ""
+ ):
+ self.login_type = login_type
+ self.browser_context = browser_context
+ self.context_page = context_page
+ self.login_phone = login_phone
+ self.scan_qrcode_time = 60
+ self.cookie_str = cookie_str
+
+ async def begin(self):
+ """
+ Start login douyin website
+ 滑块中间页面的验证准确率不太OK... 如果没有特俗要求,建议不开抖音登录,或者使用cookies登录
+ """
+
+ # popup login dialog
+ await self.popup_login_dialog()
+
+ # select login type
+ if self.login_type == "qrcode":
+ await self.login_by_qrcode()
+ elif self.login_type == "phone":
+ await self.login_by_mobile()
+ elif self.login_type == "cookie":
+ await self.login_by_cookies()
+ else:
+ raise ValueError("[DouYinLogin.begin] Invalid Login Type Currently only supported qrcode or phone or cookie ...")
+
+ # 如果页面重定向到滑动验证码页面,需要再次滑动滑块
+ await asyncio.sleep(6)
+ current_page_title = await self.context_page.title()
+ if "验证码中间页" in current_page_title:
+ await self.check_page_display_slider(move_step=3, slider_level="hard")
+
+ # check login state
+ utils.logger.info(f"[DouYinLogin.begin] login finished then check login state ...")
+ try:
+ await self.check_login_state()
+ except RetryError:
+ utils.logger.info("[DouYinLogin.begin] login failed please confirm ...")
+ sys.exit()
+
+ # wait for redirect
+ wait_redirect_seconds = 5
+ utils.logger.info(f"[DouYinLogin.begin] Login successful then wait for {wait_redirect_seconds} seconds redirect ...")
+ await asyncio.sleep(wait_redirect_seconds)
+
+ @retry(stop=stop_after_attempt(20), wait=wait_fixed(1), retry=retry_if_result(lambda value: value is False))
+ async def check_login_state(self):
+ """Check if the current login status is successful and return True otherwise return False"""
+ current_cookie = await self.browser_context.cookies()
+ _, cookie_dict = utils.convert_cookies(current_cookie)
+ if cookie_dict.get("LOGIN_STATUS") == "1":
+ return True
+ return False
+
+ async def popup_login_dialog(self):
+ """If the login dialog box does not pop up automatically, we will manually click the login button"""
+ dialog_selector = "xpath=//div[@id='login-pannel']"
+ try:
+ # check dialog box is auto popup and wait for 10 seconds
+ await self.context_page.wait_for_selector(dialog_selector, timeout=1000 * 10)
+ except Exception as e:
+ utils.logger.error(f"[DouYinLogin.popup_login_dialog] login dialog box does not pop up automatically, error: {e}")
+ utils.logger.info("[DouYinLogin.popup_login_dialog] login dialog box does not pop up automatically, we will manually click the login button")
+ login_button_ele = self.context_page.locator("xpath=//p[text() = '登录']")
+ await login_button_ele.click()
+ await asyncio.sleep(0.5)
+
+ async def login_by_qrcode(self):
+ utils.logger.info("[DouYinLogin.login_by_qrcode] Begin login douyin by qrcode...")
+ qrcode_img_selector = "xpath=//article[@class='web-login']//img"
+ base64_qrcode_img = await utils.find_login_qrcode(
+ self.context_page,
+ selector=qrcode_img_selector
+ )
+ if not base64_qrcode_img:
+ utils.logger.info("[DouYinLogin.login_by_qrcode] login qrcode not found please confirm ...")
+ sys.exit()
+
+ # show login qrcode
+ # utils.show_qrcode(base64_qrcode_img)
+ partial_show_qrcode = functools.partial(utils.show_qrcode, base64_qrcode_img)
+ asyncio.get_running_loop().run_in_executor(executor=None, func=partial_show_qrcode)
+ utils.show_qrcode(base64_qrcode_img)
+ await asyncio.sleep(2)
+
+ async def login_by_mobile(self):
+ utils.logger.info("[DouYinLogin.login_by_mobile] Begin login douyin by mobile ...")
+ mobile_tap_ele = self.context_page.locator("xpath=//li[text() = '验证码登录']")
+ await mobile_tap_ele.click()
+ await self.context_page.wait_for_selector("xpath=//article[@class='web-login-mobile-code']")
+ mobile_input_ele = self.context_page.locator("xpath=//input[@placeholder='手机号']")
+ await mobile_input_ele.fill(self.login_phone)
+ await asyncio.sleep(0.5)
+ send_sms_code_btn = self.context_page.locator("xpath=//span[text() = '获取验证码']")
+ await send_sms_code_btn.click()
+
+ # 检查是否有滑动验证码
+ await self.check_page_display_slider(move_step=10, slider_level="easy")
+ redis_obj = redis.Redis(host=config.REDIS_DB_HOST, password=config.REDIS_DB_PWD)
+ max_get_sms_code_time = 60 * 2 # 最长获取验证码的时间为2分钟
+ while max_get_sms_code_time > 0:
+ utils.logger.info(f"[DouYinLogin.login_by_mobile] get douyin sms code from redis remaining time {max_get_sms_code_time}s ...")
+ await asyncio.sleep(1)
+ sms_code_key = f"dy_{self.login_phone}"
+ sms_code_value = redis_obj.get(sms_code_key)
+ if not sms_code_value:
+ max_get_sms_code_time -= 1
+ continue
+
+ sms_code_input_ele = self.context_page.locator("xpath=//input[@placeholder='请输入验证码']")
+ await sms_code_input_ele.fill(value=sms_code_value.decode())
+ await asyncio.sleep(0.5)
+ submit_btn_ele = self.context_page.locator("xpath=//button[@class='web-login-button']")
+ await submit_btn_ele.click() # 点击登录
+ # todo ... 应该还需要检查验证码的正确性有可能输入的验证码不正确
+ break
+
+ async def check_page_display_slider(self, move_step: int = 10, slider_level: str = "easy"):
+ """
+ 检查页面是否出现滑动验证码
+ :return:
+ """
+ # 等待滑动验证码的出现
+ back_selector = "#captcha-verify-image"
+ try:
+ await self.context_page.wait_for_selector(selector=back_selector, state="visible", timeout=30 * 1000)
+ except PlaywrightTimeoutError: # 没有滑动验证码,直接返回
+ return
+
+ gap_selector = 'xpath=//*[@id="captcha_container"]/div/div[2]/img[2]'
+ max_slider_try_times = 20
+ slider_verify_success = False
+ while not slider_verify_success:
+ if max_slider_try_times <= 0:
+ utils.logger.error("[DouYinLogin.check_page_display_slider] slider verify failed ...")
+ sys.exit()
+ try:
+ await self.move_slider(back_selector, gap_selector, move_step, slider_level)
+ await asyncio.sleep(1)
+
+ # 如果滑块滑动慢了,或者验证失败了,会提示操作过慢,这里点一下刷新按钮
+ page_content = await self.context_page.content()
+ if "操作过慢" in page_content or "提示重新操作" in page_content:
+ utils.logger.info("[DouYinLogin.check_page_display_slider] slider verify failed, retry ...")
+ await self.context_page.click(selector="//a[contains(@class, 'secsdk_captcha_refresh')]")
+ continue
+
+ # 滑动成功后,等待滑块消失
+ await self.context_page.wait_for_selector(selector=back_selector, state="hidden", timeout=1000)
+ # 如果滑块消失了,说明验证成功了,跳出循环,如果没有消失,说明验证失败了,上面这一行代码会抛出异常被捕获后继续循环滑动验证码
+ utils.logger.info("[DouYinLogin.check_page_display_slider] slider verify success ...")
+ slider_verify_success = True
+ except Exception as e:
+ utils.logger.error(f"[DouYinLogin.check_page_display_slider] slider verify failed, error: {e}")
+ await asyncio.sleep(1)
+ max_slider_try_times -= 1
+ utils.logger.info(f"[DouYinLogin.check_page_display_slider] remaining slider try times: {max_slider_try_times}")
+ continue
+
+ async def move_slider(self, back_selector: str, gap_selector: str, move_step: int = 10, slider_level="easy"):
+ """
+ Move the slider to the right to complete the verification
+ :param back_selector: 滑动验证码背景图片的选择器
+ :param gap_selector: 滑动验证码的滑块选择器
+ :param move_step: 是控制单次移动速度的比例是1/10 默认是1 相当于 传入的这个距离不管多远0.1秒钟移动完 越大越慢
+ :param slider_level: 滑块难度 easy hard,分别对应手机验证码的滑块和验证码中间的滑块
+ :return:
+ """
+
+ # get slider background image
+ slider_back_elements = await self.context_page.wait_for_selector(
+ selector=back_selector,
+ timeout=1000 * 10, # wait 10 seconds
+ )
+ slide_back = str(await slider_back_elements.get_property("src")) # type: ignore
+
+ # get slider gap image
+ gap_elements = await self.context_page.wait_for_selector(
+ selector=gap_selector,
+ timeout=1000 * 10, # wait 10 seconds
+ )
+ gap_src = str(await gap_elements.get_property("src")) # type: ignore
+
+ # 识别滑块位置
+ slide_app = utils.Slide(gap=gap_src, bg=slide_back)
+ distance = slide_app.discern()
+
+ # 获取移动轨迹
+ tracks = utils.get_tracks(distance, slider_level)
+ new_1 = tracks[-1] - (sum(tracks) - distance)
+ tracks.pop()
+ tracks.append(new_1)
+
+ # 根据轨迹拖拽滑块到指定位置
+ element = await self.context_page.query_selector(gap_selector)
+ bounding_box = await element.bounding_box() # type: ignore
+
+ await self.context_page.mouse.move(bounding_box["x"] + bounding_box["width"] / 2, # type: ignore
+ bounding_box["y"] + bounding_box["height"] / 2) # type: ignore
+ # 这里获取到x坐标中心点位置
+ x = bounding_box["x"] + bounding_box["width"] / 2 # type: ignore
+ # 模拟滑动操作
+ await element.hover() # type: ignore
+ await self.context_page.mouse.down()
+
+ for track in tracks:
+ # 循环鼠标按照轨迹移动
+ # steps 是控制单次移动速度的比例是1/10 默认是1 相当于 传入的这个距离不管多远0.1秒钟移动完 越大越慢
+ await self.context_page.mouse.move(x + track, 0, steps=move_step)
+ x += track
+ await self.context_page.mouse.up()
+
+ async def login_by_cookies(self):
+ utils.logger.info("[DouYinLogin.login_by_cookies] Begin login douyin by cookie ...")
+ for key, value in utils.convert_str_cookie_to_dict(self.cookie_str).items():
+ await self.browser_context.add_cookies([{
+ 'name': key,
+ 'value': value,
+ 'domain': ".douyin.com",
+ 'path': "/"
+ }])
diff --git a/media_platform/kuaishou/__init__.py b/media_platform/kuaishou/__init__.py
new file mode 100644
index 0000000..de877e0
--- /dev/null
+++ b/media_platform/kuaishou/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from .core import KuaishouCrawler
\ No newline at end of file
diff --git a/media_platform/kuaishou/client.py b/media_platform/kuaishou/client.py
new file mode 100644
index 0000000..889cbb3
--- /dev/null
+++ b/media_platform/kuaishou/client.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+import asyncio
+import json
+from typing import Any, Callable, Dict, Optional
+from urllib.parse import urlencode
+
+import httpx
+from playwright.async_api import BrowserContext, Page
+
+import config
+from tools import utils
+
+from .exception import DataFetchError
+from .graphql import KuaiShouGraphQL
+
+
+class KuaiShouClient:
+ def __init__(
+ self,
+ timeout=10,
+ proxies=None,
+ *,
+ headers: Dict[str, str],
+ playwright_page: Page,
+ cookie_dict: Dict[str, str],
+ ):
+ self.proxies = proxies
+ self.timeout = timeout
+ self.headers = headers
+ self._host = "https://www.kuaishou.com/graphql"
+ self.playwright_page = playwright_page
+ self.cookie_dict = cookie_dict
+ self.graphql = KuaiShouGraphQL()
+
+ async def request(self, method, url, **kwargs) -> Any:
+ async with httpx.AsyncClient(proxies=self.proxies) as client:
+ response = await client.request(
+ method, url, timeout=self.timeout,
+ **kwargs
+ )
+ data: Dict = response.json()
+ if data.get("errors"):
+ raise DataFetchError(data.get("errors", "unkonw error"))
+ else:
+ return data.get("data", {})
+
+ async def get(self, uri: str, params=None) -> Dict:
+ final_uri = uri
+ if isinstance(params, dict):
+ final_uri = (f"{uri}?"
+ f"{urlencode(params)}")
+ return await self.request(method="GET", url=f"{self._host}{final_uri}", headers=self.headers)
+
+ async def post(self, uri: str, data: dict) -> Dict:
+ json_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
+ return await self.request(method="POST", url=f"{self._host}{uri}",
+ data=json_str, headers=self.headers)
+
+ async def pong(self) -> bool:
+ """get a note to check if login state is ok"""
+ utils.logger.info("[KuaiShouClient.pong] Begin pong kuaishou...")
+ ping_flag = False
+ try:
+ post_data = {
+ "operationName": "visionProfileUserList",
+ "variables": {
+ "ftype": 1,
+ },
+ "query": self.graphql.get("vision_profile")
+ }
+ res = await self.post("", post_data)
+ if res.get("visionProfileUserList", {}).get("result") == 1:
+ ping_flag = True
+ except Exception as e:
+ utils.logger.error(f"[KuaiShouClient.pong] Pong kuaishou failed: {e}, and try to login again...")
+ ping_flag = False
+ return ping_flag
+
+ async def update_cookies(self, browser_context: BrowserContext):
+ cookie_str, cookie_dict = utils.convert_cookies(await browser_context.cookies())
+ self.headers["Cookie"] = cookie_str
+ self.cookie_dict = cookie_dict
+
+ async def search_info_by_keyword(self, keyword: str, pcursor: str):
+ """
+ KuaiShou web search api
+ :param keyword: search keyword
+ :param pcursor: limite page curson
+ :return:
+ """
+ post_data = {
+ "operationName": "visionSearchPhoto",
+ "variables": {
+ "keyword": keyword,
+ "pcursor": pcursor,
+ "page": "search"
+ },
+ "query": self.graphql.get("search_query")
+ }
+ return await self.post("", post_data)
+
+ async def get_video_info(self, photo_id: str) -> Dict:
+ """
+ Kuaishou web video detail api
+ :param photo_id:
+ :return:
+ """
+ post_data = {
+ "operationName": "visionVideoDetail",
+ "variables": {
+ "photoId": photo_id,
+ "page": "search"
+ },
+ "query": self.graphql.get("video_detail")
+ }
+ return await self.post("", post_data)
+
+ async def get_video_comments(self, photo_id: str, pcursor: str = "") -> Dict:
+ """get video comments
+ :param photo_id: photo id you want to fetch
+ :param pcursor: last you get pcursor, defaults to ""
+ :return:
+ """
+ post_data = {
+ "operationName": "commentListQuery",
+ "variables": {
+ "photoId": photo_id,
+ "pcursor": pcursor
+ },
+ "query": self.graphql.get("comment_list")
+
+ }
+ return await self.post("", post_data)
+
+ async def get_video_all_comments(self, photo_id: str, crawl_interval: float = 1.0, is_fetch_sub_comments=False,
+ callback: Optional[Callable] = None):
+ """
+ get video all comments include sub comments
+ :param photo_id:
+ :param crawl_interval:
+ :param is_fetch_sub_comments:
+ :param callback:
+ :return:
+ """
+
+ result = []
+ pcursor = ""
+
+ while pcursor != "no_more":
+ comments_res = await self.get_video_comments(photo_id, pcursor)
+ vision_commen_list = comments_res.get("visionCommentList", {})
+ pcursor = vision_commen_list.get("pcursor", "")
+ comments = vision_commen_list.get("rootComments", [])
+
+ if callback: # 如果有回调函数,就执行回调函数
+ await callback(photo_id, comments)
+
+ result.extend(comments)
+ await asyncio.sleep(crawl_interval)
+ if not is_fetch_sub_comments:
+ continue
+ # todo handle get sub comments
+ return result
diff --git a/media_platform/kuaishou/core.py b/media_platform/kuaishou/core.py
new file mode 100644
index 0000000..7a007c2
--- /dev/null
+++ b/media_platform/kuaishou/core.py
@@ -0,0 +1,248 @@
+import asyncio
+import os
+import random
+import time
+from asyncio import Task
+from typing import Dict, List, Optional, Tuple
+
+from playwright.async_api import (BrowserContext, BrowserType, Page,
+ async_playwright)
+
+import config
+from base.base_crawler import AbstractCrawler
+from proxy.proxy_ip_pool import IpInfoModel, create_ip_pool
+from store import kuaishou as kuaishou_store
+from tools import utils
+from var import comment_tasks_var, crawler_type_var
+
+from .client import KuaiShouClient
+from .exception import DataFetchError
+from .login import KuaishouLogin
+
+
+class KuaishouCrawler(AbstractCrawler):
+ platform: str
+ login_type: str
+ crawler_type: str
+ context_page: Page
+ ks_client: KuaiShouClient
+ browser_context: BrowserContext
+
+ def __init__(self):
+ self.index_url = "https://www.kuaishou.com"
+ self.user_agent = utils.get_user_agent()
+
+ def init_config(self, platform: str, login_type: str, crawler_type: str):
+ self.platform = platform
+ self.login_type = login_type
+ self.crawler_type = crawler_type
+
+ async def start(self):
+ playwright_proxy_format, httpx_proxy_format = None, None
+ if config.ENABLE_IP_PROXY:
+ ip_proxy_pool = await create_ip_pool(config.IP_PROXY_POOL_COUNT, enable_validate_ip=True)
+ ip_proxy_info: IpInfoModel = await ip_proxy_pool.get_proxy()
+ playwright_proxy_format, httpx_proxy_format = self.format_proxy_info(ip_proxy_info)
+
+ async with async_playwright() as playwright:
+ # Launch a browser context.
+ chromium = playwright.chromium
+ self.browser_context = await self.launch_browser(
+ chromium,
+ None,
+ self.user_agent,
+ headless=config.HEADLESS
+ )
+ # stealth.min.js is a js script to prevent the website from detecting the crawler.
+ await self.browser_context.add_init_script(path="libs/stealth.min.js")
+ self.context_page = await self.browser_context.new_page()
+ await self.context_page.goto(f"{self.index_url}?isHome=1")
+
+ # Create a client to interact with the kuaishou website.
+ self.ks_client = await self.create_ks_client(httpx_proxy_format)
+ if not await self.ks_client.pong():
+ login_obj = KuaishouLogin(
+ login_type=self.login_type,
+ login_phone=httpx_proxy_format,
+ browser_context=self.browser_context,
+ context_page=self.context_page,
+ cookie_str=config.COOKIES
+ )
+ await login_obj.begin()
+ await self.ks_client.update_cookies(browser_context=self.browser_context)
+
+ crawler_type_var.set(self.crawler_type)
+ if self.crawler_type == "search":
+ # Search for notes and retrieve their comment information.
+ await self.search()
+ elif self.crawler_type == "detail":
+ # Get the information and comments of the specified post
+ await self.get_specified_videos()
+ else:
+ pass
+
+ utils.logger.info("[KuaishouCrawler.start] Kuaishou Crawler finished ...")
+
+ async def search(self):
+ utils.logger.info("[KuaishouCrawler.search] Begin search kuaishou keywords")
+ ks_limit_count = 20 # kuaishou limit page fixed value
+ for keyword in config.KEYWORDS.split(","):
+ utils.logger.info(f"[KuaishouCrawler.search] Current search keyword: {keyword}")
+ page = 1
+ while page * ks_limit_count <= config.CRAWLER_MAX_NOTES_COUNT:
+ video_id_list: List[str] = []
+ videos_res = await self.ks_client.search_info_by_keyword(
+ keyword=keyword,
+ pcursor=str(page),
+ )
+ if not videos_res:
+ utils.logger.error(f"[KuaishouCrawler.search] search info by keyword:{keyword} not found data")
+ continue
+
+ vision_search_photo: Dict = videos_res.get("visionSearchPhoto")
+ if vision_search_photo.get("result") != 1:
+ utils.logger.error(f"[KuaishouCrawler.search] search info by keyword:{keyword} not found data ")
+ continue
+
+ for video_detail in vision_search_photo.get("feeds"):
+ video_id_list.append(video_detail.get("photo", {}).get("id"))
+ await kuaishou_store.update_kuaishou_video(video_item=video_detail)
+
+ # batch fetch video comments
+ page += 1
+ await self.batch_get_video_comments(video_id_list)
+
+ async def get_specified_videos(self):
+ """Get the information and comments of the specified post"""
+ semaphore = asyncio.Semaphore(config.MAX_CONCURRENCY_NUM)
+ task_list = [
+ self.get_video_info_task(video_id=video_id, semaphore=semaphore) for video_id in config.KS_SPECIFIED_ID_LIST
+ ]
+ video_details = await asyncio.gather(*task_list)
+ for video_detail in video_details:
+ if video_detail is not None:
+ await kuaishou_store.update_kuaishou_video(video_detail)
+ await self.batch_get_video_comments(config.KS_SPECIFIED_ID_LIST)
+
+ async def get_video_info_task(self, video_id: str, semaphore: asyncio.Semaphore) -> Optional[Dict]:
+ """Get video detail task"""
+ async with semaphore:
+ try:
+ result = await self.ks_client.get_video_info(video_id)
+ utils.logger.info(f"[KuaishouCrawler.get_video_info_task] Get video_id:{video_id} info result: {result} ...")
+ return result.get("visionVideoDetail")
+ except DataFetchError as ex:
+ utils.logger.error(f"[KuaishouCrawler.get_video_info_task] Get video detail error: {ex}")
+ return None
+ except KeyError as ex:
+ utils.logger.error(f"[KuaishouCrawler.get_video_info_task] have not fund note detail video_id:{video_id}, err: {ex}")
+ return None
+
+ async def batch_get_video_comments(self, video_id_list: List[str]):
+ """
+ batch get video comments
+ :param video_id_list:
+ :return:
+ """
+ utils.logger.info(f"[KuaishouCrawler.batch_get_video_comments] video ids:{video_id_list}")
+ semaphore = asyncio.Semaphore(config.MAX_CONCURRENCY_NUM)
+ task_list: List[Task] = []
+ for video_id in video_id_list:
+ task = asyncio.create_task(self.get_comments(video_id, semaphore), name=video_id)
+ task_list.append(task)
+
+ comment_tasks_var.set(task_list)
+ await asyncio.gather(*task_list)
+
+ async def get_comments(self, video_id: str, semaphore: asyncio.Semaphore):
+ """
+ get comment for video id
+ :param video_id:
+ :param semaphore:
+ :return:
+ """
+ async with semaphore:
+ try:
+ utils.logger.info(f"[KuaishouCrawler.get_comments] begin get video_id: {video_id} comments ...")
+ await self.ks_client.get_video_all_comments(
+ photo_id=video_id,
+ crawl_interval=random.random(),
+ callback=kuaishou_store.batch_update_ks_video_comments
+ )
+ except DataFetchError as ex:
+ utils.logger.error(f"[KuaishouCrawler.get_comments] get video_id: {video_id} comment error: {ex}")
+ except Exception as e:
+ utils.logger.error(f"[KuaishouCrawler.get_comments] may be been blocked, err:{e}")
+ # use time.sleeep block main coroutine instead of asyncio.sleep and cacel running comment task
+ # maybe kuaishou block our request, we will take a nap and update the cookie again
+ current_running_tasks = comment_tasks_var.get()
+ for task in current_running_tasks:
+ task.cancel()
+ time.sleep(20)
+ await self.context_page.goto(f"{self.index_url}?isHome=1")
+ await self.ks_client.update_cookies(browser_context=self.browser_context)
+
+ @staticmethod
+ def format_proxy_info(ip_proxy_info: IpInfoModel) -> Tuple[Optional[Dict], Optional[Dict]]:
+ """format proxy info for playwright and httpx"""
+ playwright_proxy = {
+ "server": f"{ip_proxy_info.protocol}{ip_proxy_info.ip}:{ip_proxy_info.port}",
+ "username": ip_proxy_info.user,
+ "password": ip_proxy_info.password,
+ }
+ httpx_proxy = {
+ f"{ip_proxy_info.protocol}": f"http://{ip_proxy_info.user}:{ip_proxy_info.password}@{ip_proxy_info.ip}:{ip_proxy_info.port}"
+ }
+ return playwright_proxy, httpx_proxy
+
+ async def create_ks_client(self, httpx_proxy: Optional[str]) -> KuaiShouClient:
+ """Create xhs client"""
+ utils.logger.info("[KuaishouCrawler.create_ks_client] Begin create kuaishou API client ...")
+ cookie_str, cookie_dict = utils.convert_cookies(await self.browser_context.cookies())
+ xhs_client_obj = KuaiShouClient(
+ proxies=httpx_proxy,
+ headers={
+ "User-Agent": self.user_agent,
+ "Cookie": cookie_str,
+ "Origin": self.index_url,
+ "Referer": self.index_url,
+ "Content-Type": "application/json;charset=UTF-8"
+ },
+ playwright_page=self.context_page,
+ cookie_dict=cookie_dict,
+ )
+ return xhs_client_obj
+
+ async def launch_browser(
+ self,
+ chromium: BrowserType,
+ playwright_proxy: Optional[Dict],
+ user_agent: Optional[str],
+ headless: bool = True
+ ) -> BrowserContext:
+ """Launch browser and create browser context"""
+ utils.logger.info("[KuaishouCrawler.launch_browser] Begin create browser context ...")
+ if config.SAVE_LOGIN_STATE:
+ user_data_dir = os.path.join(os.getcwd(), "browser_data",
+ config.USER_DATA_DIR % self.platform) # type: ignore
+ browser_context = await chromium.launch_persistent_context(
+ user_data_dir=user_data_dir,
+ accept_downloads=True,
+ headless=headless,
+ proxy=playwright_proxy, # type: ignore
+ viewport={"width": 1920, "height": 1080},
+ user_agent=user_agent
+ )
+ return browser_context
+ else:
+ browser = await chromium.launch(headless=headless, proxy=playwright_proxy) # type: ignore
+ browser_context = await browser.new_context(
+ viewport={"width": 1920, "height": 1080},
+ user_agent=user_agent
+ )
+ return browser_context
+
+ async def close(self):
+ """Close browser context"""
+ await self.browser_context.close()
+ utils.logger.info("[KuaishouCrawler.close] Browser context closed ...")
diff --git a/media_platform/kuaishou/exception.py b/media_platform/kuaishou/exception.py
new file mode 100644
index 0000000..1a8642e
--- /dev/null
+++ b/media_platform/kuaishou/exception.py
@@ -0,0 +1,9 @@
+from httpx import RequestError
+
+
+class DataFetchError(RequestError):
+ """something error when fetch"""
+
+
+class IPBlockError(RequestError):
+ """fetch so fast that the server block us ip"""
diff --git a/media_platform/kuaishou/field.py b/media_platform/kuaishou/field.py
new file mode 100644
index 0000000..7c68785
--- /dev/null
+++ b/media_platform/kuaishou/field.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
\ No newline at end of file
diff --git a/media_platform/kuaishou/graphql.py b/media_platform/kuaishou/graphql.py
new file mode 100644
index 0000000..215b57f
--- /dev/null
+++ b/media_platform/kuaishou/graphql.py
@@ -0,0 +1,22 @@
+# 快手的数据传输是基于GraphQL实现的
+# 这个类负责获取一些GraphQL的schema
+from typing import Dict
+
+
+class KuaiShouGraphQL:
+ graphql_queries: Dict[str, str]= {}
+
+ def __init__(self):
+ self.graphql_dir = "media_platform/kuaishou/graphql/"
+ self.load_graphql_queries()
+
+ def load_graphql_queries(self):
+ graphql_files = ["search_query.graphql", "video_detail.graphql", "comment_list.graphql", "vision_profile.graphql"]
+
+ for file in graphql_files:
+ with open(self.graphql_dir + file, mode="r") as f:
+ query_name = file.split(".")[0]
+ self.graphql_queries[query_name] = f.read()
+
+ def get(self, query_name: str) -> str:
+ return self.graphql_queries.get(query_name, "Query not found")
diff --git a/media_platform/kuaishou/graphql/comment_list.graphql b/media_platform/kuaishou/graphql/comment_list.graphql
new file mode 100644
index 0000000..b216b8e
--- /dev/null
+++ b/media_platform/kuaishou/graphql/comment_list.graphql
@@ -0,0 +1,39 @@
+query commentListQuery($photoId: String, $pcursor: String) {
+ visionCommentList(photoId: $photoId, pcursor: $pcursor) {
+ commentCount
+ pcursor
+ rootComments {
+ commentId
+ authorId
+ authorName
+ content
+ headurl
+ timestamp
+ likedCount
+ realLikedCount
+ liked
+ status
+ authorLiked
+ subCommentCount
+ subCommentsPcursor
+ subComments {
+ commentId
+ authorId
+ authorName
+ content
+ headurl
+ timestamp
+ likedCount
+ realLikedCount
+ liked
+ status
+ authorLiked
+ replyToUserName
+ replyTo
+ __typename
+ }
+ __typename
+ }
+ __typename
+ }
+}
diff --git a/media_platform/kuaishou/graphql/search_query.graphql b/media_platform/kuaishou/graphql/search_query.graphql
new file mode 100644
index 0000000..cc3bd8f
--- /dev/null
+++ b/media_platform/kuaishou/graphql/search_query.graphql
@@ -0,0 +1,111 @@
+fragment photoContent on PhotoEntity {
+ __typename
+ id
+ duration
+ caption
+ originCaption
+ likeCount
+ viewCount
+ commentCount
+ realLikeCount
+ coverUrl
+ photoUrl
+ photoH265Url
+ manifest
+ manifestH265
+ videoResource
+ coverUrls {
+ url
+ __typename
+ }
+ timestamp
+ expTag
+ animatedCoverUrl
+ distance
+ videoRatio
+ liked
+ stereoType
+ profileUserTopPhoto
+ musicBlocked
+}
+
+fragment recoPhotoFragment on recoPhotoEntity {
+ __typename
+ id
+ duration
+ caption
+ originCaption
+ likeCount
+ viewCount
+ commentCount
+ realLikeCount
+ coverUrl
+ photoUrl
+ photoH265Url
+ manifest
+ manifestH265
+ videoResource
+ coverUrls {
+ url
+ __typename
+ }
+ timestamp
+ expTag
+ animatedCoverUrl
+ distance
+ videoRatio
+ liked
+ stereoType
+ profileUserTopPhoto
+ musicBlocked
+}
+
+fragment feedContent on Feed {
+ type
+ author {
+ id
+ name
+ headerUrl
+ following
+ headerUrls {
+ url
+ __typename
+ }
+ __typename
+ }
+ photo {
+ ...photoContent
+ ...recoPhotoFragment
+ __typename
+ }
+ canAddComment
+ llsid
+ status
+ currentPcursor
+ tags {
+ type
+ name
+ __typename
+ }
+ __typename
+}
+
+query visionSearchPhoto($keyword: String, $pcursor: String, $searchSessionId: String, $page: String, $webPageArea: String) {
+ visionSearchPhoto(keyword: $keyword, pcursor: $pcursor, searchSessionId: $searchSessionId, page: $page, webPageArea: $webPageArea) {
+ result
+ llsid
+ webPageArea
+ feeds {
+ ...feedContent
+ __typename
+ }
+ searchSessionId
+ pcursor
+ aladdinBanner {
+ imgUrl
+ link
+ __typename
+ }
+ __typename
+ }
+}
diff --git a/media_platform/kuaishou/graphql/video_detail.graphql b/media_platform/kuaishou/graphql/video_detail.graphql
new file mode 100644
index 0000000..ffb5309
--- /dev/null
+++ b/media_platform/kuaishou/graphql/video_detail.graphql
@@ -0,0 +1,80 @@
+query visionVideoDetail($photoId: String, $type: String, $page: String, $webPageArea: String) {
+ visionVideoDetail(photoId: $photoId, type: $type, page: $page, webPageArea: $webPageArea) {
+ status
+ type
+ author {
+ id
+ name
+ following
+ headerUrl
+ __typename
+ }
+ photo {
+ id
+ duration
+ caption
+ likeCount
+ realLikeCount
+ coverUrl
+ photoUrl
+ liked
+ timestamp
+ expTag
+ llsid
+ viewCount
+ videoRatio
+ stereoType
+ musicBlocked
+ manifest {
+ mediaType
+ businessType
+ version
+ adaptationSet {
+ id
+ duration
+ representation {
+ id
+ defaultSelect
+ backupUrl
+ codecs
+ url
+ height
+ width
+ avgBitrate
+ maxBitrate
+ m3u8Slice
+ qualityType
+ qualityLabel
+ frameRate
+ featureP2sp
+ hidden
+ disableAdaptive
+ __typename
+ }
+ __typename
+ }
+ __typename
+ }
+ manifestH265
+ photoH265Url
+ coronaCropManifest
+ coronaCropManifestH265
+ croppedPhotoH265Url
+ croppedPhotoUrl
+ videoResource
+ __typename
+ }
+ tags {
+ type
+ name
+ __typename
+ }
+ commentLimit {
+ canAddComment
+ __typename
+ }
+ llsid
+ danmakuSwitch
+ __typename
+ }
+}
diff --git a/media_platform/kuaishou/graphql/vision_profile.graphql b/media_platform/kuaishou/graphql/vision_profile.graphql
new file mode 100644
index 0000000..148165a
--- /dev/null
+++ b/media_platform/kuaishou/graphql/vision_profile.graphql
@@ -0,0 +1,16 @@
+query visionProfileUserList($pcursor: String, $ftype: Int) {
+ visionProfileUserList(pcursor: $pcursor, ftype: $ftype) {
+ result
+ fols {
+ user_name
+ headurl
+ user_text
+ isFollowing
+ user_id
+ __typename
+ }
+ hostName
+ pcursor
+ __typename
+ }
+}
diff --git a/media_platform/kuaishou/login.py b/media_platform/kuaishou/login.py
new file mode 100644
index 0000000..001d7b0
--- /dev/null
+++ b/media_platform/kuaishou/login.py
@@ -0,0 +1,103 @@
+import asyncio
+import functools
+import sys
+from typing import Optional
+
+import redis
+from playwright.async_api import BrowserContext, Page
+from tenacity import (RetryError, retry, retry_if_result, stop_after_attempt,
+ wait_fixed)
+
+import config
+from base.base_crawler import AbstractLogin
+from tools import utils
+
+
+class KuaishouLogin(AbstractLogin):
+ def __init__(self,
+ login_type: str,
+ browser_context: BrowserContext,
+ context_page: Page,
+ login_phone: Optional[str] = "",
+ cookie_str: str = ""
+ ):
+ self.login_type = login_type
+ self.browser_context = browser_context
+ self.context_page = context_page
+ self.login_phone = login_phone
+ self.cookie_str = cookie_str
+
+ async def begin(self):
+ """Start login xiaohongshu"""
+ utils.logger.info("[KuaishouLogin.begin] Begin login kuaishou ...")
+ if self.login_type == "qrcode":
+ await self.login_by_qrcode()
+ elif self.login_type == "phone":
+ await self.login_by_mobile()
+ elif self.login_type == "cookie":
+ await self.login_by_cookies()
+ else:
+ raise ValueError("[KuaishouLogin.begin] Invalid Login Type Currently only supported qrcode or phone or cookie ...")
+
+ @retry(stop=stop_after_attempt(20), wait=wait_fixed(1), retry=retry_if_result(lambda value: value is False))
+ async def check_login_state(self) -> bool:
+ """
+ Check if the current login status is successful and return True otherwise return False
+ retry decorator will retry 20 times if the return value is False, and the retry interval is 1 second
+ if max retry times reached, raise RetryError
+ """
+ current_cookie = await self.browser_context.cookies()
+ _, cookie_dict = utils.convert_cookies(current_cookie)
+ kuaishou_pass_token = cookie_dict.get("passToken")
+ if kuaishou_pass_token:
+ return True
+ return False
+
+ async def login_by_qrcode(self):
+ """login kuaishou website and keep webdriver login state"""
+ utils.logger.info("[KuaishouLogin.login_by_qrcode] Begin login kuaishou by qrcode ...")
+
+ # click login button
+ login_button_ele = self.context_page.locator(
+ "xpath=//p[text()=' 登录 ']"
+ )
+ await login_button_ele.click()
+
+ # find login qrcode
+ qrcode_img_selector = "//div[@class='qrcode-img']//img"
+ base64_qrcode_img = await utils.find_login_qrcode(
+ self.context_page,
+ selector=qrcode_img_selector
+ )
+ if not base64_qrcode_img:
+ utils.logger.info("[KuaishouLogin.login_by_qrcode] login failed , have not found qrcode please check ....")
+ sys.exit()
+
+
+ # show login qrcode
+ partial_show_qrcode = functools.partial(utils.show_qrcode, base64_qrcode_img)
+ asyncio.get_running_loop().run_in_executor(executor=None, func=partial_show_qrcode)
+
+ utils.logger.info(f"[KuaishouLogin.login_by_qrcode] waiting for scan code login, remaining time is 20s")
+ try:
+ await self.check_login_state()
+ except RetryError:
+ utils.logger.info("[KuaishouLogin.login_by_qrcode] Login kuaishou failed by qrcode login method ...")
+ sys.exit()
+
+ wait_redirect_seconds = 5
+ utils.logger.info(f"[KuaishouLogin.login_by_qrcode] Login successful then wait for {wait_redirect_seconds} seconds redirect ...")
+ await asyncio.sleep(wait_redirect_seconds)
+
+ async def login_by_mobile(self):
+ pass
+
+ async def login_by_cookies(self):
+ utils.logger.info("[KuaishouLogin.login_by_cookies] Begin login kuaishou by cookie ...")
+ for key, value in utils.convert_str_cookie_to_dict(self.cookie_str).items():
+ await self.browser_context.add_cookies([{
+ 'name': key,
+ 'value': value,
+ 'domain': ".kuaishou.com",
+ 'path': "/"
+ }])
diff --git a/media_platform/weibo/__init__.py b/media_platform/weibo/__init__.py
new file mode 100644
index 0000000..a033cbf
--- /dev/null
+++ b/media_platform/weibo/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/23 15:40
+# @Desc :
+from .client import WeiboClient
+from .core import WeiboCrawler
+from .login import WeiboLogin
diff --git a/media_platform/weibo/client.py b/media_platform/weibo/client.py
new file mode 100644
index 0000000..e3c975c
--- /dev/null
+++ b/media_platform/weibo/client.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/23 15:40
+# @Desc : 微博爬虫 API 请求 client
+
+import asyncio
+import copy
+import json
+import re
+from typing import Any, Callable, Dict, List, Optional
+from urllib.parse import urlencode
+
+import httpx
+from playwright.async_api import BrowserContext, Page
+
+from tools import utils
+
+from .exception import DataFetchError
+from .field import SearchType
+
+
+class WeiboClient:
+ def __init__(
+ self,
+ timeout=10,
+ proxies=None,
+ *,
+ headers: Dict[str, str],
+ playwright_page: Page,
+ cookie_dict: Dict[str, str],
+ ):
+ self.proxies = proxies
+ self.timeout = timeout
+ self.headers = headers
+ self._host = "https://m.weibo.cn"
+ self.playwright_page = playwright_page
+ self.cookie_dict = cookie_dict
+
+ async def request(self, method, url, **kwargs) -> Any:
+ async with httpx.AsyncClient(proxies=self.proxies) as client:
+ response = await client.request(
+ method, url, timeout=self.timeout,
+ **kwargs
+ )
+ data: Dict = response.json()
+ if data.get("ok") != 1:
+ utils.logger.error(f"[WeiboClient.request] request {method}:{url} err, res:{data}")
+ raise DataFetchError(data.get("msg", "unkonw error"))
+ else:
+ return data.get("data", {})
+
+ async def get(self, uri: str, params=None, headers=None) -> Dict:
+ final_uri = uri
+ if isinstance(params, dict):
+ final_uri = (f"{uri}?"
+ f"{urlencode(params)}")
+
+ if headers is None:
+ headers = self.headers
+ return await self.request(method="GET", url=f"{self._host}{final_uri}", headers=headers)
+
+ async def post(self, uri: str, data: dict) -> Dict:
+ json_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
+ return await self.request(method="POST", url=f"{self._host}{uri}",
+ data=json_str, headers=self.headers)
+
+ async def pong(self) -> bool:
+ """get a note to check if login state is ok"""
+ utils.logger.info("[WeiboClient.pong] Begin pong weibo...")
+ ping_flag = False
+ try:
+ uri = "/api/config"
+ resp_data: Dict = await self.request(method="GET", url=f"{self._host}{uri}", headers=self.headers)
+ if resp_data.get("login"):
+ ping_flag = True
+ else:
+ utils.logger.error(f"[WeiboClient.pong] cookie may be invalid and again login...")
+ except Exception as e:
+ utils.logger.error(f"[WeiboClient.pong] Pong weibo failed: {e}, and try to login again...")
+ ping_flag = False
+ return ping_flag
+
+ async def update_cookies(self, browser_context: BrowserContext):
+ cookie_str, cookie_dict = utils.convert_cookies(await browser_context.cookies())
+ self.headers["Cookie"] = cookie_str
+ self.cookie_dict = cookie_dict
+
+ async def get_note_by_keyword(
+ self,
+ keyword: str,
+ page: int = 1,
+ search_type: SearchType = SearchType.DEFAULT
+ ) -> Dict:
+ """
+ search note by keyword
+ :param keyword: 微博搜搜的关键词
+ :param page: 分页参数 -当前页码
+ :param search_type: 搜索的类型,见 weibo/filed.py 中的枚举SearchType
+ :return:
+ """
+ uri = "/api/container/getIndex"
+ containerid = f"100103type={search_type.value}&q={keyword}"
+ params = {
+ "containerid": containerid,
+ "page_type": "searchall",
+ "page": page,
+ }
+ return await self.get(uri, params)
+
+ async def get_note_comments(self, mid_id: str, max_id: int) -> Dict:
+ """get notes comments
+ :param mid_id: 微博ID
+ :param max_id: 分页参数ID
+ :return:
+ """
+ uri = "/comments/hotflow"
+ params = {
+ "id": mid_id,
+ "mid": mid_id,
+ "max_id_type": 0,
+ }
+ if max_id > 0:
+ params.update({"max_id": max_id})
+
+ referer_url = f"https://m.weibo.cn/detail/{mid_id}"
+ headers = copy.copy(self.headers)
+ headers["Referer"] = referer_url
+
+ return await self.get(uri, params, headers=headers)
+
+ async def get_note_all_comments(self, note_id: str, crawl_interval: float = 1.0, is_fetch_sub_comments=False,
+ callback: Optional[Callable] = None, ):
+ """
+ get note all comments include sub comments
+ :param note_id:
+ :param crawl_interval:
+ :param is_fetch_sub_comments:
+ :param callback:
+ :return:
+ """
+
+ result = []
+ is_end = False
+ max_id = -1
+ while not is_end:
+ comments_res = await self.get_note_comments(note_id, max_id)
+ max_id: int = comments_res.get("max_id")
+ comment_list: List[Dict] = comments_res.get("data", [])
+ is_end = max_id == 0
+ if callback: # 如果有回调函数,就执行回调函数
+ await callback(note_id, comment_list)
+ await asyncio.sleep(crawl_interval)
+ if not is_fetch_sub_comments:
+ result.extend(comment_list)
+ continue
+ # todo handle get sub comments
+ return result
+
+ async def get_note_info_by_id(self, note_id: str) -> Dict:
+ """
+ 根据帖子ID获取详情
+ :param note_id:
+ :return:
+ """
+ url = f"{self._host}/detail/{note_id}"
+ async with httpx.AsyncClient(proxies=self.proxies) as client:
+ response = await client.request(
+ "GET", url, timeout=self.timeout, headers=self.headers
+ )
+ if response.status_code != 200:
+ raise DataFetchError(f"get weibo detail err: {response.text}")
+ match = re.search(r'var \$render_data = (\[.*?\])\[0\]', response.text, re.DOTALL)
+ if match:
+ render_data_json = match.group(1)
+ render_data_dict = json.loads(render_data_json)
+ note_detail = render_data_dict[0].get("status")
+ note_item = {
+ "mblog": note_detail
+ }
+ return note_item
+ else:
+ utils.logger.info(f"[WeiboClient.get_note_info_by_id] 未找到$render_data的值")
+ return dict()
diff --git a/media_platform/weibo/core.py b/media_platform/weibo/core.py
new file mode 100644
index 0000000..318ff68
--- /dev/null
+++ b/media_platform/weibo/core.py
@@ -0,0 +1,255 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/23 15:41
+# @Desc : 微博爬虫主流程代码
+
+
+import asyncio
+import os
+import random
+from asyncio import Task
+from typing import Dict, List, Optional, Tuple
+
+from playwright.async_api import (BrowserContext, BrowserType, Page,
+ async_playwright)
+
+import config
+from base.base_crawler import AbstractCrawler
+from proxy.proxy_ip_pool import IpInfoModel, create_ip_pool
+from store import weibo as weibo_store
+from tools import utils
+from var import crawler_type_var
+
+from .client import WeiboClient
+from .exception import DataFetchError
+from .field import SearchType
+from .help import filter_search_result_card
+from .login import WeiboLogin
+
+
+class WeiboCrawler(AbstractCrawler):
+ platform: str
+ login_type: str
+ crawler_type: str
+ context_page: Page
+ wb_client: WeiboClient
+ browser_context: BrowserContext
+
+ def __init__(self):
+ self.index_url = "https://www.weibo.com"
+ self.mobile_index_url = "https://m.weibo.cn"
+ self.user_agent = utils.get_user_agent()
+ self.mobile_user_agent = utils.get_mobile_user_agent()
+
+ def init_config(self, platform: str, login_type: str, crawler_type: str):
+ self.platform = platform
+ self.login_type = login_type
+ self.crawler_type = crawler_type
+
+ async def start(self):
+ playwright_proxy_format, httpx_proxy_format = None, None
+ if config.ENABLE_IP_PROXY:
+ ip_proxy_pool = await create_ip_pool(config.IP_PROXY_POOL_COUNT, enable_validate_ip=True)
+ ip_proxy_info: IpInfoModel = await ip_proxy_pool.get_proxy()
+ playwright_proxy_format, httpx_proxy_format = self.format_proxy_info(ip_proxy_info)
+
+ async with async_playwright() as playwright:
+ # Launch a browser context.
+ chromium = playwright.chromium
+ self.browser_context = await self.launch_browser(
+ chromium,
+ None,
+ self.mobile_user_agent,
+ headless=config.HEADLESS
+ )
+ # stealth.min.js is a js script to prevent the website from detecting the crawler.
+ await self.browser_context.add_init_script(path="libs/stealth.min.js")
+ self.context_page = await self.browser_context.new_page()
+ await self.context_page.goto(self.mobile_index_url)
+
+ # Create a client to interact with the xiaohongshu website.
+ self.wb_client = await self.create_weibo_client(httpx_proxy_format)
+ if not await self.wb_client.pong():
+ login_obj = WeiboLogin(
+ login_type=self.login_type,
+ login_phone="", # your phone number
+ browser_context=self.browser_context,
+ context_page=self.context_page,
+ cookie_str=config.COOKIES
+ )
+ await self.context_page.goto(self.index_url)
+ await asyncio.sleep(1)
+ await login_obj.begin()
+
+ # 登录成功后重定向到手机端的网站,再更新手机端登录成功的cookie
+ utils.logger.info("[WeiboCrawler.start] redirect weibo mobile homepage and update cookies on mobile platform")
+ await self.context_page.goto(self.mobile_index_url)
+ await asyncio.sleep(2)
+ await self.wb_client.update_cookies(browser_context=self.browser_context)
+
+ crawler_type_var.set(self.crawler_type)
+ if self.crawler_type == "search":
+ # Search for video and retrieve their comment information.
+ await self.search()
+ elif self.crawler_type == "detail":
+ # Get the information and comments of the specified post
+ await self.get_specified_notes()
+ else:
+ pass
+ utils.logger.info("[WeiboCrawler.start] Bilibili Crawler finished ...")
+
+ async def search(self):
+ """
+ search weibo note with keywords
+ :return:
+ """
+ utils.logger.info("[WeiboCrawler.search] Begin search weibo keywords")
+ weibo_limit_count = 10
+ for keyword in config.KEYWORDS.split(","):
+ utils.logger.info(f"[WeiboCrawler.search] Current search keyword: {keyword}")
+ page = 1
+ while page * weibo_limit_count <= config.CRAWLER_MAX_NOTES_COUNT:
+ search_res = await self.wb_client.get_note_by_keyword(
+ keyword=keyword,
+ page=page,
+ search_type=SearchType.DEFAULT
+ )
+ note_id_list: List[str] = []
+ note_list = filter_search_result_card(search_res.get("cards"))
+ for note_item in note_list:
+ if note_item:
+ mblog: Dict = note_item.get("mblog")
+ note_id_list.append(mblog.get("id"))
+ await weibo_store.update_weibo_note(note_item)
+
+ page += 1
+ await self.batch_get_notes_comments(note_id_list)
+
+ async def get_specified_notes(self):
+ """
+ get specified notes info
+ :return:
+ """
+ semaphore = asyncio.Semaphore(config.MAX_CONCURRENCY_NUM)
+ task_list = [
+ self.get_note_info_task(note_id=note_id, semaphore=semaphore) for note_id in
+ config.WEIBO_SPECIFIED_ID_LIST
+ ]
+ video_details = await asyncio.gather(*task_list)
+ for note_item in video_details:
+ if note_item:
+ await weibo_store.update_weibo_note(note_item)
+ await self.batch_get_notes_comments(config.WEIBO_SPECIFIED_ID_LIST)
+
+ async def get_note_info_task(self, note_id: str, semaphore: asyncio.Semaphore) -> Optional[Dict]:
+ """
+ Get note detail task
+ :param note_id:
+ :param semaphore:
+ :return:
+ """
+ async with semaphore:
+ try:
+ result = await self.wb_client.get_note_info_by_id(note_id)
+ return result
+ except DataFetchError as ex:
+ utils.logger.error(f"[WeiboCrawler.get_note_info_task] Get note detail error: {ex}")
+ return None
+ except KeyError as ex:
+ utils.logger.error(
+ f"[WeiboCrawler.get_note_info_task] have not fund note detail note_id:{note_id}, err: {ex}")
+ return None
+
+ async def batch_get_notes_comments(self, note_id_list: List[str]):
+ """
+ batch get notes comments
+ :param note_id_list:
+ :return:
+ """
+ utils.logger.info(f"[WeiboCrawler.batch_get_notes_comments] note ids:{note_id_list}")
+ semaphore = asyncio.Semaphore(config.MAX_CONCURRENCY_NUM)
+ task_list: List[Task] = []
+ for note_id in note_id_list:
+ task = asyncio.create_task(self.get_note_comments(note_id, semaphore), name=note_id)
+ task_list.append(task)
+ await asyncio.gather(*task_list)
+
+ async def get_note_comments(self, note_id: str, semaphore: asyncio.Semaphore):
+ """
+ get comment for note id
+ :param note_id:
+ :param semaphore:
+ :return:
+ """
+ async with semaphore:
+ try:
+ utils.logger.info(f"[WeiboCrawler.get_note_comments] begin get note_id: {note_id} comments ...")
+ await self.wb_client.get_note_all_comments(
+ note_id=note_id,
+ crawl_interval=random.randint(1,10), # 微博对API的限流比较严重,所以延时提高一些
+ callback=weibo_store.batch_update_weibo_note_comments
+ )
+ except DataFetchError as ex:
+ utils.logger.error(f"[WeiboCrawler.get_note_comments] get note_id: {note_id} comment error: {ex}")
+ except Exception as e:
+ utils.logger.error(f"[WeiboCrawler.get_note_comments] may be been blocked, err:{e}")
+
+ async def create_weibo_client(self, httpx_proxy: Optional[str]) -> WeiboClient:
+ """Create xhs client"""
+ utils.logger.info("[WeiboCrawler.create_weibo_client] Begin create weibo API client ...")
+ cookie_str, cookie_dict = utils.convert_cookies(await self.browser_context.cookies())
+ weibo_client_obj = WeiboClient(
+ proxies=httpx_proxy,
+ headers={
+ "User-Agent": utils.get_mobile_user_agent(),
+ "Cookie": cookie_str,
+ "Origin": "https://m.weibo.cn",
+ "Referer": "https://m.weibo.cn",
+ "Content-Type": "application/json;charset=UTF-8"
+ },
+ playwright_page=self.context_page,
+ cookie_dict=cookie_dict,
+ )
+ return weibo_client_obj
+
+ @staticmethod
+ def format_proxy_info(ip_proxy_info: IpInfoModel) -> Tuple[Optional[Dict], Optional[Dict]]:
+ """format proxy info for playwright and httpx"""
+ playwright_proxy = {
+ "server": f"{ip_proxy_info.protocol}{ip_proxy_info.ip}:{ip_proxy_info.port}",
+ "username": ip_proxy_info.user,
+ "password": ip_proxy_info.password,
+ }
+ httpx_proxy = {
+ f"{ip_proxy_info.protocol}": f"http://{ip_proxy_info.user}:{ip_proxy_info.password}@{ip_proxy_info.ip}:{ip_proxy_info.port}"
+ }
+ return playwright_proxy, httpx_proxy
+
+ async def launch_browser(
+ self,
+ chromium: BrowserType,
+ playwright_proxy: Optional[Dict],
+ user_agent: Optional[str],
+ headless: bool = True
+ ) -> BrowserContext:
+ """Launch browser and create browser context"""
+ utils.logger.info("[WeiboCrawler.launch_browser] Begin create browser context ...")
+ if config.SAVE_LOGIN_STATE:
+ user_data_dir = os.path.join(os.getcwd(), "browser_data",
+ config.USER_DATA_DIR % self.platform) # type: ignore
+ browser_context = await chromium.launch_persistent_context(
+ user_data_dir=user_data_dir,
+ accept_downloads=True,
+ headless=headless,
+ proxy=playwright_proxy, # type: ignore
+ viewport={"width": 1920, "height": 1080},
+ user_agent=user_agent
+ )
+ return browser_context
+ else:
+ browser = await chromium.launch(headless=headless, proxy=playwright_proxy) # type: ignore
+ browser_context = await browser.new_context(
+ viewport={"width": 1920, "height": 1080},
+ user_agent=user_agent
+ )
+ return browser_context
diff --git a/media_platform/weibo/exception.py b/media_platform/weibo/exception.py
new file mode 100644
index 0000000..9aecdf4
--- /dev/null
+++ b/media_platform/weibo/exception.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/2 18:44
+# @Desc :
+
+from httpx import RequestError
+
+
+class DataFetchError(RequestError):
+ """something error when fetch"""
+
+
+class IPBlockError(RequestError):
+ """fetch so fast that the server block us ip"""
diff --git a/media_platform/weibo/field.py b/media_platform/weibo/field.py
new file mode 100644
index 0000000..cbc9d27
--- /dev/null
+++ b/media_platform/weibo/field.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/23 15:41
+# @Desc :
+from enum import Enum
+
+
+class SearchType(Enum):
+ # 综合
+ DEFAULT = "1"
+
+ # 实时
+ REAL_TIME = "61"
+
+ # 热门
+ POPULAR = "60"
+
+ # 视频
+ VIDEO = "64"
diff --git a/media_platform/weibo/help.py b/media_platform/weibo/help.py
new file mode 100644
index 0000000..1838555
--- /dev/null
+++ b/media_platform/weibo/help.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/24 17:37
+# @Desc :
+
+from typing import Dict, List
+
+
+def filter_search_result_card(card_list: List[Dict]) -> List[Dict]:
+ """
+ 过滤微博搜索的结果,只保留card_type为9类型的数据
+ :param card_list:
+ :return:
+ """
+ note_list: List[Dict] = []
+ for card_item in card_list:
+ if card_item.get("card_type") == 9:
+ note_list.append(card_item)
+ if len(card_item.get("card_group", [])) > 0:
+ card_group = card_item.get("card_group")
+ for card_group_item in card_group:
+ if card_group_item.get("card_type") == 9:
+ note_list.append(card_group_item)
+
+ return note_list
diff --git a/media_platform/weibo/login.py b/media_platform/weibo/login.py
new file mode 100644
index 0000000..dd0d737
--- /dev/null
+++ b/media_platform/weibo/login.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+# @Author : relakkes@gmail.com
+# @Time : 2023/12/23 15:42
+# @Desc : 微博登录实现
+
+import asyncio
+import functools
+import sys
+from typing import Optional
+
+from playwright.async_api import BrowserContext, Page
+from tenacity import (RetryError, retry, retry_if_result, stop_after_attempt,
+ wait_fixed)
+
+from base.base_crawler import AbstractLogin
+from tools import utils
+
+
+class WeiboLogin(AbstractLogin):
+ def __init__(self,
+ login_type: str,
+ browser_context: BrowserContext,
+ context_page: Page,
+ login_phone: Optional[str] = "",
+ cookie_str: str = ""
+ ):
+ self.login_type = login_type
+ self.browser_context = browser_context
+ self.context_page = context_page
+ self.login_phone = login_phone
+ self.cookie_str = cookie_str
+
+ async def begin(self):
+ """Start login weibo"""
+ utils.logger.info("[WeiboLogin.begin] Begin login weibo ...")
+ if self.login_type == "qrcode":
+ await self.login_by_qrcode()
+ elif self.login_type == "phone":
+ await self.login_by_mobile()
+ elif self.login_type == "cookie":
+ await self.login_by_cookies()
+ else:
+ raise ValueError(
+ "[WeiboLogin.begin] Invalid Login Type Currently only supported qrcode or phone or cookie ...")
+
+
+ @retry(stop=stop_after_attempt(20), wait=wait_fixed(1), retry=retry_if_result(lambda value: value is False))
+ async def check_login_state(self, no_logged_in_session: str) -> bool:
+ """
+ Check if the current login status is successful and return True otherwise return False
+ retry decorator will retry 20 times if the return value is False, and the retry interval is 1 second
+ if max retry times reached, raise RetryError
+ """
+ current_cookie = await self.browser_context.cookies()
+ _, cookie_dict = utils.convert_cookies(current_cookie)
+ current_web_session = cookie_dict.get("WBPSESS")
+ if current_web_session != no_logged_in_session:
+ return True
+ return False
+
+ async def popup_login_dialog(self):
+ """If the login dialog box does not pop up automatically, we will manually click the login button"""
+ dialog_selector = "xpath=//div[@class='woo-modal-main']"
+ try:
+ # check dialog box is auto popup and wait for 4 seconds
+ await self.context_page.wait_for_selector(dialog_selector, timeout=1000 * 4)
+ except Exception as e:
+ utils.logger.error(
+ f"[WeiboLogin.popup_login_dialog] login dialog box does not pop up automatically, error: {e}")
+ utils.logger.info(
+ "[WeiboLogin.popup_login_dialog] login dialog box does not pop up automatically, we will manually click the login button")
+
+ # 向下滚动1000像素
+ await self.context_page.mouse.wheel(0,500)
+ await asyncio.sleep(0.5)
+
+ try:
+ # click login button
+ login_button_ele = self.context_page.locator(
+ "xpath=//a[text()='登录']",
+ )
+ await login_button_ele.click()
+ await asyncio.sleep(0.5)
+ except Exception as e:
+ utils.logger.info(f"[WeiboLogin.popup_login_dialog] manually click the login button faield maybe login dialog Appear:{e}")
+
+ async def login_by_qrcode(self):
+ """login weibo website and keep webdriver login state"""
+ utils.logger.info("[WeiboLogin.login_by_qrcode] Begin login weibo by qrcode ...")
+
+ await self.popup_login_dialog()
+
+ # find login qrcode
+ qrcode_img_selector = "//div[@class='woo-modal-main']//img"
+ base64_qrcode_img = await utils.find_login_qrcode(
+ self.context_page,
+ selector=qrcode_img_selector
+ )
+ if not base64_qrcode_img:
+ utils.logger.info("[WeiboLogin.login_by_qrcode] login failed , have not found qrcode please check ....")
+ sys.exit()
+
+ # show login qrcode
+ partial_show_qrcode = functools.partial(utils.show_qrcode, base64_qrcode_img)
+ asyncio.get_running_loop().run_in_executor(executor=None, func=partial_show_qrcode)
+
+ utils.logger.info(f"[WeiboLogin.login_by_qrcode] Waiting for scan code login, remaining time is 20s")
+
+ # get not logged session
+ current_cookie = await self.browser_context.cookies()
+ _, cookie_dict = utils.convert_cookies(current_cookie)
+ no_logged_in_session = cookie_dict.get("WBPSESS")
+
+ try:
+ await self.check_login_state(no_logged_in_session)
+ except RetryError:
+ utils.logger.info("[WeiboLogin.login_by_qrcode] Login weibo failed by qrcode login method ...")
+ sys.exit()
+
+ wait_redirect_seconds = 5
+ utils.logger.info(
+ f"[WeiboLogin.login_by_qrcode] Login successful then wait for {wait_redirect_seconds} seconds redirect ...")
+ await asyncio.sleep(wait_redirect_seconds)
+
+ async def login_by_mobile(self):
+ pass
+
+ async def login_by_cookies(self):
+ utils.logger.info("[WeiboLogin.login_by_qrcode] Begin login weibo by cookie ...")
+ for key, value in utils.convert_str_cookie_to_dict(self.cookie_str).items():
+ await self.browser_context.add_cookies([{
+ 'name': key,
+ 'value': value,
+ 'domain': ".weibo.cn",
+ 'path': "/"
+ }])
diff --git a/media_platform/xhs/__init__.py b/media_platform/xhs/__init__.py
new file mode 100644
index 0000000..947f034
--- /dev/null
+++ b/media_platform/xhs/__init__.py
@@ -0,0 +1,2 @@
+from .core import XiaoHongShuCrawler
+from .field import *
diff --git a/media_platform/xhs/client.py b/media_platform/xhs/client.py
new file mode 100644
index 0000000..72e9d89
--- /dev/null
+++ b/media_platform/xhs/client.py
@@ -0,0 +1,325 @@
+import asyncio
+import json
+import re
+from typing import Callable, Dict, List, Optional
+from urllib.parse import urlencode
+
+import httpx
+from playwright.async_api import BrowserContext, Page
+
+from tools import utils
+
+from .exception import DataFetchError, IPBlockError
+from .field import SearchNoteType, SearchSortType
+from .help import get_search_id, sign
+
+
+class XHSClient:
+ def __init__(
+ self,
+ timeout=10,
+ proxies=None,
+ *,
+ headers: Dict[str, str],
+ playwright_page: Page,
+ cookie_dict: Dict[str, str],
+ ):
+ self.proxies = proxies
+ self.timeout = timeout
+ self.headers = headers
+ self._host = "https://edith.xiaohongshu.com"
+ self.IP_ERROR_STR = "网络连接异常,请检查网络设置或重启试试"
+ self.IP_ERROR_CODE = 300012
+ self.NOTE_ABNORMAL_STR = "笔记状态异常,请稍后查看"
+ self.NOTE_ABNORMAL_CODE = -510001
+ self.playwright_page = playwright_page
+ self.cookie_dict = cookie_dict
+
+ async def _pre_headers(self, url: str, data=None) -> Dict:
+ """
+ 请求头参数签名
+ Args:
+ url:
+ data:
+
+ Returns:
+
+ """
+ encrypt_params = await self.playwright_page.evaluate("([url, data]) => window._webmsxyw(url,data)", [url, data])
+ local_storage = await self.playwright_page.evaluate("() => window.localStorage")
+ signs = sign(
+ a1=self.cookie_dict.get("a1", ""),
+ b1=local_storage.get("b1", ""),
+ x_s=encrypt_params.get("X-s", ""),
+ x_t=str(encrypt_params.get("X-t", ""))
+ )
+
+ headers = {
+ "X-S": signs["x-s"],
+ "X-T": signs["x-t"],
+ "x-S-Common": signs["x-s-common"],
+ "X-B3-Traceid": signs["x-b3-traceid"]
+ }
+ self.headers.update(headers)
+ return self.headers
+
+ async def request(self, method, url, **kwargs) -> Dict:
+ """
+ 封装httpx的公共请求方法,对请求响应做一些处理
+ Args:
+ method: 请求方法
+ url: 请求的URL
+ **kwargs: 其他请求参数,例如请求头、请求体等
+
+ Returns:
+
+ """
+ # return response.text
+ return_response = kwargs.pop('return_response', False)
+
+ async with httpx.AsyncClient(proxies=self.proxies) as client:
+ response = await client.request(
+ method, url, timeout=self.timeout,
+ **kwargs
+ )
+
+ if return_response:
+ return response.text
+
+ data: Dict = response.json()
+ if data["success"]:
+ return data.get("data", data.get("success", {}))
+ elif data["code"] == self.IP_ERROR_CODE:
+ raise IPBlockError(self.IP_ERROR_STR)
+ else:
+ raise DataFetchError(data.get("msg", None))
+
+ async def get(self, uri: str, params=None) -> Dict:
+ """
+ GET请求,对请求头签名
+ Args:
+ uri: 请求路由
+ params: 请求参数
+
+ Returns:
+
+ """
+ final_uri = uri
+ if isinstance(params, dict):
+ final_uri = (f"{uri}?"
+ f"{urlencode(params)}")
+ headers = await self._pre_headers(final_uri)
+ return await self.request(method="GET", url=f"{self._host}{final_uri}", headers=headers)
+
+ async def post(self, uri: str, data: dict) -> Dict:
+ """
+ POST请求,对请求头签名
+ Args:
+ uri: 请求路由
+ data: 请求体参数
+
+ Returns:
+
+ """
+ headers = await self._pre_headers(uri, data)
+ json_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
+ return await self.request(method="POST", url=f"{self._host}{uri}",
+ data=json_str, headers=headers)
+
+ async def pong(self) -> bool:
+ """
+ 用于检查登录态是否失效了
+ Returns:
+
+ """
+ """get a note to check if login state is ok"""
+ utils.logger.info("[XHSClient.pong] Begin to pong xhs...")
+ ping_flag = False
+ try:
+ note_card: Dict = await self.get_note_by_keyword(keyword="小红书")
+ if note_card.get("items"):
+ ping_flag = True
+ except Exception as e:
+ utils.logger.error(f"[XHSClient.pong] Ping xhs failed: {e}, and try to login again...")
+ ping_flag = False
+ return ping_flag
+
+ async def update_cookies(self, browser_context: BrowserContext):
+ """
+ API客户端提供的更新cookies方法,一般情况下登录成功后会调用此方法
+ Args:
+ browser_context: 浏览器上下文对象
+
+ Returns:
+
+ """
+ cookie_str, cookie_dict = utils.convert_cookies(await browser_context.cookies())
+ self.headers["Cookie"] = cookie_str
+ self.cookie_dict = cookie_dict
+
+ async def get_note_by_keyword(
+ self, keyword: str,
+ page: int = 1, page_size: int = 20,
+ sort: SearchSortType = SearchSortType.GENERAL,
+ note_type: SearchNoteType = SearchNoteType.ALL
+ ) -> Dict:
+ """
+ 根据关键词搜索笔记
+ Args:
+ keyword: 关键词参数
+ page: 分页第几页
+ page_size: 分页数据长度
+ sort: 搜索结果排序指定
+ note_type: 搜索的笔记类型
+
+ Returns:
+
+ """
+ uri = "/api/sns/web/v1/search/notes"
+ data = {
+ "keyword": keyword,
+ "page": page,
+ "page_size": page_size,
+ "search_id": get_search_id(),
+ "sort": sort.value,
+ "note_type": note_type.value
+ }
+ return await self.post(uri, data)
+
+ async def get_creator_info_and_notes(self, creator: str) -> Dict:
+ """
+ 获取博主的信息和第一页的笔记
+ Args:
+ creator: 博主ID
+ Returns:
+ {"creator":{}, "notes":[]}
+ """
+ path = '/user/profile/'+creator
+ content = await self.request(method="GET", url=f"https://www.xiaohongshu.com{path}", return_response=True)
+ match = re.search(r'