diff --git a/.github/workflows/Codecov.yml b/.github/workflows/Codecov.yml index 28690aa4..ae433056 100644 --- a/.github/workflows/Codecov.yml +++ b/.github/workflows/Codecov.yml @@ -19,12 +19,20 @@ jobs: pip install pytest-cov pip install -e . + - name: Print environment variable + run: echo $SKIP_IN_CI + env: + SKIP_IN_CI: ${{ secrets.SKIP_IN_CI }} + - name: Run tests and collect coverage + env: + SKIP_IN_CI: ${{ secrets.SKIP_IN_CI }} run: pytest --cov=./ --cov-report=xml - name: Run ATS uses: codecov/codecov-ats@v0 env: + SKIP_IN_CI: ${{ secrets.SKIP_IN_CI }} # 从GitHub Secrets中获取环境变量 CODECOV_STATIC_TOKEN: ${{ secrets.CODECOV_STATIC_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..b6ba7005 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Black 格式化代码", + "type": "debugpy", + "request": "launch", + "module": "black", + "args": [ + "${input:pythonPath}", + "--exclude", + "venv/*", + ], + "env": { + "PYTHONDEVMODE": "1", + "PYDEVD_DISABLE_FILE_VALIDATION": "1" + } + } + ], + "inputs": [ + { + "id": "pythonPath", + "type": "promptString", + "description": "输入需要格式化的目录", + "default": "**/*.py" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 98bd006b..2bdf719b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "python.analysis.typeCheckingMode": "off" + "python.analysis.typeCheckingMode": "off", + "python.analysis.autoImportCompletions": false } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0cd883..a863e552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,112 @@ ## [Unreleased] -- `0.0.1.6`版本中添加对`weibo`,`x`的支持 +- `0.0.1.7`版本中将会添加接口本地转发的支持,添加更多`douyin`,`tiktok`,`weibo`和`x`的接口。 + + +## [0.0.1.6] - 2024-05-04 + +### Added + +- 添加`weibo`应用 +- 添加`abogus(limit ua)`加密 +- 添加`douyin`加密算法切换配置 +- 添加基础接口模型转url类 +- 添加`WebSocket`爬虫客户端 +- 添加`douyin`直播wss签名管理器 +- 添加`douyin`直播wss签名生成类 +- 添加`douyin`工具JS库`webmssdk.es5-1.0.0.53` +- 添加`douyin`直播间弹幕wss接口 +- 添加`F2`版本检测 +- 添加`tiktok`直播间开播状态 +- 添加`PyExecJS==1.5.1`依赖 +- 添加`protobuf==4.23.0`依赖 +- 添加`websockets>=11.0`依赖 +- 添加`tiktok`的`device_id注册`与`cookie`管理类 +- 添加`douyin`生成`webid`配置 +- 添加`douyin`关注用户直播 +- 添加`douyin`,`tiktok`模型配置 +- 添加`conf.yaml`配置版本号 +- 添加`tiktok`集成测试 +- 添加`traceback`输出 +- 添加`douyin`短剧作品 +- 添加同步客户端的同步`transport` +- 添加同步客户端 +- 添加`douyin`直播弹幕初始化 +- 添加`douyin`合集`mix_id`获取方法 +- 添加`douyin`查询用户 +- 添加时间戳转换的默认时区设置(`UTC/GMT+08:00`) +- 添加`ClientConfManager`为每个应用提供方便的配置读取 +- 添加`uniqueId`查询`tiktok`的`user_db` +- 添加获取`segments`的`duration`列表方法 +- 添加应用运行模式的输出 +- 新增`tiktok`作品搜索 +- 新增`tiktok`用户直播 +- 添加反转义`JSON`方法 +- 新增`douyin`相关推荐 +- 新增`douyin`好友作品 + +### Changed + +- 更新`__aexit__`方法 +- 更新`douyin`加密算法代码片段 +- 更新`weibo`测试用例 +- 优化命令不存在的输出 +- 取消接口数据过滤器对`bool`的预处理 +- 调整停止异步任务信号 +- 更新`douyin`的`xbogus`调用 +- 为装饰器文件重命名 +- 更新获取`Content-Length`的方法 +- 防止`douyin`直播结束时下载崩溃 +- 更新`BaseCrawler`类处理`httpx`即将弃用`proxies`参数 +- 更新`tiktok`的`msToken`配置 +- 修复`ClientConfManager`参数 +- 更新了所有应用配置 +- 重构了所有工具类方法 +- 更新`base_downloader`的区块下载参数 +- 修改`douyin`生成的`ttwid`将绑定`ua` +- 修改`tiktok`用户直播下载流地址 +- 修改`douyin`,`tiktok`获取用户信息方法名 +- 完善时间戳转换类型,支持30位 +- 修改应用的代理配置名(`http: https: -> http://: https://:`) +- 更新`xb`算法示例部分 +- 更新`base_crawler`异常捕获与输出 +- 更新应用初始化配置文件后退出 (#70) +- 更新应用使用`--auto-cookie`命令后退出 +- 更新`douyin`过滤器,将`video_play_addr`返回完整视频列表便于下载失败轮替 +- 更改`douyin`图集文件名(`jpg -> webp`) +- 更改应用直播下载文件名(`mp4 -> flv`) +- 更新应用工具类网络错误捕获 + +### Deprecated + +- 弃用`douyin`SSO扫码登录 +- 类`BaseModel`中的`dict`方法已弃用(`pydantic>=2.6.4`) +- 类`datetime`中的`utcnow`方法已弃用 +- 弃用`douyin`,`tiktok`获取用户名方法 + +### Removed + +- 删除`tiktok`基础请求模型的无用参数 +- 删除`f2\utils\utils.py`无效导入 + +### Fixed + +- 修复`douyin`接口更新导致的错误 #104 +- 修复`_dl`日志输出 +- 修复`douyin`下载合集时合集链接无法识别的情况 +- 修复`tiktok`下载播放列表(合集)的错误 +- 修复`m3u8`流下载时会重复下载`ts`片段的问题 +- 修复`m3u8`流获取`content_length`时没有提供代理参数造成的访问失败 +- 修复`douyin`,`tiktok`因提前引发异常导致无法生成虚假的msToken + +### Security + +- 更新`pytest`版本到`8.2.1` +- 更新`pydantic`版本到`2.6.4` +- 更新`httpx`版本到`0.27.0` +- 更新`aiosqlite`版本到`0.20.0` + ## [0.0.1.5] - 2024-04-04 diff --git a/README.en.md b/README.en.md index ef247502..ba923bdb 100644 --- a/README.en.md +++ b/README.en.md @@ -4,7 +4,10 @@ [![Downloads](https://pepy.tech/badge/f2/month)](https://pepy.tech/project/f2) [![PyPI version](https://badge.fury.io/py/f2.svg)](https://badge.fury.io/py/f2) +[![Dev Branch](https://badgen.net/badge/branch/v0.0.1.6-pw2/blue)](https://github.com/Johnserf-Seed/f2/tree/v0.0.1.6-pw2) +[![Discord](https://img.shields.io/discord/1146473603450282004?label=Discord)](https://discord.gg/3PhtPmgHf8) [![codecov](https://codecov.io/gh/Johnserf-Seed/f2/graph/badge.svg?token=T9DH4QPZSS)](https://codecov.io/gh/Johnserf-Seed/f2) +[![TikHub](https://img.shields.io/badge/Sponsor-TikHub-orange?style=flat-square&logo=tiktok)](https://beta-web.tikhub.io/users/signup?referral_code=6hLcGD94) [![APACHE-2.0](https://img.shields.io/github/license/johnserf-seed/f2)](https://github.com/Johnserf-Seed/f2/blob/main/LICENSE) @@ -53,15 +56,30 @@ ## ✨ New Changes -When upgrading to version `0.0.1.5` of `F2`, please note the following key updates. +When downloading or upgrading to a different version of `F2`, please note the following critical version updates. -- `XBogus` parameter in `0.0.1.5` version now supports custom User-Agent (UA), please pay attention to UA specification. -- The rebuilt database contains original data of interfaces, so you need to delete the old database file. If you want to retain records, please pay attention to migration. -- The return types of all `fetch` methods have been unified to filter types, so you need to pay attention to this change. -- Filter has added the `_to_raw` method, which can convert the filter to original interface data. -- The file name template has been updated, and if your file name does not meet the specifications, an exception will be thrown. -- `douyin` collection page links cannot be resolved, see [Douyin Collection Works](#抖音合集作品). -- For more changes, see [ChangeLog](https://github.com/Johnserf-Seed/f2/blob/main/CHANGELOG.md#0015---2024-04-04). +
+ 📡 v0.0.1.6-pw2 + + - The configuration file format has been updated. If you are using an old configuration file, please migrate accordingly. + - The default timezone for all timestamps is now (`UTC/GMT+08:00`). + - The `douyin` live stream filenames have been adjusted to `flv`, and albums have been reverted to `webp`. + - The 403 error for `tiktok` video URLs has been fixed. [Solution for TikTok video URL 403](https://johnserf-seed.github.io/f2/question-answer/qa.html#tiktok-403-forbidden) + - `douyin` now defaults to using the `ab` algorithm for requests. (The full-powered ab algorithm will be open-sourced later). + - For more changes, see [ChangeLog](https://github.com/Johnserf-Seed/f2/blob/main/CHANGELOG.md#0015---2024-04-04). +
+ +
+ 📡 v0.0.1.5-pw2 + + - `XBogus` parameter in `0.0.1.5` version now supports custom User-Agent (UA), please pay attention to UA specification. + - The rebuilt database contains original data of interfaces, so you need to delete the old database file. If you want to retain records, please pay attention to migration. + - The return types of all `fetch` methods have been unified to filter types, so you need to pay attention to this change. + - Filter has added the `_to_raw` method, which can convert the filter to original interface data. + - The file name template has been updated, and if your file name does not meet the specifications, an exception will be thrown. + - `douyin` collection page links cannot be resolved, see [Douyin Collection Works](#抖音合集作品). + - For more changes, see [ChangeLog](https://github.com/Johnserf-Seed/f2/blob/main/CHANGELOG.md#0015---2024-04-04). +
## 📑 Documentation @@ -77,9 +95,9 @@ Many features are not fully developed in the `preview` version. If you find any ## 🗓️ Todo -- Support for `weibo` and `x` will be added in version `0.0.1.6`. -- More `douyin` and `tiktok` interfaces will be added in version `0.0.1.6`. -- Known issues from previous versions will be fixed in version `0.0.1.6`. +- Local forwarding functionality will be added in version `0.0.1.7`. +- More interfaces for `douyin`, `tiktok`, `weibo`, and `x` will be added in version `0.0.1.7`. +- Known issues with `x` will be fixed in version `0.0.1.7`. ## 🐛 Updates @@ -98,37 +116,62 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores - 🟣 Indicates that login is required to download works that are only visible to oneself, favorited works, works in collection folders, or liked works. (After login, ignores own privacy settings and obtains personalized content) - ⚫ Indicates that login is not required to download public works, works in collection folders, liked works, etc. (Only downloads works visible to others and pages) - | Feature | Account Status | Interface | Feature Status | + | Feature | Account Status | API | Status | | --- | --- | --- | --- | - | User Information | 🟣⚫ | `handler_user_profile` | 🟢 | - | Single Work (Video, Album, Daily) | 🟣⚫ | `fetch_one_video` | 🟢 | - | Home Page Works | 🟣⚫ | `fetch_user_post_videos` | 🟢 | - | Liked Works | 🟣⚫ | `fetch_user_like_videos` | 🟢 | - | Favorite Works | 🟣 | `fetch_user_collects_videos` | 🟢 | - | Collection Works | 🟣 | `fetch_user_collection_videos` | 🟢 | - | Collected Original Sound | 🟣 | `fetch_user_music_collection` | 🟢 | - | Collected Collections | 🟣 | `fetch_user_mix_collection` | 🔵 | - | Collected Short Films | 🟣 | `fetch_user_series_collection` | 🟤 | - | Collection Works | ⚫ | `fetch_user_mix_videos` | 🟢 | - | Home Page Recommended Works | 🟣⚫ | `fetch_user_feed_videos` | 🟡 | - | Similar Recommended Works | ⚫ | `fetch_related_videos` | 🔵 | + | User Information | 🟣⚫ | `fetch_user_profile` | 🟢 | + | Single Video (Video, Album, Daily) | 🟣⚫ | `fetch_one_video` | 🟢 | + | Homepage Videos | 🟣⚫ | `fetch_user_post_videos` | 🟢 | + | Liked Videos | 🟣⚫ | `fetch_user_like_videos` | 🟢 | + | Collection Folder Videos | 🟣⚫ | `fetch_user_collects_videos` | 🟢 | + | Collected Videos | 🟣 | `fetch_user_collection_videos` | 🟢 | + | Collected Music | 🟣 | `fetch_user_music_collection` | 🟢 | + | Collected Playlist | 🟣 | `fetch_user_mix_collection` | 🔵 | + | Collected Series | 🟣 | `fetch_user_series_collection` | 🟤 | + | Playlist Videos | ⚫ | `fetch_user_mix_videos` | 🟢 | + | Recommended Videos | 🟣⚫ | `fetch_user_feed_videos` | 🟢 | + | Related Videos | ⚫ | `fetch_related_videos` | 🟢 | | Live Room Information (Stream Download) | ⚫ | `fetch_user_live_videos`, `fetch_user_live_videos_by_room_id` | 🟢 | - | Live Room Danmaku | ⚫ | `fetch_user_live_danmu` | 🔵 | - | Following Users' Live Broadcasts | 🟣⚫ | `fetch_user_following_lives` | 🔵 | - | Following User Information | 🟣⚫ | `fetch_user_following` | 🟢 | - | Fan User Information | 🟣⚫ | `fetch_user_follower` | 🟢 | - | Following User Works | 🟣⚫ | `fetch_user_following_videos` | 🟤 | - | Fan User Works | 🟣⚫ | `fetch_user_follower_videos` | 🟤 | - | Friend's Works | 🟣 | `fetch_user_friend_videos` | 🔵 | + | Live Room Load | ⚫ | `fetch_live_im` | 🟢 | + | Live Room Danmaku | ⚫ | `fetch_user_live_danmu` | 🟢 | + | Followed Users Live | 🟣⚫ | `fetch_user_following_lives` | 🟢 | + | Followed Users Information | 🟣⚫ | `fetch_user_following` | 🟢 | + | Followers Information | 🟣⚫ | `fetch_user_follower` | 🟢 | + | Followed Users Videos | 🟣⚫ | `fetch_user_following_videos` | 🟤 | + | Followers Videos | 🟣⚫ | `fetch_user_follower_videos` | 🟤 | + | Friends' Videos | 🟣 | `fetch_friend_feed_videos` | 🟢 | | Search Videos | ⚫ | `fetch_search_videos` | 🔵 | | Search Users | ⚫ | `fetch_search_users` | 🔵 | - | Search Lives | ⚫ | `fetch_search_lives` | 🔵 | - | Guess What You Want to Search (Related Search) | ⚫ | `fetch_search_suggest` | 🟤 | - | DouYin Hotspot | ⚫ | `fetch_hot_search` | 🟤 | - | Work Comments | 🟣⚫ | `fetch_video_comments` | 🔵 | - | Viewing History | 🟣 | `fetch_user_history_read` | 🟤 | + | Search Live | ⚫ | `fetch_search_lives` | 🔵 | + | Search Suggestions | ⚫ | `fetch_search_suggest` | 🟤 | + | Douyin Hot Search | ⚫ | `fetch_hot_search` | 🟤 | + | Video Comments | 🟣⚫ | `fetch_video_comments` | 🔵 | + | Watch History | 🟣 | `fetch_user_history_read` | 🟤 | | Watch Later | 🟣 | `fetch_user_watch_later` | 🟤 | | ... | ... | ... | ... | + + | Tool | Class | API | Status | + | --- | --- | --- | --- | + | Manage Client Configuration | `ClientConfManager` | | 🟢 | + | Generate Real msToken | `TokenManager` | `gen_real_msToken` | 🟢 | + | Generate Fake msToken | `TokenManager` | `gen_false_msToken` | 🟢 | + | Generate ttwid | `TokenManager` | `gen_ttwid` | 🟢 | + | Generate webid | `TokenManager` | `gen_webid` | 🟢 | + | Generate verify_fp | `VerifyFpManager` | `gen_verify_fp` | 🟢 | + | Generate s_v_web_id | `VerifyFpManager` | `gen_s_v_web_id` | 🟢 | + | Generate Live Signature | `DouyinWebcastSignature` | `get_signature` | 🟢 | + | Generate wss Signature Parameters from API Model | `WebcastSignatureManager` | `model_2_endpoint` | 🟢 | + | Generate Xb Parameters from API URL | `XBogusManager` | `str_2_endpoint` | 🟢 | + | Generate Xb Parameters from API Model | `XBogusManager` | `model_2_endpoint` | 🟢 | + | Generate Ab Parameters from API URL | `ABogusManager` | `str_2_endpoint` | 🟢 | + | Generate Ab Parameters from API Model | `ABogusManager` | `model_2_endpoint` | 🟢 | + | Extract Single User ID | `SecUserIdFetcher` | `get_sec_user_id` | 🟢 | + | Extract User IDs from List | `SecUserIdFetcher` | `get_all_sec_user_id` | 🟢 | + | Extract Single Video ID | `AwemeIdFetcher` | `get_aweme_id` | 🟢 | + | Extract Video IDs from List | `AwemeIdFetcher` | `get_all_aweme_id` | 🟢 | + | Extract Single Playlist ID | `MixIdFetcher` | `get_mix_id` | 🟢 | + | Extract Playlist IDs from List | `MixIdFetcher` | `get_all_mix_id` | 🟢 | + | Extract Single Live Room ID | `WebCastIdFetcher` | `get_webcast_id` | 🟢 | + | Extract Live Room IDs from List | `WebCastIdFetcher` | `get_all_webcast_id` | 🟢 |
@@ -139,12 +182,16 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores | Feature | Account Status | Interface | Feature Status | | --- | --- | --- | --- | - | User Information | 🟣⚫ | `handler_user_profile` | 🟢 | + | User Information | 🟣⚫ | `fetch_user_profile` | 🟢 | | Single Work | 🟣⚫ | `fetch_one_video` | 🟢 | | Home Page Works | 🟣⚫ | `fetch_user_post_videos` | 🟢 | | Liked Works | 🟣⚫ | `fetch_user_like_videos` | 🟢 | | Favorite Works | 🟣⚫ | `fetch_user_collect_videos` | 🟢 | + | Playlist | 🟣⚫ | `fetch_play_list` | 🟢 | | Playlist Works | 🟣⚫ | `fetch_user_mix_videos` | 🟢 | + | Post Search|🟣⚫|`fetch_search_videos`|🟢| + | Live Room Information (Stream Download) |⚫|`fetch_user_live_videos`|🟢| + | Check If The webcast Is Alive|🟣⚫|`fetch_check_live_alive`|🟢| | ... | ... | ... | ... |
@@ -198,6 +245,18 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores + ### DouYin Related Videos + + + + ### DouYin Friend Videos + + + + ### DouYin Webcast Danmaku + + https://github.com/Johnserf-Seed/f2/assets/40727745/500d1eaf-59ba-44ba-849b-666c0ddf8469 +
@@ -227,6 +286,9 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores + ### TikTok Post Search + +
@@ -237,17 +299,6 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores ```bash . - ├── .github - │   ├── ISSUE_TEMPLATE - │   │   ├── ask-question.md - │   │   ├── bug-report.md - │   │   └── feature_request.md - │   └── workflows - │   └── Codecov.yml - │   └── deploy.yml - ├── .gitignore - ├── .vscode - │   └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md @@ -256,13 +307,9 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores ├── README.en.md ├── README.md ├── SECURITY.md + ├── babel.cfg + ├── coverage.xml ├── docs - │   ├── .vitepress - │   │   ├── config.mts - │   │   └── theme - │   │   ├── index.ts - │   │   └── styles - │   │   └── vars.css │   ├── advance-guide.md │   ├── cli.md │   ├── en @@ -281,11 +328,14 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   │   ├── apps │   │   │   ├── douyin │   │   │   │   └── index.md - │   │   │   └── tiktok + │   │   │   ├── tiktok + │   │   │   │   └── index.md + │   │   │   ├── weibo + │   │   │   │   └── index.md + │   │   │   └── x │   │   │   └── index.md │   │   └── what-is-f2.md │   ├── index.md - │   ├── install.md │   ├── package-lock.json │   ├── package.json │   ├── public @@ -317,11 +367,17 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   ├── snippets │   │   ├── QA.md │   │   ├── douyin + │   │   │   ├── abogus.py │   │   │   ├── aweme-id.py + │   │   │   ├── aweme-related.py + │   │   │   ├── client-config.py │   │   │   ├── format-file-name.py + │   │   │   ├── json-2-lrc.py + │   │   │   ├── mix-id.py │   │   │   ├── mstoken-false.py │   │   │   ├── mstoken-real.py │   │   │   ├── one-video.py + │   │   │   ├── query-user.py │   │   │   ├── s_v_web_id.py │   │   │   ├── sec-user-id.py │   │   │   ├── show-qrcode.py @@ -330,24 +386,32 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   │   │   ├── ttwid.py │   │   │   ├── user-collection.py │   │   │   ├── user-collects.py + │   │   │   ├── user-feed.py │   │   │   ├── user-folder.py + │   │   │   ├── user-follow-live.py │   │   │   ├── user-follower.py │   │   │   ├── user-following.py + │   │   │   ├── user-friend.py │   │   │   ├── user-get-add.py │   │   │   ├── user-like.py + │   │   │   ├── user-live-im-fetch.py │   │   │   ├── user-live-room-id.py │   │   │   ├── user-live.py │   │   │   ├── user-mix.py - │   │   │   ├── user-nickname.py │   │   │   ├── user-post.py │   │   │   ├── user-profile.py │   │   │   ├── verify_fp.py │   │   │   ├── video-get-add.py │   │   │   ├── webcast-id.py + │   │   │   ├── webcast-signature.py + │   │   │   ├── webid.py │   │   │   └── xbogus.py │   │   ├── set-debug.py │   │   ├── tiktok │   │   │   ├── aweme-id.py + │   │   │   ├── check-live-alive.py + │   │   │   ├── client-config.py + │   │   │   ├── device-id.py │   │   │   ├── format-file-name.py │   │   │   ├── one-video.py │   │   │   ├── sec-uid.py @@ -359,13 +423,16 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   │   │   ├── user-get-add.py │   │   │   ├── user-like.py │   │   │   ├── user-mix.py - │   │   │   ├── user-nickname.py │   │   │   ├── user-playlist.py │   │   │   ├── user-post.py │   │   │   ├── user-profile.py │   │   │   ├── video-get-add.py │   │   │   └── xbogus.py - │   │   └── user-profile.py + │   │   ├── twitter + │   │   └── weibo + │   │   ├── user-profile.py + │   │   └── user-weibo.py + │   └── vite-.zip ├── f2 │   ├── __init__.py │   ├── __main__.py @@ -373,6 +440,9 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   │   ├── __apps__.py │   │   ├── __init__.py │   │   ├── douyin + │   │   │   ├── algorithm + │   │   │   │   ├── webcast_signature.js + │   │   │   │   └── webcast_signature.py │   │   │   ├── api.py │   │   │   ├── cli.py │   │   │   ├── crawler.py @@ -382,15 +452,20 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   │   │   ├── handler.py │   │   │   ├── help.py │   │   │   ├── model.py + │   │   │   ├── proto + │   │   │   │   ├── douyin_webcast.proto + │   │   │   │   └── douyin_webcast_pb2.py │   │   │   ├── test - │   │   │   │   ├── test_apps_model.py - │   │   │   │   ├── test_aweme_id.py - │   │   │   │   ├── test_crawler.py - │   │   │   │   ├── test_handler.py - │   │   │   │   ├── test_lrc.py - │   │   │   │   ├── test_room_id.py - │   │   │   │   ├── test_sec_user_id.py - │   │   │   │   └── test_webcast_id.py + │   │   │   │   ├── test_douyin_apps_model.py + │   │   │   │   ├── test_douyin_aweme_id.py + │   │   │   │   ├── test_douyin_crawler.py + │   │   │   │   ├── test_douyin_handler.py + │   │   │   │   ├── test_douyin_lrc.py + │   │   │   │   ├── test_douyin_room_id.py + │   │   │   │   ├── test_douyin_sec_user_id.py + │   │   │   │   ├── test_douyin_token.py + │   │   │   │   ├── test_douyin_webcast_id.py + │   │   │   │   └── test_douyin_webcast_signature.py │   │   │   └── utils.py │   │   ├── tiktok │   │   │   ├── api.py @@ -402,6 +477,10 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   │   │   ├── handler.py │   │   │   ├── help.py │   │   │   ├── model.py + │   │   │   ├── test + │   │   │   │   ├── test_tiktok_crawler.py + │   │   │   │   ├── test_tiktok_device_id.py + │   │   │   │   └── test_tiktok_token.py │   │   │   └── utils.py │   │   ├── twitter │   │   │   ├── api.py @@ -413,7 +492,27 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   │   │   ├── handler.py │   │   │   ├── help.py │   │   │   ├── model.py + │   │   │   ├── test + │   │   │   │   ├── test_model.py + │   │   │   │   ├── test_tweet_id.py + │   │   │   │   └── ttt.py │   │   │   └── utils.py + │   │   └── weibo + │   │   ├── api.py + │   │   ├── cli.py + │   │   ├── crawler.py + │   │   ├── db.py + │   │   ├── dl.py + │   │   ├── filter.py + │   │   ├── handler.py + │   │   ├── help.py + │   │   ├── model.py + │   │   ├── test + │   │   │   ├── test_gen_visitor.py + │   │   │   ├── test_handler.py + │   │   │   ├── test_weibo_id.py + │   │   │   └── test_weibo_uid.py + │   │   └── utils.py │   ├── cli │   │   ├── __init__.py │   │   ├── cli_commands.py @@ -451,17 +550,19 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   ├── _dl.py │   ├── _signal.py │   ├── _singleton.py + │   ├── abogus.py + │   ├── abogus_async.py + │   ├── abogus_full.py │   ├── conf_manager.py + │   ├── decorators.py │   ├── json_filter.py - │   ├── mode_handler.py │   ├── utils.py │   └── xbogus.py - │   ├── app.yaml - │   ├── conf.yaml - │   └── defaults.yaml + ├── messages.pot ├── package-lock.json ├── package.json ├── pyproject.toml + ├── pytest.ini ├── tests │   ├── test_cli_console.py │   ├── test_desc_limit.py @@ -471,6 +572,7 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores │   ├── test_logger.py │   ├── test_signal.py │   ├── test_singleton.py + │   ├── test_timestamp.py │   ├── test_utils.py │   └── test_xbogus.py @@ -479,6 +581,21 @@ Account status: ⚪ Represents unknown, 🟣 Represents login required (ignores +## 💰 Sponsor + + + +[TikHub](https://tikhub.io/) is a provider of premium data interface services. You can get free credits by signing up daily. You can use my signup invite link: [https://beta-web.tikhub.io/users/signup?referral_code=6hLcGD94](https://beta-web.tikhub.io/users/signup?referral_code=6hLcGD94) or Invitation code: `6hLcGD94` to get `$2` credit by signing up and recharging. + +[TikHub](https://tikhub.io/) offers the following services: + +- Rich data interface +- Sign up daily to get free credits +- High quality API service +- Official website: https://tikhub.io/ +- Project address: https://github.com/TikHubIO/ + + ## 👨‍💻 Contributions If you are interested in extending new applications for `F2`, please refer to the [contribution guidelines](https://github.com/Johnserf-Seed/f2/blob/main/.github/CONTRIBUTING.md). @@ -501,6 +618,9 @@ If you are interested in extending new applications for `F2`, please refer to th - [pydantic](https://github.com/samuelcolvin/pydantic) - [qrcode](https://github.com/lincolnloop/python-qrcode) - [vitepress](https://github.com/vuejs/vitepress) +- [websockets](https://github.com/python-websockets/websockets) +- [protobuf](https://github.com/protocolbuffers/protobuf) +- [PyExecJS](https://github.com/doloopwhile/PyExecJS) Without these libraries and programs, `F2` would not be able to achieve these functionalities. Sincere thanks for their contributions and efforts. diff --git a/README.md b/README.md index 9eaed4aa..4cd06a11 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,10 @@ [![Downloads](https://pepy.tech/badge/f2/month)](https://pepy.tech/project/f2) [![PyPI version](https://badge.fury.io/py/f2.svg)](https://badge.fury.io/py/f2) +[![Dev Branch](https://badgen.net/badge/branch/v0.0.1.6-pw2/blue)](https://github.com/Johnserf-Seed/f2/tree/v0.0.1.6-pw2) +[![Discord](https://img.shields.io/discord/1146473603450282004?label=Discord)](https://discord.gg/3PhtPmgHf8) [![codecov](https://codecov.io/gh/Johnserf-Seed/f2/graph/badge.svg?token=T9DH4QPZSS)](https://codecov.io/gh/Johnserf-Seed/f2) +[![TikHub](https://img.shields.io/badge/%E8%B5%9E%E5%8A%A9%E5%95%86-TikHub-orange?style=flat-square&logo=tiktok)](https://beta-web.tikhub.io/users/signup?referral_code=6hLcGD94) [![APACHE-2.0](https://img.shields.io/github/license/johnserf-seed/f2)](https://github.com/Johnserf-Seed/f2/blob/main/LICENSE) @@ -54,15 +57,30 @@ ## ✨ 新变化 -当升级到`F2`的`0.0.1.5`版本时,请注意以下关键更新。 +当下载或升级到`F2`的不同版本时,请注意以下关键的版本更新。 -- `0.0.1.5`的`XBogus`参数支持了自定义UA,请注意UA规范。 -- 重建的数据库包含接口的原始数据,所以你需要删除旧的数据库文件。如果你想保留记录请注意迁移。 -- 所有的`fetch`方法返回的类型已统一为过滤器类型,所以你需要注意这个变化。 -- 过滤器添加了`_to_raw`方法,可以将过滤器转换为原始接口数据。 -- 文件名模板已经更新,如果你的文件名不符合规范,将会抛出异常。 -- `douyin`合集页链接无法解析的查看[抖音合集作品](#抖音合集作品)。 -- 更多变化查看[ChangeLog](https://github.com/Johnserf-Seed/f2/blob/main/CHANGELOG.md#0015---2024-04-04)。 +
+ 📡 v0.0.1.6-pw2 + + - 配置文件格式已经更新,如果你使用了旧的配置文件,请注意迁移。 + - 所有时间戳的默认时区为(`UTC/GMT+08:00`)。 + - `douyin`直播流文件名调整为`flv`,图集调整回`webp`。 + - `tiktok`视频地址`403`的错误已经修复。[TikTok视频地址403解决办法](https://johnserf-seed.github.io/f2/question-answer/qa.html#tiktok-403-forbidden) + - 现在`douyin`默认会使用`ab`算法来请求。(满血版ab算法待时开源)。 + - 更多变化查看[ChangeLog](https://github.com/Johnserf-Seed/f2/blob/main/CHANGELOG.md#0016---2024-05-04)。 +
+ +
+ 📡 v0.0.1.5-pw2 + + - `0.0.1.5`的`XBogus`参数支持了自定义UA,请注意UA规范。 + - 重建的数据库包含接口的原始数据,所以你需要删除旧的数据库文件。如果你想保留记录请注意迁移。 + - 所有的`fetch`方法返回的类型已统一为过滤器类型,所以你需要注意这个变化。 + - 过滤器添加了`_to_raw`方法,可以将过滤器转换为原始接口数据。 + - 文件名模板已经更新,如果你的文件名不符合规范,将会抛出异常。 + - `douyin`合集页链接无法解析的查看[抖音合集作品](#抖音合集作品)。 + - 更多变化查看[ChangeLog](https://github.com/Johnserf-Seed/f2/blob/main/CHANGELOG.md#0015---2024-04-04)。 +
## 📑 文档 @@ -78,9 +96,9 @@ ## 🗓️ Todo -- 将在`0.0.1.6`版本中添加对`weibo`,`x`的支持。 -- 将在`0.0.1.6`版本中添加更多`douyin`,`tiktok`的接口。 -- 将在`0.0.1.6`版本中修复旧版本已知的问题。 +- 将在`0.0.1.7`版本中添加本地转发功能。 +- 将在`0.0.1.7`版本中添加更多`douyin`,`tiktok`,`weibo`,`x`的接口。 +- 将在`0.0.1.7`版本中修复`x`已知的问题。 ## 🐛 更新 @@ -101,7 +119,7 @@ |功能|账号状态|接口|功能状态| |---|---|---|---| - |用户信息|🟣⚫|`handler_user_profile`|🟢| + |用户信息|🟣⚫|`fetch_user_profile`|🟢| |单个作品(视频、图集、日常)|🟣⚫|`fetch_one_video`|🟢| |主页作品|🟣⚫|`fetch_user_post_videos`|🟢| |点赞作品|🟣⚫|`fetch_user_like_videos`|🟢| @@ -111,16 +129,17 @@ |收藏合集|🟣|`fetch_user_mix_collection`|🔵| |收藏短剧|🟣|`fetch_user_series_collection`|🟤| |合集作品|⚫|`fetch_user_mix_videos`|🟢| - |首页推荐作品|🟣⚫|`fetch_user_feed_videos`|🟡| - |相似推荐作品|⚫|`fetch_related_videos`|🔵| + |首页推荐作品|🟣⚫|`fetch_user_feed_videos`|🟢| + |相似推荐作品|⚫|`fetch_related_videos`|🟢| |直播间信息(流下载)|⚫|`fetch_user_live_videos`、`fetch_user_live_videos_by_room_id`|🟢| - |直播间弹幕|⚫|`fetch_user_live_danmu`|🔵| - |关注用户开播|🟣⚫|`fetch_user_following_lives`|🔵| + |直播间弹幕负载|⚫|`fetch_live_im`|🟢| + |直播间弹幕|⚫|`fetch_user_live_danmu`|🟢| + |关注用户开播|🟣⚫|`fetch_user_following_lives`|🟢| |关注用户信息|🟣⚫|`fetch_user_following`|🟢| |粉丝用户信息|🟣⚫|`fetch_user_follower`|🟢| |关注用户作品|🟣⚫|`fetch_user_following_videos`|🟤| |粉丝用户作品|🟣⚫|`fetch_user_follower_videos`|🟤| - |朋友作品|🟣|`fetch_user_friend_videos`|🔵| + |朋友作品|🟣|`fetch_friend_feed_videos`|🟢| |搜索视频|⚫|`fetch_search_videos`|🔵| |搜索用户|⚫|`fetch_search_users`|🔵| |搜索直播|⚫|`fetch_search_lives`|🔵| @@ -130,6 +149,30 @@ |观看历史|🟣|`fetch_user_history_read`|🟤| |稍后再看|🟣|`fetch_user_watch_later`|🟤| |...|...|...|...| + + |工具类|类名|接口|功能状态| + |---|---|---|---| + | 管理客户端配置 | `ClientConfManager` | | 🟢 | + | 生成真实msToken | `TokenManager` | `gen_real_msToken` | 🟢 | + | 生成虚假msToken | `TokenManager` | `gen_false_msToken` | 🟢 | + | 生成ttwid | `TokenManager` | `gen_ttwid` | 🟢 | + | 生成webid | `TokenManager` | `gen_webid` | 🟢 | + | 生成verify_fp | `VerifyFpManager` | `gen_verify_fp` | 🟢 | + | 生成s_v_web_id | `VerifyFpManager` | `gen_s_v_web_id` | 🟢 | + | 生成直播signature | `DouyinWebcastSignature` | `get_signature` | 🟢 | + | 使用接口模型生成直播wss签名参数 | `WebcastSignatureManager` | `model_2_endpoint` | 🟢 | + | 使用接口地址生成Xb参数 | `XBogusManager` | `str_2_endpoint` | 🟢 | + | 使用接口模型生成Xb参数 | `XBogusManager` | `model_2_endpoint` | 🟢 | + | 使用接口地址生成Ab参数 | `ABogusManager` | `str_2_endpoint` | 🟢 | + | 使用接口模型生成Ab参数 | `ABogusManager` | `model_2_endpoint` | 🟢 | + | 提取单个用户id | `SecUserIdFetcher` | `get_sec_user_id` | 🟢 | + | 提取列表用户id | `SecUserIdFetcher` | `get_all_sec_user_id` | 🟢 | + | 提取单个作品id | `AwemeIdFetcher` | `get_aweme_id` | 🟢 | + | 提取列表作品id | `AwemeIdFetcher` | `get_all_aweme_id` | 🟢 | + | 提取单个合集id | `MixIdFetcher` | `get_mix_id` | 🟢 | + | 提取列表合集id | `MixIdFetcher` | `get_all_mix_id` | 🟢 | + | 提取单个直播间号 | `WebCastIdFetcher` | `get_webcast_id` | 🟢 | + | 提取列表直播间号 | `WebCastIdFetcher` | `get_all_webcast_id` | 🟢 |
@@ -140,12 +183,16 @@ |功能|账号状态|接口|功能状态| |---|---|---|---| - |用户信息|🟣⚫|`handler_user_profile`|🟢| + |用户信息|🟣⚫|`fetch_user_profile`|🟢| |单个作品|🟣⚫|`fetch_one_video`|🟢| |主页作品|🟣⚫|`fetch_user_post_videos`|🟢| |点赞作品|🟣⚫|`fetch_user_like_videos`|🟢| |收藏作品|🟣⚫|`fetch_user_collect_videos`|🟢| + |播放列表|🟣⚫|`fetch_play_list`|🟢| |播放列表作品|🟣⚫|`fetch_user_mix_videos`|🟢| + |作品搜索|🟣⚫|`fetch_search_videos`|🟢| + |直播间信息(流下载)|⚫|`fetch_user_live_videos`|🟢| + |检查开播|🟣⚫|`fetch_check_live_alive`|🟢| |...|...|...|...|
@@ -187,14 +234,23 @@ 合集链接解析 - **ps. 0.0.1.5 relase版本需要拉取这2个提交补丁来修复 [4b81457](https://github.com/Johnserf-Seed/f2/commit/4b81457a66f629eb8e1bf5c79b96445e9f6f0f9e) [eb763eb](https://github.com/Johnserf-Seed/f2/commit/eb763ebe67d9b71e597b95959416c149b7d67d88)** - **ps. 从main分支安装的不需要更新** - ### 抖音直播录制 - + ### 抖音相关推荐 + + + + ### 抖音好友作品 + + + + ### 抖音直播弹幕 + + https://github.com/Johnserf-Seed/f2/assets/40727745/500d1eaf-59ba-44ba-849b-666c0ddf8469 + +
🎬 TikTok @@ -219,8 +275,8 @@ - **ps. 0.0.1.5 relase版本需要拉取这个提交补丁来修复 [05ee1c4](https://github.com/Johnserf-Seed/f2/commit/05ee1c4293d1fb9f01c25739372a2fbac18454cd)** - **ps. 从main分支安装的不需要更新** + ### TikTok作品搜索 +
@@ -232,17 +288,6 @@ ```bash . - ├── .github - │   ├── ISSUE_TEMPLATE - │   │   ├── ask-question.md - │   │   ├── bug-report.md - │   │   └── feature_request.md - │   └── workflows - │   └── Codecov.yml - │   └── deploy.yml - ├── .gitignore - ├── .vscode - │   └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md @@ -251,13 +296,9 @@ ├── README.en.md ├── README.md ├── SECURITY.md + ├── babel.cfg + ├── coverage.xml ├── docs - │   ├── .vitepress - │   │   ├── config.mts - │   │   └── theme - │   │   ├── index.ts - │   │   └── styles - │   │   └── vars.css │   ├── advance-guide.md │   ├── cli.md │   ├── en @@ -276,11 +317,14 @@ │   │   ├── apps │   │   │   ├── douyin │   │   │   │   └── index.md - │   │   │   └── tiktok + │   │   │   ├── tiktok + │   │   │   │   └── index.md + │   │   │   ├── weibo + │   │   │   │   └── index.md + │   │   │   └── x │   │   │   └── index.md │   │   └── what-is-f2.md │   ├── index.md - │   ├── install.md │   ├── package-lock.json │   ├── package.json │   ├── public @@ -312,11 +356,17 @@ │   ├── snippets │   │   ├── QA.md │   │   ├── douyin + │   │   │   ├── abogus.py │   │   │   ├── aweme-id.py + │   │   │   ├── aweme-related.py + │   │   │   ├── client-config.py │   │   │   ├── format-file-name.py + │   │   │   ├── json-2-lrc.py + │   │   │   ├── mix-id.py │   │   │   ├── mstoken-false.py │   │   │   ├── mstoken-real.py │   │   │   ├── one-video.py + │   │   │   ├── query-user.py │   │   │   ├── s_v_web_id.py │   │   │   ├── sec-user-id.py │   │   │   ├── show-qrcode.py @@ -325,24 +375,32 @@ │   │   │   ├── ttwid.py │   │   │   ├── user-collection.py │   │   │   ├── user-collects.py + │   │   │   ├── user-feed.py │   │   │   ├── user-folder.py + │   │   │   ├── user-follow-live.py │   │   │   ├── user-follower.py │   │   │   ├── user-following.py + │   │   │   ├── user-friend.py │   │   │   ├── user-get-add.py │   │   │   ├── user-like.py + │   │   │   ├── user-live-im-fetch.py │   │   │   ├── user-live-room-id.py │   │   │   ├── user-live.py │   │   │   ├── user-mix.py - │   │   │   ├── user-nickname.py │   │   │   ├── user-post.py │   │   │   ├── user-profile.py │   │   │   ├── verify_fp.py │   │   │   ├── video-get-add.py │   │   │   ├── webcast-id.py + │   │   │   ├── webcast-signature.py + │   │   │   ├── webid.py │   │   │   └── xbogus.py │   │   ├── set-debug.py │   │   ├── tiktok │   │   │   ├── aweme-id.py + │   │   │   ├── check-live-alive.py + │   │   │   ├── client-config.py + │   │   │   ├── device-id.py │   │   │   ├── format-file-name.py │   │   │   ├── one-video.py │   │   │   ├── sec-uid.py @@ -354,13 +412,16 @@ │   │   │   ├── user-get-add.py │   │   │   ├── user-like.py │   │   │   ├── user-mix.py - │   │   │   ├── user-nickname.py │   │   │   ├── user-playlist.py │   │   │   ├── user-post.py │   │   │   ├── user-profile.py │   │   │   ├── video-get-add.py │   │   │   └── xbogus.py - │   │   └── user-profile.py + │   │   ├── twitter + │   │   └── weibo + │   │   ├── user-profile.py + │   │   └── user-weibo.py + │   └── vite-.zip ├── f2 │   ├── __init__.py │   ├── __main__.py @@ -368,6 +429,9 @@ │   │   ├── __apps__.py │   │   ├── __init__.py │   │   ├── douyin + │   │   │   ├── algorithm + │   │   │   │   ├── webcast_signature.js + │   │   │   │   └── webcast_signature.py │   │   │   ├── api.py │   │   │   ├── cli.py │   │   │   ├── crawler.py @@ -377,15 +441,20 @@ │   │   │   ├── handler.py │   │   │   ├── help.py │   │   │   ├── model.py + │   │   │   ├── proto + │   │   │   │   ├── douyin_webcast.proto + │   │   │   │   └── douyin_webcast_pb2.py │   │   │   ├── test - │   │   │   │   ├── test_apps_model.py - │   │   │   │   ├── test_aweme_id.py - │   │   │   │   ├── test_crawler.py - │   │   │   │   ├── test_handler.py - │   │   │   │   ├── test_lrc.py - │   │   │   │   ├── test_room_id.py - │   │   │   │   ├── test_sec_user_id.py - │   │   │   │   └── test_webcast_id.py + │   │   │   │   ├── test_douyin_apps_model.py + │   │   │   │   ├── test_douyin_aweme_id.py + │   │   │   │   ├── test_douyin_crawler.py + │   │   │   │   ├── test_douyin_handler.py + │   │   │   │   ├── test_douyin_lrc.py + │   │   │   │   ├── test_douyin_room_id.py + │   │   │   │   ├── test_douyin_sec_user_id.py + │   │   │   │   ├── test_douyin_token.py + │   │   │   │   ├── test_douyin_webcast_id.py + │   │   │   │   └── test_douyin_webcast_signature.py │   │   │   └── utils.py │   │   ├── tiktok │   │   │   ├── api.py @@ -397,6 +466,10 @@ │   │   │   ├── handler.py │   │   │   ├── help.py │   │   │   ├── model.py + │   │   │   ├── test + │   │   │   │   ├── test_tiktok_crawler.py + │   │   │   │   ├── test_tiktok_device_id.py + │   │   │   │   └── test_tiktok_token.py │   │   │   └── utils.py │   │   ├── twitter │   │   │   ├── api.py @@ -408,7 +481,27 @@ │   │   │   ├── handler.py │   │   │   ├── help.py │   │   │   ├── model.py + │   │   │   ├── test + │   │   │   │   ├── test_model.py + │   │   │   │   ├── test_tweet_id.py + │   │   │   │   └── ttt.py │   │   │   └── utils.py + │   │   └── weibo + │   │   ├── api.py + │   │   ├── cli.py + │   │   ├── crawler.py + │   │   ├── db.py + │   │   ├── dl.py + │   │   ├── filter.py + │   │   ├── handler.py + │   │   ├── help.py + │   │   ├── model.py + │   │   ├── test + │   │   │   ├── test_gen_visitor.py + │   │   │   ├── test_handler.py + │   │   │   ├── test_weibo_id.py + │   │   │   └── test_weibo_uid.py + │   │   └── utils.py │   ├── cli │   │   ├── __init__.py │   │   ├── cli_commands.py @@ -446,17 +539,19 @@ │   ├── _dl.py │   ├── _signal.py │   ├── _singleton.py + │   ├── abogus.py + │   ├── abogus_async.py + │   ├── abogus_full.py │   ├── conf_manager.py + │   ├── decorators.py │   ├── json_filter.py - │   ├── mode_handler.py │   ├── utils.py │   └── xbogus.py - │   ├── app.yaml - │   ├── conf.yaml - │   └── defaults.yaml + ├── messages.pot ├── package-lock.json ├── package.json ├── pyproject.toml + ├── pytest.ini ├── tests │   ├── test_cli_console.py │   ├── test_desc_limit.py @@ -466,6 +561,7 @@ │   ├── test_logger.py │   ├── test_signal.py │   ├── test_singleton.py + │   ├── test_timestamp.py │   ├── test_utils.py │   └── test_xbogus.py @@ -474,6 +570,21 @@ +## 💰 赞助商 + + + +[TikHub](https://tikhub.io/) 是一家提供优质数据接口服务的供应商。通过每日签到,可以获取免费额度。可以使用我的注册邀请链接:[https://beta-web.tikhub.io/users/signup?referral_code=6hLcGD94](https://beta-web.tikhub.io/users/signup?referral_code=6hLcGD94) 或 邀请码:`6hLcGD94`,注册并充值即可获得`$2`额度。 + +[TikHub](https://tikhub.io/) 提供以下服务: + +- 丰富的数据接口 +- 每日签到免费获取额度 +- 高质量的API服务 +- 官网:https://tikhub.io/ +- 项目地址:https://github.com/TikHubIO/ + + ## 👨‍💻 贡献 如果你有兴趣为`F2`做拓展新应用,请查看[贡献指南](https://github.com/Johnserf-Seed/f2/blob/main/.github/CONTRIBUTING.md)。 @@ -496,6 +607,9 @@ - [pydantic](https://github.com/samuelcolvin/pydantic) - [qrcode](https://github.com/lincolnloop/python-qrcode) - [vitepress](https://github.com/vuejs/vitepress) +- [websockets](https://github.com/python-websockets/websockets) +- [protobuf](https://github.com/protocolbuffers/protobuf) +- [PyExecJS](https://github.com/doloopwhile/PyExecJS) 没有这些库和程序,`F2`将无法实现这些功能,对于他们的贡献和努力,表示由衷的感谢。 diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 4fa7db76..c37464a1 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -6,7 +6,7 @@ const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') -const version = "v0.0.1.5-pw.2" +const version = "v0.0.1.6-pw.2" // https://vitepress.dev/reference/site-config export default defineConfig({ @@ -78,9 +78,20 @@ export default defineConfig({ text: '开发者接口', items: [ {text: 'DouYin', link: '/guide/apps/douyin/index'}, - {text: 'TikTok', link: '/guide/apps/tiktok/index'} + {text: 'TikTok', link: '/guide/apps/tiktok/index'}, + {text: 'X', link: '/guide/apps/x/index'}, + {text: 'WeiBo', link: '/guide/apps/weibo/index'}, ] }, + { + text: '命令行指引', + items: [ + {text: 'DouyYin', link: '/guide/apps/douyin/cli'}, + {text: 'TikTok', link: '/guide/apps/tiktok/cli'}, + {text: 'X', link: '/guide/apps/x/cli'}, + {text: 'WeiBo', link: '/guide/apps/weibo/cli'}, + ] + } ], '/question-answer/': [ diff --git a/docs/cli.md b/docs/cli.md index 1f03d1f4..65902452 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -103,4 +103,22 @@ F2 会智能的识别出混乱文本中的链接,同时也支持长短链的 $ f2 dy -M one -u '7.64 gOX:/ w@f.oD 05/14 世界这本书 又多读了一页。冰岛????旅行记# 冰岛 https://v.douyin.com/iR2syBRn/ 复制此链接,打开Dou音搜索,直接观看视! ' -c conf/app.yaml ``` -::: \ No newline at end of file +::: + +## 应用命令行 + +### DouYin + +- [CLI 参考](/guide/apps/douyin/cli) + +### TikTok + +- [CLI 参考](/guide/apps/tiktok/cli) + +### Twitter + +- [CLI 参考](/guide/apps/twitter/cli) + +### WeiBo + +- [CLI 参考](/guide/apps/weibo/cli) \ No newline at end of file diff --git a/docs/en/quick-start.md b/docs/en/quick-start.md index f33736d7..9be0a91c 100644 --- a/docs/en/quick-start.md +++ b/docs/en/quick-start.md @@ -47,7 +47,7 @@ The default configuration file (./conf/app.yaml) is a yaml file with a basic str ```yaml douyin: headers: - User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0 Referer: https://www.douyin.com/ cookie: diff --git a/docs/guide/apps/douyin/cli.md b/docs/guide/apps/douyin/cli.md new file mode 100644 index 00000000..f9b800ae --- /dev/null +++ b/docs/guide/apps/douyin/cli.md @@ -0,0 +1,91 @@ +## CLI 帮助 - 抖音 + +### 参数列表 + +| 短参数 | 长参数 | 类型 | 说明 | +| ------ | ------ | ---- | ---- | +| `-c` | `--config` | `TEXT` | 配置文件的路径,最低优先 | +| `-u` | `--url` | `TEXT` | 根据模式提供相应的链接 | +| `-m` | `--music` | `BOOLEAN` | 是否保存视频原声 | +| `-v` | `--cover` | `BOOLEAN` | 是否保存视频封面 | +| `-d` | `--desc` | `BOOLEAN` | 是否保存视频文案 | +| `-p` | `--path` | `TEXT` | 作品保存位置 | +| `-f` | `--folderize` | `BOOLEAN` | 是否将作品保存到单独的文件夹 | +| `-M` | `--mode` | `ENUM` | 下载模式 | +| `-n` | `--naming` | `TEXT` | 全局作品文件命名方式 | +| `-k` | `--cookie` | `TEXT` | 登录后的cookie | +| `-i` | `--interval` | `TEXT` | 下载日期区间 | +| `-e` | `--timeout` | `INTEGER` | 网络请求超时时间 | +| `-r` | `--max_retries` | `INTEGER` | 网络请求超时重试数 | +| `-x` | `--max-connections` | `INTEGER` | 网络请求并发连接数 | +| `-t` | `--max-tasks` | `INTEGER` | 异步的任务数 | +| `-o` | `--max-counts` | `INTEGER` | 最大作品下载数 | +| `-s` | `--page-counts` | `INTEGER` | 每页获取作品数 | +| `-l` | `--languages` | `ENUM` | 显示语言 | +| `-P` | `--proxies` | `TEXT...` | 代理服务器 | +| `-L` | `--lyric` | `BOOLEAN` | 是否保存原声歌词 | +| | `--update-config` | `BOOLEAN` | 更新配置文件 | +| | `--init-config` | `TEXT` | 初始化配置文件 | +| | `--auto-cookie` | `ENUM` | 自动获取cookie | +| `-h` | | `FLAG` | 显示富文本帮助 | +| | `--help` | `FLAG` | 显示帮助信息并退出 | + +### 详细说明 + +#### `--config` + +配置文件的路径,最低优先。默认配置文件路径为 `f2/conf/app.yaml`。支持绝对路径与相对路径。 + +#### `--url` + +根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合集与直播同上。 + +#### `--mode` + +下载模式: +- `one`:单个作品 +- `post`:主页作品 +- `like`:点赞作品 +- `collection`:收藏作品 +- `collects`:收藏夹作品 +- `music`:收藏音乐 +- `mix`:合集 +- `live`:直播 + +#### `--naming` + +全局作品文件命名方式。默认为 `{create}_{desc}`,支持的变量有:`{nickname}`,`{create}`,`{aweme_id}`,`{desc}`,`{uid}`。支持的分割符有:`_`,`-`。 + +- `{nickname}`:作者昵称 +- `{create}`:作品创建时间 +- `{aweme_id}`:作品ID +- `{desc}`:作品文案 +- `{uid}`:作者ID + +#### `--interval` + +下载日期区间发布的作品,格式:`年-月-日` 如:`2022-01-01|2023-01-01`,设置`all` 为下载所有作品。 + +#### `--languages` + +显示语言。默认为 `zh_CN`,可选:`zh_CN`、`en_US`,不支持配置文件修改。 + +#### `--proxies` + +代理服务器,最多 2 个参数,`http://`与`https://`。空格区分 2 个参数,例如:`http://x.x.x.x https://x.x.x.x`。如果你的代理不支持`出口HTTPS`,那么请使用`http://x.x.x.x http://x.x.x.x`。 + +#### `--auto-cookie` + +自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器。支持的浏览器有: +- `chrome` +- `firefox` +- `edge` +- `opera` +- `opera_gx` +- `safari` +- `chromium` +- `brave` +- `vivaldi` +- `librewolf` + +不支持切换浏览器用户配置。 diff --git a/docs/guide/apps/douyin/index.md b/docs/guide/apps/douyin/index.md index c95294e9..3c93c076 100644 --- a/docs/guide/apps/douyin/index.md +++ b/docs/guide/apps/douyin/index.md @@ -12,83 +12,121 @@ outline: deep | CLI接口 | 方法 | | :------------------ | :------------------- | -| 下载单个作品 | handle_one_video | -| 下载用户发布作品 | handle_user_post | -| 下载用户喜欢作品 | handle_user_like | -| 下载用户收藏作品 | handle_user_collection | -| 下载用户合辑作品 | handle_user_mix | -| 下载用户直播流 | handle_user_live | -| 下载用户首页推荐作品 | handle_user_feed | - -| 数据与功能接口 | 方法 | 开发者接口 | -| :------------------ | :------------------- | :--------: | -| 单个作品数据 | fetch_one_video | 🟢 | -| 用户发布作品数据 | fetch_user_post_videos | 🟢 | -| 用户喜欢作品数据 | fetch_user_like_videos | 🟢 | -| 用户收藏作品数据 | fetch_user_collection_videos | 🟢 | -| 用户合辑作品数据 | fetch_user_mix_videos | 🟢 | -| 用户直播流数据 | fetch_user_live_videos | 🟢 | -| 用户直播流数据2 | fetch_user_live_videos_by_room_id | 🟢 | -| 用户首页推荐作品数据 | fetch_user_feed_videos | 🟢 | -| ...... | ...... | 🔵 | -| 用户信息 | handler_user_profile | 🟢 | -| 获取指定用户名 | get_user_nickname | 🔴 | -| 创建用户记录与目录 | get_or_add_user_data | 🟡 | -| 创建作品下载记录 | get_or_add_video_data | 🟢 | -| SSO登录 | handle_sso_login | 🟢 | +| 下载单个作品 | `handle_one_video` | +| 下载用户发布作品 | `handle_user_post` | +| 下载用户喜欢作品 | `handle_user_like` | +| 下载用户收藏原声 | `handle_user_music_collection` | +| 下载用户收藏作品 | `handle_user_collection` | +| 下载用户合集作品 | `handle_user_mix` | +| 下载用户直播流 | `handle_user_live` | +| 下载用户首页推荐作品 | `handle_user_feed` | +| 下载相似作品 | `handle_related` | +| 下载好友作品 | `handle_friend_feed` | + +| 数据方法接口 | 方法 | 开发者接口 | +| :------------------ | :------------------- | :--------: | +| 创建用户记录与目录 | `get_or_add_user_data` | 🟢 | +| 创建作品下载记录 | `get_or_add_video_data` | 🟢 | +| 获取用户信息 | `fetch_user_profile` | 🟢 | +| 单个作品数据 | `fetch_one_video` | 🟢 | +| 用户发布作品数据 | `fetch_user_post_videos` | 🟢 | +| 用户喜欢作品数据 | `fetch_user_like_videos` | 🟢 | +| 用户收藏原声数据 | `fetch_user_music_collection` | 🟢 | +| 用户收藏作品数据 | `fetch_user_collection_videos` | 🟢 | +| 用户收藏夹数据 | `fetch_user_collects` | 🟢 | +| 用户收藏夹作品数据 | `fetch_user_collects_videos` | 🟢 | +| 用户合集作品数据 | `fetch_user_mix_videos` | 🟢 | +| 用户直播流数据 | `fetch_user_live_videos` | 🟢 | +| 用户直播流数据2 | `fetch_user_live_videos_by_room_id` | 🟢 | +| 用户首页推荐作品数据 | `fetch_user_feed_videos` | 🟢 | +| 相似作品数据 | `fetch_related_videos` | 🟢 | +| 好友作品数据 | `fetch_friend_feed_videos` | 🟢 | +| 关注用户数据 | `fetch_user_following` | 🟢 | +| 粉丝用户数据 | `fetch_user_follower` | 🟢 | +| 查询用户数据 | `fetch_query_user` | 🟢 | +| 直播间wss负载数据 | `fetch_live_im` | 🟢 | +| 直播间wss弹幕 | `fetch_live_danmaku` | 🟢 | +| 关注用户的直播间信息 | `fetch_user_following_lives` | 🟢 | ::: ::: details utils接口列表 -| 开发者接口 | 类名 | 方法 | 状态 | +| 工具类接口 | 类名 | 方法 | 状态 | | :---------------- | :-------------- | :------------------ | :--: | -| 生成真实msToken | TokenManager | gen_real_msToken | 🟢 | -| 生成虚假msToken | TokenManager | gen_false_msToken | 🟢 | -| 生成ttwid | TokenManager | gen_ttwid | 🟢 | -| 生成verify_fp | VerifyFpManager | gen_verify_fp | 🟢 | -| 生成s_v_web_id | VerifyFpManager | gen_s_v_web_id | 🟢 | -| 使用接口地址生成Xb参数 | XBogusManager | str_2_endpoint | 🟢 | -| 使用接口模型生成Xb参数 | XBogusManager | model_2_endpoint | 🟢 | -| 提取单个用户id | SecUserIdFetcher | get_sec_user_id | 🟢 | -| 提取列表用户id | SecUserIdFetcher | get_all_sec_user_id | 🟢 | -| 提取单个作品id | AwemeIdFetcher | get_aweme_id | 🟢 | -| 提取列表作品id | AwemeIdFetcher | get_all_aweme_id | 🟢 | -| 提取合辑id | MixIdFetcher | - | 🟤 | -| 提取单个直播间号 | WebCastIdFetcher | get_webcast_id | 🟢 | -| 提取列表直播间号 | WebCastIdFetcher | get_all_webcast_id | 🟢 | -| 获取请求count数列表 | - | get_request_sizes | 🔴 | -| 全局格式化文件名 | - | format_file_name | 🟢 | -| 创建用户目录 | - | create_user_folder | 🟢 | -| 重命名用户目录 | - | rename_user_folder | 🟢 | -| 创建或重命名用户目录 | - | create_or_rename_user_folder | 🟢 | -| 提取低版本接口的desc | - | extract_desc_from_share_desc | 🔴 | -| 显示二维码 | - | show_qrcode | 🟢 | -::: tip 注意 -合辑id其实就是作品id,使用`AwemeIdFetcher`即可。 +| 管理客户端配置 | `ClientConfManager` | | 🟢 | +| 生成真实msToken | `TokenManager` | `gen_real_msToken` | 🟢 | +| 生成虚假msToken | `TokenManager` | `gen_false_msToken` | 🟢 | +| 生成ttwid | `TokenManager` | `gen_ttwid` | 🟢 | +| 生成webid | `TokenManager` | `gen_webid` | 🟢 | +| 生成verify_fp | `VerifyFpManager` | `gen_verify_fp` | 🟢 | +| 生成s_v_web_id | `VerifyFpManager` | `gen_s_v_web_id` | 🟢 | +| 生成直播signature | `DouyinWebcastSignature` | `get_signature` | 🟢 | +| 使用接口模型生成直播wss签名参数 | `WebcastSignatureManager` | `model_2_endpoint` | 🟢 | +| 使用接口地址生成Xb参数 | `XBogusManager` | `str_2_endpoint` | 🟢 | +| 使用接口模型生成Xb参数 | `XBogusManager` | `model_2_endpoint` | 🟢 | +| 使用接口地址生成Ab参数 | `ABogusManager` | `str_2_endpoint` | 🟢 | +| 使用接口模型生成Ab参数 | `ABogusManager` | `model_2_endpoint` | 🟢 | +| 提取单个用户id | `SecUserIdFetcher` | `get_sec_user_id` | 🟢 | +| 提取列表用户id | `SecUserIdFetcher` | `get_all_sec_user_id` | 🟢 | +| 提取单个作品id | `AwemeIdFetcher` | `get_aweme_id` | 🟢 | +| 提取列表作品id | `AwemeIdFetcher` | `get_all_aweme_id` | 🟢 | +| 提取单个合集id | `MixIdFetcher` | `get_mix_id` | 🟢 | +| 提取列表合集id | `MixIdFetcher` | `get_all_mix_id` | 🟢 | +| 提取单个直播间号 | `WebCastIdFetcher` | `get_webcast_id` | 🟢 | +| 提取列表直播间号 | `WebCastIdFetcher` | `get_all_webcast_id` | 🟢 | +| 全局格式化文件名 | - | `format_file_name` | 🟢 | +| 创建用户目录 | - | `create_user_folder` | 🟢 | +| 重命名用户目录 | - | `rename_user_folder` | 🟢 | +| 创建或重命名用户目录 | - | `create_or_rename_user_folder` | 🟢 | +| 显示二维码 | - | `show_qrcode` | 🟢 | +| json歌词转lrc歌词 | - | `json_2_lrc` | 🟢 | ::: ::: details cralwer接口列表 | 爬虫url接口 | 类名 | 方法 | 状态 | | :----------- | :--------- | :---------- | :--: | -| 用户信息接口地址 | DouyinCrawler | fetch_user_profile | 🟢 | -| 主页作品接口地址 | DouyinCrawler | fetch_user_post | 🟢 | -| 喜欢作品接口地址 | DouyinCrawler | fetch_user_like | 🟢 | -| 收藏作品接口地址 | DouyinCrawler | fetch_user_collection | 🟢 | -| 合辑作品接口地址 | DouyinCrawler | fetch_user_mix | 🟢 | -| 作品详情接口地址 | DouyinCrawler | fetch_post_detail | 🟢 | -| 作品评论接口地址 | DouyinCrawler | fetch_post_comment | 🟡 | -| 推荐作品接口地址 | DouyinCrawler | fetch_post_feed | 🟡 | -| 关注作品接口地址 | DouyinCrawler | fetch_follow_feed | 🟡 | -| 朋友作品接口地址 | DouyinCrawler | fetch_friend_feed | 🟡 | -| 相关推荐作品接口地址 | DouyinCrawler | fetch_post_related | 🟡 | -| 直播接口地址 | DouyinCrawler | fetch_live | 🟢 | -| 直播接口地址(room_id) | DouyinCrawler | fetch_live_room_id | 🟢 | -| 关注用户直播接口地址 | DouyinCrawler | fetch_follow_live | 🟡 | -| 定位上一次作品接口地址 | DouyinCrawler | fetch_locate_post | 🟡 | -| SSO获取二维码接口地址 | DouyinCrawler | fetch_login_qrcode | 🟢 | -| SSO检查扫码状态接口地址 | DouyinCrawler | fetch_check_qrcode | 🟢 | -| SSO检查登录状态接口地址 | DouyinCrawler | fetch_check_login | 🟡 | +| 用户信息接口地址 | `DouyinCrawler` | `fetch_user_profile` | 🟢 | +| 主页作品接口地址 | `DouyinCrawler` | `fetch_user_post` | 🟢 | +| 喜欢作品接口地址 | `DouyinCrawler` | `fetch_user_like` | 🟢 | +| 收藏作品接口地址 | `DouyinCrawler` | `fetch_user_collection` | 🟢 | +| 收藏夹接口地址 | `DouyinCrawler` | `fetch_user_collects` | 🟢 | +| 收藏夹作品接口地址 | `DouyinCrawler` | `fetch_user_collects_video` | 🟢 | +| 音乐收藏接口地址 | `DouyinCrawler` | `fetch_user_music_collection` | 🟢 | +| 合集作品接口地址 | `DouyinCrawler` | `fetch_user_mix` | 🟢 | +| 作品详情接口地址 | `DouyinCrawler` | `fetch_post_detail` | 🟢 | +| 作品评论接口地址 | `DouyinCrawler` | `fetch_post_comment` | 🟢 | +| 推荐作品接口地址 | `DouyinCrawler` | `fetch_post_feed` | 🟢 | +| 关注作品接口地址 | `DouyinCrawler` | `fetch_follow_feed` | 🟢 | +| 朋友作品接口地址 | `DouyinCrawler` | `fetch_friend_feed` | 🟢 | +| 相关推荐作品接口地址 | `DouyinCrawler` | `fetch_post_related` | 🟢 | +| 直播接口地址 | `DouyinCrawler` | `fetch_live` | 🟢 | +| 直播接口地址(room_id) | `DouyinCrawler` | `fetch_live_room_id` | 🟢 | +| 关注用户直播接口地址 | `DouyinCrawler` | `fetch_following_live` | 🟢 | +| 定位上一次作品接口地址 | `DouyinCrawler` | `fetch_locate_post` | 🟢 | +| SSO获取二维码接口地址 | `DouyinCrawler` | `fetch_login_qrcode` | 🔴 | +| SSO检查扫码状态接口地址 | `DouyinCrawler` | `fetch_check_qrcode` | 🔴 | +| SSO检查登录状态接口地址 | `DouyinCrawler` | `fetch_check_login` | 🔴 | +| 用户关注列表接口地址 | `DouyinCrawler` | `fetch_user_following` | 🟢 | +| 用户粉丝列表接口地址 | `DouyinCrawler` | `fetch_user_follower` | 🟢 | +| 直播弹幕初始化接口地址 | `DouyinCrawler` | `fetch_live_im_fetch` | 🟢 | +| 查询用户接口地址 | `DouyinCrawler` | `fetch_query_user` | 🟢 | +| 直播弹幕接口地址 | `DouyinWebSocketCrawler` | `fetch_live_danmaku` | 🟢 | +| 处理 WebSocket 消息 | `DouyinWebSocketCrawler` | `handle_wss_message` | 🟢 | +| 发送 ack 包 | `DouyinWebSocketCrawler` | `send_ack` | 🟢 | +| 发送 ping 包 | `DouyinWebSocketCrawler` | `send_ping` | 🟢 | +| 直播间房间消息 | `DouyinWebSocketCrawler` | `WebcastRoomMessage` | 🟢 | +| 直播间点赞消息 | `DouyinWebSocketCrawler` | `WebcastLikeMessage` | 🟢 | +| 直播间观众加入消息 | `DouyinWebSocketCrawler` | `WebcastMemberMessage` | 🟢 | +| 直播间聊天消息 | `DouyinWebSocketCrawler` | `WebcastLeaveMessage` | 🟢 | +| 直播间礼物消息 | `DouyinWebSocketCrawler` | `WebcastGiftMessage` | 🟢 | +| 直播间用户关注消息 | `DouyinWebSocketCrawler` | `WebcastSocialMessage` | 🟢 | +| 直播间用户关注消息 | `DouyinWebSocketCrawler` | `WebcastFollowMessage` | 🟢 | +| 直播间在线观众排行榜 | `DouyinWebSocketCrawler` | `WebcastRoomUserSeqMessage` | 🟢 | +| 直播间粉丝团更新消息 | `DouyinWebSocketCrawler` | `WebcastUpdateFanTicketMessage` | 🟢 | +| 直播间文本消息 | `DouyinWebSocketCrawler` | `WebcastCommonTextMessage` | 🟢 | +| 直播间对战积分消息 | `DouyinWebSocketCrawler` | `WebcastMatchAgainstScoreMessage` | 🟢 | +| 直播间粉丝团消息 | `DouyinWebSocketCrawler` | `WebcastFansclubMessage` | 🟢 | ::: @@ -96,15 +134,68 @@ outline: deep | 下载器接口 | 类名 | 方法 | 状态 | | :----------- | :--------- | :---------- | :--: | -| 保存最后一次请求的aweme_id | DouyinDownloader | save_last_aweme_id | 🟢 | -| 创建下载任务 | DouyinDownloader | create_download_task | 🟢 | -| 处理下载任务 | DouyinDownloader | handle_download | 🟢 | -| 创建流下载任务 | DouyinDownloader | create_stream_tasks | 🟢 | -| 处理流下载任务 | DouyinDownloader | handle_stream | 🟢 | +| 保存最后一次请求的aweme_id | `DouyinDownloader` | `save_last_aweme_id` | 🟢 | +| 筛选指定日期区间内的作品 | `DouyinDownloader` | `filter_aweme_datas_by_interval` | 🟢 | +| 创建下载任务 | `DouyinDownloader` | `create_download_task` | 🟢 | +| 处理下载任务 | `DouyinDownloader` | `handler_download` | 🟢 | +| 创建原声下载任务 | `DouyinDownloader` | `create_music_download_tasks` | 🟢 | +| 处理原声下载任务 | `DouyinDownloader` | `handler_music_download` | 🟢 | +| 创建流下载任务 | `DouyinDownloader` | `create_stream_tasks` | 🟢 | +| 处理流下载任务 | `DouyinDownloader` | `handle_stream` | 🟢 | ::: ## handler接口列表 +### 创建用户记录与目录 🟢 + +异步方法,用于获取或创建用户数据同时创建用户目录。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| kwargs | dict | cli字典数据,需获取path参数 | +| sec_user_id| str | 用户ID | +| db | AsyncUserDB | 用户数据库 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| user_path | Path | 用户目录路径对象 | + +<<< @/snippets/douyin/user-get-add.py{18,20-22} + +::: tip 提示 +此为cli模式的接口,开发者可自行定义创建用户目录的功能。 +::: + +### 创建作品下载记录 🟢 + +异步方法,用于获取或创建作品数据同时创建作品目录。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| aweme_data | dict | 作品数据字典 | +| db | AsyncVideoDB | 作品数据库 | +| ignore_fields | list | 忽略的字段列表 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| 无 | 无 | 无 | + +<<< @/snippets/douyin/video-get-add.py{6,19-25} + +### 获取用户信息 🟢 + +异步方法,用于获取指定用户的信息。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| sec_user_id| str | 用户ID | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| UserProfileFilter | model | 用户数据过滤器,包含用户数据的_to_raw、_to_dict方法 | + +<<< @/snippets/douyin/user-profile.py{15,16} + ### 单个作品数据 🟢 异步方法,用于获取单个视频。 @@ -115,9 +206,9 @@ outline: deep | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -| video_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称等 | +| PostDetailFilter | model | 单个作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 | -<<< @/snippets/douyin/one-video.py{15,17} +<<< @/snippets/douyin/one-video.py{15} ### 用户发布作品数据 🟢 @@ -132,9 +223,9 @@ outline: deep | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -| video_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称、页码等 | +| UserPostFilter | AsyncGenerator | 发布作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 | -<<< @/snippets/douyin/user-post.py{15,17-20,25-28} +<<< @/snippets/douyin/user-post.py{16-20} ### 用户喜欢作品数据 🟢 @@ -149,9 +240,25 @@ outline: deep | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -| aweme_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称、页码等 | +| UserPostFilter | AsyncGenerator | 喜欢作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 | + +<<< @/snippets/douyin/user-like.py{16-20} + +### 用户收藏原声数据 🟢 + +异步方法,用于获取指定用户收藏的音乐列表,只能获取登录了账号的收藏音乐。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| max_cursor| int | 页码,初始为0 | +| page_counts| int | 页数,初始为20 | +| max_counts| int | 最大页数,初始为None | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| UserMusicCollectionFilter | AsyncGenerator | 收藏音乐数据过滤器,包含音乐数据的_to_raw、_to_dict、_to_list方法 | -<<< @/snippets/douyin/user-like.py{15,17-20,25-28} +<<< @/snippets/douyin/user-collection.py#user-collection-music-snippet{17} ### 用户收藏作品数据 🟢 @@ -165,13 +272,46 @@ outline: deep | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -| aweme_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称、页码等 | +| UserCollectionFilter | AsyncGenerator | 收藏作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 | + +<<< @/snippets/douyin/user-collection.py#user-collection-music-snippet{16} + +### 用户收藏夹数据 🟢 + +异步方法,用于获取指定用户的收藏夹列表,不是收藏夹作品数据。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| max_cursor| int | 页码,初始为0 | +| page_counts| int | 页数,初始为20 | +| max_counts| int | 最大页数,初始为None | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| UserCollectsFilter | AsyncGenerator | 收藏夹数据过滤器,包含收藏夹数据的_to_raw、_to_dict、_to_list方法 | + +<<< @/snippets/douyin/user-collects.py#user-collects-snippet{17} + +### 用户收藏夹作品数据 🟢 + +异步方法,用于获取指定用户收藏夹的视频列表,收藏夹作品数据。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| collect_id| str | 收藏夹ID | +| max_cursor| int | 页码,初始为0 | +| page_counts| int | 页数,初始为20 | +| max_counts| int | 最大页数,初始为None | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| UserCollectsVideosFilter | AsyncGenerator | 收藏夹作品数据过滤器,包含收藏夹作品数据的_to_raw、_to_dict、_to_list方法 | -<<< @/snippets/douyin/user-collection.py{16-17,22-25} +<<< @/snippets/douyin/user-collects.py#user-collects-videos-snippet{17-20} -### 用户合辑作品数据 🟢 +### 用户合集作品数据 🟢 -异步方法,用于获取指定用户合辑的视频列表,合辑视频的mix_id是一致的,从单个作品数据接口中获取即可。 +异步方法,用于获取指定用户合集的视频列表,合集视频的mix_id是一致的,从单个作品数据接口中获取即可。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -182,9 +322,9 @@ outline: deep | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -| aweme_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称、页码等 | +| UserMixFilter | AsyncGenerator | 合集作品数据过滤器,包含合集作品数据的_to_raw、_to_dict、_to_list方法 | -<<< @/snippets/douyin/user-mix.py{16-18,21-24,29-32} +<<< @/snippets/douyin/user-mix.py{16,19-21} ### 用户直播流数据 🟢 @@ -212,82 +352,191 @@ outline: deep | :--- | :--- | :--- | | webcast_data | dict | 直播数据字典,包含直播ID、直播标题、直播状态、观看人数、子分区、主播昵称等 | -<<< @/snippets/douyin/user-live-room-id.py{16-18} +<<< @/snippets/douyin/user-live-room-id.py{15-17} + +### 用户首页推荐作品数据 🟢 -### 用户信息 🟢 +异步方法,用于获取指定用户的首页推荐作品。 -异步方法,用于获取指定用户的信息,不可以直接解析Filter的数据,需要使用自定义的_to_dict()或_to_list()方法。 +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| sec_user_id| str | 用户ID | +| max_cursor| int | 页码,初始为0 | +| page_counts| int | 页数,初始为20 | +| max_counts| int | 最大页数,初始为None | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| UserPostFilter | AsyncGenerator | 首页推荐作品数据过滤器,包含推荐作品数据的_to_raw、_to_dict、_to_list方法 | + +<<< @/snippets/douyin/user-feed.py{17-20} + +### 相似作品数据 🟢 + +异步方法,用于获取指定作品的相似作品。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | +| aweme_id| str | 作品ID | +| filterGids| str | 过滤的Gids | +| page_counts| int | 页数,初始为20 | +| max_counts| int | 最大页数,初始为None | + + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| PostRelatedFilter | dict | 相关推荐作品数据过滤器,包含相关作品数据的_to_raw、_to_dict、_to_list方法 | + +<<< @/snippets/douyin/aweme-related.py{16-18} + + +### 好友作品数据 🟢 + +异步方法,用于获取好友的作品。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| cursor| str | 页码,初始为0 | +| level| int | 作品级别,初始为1 | +| pull_type| int | 拉取类型,初始为0 | +| max_counts| int | 最大页数,初始为None | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| FriendFeedFilter | AsyncGenerator | 好友作品数据过滤器,包含好友作品数据的_to_raw、_to_dict、_to_list方法 | + +<<< @/snippets/douyin/user-friend.py{16} + +### 关注用户数据 🟢 + +异步方法,用于获取指定用户关注的用户列表。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| user_id| str | 用户ID | | sec_user_id| str | 用户ID | +| offset| int | 页码,初始为0 | +| count| int | 页数,初始为20 | +| source_type| int | 源类型,初始为4 | +| min_time | int | 最早关注时间戳,初始为0 | +| max_time | int | 最晚关注时间戳,初始为0 | +| max_counts| float | 最大页数,初始为None | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -| UserProfileFilter | _to_dict() | 自定义的接口数据过滤器 | 用户数据字典,包含用户ID、用户昵称、用户签名、用户头像等 | +| UserFollowingFilter | AsyncGenerator | 关注用户数据过滤器,包含关注用户数据的_to_raw、_to_dict、_to_list方法 | -<<< @/snippets/douyin/user-profile.py{15-16} +<<< @/snippets/douyin/user-following.py{18-20,22-29} -### 获取指定用户名 🔴 +### 粉丝用户数据 🟢 -异步方法,用于获取指定用户的昵称,如果不存在,则从服务器获取并存储到数据库中。 +异步方法,用于获取指定用户的粉丝列表。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | +| user_id| str | 用户ID | | sec_user_id| str | 用户ID | -| db | AsyncUserDB | 用户数据库 | +| offset| int | 页码,初始为0 | +| count| int | 页数,初始为20 | +| source_type| int | 源类型,初始为1 | +| min_time | int | 最早关注时间戳,初始为0 | +| max_time | int | 最晚关注时间戳,初始为0 | +| max_counts| float | 最大页数,初始为None | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -| user_nickname | str | 用户昵称 | +| UserFollowerFilter | AsyncGenerator | 粉丝用户数据过滤器,包含粉丝用户数据的_to_raw、_to_dict、_to_list方法 | -<<< @/snippets/douyin/user-nickname.py{17,19-21} +<<< @/snippets/douyin/user-follower.py{18-20,22-29} -### 创建用户记录与目录 🟡 +### 查询用户信息 🟢 -异步方法,用于获取或创建用户数据同时创建用户目录。 +通过`ttwid`的参数用于查询用户基本信息,若需要获取更多信息请使用`fetch_user_profile`。 +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| 无 | 无 | 无 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| QueryUserFilter | model | 查询用户数据过滤器,包含用户数据的_to_raw、_to_dict方法 | + +<<< @/snippets/douyin/query-user.py{18} + +### 直播间wss负载数据 🟢 + +异步方法,用于获取直播间wss负载数据,是弹幕wss的必要参数。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| kwargs | dict | cli字典数据,需获取path参数 | -| sec_user_id| str | 用户ID | -| db | AsyncUserDB | 用户数据库 | +| room_id| str | 直播间ID | +| unique_id| str | 用户ID | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -| user_path | Path | 用户目录路径对象 | +| LiveImFetchFilter | model | 直播间wss负载数据过滤器,包含直播间wss负载数据的_to_raw、_to_dict方法 | -<<< @/snippets/douyin/user-get-add.py{18,20-22} +<<< @/snippets/douyin/user-live-im-fetch.py{5-14,30-42} -::: tip 提示 -此为cli模式的接口,开发者可自行定义创建用户目录的功能。 -::: +### 直播间wss弹幕 🟢 -### 创建作品下载记录 🟢 +异步方法,用于获取直播间wss弹幕数据,使用内置多个回调处理不同类型的消息。 -异步方法,用于获取或创建作品数据同时创建作品目录。 +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| room_id| str | 直播间ID | +| user_unique_id| str | 用户ID | +| internal_ext| str | 内部扩展参数 | +| cursor| str | 弹幕页码 | +| callback| dict | 自定义弹幕回调函数(待加入) | + +| 回调 | 说明 | +| :--- | :--- | +| WebcastRoomMessage | 直播间房间消息 | +| WebcastLikeMessage | 直播间点赞消息 | +| WebcastMemberMessage | 直播间观众加入消息 | +| WebcastChatMessage | 直播间聊天消息 | +| WebcastGiftMessage | 直播间礼物消息 | +| WebcastSocialMessage | 直播间用户关注消息 | +| WebcastRoomUserSeqMessage | 直播间在线观众排行榜 | +| WebcastUpdateFanTicketMessage | 直播间粉丝团更新消息 | +| WebcastCommonTextMessage | 直播间文本消息 | +| WebcastMatchAgainstScoreMessage | 直播间对战积分消息 | +| WebcastFansclubMessage | 直播间粉丝团消息 | +| TODO: WebcastRanklistHourEntranceMessage | 直播间小时榜消息 | +| TODO: WebcastRoomStatsMessage | 直播间统计消息 | +| TODO: WebcastLiveShoppingMessage | 直播间购物车消息 | +| TODO: WebcastLiveEcomGeneralMessage | 直播间电商消息 | +| TODO: WebcastProductChangeMessage | 直播间商品变更消息 | +| TODO: WebcastRoomStreamAdaptationMessage | 直播间流适配消息 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| self.websocket | WebSocket | 弹幕WebSocket对象 | + +<<< @/snippets/douyin/user-live-im-fetch.py{17-26,44-50} + +### 关注用户的直播间信息 🟢 + +异步方法,用于获取关注用户的直播间信息列表,需要登录账号。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| aweme_data | dict | 作品数据字典 | -| db | AsyncVideoDB | 作品数据库 | -| ignore_fields | list | 忽略的字段列表 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -|None | None | 无 | +| FollowingUserLiveFilter | model | 关注用户直播间数据过滤器,包含关注用户直播间数据的_to_raw、_to_dict方法 | -<<< @/snippets/douyin/video-get-add.py{6,23-25} +<<< @/snippets/douyin/user-follow-live.py{16} -### SSO登录 🟢 +### SSO登录 🔴 异步方法,用于处理用户SSO登录,获取用户的cookie。 - | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -|None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -300,15 +549,30 @@ outline: deep 由于扫码登录受风控影响较大,多数cookie都无法使用。为了保障体验,建议使用--auto-cookie命令自动从浏览器获取cookie,更多使用帮助参考cli命令。 ::: + ## utils接口列表 +### 管理客户端配置 🟢 + +类方法,用于管理客户端配置 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| 无 | 无 | 无 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| 配置文件值 | Any | 配置文件值 | + +<<< @/snippets/douyin/client-config.py{4,5,7,8,10,11} + ### 生成真实msToken 🟢 -静态方法,用于生成真实的msToken,当出现错误时返回虚假的值。 +类方法,用于生成真实的msToken,当出现错误时返回虚假的值。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -318,11 +582,11 @@ outline: deep ### 生成虚假msToken 🟢 -静态方法,用于生成随机虚假的msToken,不同端点的msToken长度不同。 +类方法,用于生成随机虚假的msToken,不同端点的msToken长度不同。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -336,11 +600,11 @@ outline: deep ### 生成ttwid 🟢 -静态方法,用于生成ttwid,部分请求必带。 +类方法,用于生成ttwid,部分请求必带,游客状态必须有。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -348,13 +612,27 @@ outline: deep <<< @/snippets/douyin/ttwid.py{4} +### 生成webid 🟢 + +类方法,用于生成个性化追踪webid。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| 无 | 无 | 无 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| webid | str | webid参数 | + +<<< @/snippets/douyin/webid.py{4} + ### 生成verify_fp 🟢 -静态方法,用于生成verify_fp,部分请求必带。 +类方法,用于生成verify_fp,部分请求必带。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -364,11 +642,11 @@ outline: deep ### 生成s_v_web_id 🟢 -静态方法,用于生成s_v_web_id,部分请求必带,即verify_fp值。 +类方法,用于生成s_v_web_id,部分请求必带,即verify_fp值。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -376,12 +654,45 @@ outline: deep <<< @/snippets/douyin/s_v_web_id.py{4} +### 生成直播signature 🟢 + +用于生成直播signature,请求弹幕wss必带。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| room_id | str | 直播间ID | +| user_unique_id | str | 用户ID | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| signature | str | 直播signature | + +<<< @/snippets/douyin/webcast-signature.py#webcast-signature-snippet{4-8} + +### 使用接口模型生成直播wss签名参数 🟢 + +类方法,用于使用不同接口数据模型生成直播wss签名参数。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| user_agent | str | 用户代理 | +| base_endpoint | str | 端点 | +| params | dict | 请求参数 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| final_endpoint | str | 带wss签名参数的完整地址 | + +<<< @/snippets/douyin/webcast-signature.py#webcast-signature-manager-snippet{10-14} + + ### 使用接口地址生成Xb参数 🟢 -静态方法,用于直接使用接口地址生成Xbogus参数,部分接口不校验。 +类方法,用于直接使用接口地址生成Xbogus参数,部分接口不校验。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | +| user_agent | str | 用户代理 | | endpoint | str | 接口端点 | | 返回 | 类型 | 说明 | @@ -392,11 +703,12 @@ outline: deep ### 使用接口模型生成Xb参数 🟢 -静态方法,用于使用不同接口数据模型生成Xbogus参数,部分接口不校验。 +类方法,用于使用不同接口数据模型生成Xbogus参数,部分接口不校验。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | +| user_agent | str | 用户代理 | | endpoint | str | 端点 | | params | dict | 请求参数 | @@ -406,22 +718,63 @@ outline: deep 使用模型生成接口地址,需要先创建一个模型对象,然后调用`model_2_endpoint`方法。 -<<< @/snippets/douyin/xbogus.py#model-2-endpoint-snippet{8-10,13-15} +<<< @/snippets/douyin/xbogus.py#model-2-endpoint-snippet{9-13,17-19} 还可以使用爬虫引擎与过滤器采集数据。 <<< @/snippets/douyin/xbogus.py#model-2-endpoint-2-filter-snippet{22-27} -更加抽象的高级方法可以直接调用handler接口的`handler_user_profile`。 +更加抽象的高级方法可以直接调用handler接口的`fetch_user_profile`。 + + +### 使用接口地址生成Ab参数 🟢 + +类方法,用于直接使用接口地址生成Ab参数,新接口都需要校验。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| user_agent | str | 用户代理 | +| params | str | 请求参数 | +| request_type | str | 请求类型 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| final_params | str | 带Ab参数的请求参数 | + +<<< @/snippets/douyin/abogus.py#str-2-endpoint-snippet{7-13} + +### 使用接口模型生成Ab参数 🟢 + +类方法,用于使用不同接口数据模型生成Ab参数,新接口都需要校验。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| user_agent | str | 用户代理 | +| base_endpoint | str | 端点 | +| params | dict | 请求参数模型 | +| request_type | str | 请求类型 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| final_params | str | 带Ab参数的请求参数 | + +使用模型生成接口地址,需要先创建一个模型对象,然后调用`model_2_endpoint`方法。 + +<<< @/snippets/douyin/abogus.py#model-2-endpoint-snippet{9-14,18-20} + +还可以使用爬虫引擎与过滤器采集数据。 + +<<< @/snippets/douyin/abogus.py#model-2-endpoint-2-filter-snippet{20-26} + +更加抽象的高级方法可以直接调用handler接口的`fetch_user_profile`。 ::: tip 提示 -本项目中的UA参数为固定值,`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, - like Gecko) Chrome/104.0.0.0 Safari/537.36`。 +本项目的残血版Ab算法的UA参数为固定值,`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0`。 ::: ### 提取单个用户id 🟢 -静态方法,用于提取单个用户id。 +类方法,用于提取单个用户id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -435,7 +788,7 @@ outline: deep ### 提取列表用户id 🟢 -静态方法,用于提取列表用户id。 +类方法,用于提取列表用户id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -449,7 +802,7 @@ outline: deep ### 提取单个作品id 🟢 -静态方法,用于提取单个作品id。 +类方法,用于提取单个作品id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -463,7 +816,7 @@ outline: deep ### 提取列表作品id 🟢 -静态方法,用于提取列表作品id。 +类方法,用于提取列表作品id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -475,14 +828,37 @@ outline: deep <<< @/snippets/douyin/aweme-id.py#multi-aweme-id-snippet{15,18} -### 提取合辑id 🟤 +### 提取合集id 🟢 + +类方法,用于从合集链接中提取合集id。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| url | str | 合集地址 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| mix_id | str | 合集ID | + +<<< @/snippets/douyin/mix-id.py#single-mix-id-snippet{6,7} + +### 提取列表合集id 🟢 -静态方法,用于提取合辑id,合辑id其实就是作品id,使用`AwemeIdFetcher`即可。 +类方法,用于从合集链接列表中提取合集id。 +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| urls | list | 合集地址列表 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| mix_ids | list | 合集ID列表 | + +<<< @/snippets/douyin/mix-id.py#multi-mix-id-snippet{7-10,13,16} ### 提取单个直播间号 🟢 -静态方法,用于提取单个直播间号。 +类方法,用于提取单个直播间号。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -493,11 +869,11 @@ outline: deep | webcast_id | str | 直播间RID | -<<< @/snippets/douyin/webcast-id.py#single-webcast-id-snippet{5,6} +<<< @/snippets/douyin/webcast-id.py#single-webcast-id-snippet{6,7} ### 提取列表直播间号 🟢 -静态方法,用于提取列表直播间号。 +类方法,用于提取列表直播间号。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -507,11 +883,11 @@ outline: deep | :--- | :--- | :--- | | webcast_ids | list | 直播间RID列表 | -<<< @/snippets/douyin/webcast-id.py#multi-webcast-id-snippet{15,18} +<<< @/snippets/douyin/webcast-id.py#multi-webcast-id-snippet{7-13,16,19} -::: tip 如何分辨Rid与room_id -Rid是直播间的短链标识,room_id是直播间的唯一标识。 -如`https://live.douyin.com/775841227732`中的775841227732就是Rid,而`https://webcast.amemv.com/douyin/webcast/reflow/7318296342189919011`中的7318296342189919011就是room_id。 +::: tip 如何分辨r_id与room_id +r_id是直播间的短链标识,room_id是直播间的唯一标识。 +如`https://live.douyin.com/775841227732`中的`775841227732`就是r_id,而`https://webcast.amemv.com/douyin/webcast/reflow/7318296342189919011`中的`7318296342189919011`就是room_id。 这2个链接都指向同一个直播间。 ::: @@ -606,7 +982,7 @@ Rid是直播间的短链标识,room_id是直播间的唯一标识。 | user_path | Path | 用户目录路径对象 | ::: tip 提示 -该接口很好的解决了用户改名之后重复重新下载的问题。集合在hanlder接口的`get_or_add_user_data`中,开发者无需关心直接调用hanlder的数据接口即可。 +该接口很好的解决了用户改名之后重复重新下载的问题。集合在handler接口的`get_or_add_user_data`中,开发者无需关心直接调用handler的数据接口即可。 ::: @@ -621,7 +997,7 @@ Rid是直播间的短链标识,room_id是直播间的唯一标识。 | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | <<< @/snippets/douyin/show-qrcode.py{4,5} @@ -629,7 +1005,148 @@ Rid是直播间的短链标识,room_id是直播间的唯一标识。 show_image (bool): 是否显示图像,True 表示显示,False 表示在控制台显示 ::: +### json歌词转lrc歌词 🟢 + +用于将抖音原声的json格式的歌词转换为lrc格式的歌词。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| data | Union[str, list, dict] | json格式的歌词 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| lrc_lines | str | lrc格式的歌词 | + +<<< @/snippets/douyin/json-2-lrc.py{94} + ## crawler接口 +### 用户信息接口地址 🟢 + +异步方法,用于获取用户信息数据。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| params | UserProfile | 请求参数 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| parse_json() | dict | 用户信息数据 | + +### 主页作品接口地址 🟢 + +异步方法,用于获取主页作品数据。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| params | UserPost | 请求参数 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| parse_json() | dict | 主页作品数据 | + +### 喜欢作品接口地址 🟢 + +异步方法,用于获取喜欢作品数据。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| params | UserLike | 请求参数 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| parse_json() | dict | 喜欢作品数据 | + +### 收藏作品接口地址 🟢 + +异步方法,用于获取收藏作品数据。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| params | UserCollection | 请求参数 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| parse_json() | dict | 收藏作品数据 | + +### 收藏夹接口地址 🟢 + +异步方法,用于获取收藏夹数据。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| params | UserCollects | 请求参数 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| parse_json() | dict | 收藏夹数据 | + +### 收藏夹作品接口地址 🟢 + +异步方法,用于获取收藏夹作品数据。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| params | UserCollectsVideo | 请求参数 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| parse_json() | dict | 收藏夹作品数据 | + +### 音乐收藏接口地址 🟢 + +异步方法,用于获取音乐收藏数据。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| params | UserMusicCollection | 请求参数 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| parse_json() | dict | 音乐收藏数据 | + +## dl接口 + +### 保存最后一次请求的aweme_id 🟢 + +用于保存最后一次请求的aweme_id,用于下一次请求主页作品的参数。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| sec_user_id | str | 用户ID | +| aweme_id | str | 作品ID | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| 无 | 无 | 无 | + + +### 筛选指定日期区间内的作品 🟢 + +用于筛选指定日期区间内的作品。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| aweme_data | dict | 作品数据的字典 | +| interval | str | 日期区间 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| filtered_aweme_datas | Union[list[dict], dict, None] | 筛选后的作品数据 | + +### 创建下载任务 🟢 + + +### 处理下载任务 🟢 + + +### 创建原声下载任务 🟢 + + +### 处理原声下载任务 🟢 + + +### 创建流下载任务 🟢 + -## dl接口 \ No newline at end of file +### 处理流下载任务 🟢 \ No newline at end of file diff --git a/docs/guide/apps/tiktok/cli.md b/docs/guide/apps/tiktok/cli.md new file mode 100644 index 00000000..6510d595 --- /dev/null +++ b/docs/guide/apps/tiktok/cli.md @@ -0,0 +1,90 @@ +## CLI 帮助 - TikTok + +### 参数列表 + +| 短参数 | 长参数 | 类型 | 说明 | +| ------ | ------ | ---- | ---- | +| `-c` | `--config` | `FILE` | 配置文件的路径,最低优先 | +| `-u` | `--url` | `TEXT` | 根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合集与直播同上 | +| `-m` | `--music` | `BOOLEAN` | 是否保存视频原声 | +| `-v` | `--cover` | `BOOLEAN` | 是否保存视频封面 | +| `-d` | `--desc` | `BOOLEAN` | 是否保存视频文案 | +| `-p` | `--path` | `TEXT` | 作品保存位置,支持绝对与相对路径 | +| `-f` | `--folderize` | `BOOLEAN` | 是否将作品保存到单独的文件夹 | +| `-M` | `--mode` | `[one\|post\|like\|collect\|mix\|search\|live]` | 下载模式:单个作品(one),主页作品(post),点赞作品(like),收藏作品(collect),合集播放列表(mix),搜索(search),直播(live) | +| `-n` | `--naming` | `TEXT` | 全局作品文件命名方式,前往文档查看更多帮助 | +| `-k` | `--cookie` | `TEXT` | 登录后的cookie,如果使用未登录的cookie,则无法持久稳定下载作品 | +| `-i` | `--interval` | `TEXT` | 下载日期区间发布的作品,格式:`2022-01-01|2023-01-01`,`all` 为下载所有作品 | +| `-w` | `--keyword` | `TEXT` | 搜索关键字,用于搜索作品 | +| `-e` | `--timeout` | `INTEGER` | 网络请求超时时间 | +| `-r` | `--max_retries` | `INTEGER` | 网络请求超时重试数 | +| `-x` | `--max-connections` | `INTEGER` | 网络请求并发连接数 | +| `-t` | `--max-tasks` | `INTEGER` | 异步的任务数 | +| `-o` | `--max-counts` | `INTEGER` | 最大作品下载数。`0` 表示无限制 | +| `-s` | `--page-counts` | `INTEGER` | 从接口每页可获取作品数,不建议超过 `20` | +| `-l` | `--languages` | `[zh_CN\|en_US]` | 显示语言。默认为 `zh_CN`,可选:`zh_CN`、`en_US`,不支持配置文件修改 | +| `-P` | `--proxies` | `TEXT...` | 代理服务器,最多 2 个参数,`http://`与`https://`。空格区分 2 个参数,例如:`http://x.x.x.x https://x.x.x.x` | +| | `--update-config` | `BOOLEAN` | 使用命令行选项更新配置文件。需要先使用`-c`选项提供一个配置文件路径 | +| | `--init-config` | `TEXT` | 初始化配置文件。不能同时初始化和更新配置文件 | +| | `--auto-cookie` | `[chrome\|firefox\|edge\|opera\|opera_gx\|safari\|chromium\|brave\|vivaldi\|librewolf]` | 自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器 | +| `-h` | | `FLAG` | 显示富文本帮助 | +| | `--help` | `FLAG` | 显示帮助信息并退出 | + +### 详细说明 + +#### `--config` + +配置文件的路径,最低优先。默认配置文件路径为 `f2/conf/app.yaml`。支持绝对路径与相对路径。 + +#### `--url` + +根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合集与直播同上。 + +#### `--mode` + +下载模式: +- `one`:单个作品 +- `post`:主页作品 +- `like`:点赞作品 +- `collect`:收藏作品 +- `mix`:合集播放列表 +- `search`:搜索 +- `live`:直播 + +#### `--naming` + +全局作品文件命名方式。默认为 `{create}_{desc}`,支持的变量有:`{nickname}`,`{create}`,`{aweme_id}`,`{desc}`,`{uid}`。支持的分割符有:`_`,`-`。 + +- `{nickname}`:作者昵称 +- `{create}`:作品创建时间 +- `{aweme_id}`:作品ID +- `{desc}`:作品文案 +- `{uid}`:作者ID + +#### `--interval` + +下载日期区间发布的作品,格式:`年-月-日` 如:`2022-01-01|2023-01-01`,设置`all` 为下载所有作品。 + +#### `--languages` + +显示语言。默认为 `zh_CN`,可选:`zh_CN`、`en_US`,不支持配置文件修改。 + +#### `--proxies` + +代理服务器,最多 2 个参数,`http://`与`https://`。空格区分 2 个参数,例如:`http://x.x.x.x https://x.x.x.x`。如果你的代理不支持`出口HTTPS`,那么请使用`http://x.x.x.x http://x.x.x.x`。 + +#### `--auto-cookie` + +自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器。支持的浏览器有: +- `chrome` +- `firefox` +- `edge` +- `opera` +- `opera_gx` +- `safari` +- `chromium` +- `brave` +- `vivaldi` +- `librewolf` + +不支持切换浏览器用户配置。 diff --git a/docs/guide/apps/tiktok/index.md b/docs/guide/apps/tiktok/index.md index 968a60c5..fb6fb5c9 100644 --- a/docs/guide/apps/tiktok/index.md +++ b/docs/guide/apps/tiktok/index.md @@ -12,76 +12,84 @@ outline: deep | CLI接口 | 方法 | | :------------------ | :------------------- | -| 下载单个作品 | handle_one_video | -| 下载用户发布作品 | handle_user_post | -| 下载用户喜欢作品 | handle_user_like | -| 下载用户收藏作品 | handle_user_collect | -| 下载用户合辑(播放列表)作品 | handle_user_mix | -| 下载用户直播流 | handle_user_live | -| 下载用户首页推荐作品 | handle_user_feed | +| 下载单个作品 | `handle_one_video` | +| 下载用户发布作品 | `handle_user_post` | +| 下载用户喜欢作品 | `handle_user_like` | +| 下载用户收藏作品 | `handle_user_collect` | +| 下载用户合集(播放列表)作品 | `handle_user_mix` | +| 下载搜索作品 | `handle_search_video` | +| 下载用户直播流 | `handle_user_live` | | 数据与功能接口 | 方法 | 开发者接口 | | :------------------ | :------------------- | :--------: | -| 单个作品数据 | fetch_one_video | 🟢 | -| 用户发布作品数据 | fetch_user_post_videos | 🟢 | -| 用户喜欢作品数据 | fetch_user_like_videos | 🟢 | -| 用户收藏作品数据 | fetch_user_collect_videos | 🟢 | -| 用户播放列表作品数据 | fetch_play_list | 🟢 | -| 用户合辑(播放列表)作品 | fetch_user_mix_videos | 🟢 | -| ...... | ...... | 🔵 | -| 用户信息 | handler_user_profile | 🟢 | -| 获取指定用户名 | get_user_nickname | 🔴 | -| 创建用户记录与目录 | get_or_add_user_data | 🟡 | -| 创建作品下载记录 | get_or_add_video_data | 🟢 | +| 用户信息 | `fetch_user_profile` | 🟢 | +| 创建用户记录与目录 | `get_or_add_user_data` | 🟢 | +| 创建作品下载记录 | `get_or_add_video_data` | 🟢 | +| 单个作品数据 | `fetch_one_video` | 🟢 | +| 用户发布作品数据 | `fetch_user_post_videos` | 🟢 | +| 用户喜欢作品数据 | `fetch_user_like_videos` | 🟢 | +| 用户收藏作品数据 | `fetch_user_collect_videos` | 🟢 | +| 用户播放列表数据 | `fetch_play_list` | 🟢 | +| 用户合集(播放列表)作品数据 | `fetch_user_mix_videos` | 🟢 | +| 搜索作品数据 | `fetch_search_videos` | 🟢 | +| 用户直播流数据 | `fetch_user_live_videos` | 🟢 | +| 检查直播流状态 | `fetch_check_live_alive` | 🟢 | ::: ::: details utils接口列表 | 开发者接口 | 类名 | 方法 | 状态 | | :---------------- | :-------------- | :------------------ | :--: | -| 生成真实msToken | TokenManager | gen_real_msToken | 🟢 | -| 生成虚假msToken | TokenManager | gen_false_msToken | 🟢 | -| 生成ttwid | TokenManager | gen_ttwid | 🟢 | -| 生成odin_tt | TokenManager | gen_odin_tt | 🟢 | -| 使用接口地址生成Xb参数 | XBogusManager | str_2_endpoint | 🟢 | -| 使用接口模型生成Xb参数 | XBogusManager | model_2_endpoint | 🟢 | -| 提取单个用户id | SecUserIdFetcher | get_secuid | 🟢 | -| 提取列表用户id | SecUserIdFetcher | get_all_secuid | 🟢 | -| 提取单个用户唯一id | SecUserIdFetcher | get_uniqueid | 🟢 | -| 提取列表用户唯一id | SecUserIdFetcher | get_all_uniqueid | 🟢 | -| 提取列表用户id | SecUserIdFetcher | get_all_secUid | 🟢 | -| 提取单个作品id | AwemeIdFetcher | get_aweme_id | 🟢 | -| 提取列表作品id | AwemeIdFetcher | get_all_aweme_id | 🟢 | -| 提取合辑id | MixIdFetcher | - | 🟤 | -| 全局格式化文件名 | - | format_file_name | 🟢 | -| 创建用户目录 | - | create_user_folder | 🟢 | -| 重命名用户目录 | - | rename_user_folder | 🟢 | -| 创建或重命名用户目录 | - | create_or_rename_user_folder | 🟢 | +| 管理客户端配置 | `ClientConfManager` | | 🟢 | +| 生成真实msToken | `TokenManager` | `gen_real_msToken` | 🟢 | +| 生成虚假msToken | `TokenManager` | `gen_false_msToken` | 🟢 | +| 生成ttwid | `TokenManager` | `gen_ttwid` | 🟢 | +| 生成odin_tt | `TokenManager` | `gen_odin_tt` | 🟢 | +| 使用接口地址生成Xb参数 | `XBogusManager` | `str_2_endpoint` | 🟢 | +| 使用接口模型生成Xb参数 | `XBogusManager` | `model_2_endpoint` | 🟢 | +| 提取单个用户id | `SecUserIdFetcher` | `get_secuid` | 🟢 | +| 提取列表用户id | `SecUserIdFetcher` | `get_all_secuid` | 🟢 | +| 提取单个用户唯一id | `SecUserIdFetcher` | `get_uniqueid` | 🟢 | +| 提取列表用户唯一id | `SecUserIdFetcher` | `get_all_uniqueid` | 🟢 | +| 提取列表用户id | `SecUserIdFetcher` | `get_all_secUid` | 🟢 | +| 提取单个作品id | `AwemeIdFetcher` | `get_aweme_id` | 🟢 | +| 提取列表作品id | `AwemeIdFetcher` | `get_all_aweme_id` | 🟢 | +| 生成deviceId | `DeviceIdManager` | `gen_device_id` | 🟢 | +| 生成devideId列表 | `DeviceIdManager` | `gen_device_ids` | 🟢 | +| 全局格式化文件名 | - | `format_file_name` | 🟢 | +| 创建用户目录 | - | `create_user_folder` | 🟢 | +| 重命名用户目录 | - | `rename_user_folder` | 🟢 | +| 创建或重命名用户目录 | - | `create_or_rename_user_folder` | 🟢 | +::: ::: details crawler接口列表 | 爬虫url接口 | 类名 | 方法 | 状态 | | :----------- | :--------- | :---------- | :--: | -| 用户信息接口地址 | TiktokCrawler | fetch_user_profile | 🟢 | -| 主页作品接口地址 | TiktokCrawler | fetch_user_post | 🟢 | -| 喜欢作品接口地址 | TiktokCrawler | fetch_user_like | 🟢 | -| 收藏作品接口地址 | TiktokCrawler | fetch_user_collect | 🟢 | -| 合辑列表接口地址 | TiktokCrawler | fetch_user_play_list | 🟢 | -| 合辑作品接口地址 | TiktokCrawler | fetch_user_mix | 🟢 | -| 作品详情接口地址 | TiktokCrawler | fetch_post_detail | 🟢 | -| 作品评论接口地址 | TiktokCrawler | fetch_post_comment | 🟡 | -| 推荐作品接口地址 | TiktokCrawler | fetch_post_feed | 🟡 | +| 用户信息接口地址 | `TiktokCrawler` | `fetch_user_profile` | 🟢 | +| 主页作品接口地址 | `TiktokCrawler` | `fetch_user_post` | 🟢 | +| 喜欢作品接口地址 | `TiktokCrawler` | `fetch_user_like` | 🟢 | +| 收藏作品接口地址 | `TiktokCrawler` | `fetch_user_collect` | 🟢 | +| 合集列表接口地址 | `TiktokCrawler` | `fetch_user_play_list` | 🟢 | +| 合集作品接口地址 | `TiktokCrawler` | `fetch_user_mix` | 🟢 | +| 作品详情接口地址 | `TiktokCrawler` | `fetch_post_detail` | 🟢 | +| 作品评论接口地址 | `TiktokCrawler` | `fetch_post_comment` | 🟢 | +| 首页推荐作品接口地址 | `TiktokCrawler` | `fetch_post_feed` | 🟢 | +| 搜索作品接口地址 | `TiktokCrawler` | `fetch_post_search` | 🟢 | +| 用户直播接口地址 | `TiktokCrawler` | `fetch_user_live` | 🟢 | +| 检测直播状态接口地址 | `TiktokCrawler` | `fetch_check_live_alive` | 🟢 | ::: ::: details dl接口列表 | 下载器接口 | 类名 | 方法 | 状态 | | :----------- | :--------- | :---------- | :--: | -| 保存最后一次请求的aweme_id | TiktokDownloader | save_last_aweme_id | 🟢 | -| 创建下载任务 | TiktokDownloader | create_download_task | 🟢 | -| 处理下载任务 | TiktokDownloader | handle_download | 🟢 | -| 创建流下载任务 | TiktokDownloader | create_stream_tasks | 🟢 | -| 处理流下载任务 | TiktokDownloader | handle_stream | 🟢 | +| 保存最后一次请求的aweme_id | `TiktokDownloader` | `save_last_aweme_id` | 🟢 | +| 筛选指定时间区间的作品 | `TiktokDownloader` | `filter_aweme_datas_by_interval` | 🟢 | +| 创建下载任务 | `TiktokDownloader` | `create_download_task` | 🟢 | +| 处理下载任务 | `TiktokDownloader` | `handle_download` | 🟢 | +| 创建流下载任务 | `TiktokDownloader` | `create_stream_tasks` | 🟢 | +| 处理流下载任务 | `TiktokDownloader` | `handle_stream` | 🟢 | ::: ## handler接口列表 @@ -98,7 +106,7 @@ outline: deep | :--- | :--- | :--- | | video_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称等 | -<<< @/snippets/tiktok/one-video.py{15,17} +<<< @/snippets/tiktok/one-video.py{15} ### 用户发布作品数据 🟢 @@ -115,7 +123,7 @@ outline: deep | :--- | :--- | :--- | | aweme_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称、页码等 | -<<< @/snippets/tiktok/user-post.py{16,19-22} +<<< @/snippets/tiktok/user-post.py{18,20-22} ### 用户喜欢作品数据 🟢 @@ -132,7 +140,7 @@ outline: deep | :--- | :--- | :--- | | aweme_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称、页码等 | -<<< @/snippets/tiktok/user-like.py{16-18,21-25} +<<< @/snippets/tiktok/user-like.py{17-19,21-23} ### 用户收藏作品数据 🟢 @@ -149,7 +157,7 @@ outline: deep | :--- | :--- | :--- | | aweme_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称、页码等 | -<<< @/snippets/tiktok/user-collect.py{16-18,21-24} +<<< @/snippets/tiktok/user-collect.py{17-19,21-23} ### 用户播放列表作品数据 🟢 @@ -165,11 +173,11 @@ outline: deep | :--- | :--- | :--- | | aweme_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称、页码等 | -<<< @/snippets/tiktok/user-playlist.py{16-17,21} +<<< @/snippets/tiktok/user-playlist.py{17-18} -### 用户合辑作品数据 🟢 +### 用户合集作品数据 🟢 -异步方法,用于获取指定用户合辑的视频列表,合辑视频的mix_id是一致的,从单个作品数据接口中获取即可。 +异步方法,用于获取指定用户合集的视频列表,合集视频的mix_id是一致的,从单个作品数据接口中获取即可。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -182,13 +190,13 @@ outline: deep | :--- | :--- | :--- | | aweme_data | dict | 视频数据字典,包含视频ID、视频文案、作者昵称、页码等 | -<<< @/snippets/tiktok/user-mix.py#playlist-sinppet{17-18,21-23} +<<< @/snippets/tiktok/user-mix.py#playlist-sinppet{18-19,21-22} ::: tip 注意 -多个播放列表会包含多个`mix_id`,使用`select_playlist`方法来返回用户输入的合辑下标。 +多个播放列表会包含多个`mix_id`,使用`select_playlist`方法来返回用户输入的合集下标。 ::: -<<< @/snippets/tiktok/user-mix.py#select-playlist-sinppet{19-21} +<<< @/snippets/tiktok/user-mix.py#select-playlist-sinppet{19-22} ### 用户信息 🟢 @@ -203,28 +211,14 @@ outline: deep | :--- | :--- | :--- | | UserProfileFilter | _to_dict() | 自定义的接口数据过滤器 | 用户数据字典,包含用户ID、用户昵称、用户签名、用户头像等 | -<<< @/snippets/tiktok/user-profile.py{16,18-19,21} +<<< @/snippets/tiktok/user-profile.py{16-20,26} ::: tip 提示 TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 ::: -### 获取指定用户名 🔴 - -异步方法,用于获取指定用户的昵称,如果不存在,则从服务器获取并存储到数据库中。 - -| 参数 | 类型 | 说明 | -| :--- | :--- | :--- | -| secUid| str | 用户ID | -| db | AsyncUserDB | 用户数据库 | - -| 返回 | 类型 | 说明 | -| :--- | :--- | :--- | -| user_nickname | str | 用户昵称 | - -<<< @/snippets/tiktok/user-nickname.py{17-20} -### 创建用户记录与目录 🟡 +### 创建用户记录与目录 🟢 异步方法,用于获取或创建用户数据同时创建用户目录。 @@ -239,7 +233,7 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | :--- | :--- | :--- | | user_path | Path | 用户目录路径对象 | -<<< @/snippets/tiktok/user-get-add.py{18-22} +<<< @/snippets/tiktok/user-get-add.py{17-23} ::: tip 提示 此为cli模式的接口,开发者可自行定义创建用户目录的功能。 @@ -257,19 +251,33 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | 返回 | 类型 | 说明 | | :--- | :--- | :--- | -|None | None | 无 | +|无 | 无 | 无 | -<<< @/snippets/tiktok/video-get-add.py{6,23-25} +<<< @/snippets/tiktok/video-get-add.py{6,7,23-26} ## utils接口列表 +### 管理客户端配置 🟢 + +类方法,用于管理客户端配置 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| 无 | 无 | 无 | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| 配置文件值 | Any | 配置文件值 | + +<<< @/snippets/tiktok/client-config.py{4,5,7,8,10,11} + ### 生成真实msToken 🟢 -静态方法,用于生成真实的msToken,当出现错误时返回虚假的值。 +类方法,用于生成真实的msToken,当出现错误时返回虚假的值。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -279,11 +287,11 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 ### 生成虚假msToken 🟢 -静态方法,用于生成随机虚假的msToken,不同端点的msToken长度不同。 +类方法,用于生成随机虚假的msToken,不同端点的msToken长度不同。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -297,11 +305,11 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 ### 生成ttwid 🟢 -静态方法,用于生成ttwid,部分请求必带。 +类方法,用于生成ttwid,部分请求必带。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -315,11 +323,11 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 ### 生成odin_tt 🟢 -静态方法,用于生成odin_tt,部分请求必带。 +类方法,用于生成odin_tt,部分请求必带。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | -| None | None | 无 | +| 无 | 无 | 无 | | 返回 | 类型 | 说明 | | :--- | :--- | :--- | @@ -333,7 +341,7 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 ### 使用接口地址生成Xb参数 🟢 -静态方法,用于直接使用接口地址生成`Xbogus`参数,部分接口不校验。 +类方法,用于直接使用接口地址生成`Xbogus`参数,部分接口不校验。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -343,11 +351,11 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | :--- | :--- | :--- | | final_endpoint | str | 带Xbogus参数的完整地址 | -<<< @/snippets/tiktok/xbogus.py#str-2-endpoint-snippet{7} +<<< @/snippets/tiktok/xbogus.py#str-2-endpoint-snippet{7,8} ### 使用接口模型生成Xb参数 🟢 -静态方法,用于使用不同接口数据模型生成`Xbogus`参数,部分接口不校验。 +类方法,用于使用不同接口数据模型生成`Xbogus`参数,部分接口不校验。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -366,16 +374,12 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 <<< @/snippets/tiktok/xbogus.py#model-2-endpoint-2-filter-snippet{21-26} -更加抽象的高级方法可以直接调用handler接口的`handler_user_profile`。 +更加抽象的高级方法可以直接调用handler接口的`fetch_user_profile`。 -::: tip 提示 -本项目中的UA参数为固定值,`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, - like Gecko) Chrome/104.0.0.0 Safari/537.36`。 -::: ### 提取单个用户id 🟢 -静态方法,用于提取单个用户id。 +类方法,用于提取单个用户id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -385,11 +389,11 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | :--- | :--- | :--- | | sec_uid | str | 用户ID | -<<< @/snippets/tiktok/sec-uid.py#single-secuid-snippet{7} +<<< @/snippets/tiktok/sec-uid.py#single-secuid-snippet{8} ### 提取列表用户id 🟢 -静态方法,用于提取列表用户id。 +类方法,用于提取列表用户id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -399,11 +403,11 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | :--- | :--- | :--- | | secuids | list | 用户ID列表 | -<<< @/snippets/tiktok/sec-uid.py#multi-secuid-snippet{13,16} +<<< @/snippets/tiktok/sec-uid.py#multi-secuid-snippet{14,17} ### 提取单个用户唯一id 🟢 -静态方法,用于提取单个用户唯一id。 +类方法,用于提取单个用户唯一id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -413,11 +417,11 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | :--- | :--- | :--- | | unique_id | str | 用户唯一ID | -<<< @/snippets/tiktok/unique-id.py#single-unique-id-snippet{7} +<<< @/snippets/tiktok/unique-id.py#single-unique-id-snippet{8} ### 提取列表用户唯一id 🟢 -静态方法,用于提取列表用户唯一id。 +类方法,用于提取列表用户唯一id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -427,11 +431,11 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | :--- | :--- | :--- | | unique_ids | list | 用户唯一ID列表 | -<<< @/snippets/tiktok/unique-id.py#multi-unique-id-snippet{13,16} +<<< @/snippets/tiktok/unique-id.py#multi-unique-id-snippet{14,17} ### 提取单个作品id 🟢 -静态方法,用于提取单个作品id。 +类方法,用于提取单个作品id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -441,11 +445,11 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | :--- | :--- | :--- | | aweme_id | str | 作品ID | -<<< @/snippets/tiktok/aweme-id.py#single-aweme-id-snippet{7} +<<< @/snippets/tiktok/aweme-id.py#single-aweme-id-snippet{8} ### 提取列表作品id 🟢 -静态方法,用于提取列表作品id。 +类方法,用于提取列表作品id。 | 参数 | 类型 | 说明 | | :--- | :--- | :--- | @@ -461,6 +465,39 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 从网页复制的链接和app分享的链接都是有效的。 ::: +### 生成deviceId 🟢 + +类方法,用于生成`deviceId`和`tt_chain_token`。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| full_cookie | bool | 是否返回完整的cookie | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| device_id | dict | 设备ID和cookie的字典 | + +<<< @/snippets/tiktok/device-id.py#device-id-snippet{6,8} + +### 生成devideId列表 🟢 + +类方法,用于生成多个`deviceId`和`tt_chain_token`。 + +| 参数 | 类型 | 说明 | +| :--- | :--- | :--- | +| count | int | 设备ID数量 | +| full_cookie | bool | 是否返回完整的cookie | + +| 返回 | 类型 | 说明 | +| :--- | :--- | :--- | +| device_ids | dict | 设备ID和cookie的列表字典 | + +<<< @/snippets/tiktok/device-id.py#device-ids-snippet{6,8} + +::: tip 提示 +`deviceId`和`tt_chain_token`影响视频地址的访问,403的情况就是这个问题。 +::: + ### 全局格式化文件名 🟢 根据配置文件的全局格式化文件名。 @@ -480,7 +517,7 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | :--- | :--- | :--- | | file_name | str | 格式化后的文件名 | -<<< @/snippets/tiktok/format-file-name.py{18,20,23,25,27-30} +<<< @/snippets/tiktok/format-file-name.py{18,20,23,25,27-32} ### 创建用户目录 🟢 @@ -527,7 +564,7 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | :--- | :--- | :--- | | new_path | Path | 新的用户目录路径对象 | -<<< @/snippets/tiktok/user-folder.py#rename-user-folder{22-24,26-29} +<<< @/snippets/tiktok/user-folder.py#rename-user-folder{20-24,26-29} ::: tip 提示 如果目录不存在会先创建该用户目录再重命名。 @@ -548,7 +585,7 @@ TikTok的用户接口支持`secUid`和`uniqueId`两种用户ID。 | user_path | Path | 用户目录路径对象 | ::: tip 提示 -该接口很好的解决了用户改名之后重复重新下载的问题。集合在hanlder接口的`get_or_add_user_data`中,开发者无需关心直接调用hanlder的数据接口即可。 +该接口很好的解决了用户改名之后重复重新下载的问题。集合在handler接口的`get_or_add_user_data`中,开发者无需关心直接调用handler的数据接口即可。 ::: ## crawler接口 diff --git a/docs/guide/apps/weibo/cli.md b/docs/guide/apps/weibo/cli.md new file mode 100644 index 00000000..82a1905e --- /dev/null +++ b/docs/guide/apps/weibo/cli.md @@ -0,0 +1,71 @@ +## CLI 帮助 - 微博 + +### 参数列表 + +| 短参数 | 长参数 | 类型 | 说明 | +| ------ | ------ | ---- | ---- | +| `-c` | `--config` | `FILE` | 配置文件的路径,最低优先 | +| `-u` | `--url` | `TEXT` | 根据模式提供相应的链接 | +| `-p` | `--path` | `TEXT` | 作品保存位置,支持绝对与相对路径 | +| `-f` | `--folderize` | `BOOLEAN` | 是否将作品保存到单独的文件夹 | +| `-M` | `--mode` | `[one\|post\|like]` | 下载模式:单个微博(one),主页微博(post),点赞微博(like) | +| `-n` | `--naming` | `TEXT` | 全局微博文件命名方式,前往文档查看更多帮助 | +| `-k` | `--cookie` | `TEXT` | 登录后的cookie | +| `-e` | `--timeout` | `INTEGER` | 网络请求超时时间 | +| `-r` | `--max_retries` | `INTEGER` | 网络请求超时重试数 | +| `-x` | `--max-connections` | `INTEGER` | 网络请求并发连接数 | +| `-t` | `--max-tasks` | `INTEGER` | 异步的任务数 | +| `-o` | `--max-counts` | `INTEGER` | 最大微博下载数。`0` 表示无限制 | +| `-s` | `--page-counts` | `INTEGER` | 从接口每页可获取微博数,不建议超过 `20` | +| `-l` | `--languages` | `[zh_CN\|en_US]` | 显示语言。默认为 `zh_CN`,可选:`zh_CN`、`en_US`,不支持配置文件修改 | +| `-P` | `--proxies` | `TEXT...` | 代理服务器,最多 2 个参数,`http://`与`https://`。空格区分 2 个参数,例如:`http://x.x.x.x https://x.x.x.x` | +| | `--update-config` | `BOOLEAN` | 使用命令行选项更新配置文件。需要先使用`-c`选项提供一个配置文件路径 | +| | `--init-config` | `TEXT` | 初始化配置文件。不能同时初始化和更新配置文件 | +| | `--auto-cookie` | `[chrome\|firefox\|edge\|opera\|opera_gx\|safari\|chromium\|brave\|vivaldi\|librewolf]` | 自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器 | +| `-h` | `--help` | `FLAG` | 显示富文本帮助 | +| | `--help` | `FLAG` | 显示帮助信息并退出 | + +### 详细说明 + +#### `--config` + +配置文件的路径,最低优先。默认配置文件路径为 `f2/conf/app.yaml`。支持绝对路径与相对路径。 + +#### `--url` + +根据模式提供相应的链接。 + +#### `--mode` + +下载模式: +- `one`:单个微博 +- `post`:主页微博 +- `like`:点赞微博 + +#### `--interval` + +下载日期区间发布的微博,格式:`2022-01-01|2023-01-01`,`all` 为下载所有作品。 + +#### `--languages` + +显示语言。默认为 `zh_CN`,可选:`zh_CN`、`en_US`,不支持配置文件修改。 + +#### `--proxies` + +代理服务器,最多 2 个参数,`http://`与`https://`。空格区分 2 个参数,例如:`http://x.x.x.x https://x.x.x.x`。如果你的代理不支持`出口HTTPS`,那么请使用`http://x.x.x.x http://x.x.x.x`。 + +#### `--auto-cookie` + +自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器。支持的浏览器有: +- `chrome` +- `firefox` +- `edge` +- `opera` +- `opera_gx` +- `safari` +- `chromium` +- `brave` +- `vivaldi` +- `librewolf` + +不支持切换浏览器用户配置。 diff --git a/docs/guide/apps/weibo/index.md b/docs/guide/apps/weibo/index.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/guide/apps/x/cli.md b/docs/guide/apps/x/cli.md new file mode 100644 index 00000000..38006d10 --- /dev/null +++ b/docs/guide/apps/x/cli.md @@ -0,0 +1,69 @@ +## CLI 帮助 - 推特 + +### 参数列表 + +| 短参数 | 长参数 | 类型 | 说明 | +| ------ | ------ | ---- | ---- | +| `-c` | `--config` | `FILE` | 配置文件的路径,最低优先 | +| `-u` | `--url` | `TEXT` | 根据模式提供相应的链接 | +| `-p` | `--path` | `TEXT` | 作品保存位置,支持绝对与相对路径 | +| `-f` | `--folderize` | `BOOLEAN` | 是否将作品保存到单独的文件夹 | +| `-M` | `--mode` | `[one\|post\|retweet\|like\|bookmark]` | 下载模式:单个推文(one),主页推文(post),转推(retweet),点赞(like),书签(bookmark) | +| `-n` | `--naming` | `TEXT` | 全局推文文件命名方式,前往文档查看更多帮助 | +| `-k` | `--cookie` | `TEXT` | 登录后的cookie | +| `-e` | `--timeout` | `INTEGER` | 网络请求超时时间 | +| `-r` | `--max_retries` | `INTEGER` | 网络请求超时重试数 | +| `-x` | `--max-connections` | `INTEGER` | 网络请求并发连接数 | +| `-t` | `--max-tasks` | `INTEGER` | 异步的任务数 | +| `-o` | `--max-counts` | `INTEGER` | 最大推文下载数。`0` 表示无限制 | +| `-s` | `--page-counts` | `INTEGER` | 从接口每页可获取推文数,不建议超过 `20` | +| `-l` | `--languages` | `[zh_CN\|en_US]` | 显示语言。默认为 `zh_CN`,可选:`zh_CN`、`en_US`,不支持配置文件修改 | +| `-P` | `--proxies` | `TEXT...` | 代理服务器,最多 2 个参数,`http://`与`https://`。空格区分 2 个参数,例如:`http://x.x.x.x https://x.x.x.x` | +| | `--update-config` | `BOOLEAN` | 使用命令行选项更新配置文件。需要先使用`-c`选项提供一个配置文件路径 | +| | `--init-config` | `TEXT` | 初始化配置文件。不能同时初始化和更新配置文件 | +| | `--auto-cookie` | `[chrome\|firefox\|edge\|opera\|opera_gx\|safari\|chromium\|brave\|vivaldi\|librewolf]` | 自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器 | +| `-h` | `--help` | `FLAG` | 显示富文本帮助 | +| | `--help` | `FLAG` | 显示帮助信息并退出 | + +### 详细说明 + +#### `--config` + +配置文件的路径,最低优先。默认配置文件路径为 `f2/conf/app.yaml`。支持绝对路径与相对路径。 + +#### `--url` + +根据模式提供相应的链接。 + +#### `--mode` + +下载模式: +- `one`:单个推文 +- `post`:主页推文 +- `retweet`:转推 +- `like`:点赞 +- `bookmark`:书签 + +#### `--languages` + +显示语言。默认为 `zh_CN`,可选:`zh_CN`、`en_US`,不支持配置文件修改。 + +#### `--proxies` + +代理服务器,最多 2 个参数,`http://`与`https://`。空格区分 2 个参数,例如:`http://x.x.x.x https://x.x.x.x`。如果你的代理不支持`出口HTTPS`,那么请使用`http://x.x.x.x http://x.x.x.x`。 + +#### `--auto-cookie` + +自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器。支持的浏览器有: +- `chrome` +- `firefox` +- `edge` +- `opera` +- `opera_gx` +- `safari` +- `chromium` +- `brave` +- `vivaldi` +- `librewolf` + +不支持切换浏览器用户配置。 diff --git a/docs/guide/apps/x/index.md b/docs/guide/apps/x/index.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/public/f2-logo-with-no-shadow.png b/docs/public/f2-logo-with-no-shadow.png index e962da99..b18c64cb 100644 Binary files a/docs/public/f2-logo-with-no-shadow.png and b/docs/public/f2-logo-with-no-shadow.png differ diff --git a/docs/question-answer/qa.md b/docs/question-answer/qa.md index 5cbbddb6..0d050349 100644 --- a/docs/question-answer/qa.md +++ b/docs/question-answer/qa.md @@ -19,3 +19,16 @@ ## WARNING 没有找到符合条件的作品 <<< @/snippets/QA.md#no-matching-videos-found + +## EOF occurred in violation of protocol (_ssl.c:992) + +<<< @/snippets/QA.md#ssl-faild-01 + +## _ssl.c:975 The handshake operation timed out + +<<< @/snippets/QA.md#ssl-faild-02 + + +## tiktok 403 Forbidden + +<<< @/snippets/QA.md#tiktok-403-forbidden diff --git a/docs/snippets/QA.md b/docs/snippets/QA.md index 1a0b383c..b9b794a1 100644 --- a/docs/snippets/QA.md +++ b/docs/snippets/QA.md @@ -49,3 +49,44 @@ https://datatracker.ietf.org/doc/html/rfc6585#section-7.2 https://github.com/Johnserf-Seed/f2/issues/42 https://github.com/Johnserf-Seed/TikTokDownload/issues/660 // #endregion no-matching-videos-found + + +// #region ssl-faild-01 +出现`EOF occurred in violation of protocol (_ssl.c:992)`说明SSL握手失败。 + +解决办法: +1. 请检查你的网络环境是否支持SSL协议。 +2. 确保代理网络连接稳定。 + +这个问题可能涉及到多个方面,需要自己逐步排查和解决。 +// #endregion ssl-faild-01 + + +// #region ssl-faild-02 +出现`_ssl.c:975: The handshake operation timed out`说明SSL握手超时。可能是网络连接不稳定或延迟过高导致。 + +解决办法: +1. 检查网络连接: 确保网络连接稳定,尽量减少网络延迟。 +2. 检查服务器状态: 确保服务器运行正常,并且响应速度良好。 +3. 检查防火墙和代理设置: 确保防火墙和代理服务器的设置正确,并且不会影响 SSL/TLS 握手过程。 +4. 调整超时设置 + +这个问题可能涉及到多个方面,需要自己逐步排查和解决。 +// #endregion ssl-faild-02 + + +// #region tiktok-403-forbidden +当下载`tiktok`视频时出现`403 Forbidden`错误时,是由于`设备id`被封禁导致的。 + +`设备id`与生成的`cookie`是一一对应的,如果`设备id`被封禁,那么生成的`cookie`也会被封禁。 + +解决办法: +1. 运行生成`device_Id`的代码片段,获取新的`device_Id`。 +2. 将新的`device_Id`替换到配置文件中。 +3. 将新的`cookie`里的值替换到配置文件的`cookie`中(增量非覆盖)。 +4. 重新运行下载命令。 + +代码片段: +https://johnserf-seed.github.io/f2/guide/apps/tiktok/#%E7%94%9F%E6%88%90deviceid-%F0%9F%9F%A2 + +// #endregion tiktok-403-forbidden \ No newline at end of file diff --git a/docs/snippets/douyin/abogus.py b/docs/snippets/douyin/abogus.py new file mode 100644 index 00000000..e9954196 --- /dev/null +++ b/docs/snippets/douyin/abogus.py @@ -0,0 +1,83 @@ +// #region str-2-endpoint-snippet +# 使用接口地址直接生成请求链接 +import asyncio +from f2.apps.douyin.utils import ABogusManager, ClientConfManager + + +async def main(): + request = "GET" + test_endpoint = "device_platform=webapp&aid=6383&channel=channel_pc_web&aweme_id=7380308675841297704&update_version_code=170400&pc_client_type=1&version_code=190500&version_name=19.5.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=125.0.0.0&browser_online=true&engine_name=Blink&engine_version=125.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7376294349792396827" + return ABogusManager.str_2_endpoint( + ClientConfManager.user_agent(), + endpoint=test_endpoint, + request=request, + ) + + +if __name__ == "__main__": + print(asyncio.run(main())) + +// #endregion str-2-endpoint-snippet + + +// #region model-2-endpoint-snippet +# 使用用户信息模型生成请求链接 +import asyncio +from f2.apps.douyin.api import DouyinAPIEndpoints as dyendpoint +from f2.apps.douyin.model import UserProfile +from f2.apps.douyin.utils import ABogusManager, ClientConfManager + + +async def gen_user_profile(params: UserProfile): + return ABogusManager.model_2_endpoint( + ClientConfManager.user_agent(), + base_endpoint=dyendpoint.USER_DETAIL, + params=params.model_dump(), + request = "GET", + ) + + +async def main(): + sec_user_id="MS4wLjABAAAANXSltcLCzDGmdNFI2Q_QixVTr67NiYzjKOIP5s03CAE" + params = UserProfile(sec_user_id=sec_user_id) + return await gen_user_profile(params) + + +if __name__ == "__main__": + print(asyncio.run(main())) + +// #endregion model-2-endpoint-snippet + + +// #region model-2-endpoint-2-filter-snippet +# 使用用户信息模型生成请求链接,请求接口并使用自定义过滤器输出所需接口数据 +import asyncio +from f2.apps.douyin.api import DouyinAPIEndpoints as dyendpoint +from f2.apps.douyin.crawler import DouyinCrawler +from f2.apps.douyin.model import UserProfile +from f2.apps.douyin.filter import UserProfileFilter + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.douyin.com/", + }, + "proxies": {"http://": None, "https://": None}, + "cookie": "YOUR_COOKIE_HERE", +} + + +async def main(): + async with DouyinCrawler(kwargs) as crawler: + sec_user_id="MS4wLjABAAAANXSltcLCzDGmdNFI2Q_QixVTr67NiYzjKOIP5s03CAE" + params = UserProfile(sec_user_id=sec_user_id) + response = await crawler.fetch_user_profile(params) + user = UserProfileFilter(response) + # return user # user为UserProfileFilter对象,需要调用_to_dict()方法转为字典格式 + return user._to_dict() + +if __name__ == "__main__": + print(asyncio.run(main())) + +// #endregion model-2-endpoint-2-filter-snippet \ No newline at end of file diff --git a/docs/snippets/douyin/aweme-id.py b/docs/snippets/douyin/aweme-id.py index 9d2efd18..9734e471 100644 --- a/docs/snippets/douyin/aweme-id.py +++ b/docs/snippets/douyin/aweme-id.py @@ -2,10 +2,14 @@ import asyncio from f2.apps.douyin.utils import AwemeIdFetcher + async def main(): - raw_url = "https://www.douyin.com/video/7298145681699622182?previous_page=web_code_link" + raw_url = ( + "https://www.douyin.com/video/7298145681699622182?previous_page=web_code_link" + ) return await AwemeIdFetcher.get_aweme_id(raw_url) + if __name__ == "__main__": print(asyncio.run(main())) @@ -17,6 +21,7 @@ async def main(): from f2.apps.douyin.utils import AwemeIdFetcher from f2.utils.utils import extract_valid_urls + async def main(): raw_urls = [ "0.53 02/26 I@v.sE Fus:/ 你别太帅了郑润泽# 现场版live # 音乐节 # 郑润泽 https://v.douyin.com/iRNBho6u/ 复制此链接,打开Dou音搜索,直接观看视频!", @@ -32,6 +37,7 @@ async def main(): # 对于URL列表 return await AwemeIdFetcher.get_all_aweme_id(urls) + if __name__ == "__main__": print(asyncio.run(main())) diff --git a/docs/snippets/douyin/aweme-related.py b/docs/snippets/douyin/aweme-related.py new file mode 100644 index 00000000..c68d2ffc --- /dev/null +++ b/docs/snippets/douyin/aweme-related.py @@ -0,0 +1,28 @@ +import asyncio +from f2.apps.douyin.handler import DouyinHandler + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.douyin.com/", + }, + "cookie": "YOUR_COOKIE_HERE", + "proxies": {"http://": None, "https://": None}, +} + + +async def main(): + video = await DouyinHandler(kwargs).fetch_related_videos( + aweme_id="7294994585925848359" + ) + print("=================_to_raw================") + print(video._to_raw()) + # print("=================_to_dict================") + # print(video._to_dict()) + # print("=================_to_list================") + # print(video._to_list()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/snippets/douyin/client-config.py b/docs/snippets/douyin/client-config.py new file mode 100644 index 00000000..130bde62 --- /dev/null +++ b/docs/snippets/douyin/client-config.py @@ -0,0 +1,11 @@ +from f2.apps.douyin.utils import ClientConfManager + +if __name__ == "__main__": + print("Client Configuration:") + print(ClientConfManager.client()) + + print("Client Configuration version:") + print(ClientConfManager.conf_version()) + + print("Client Configuration user-agent:") + print(ClientConfManager.user_agent()) diff --git a/docs/snippets/douyin/format-file-name.py b/docs/snippets/douyin/format-file-name.py index 510cf80e..5980f071 100644 --- a/docs/snippets/douyin/format-file-name.py +++ b/docs/snippets/douyin/format-file-name.py @@ -7,7 +7,7 @@ async def main(): # 文件名模板 kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, "naming": "{create}_{desc}_{aweme_id}", @@ -21,10 +21,10 @@ async def main(): # 文件名模板 kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "naming": "{create}_{desc}_{aweme_id}_{location}", "cookie": "", } diff --git a/docs/snippets/douyin/json-2-lrc.py b/docs/snippets/douyin/json-2-lrc.py new file mode 100644 index 00000000..5b118b4f --- /dev/null +++ b/docs/snippets/douyin/json-2-lrc.py @@ -0,0 +1,94 @@ +from f2.apps.douyin.utils import json_2_lrc + + +data = [ + {"text": "CB on the beat,ho", "timeId": "5.700"}, + {"text": "Wasted CTA lovees wasted", "timeId": "10.210"}, + {"text": "Wasted I'm on these drugsI feel wasted", "timeId": "12.760"}, + {"text": "Wasted get her off my mind when I'm wasted", "timeId": "15.350"}, + {"text": "Wasted I'm waste all my time when I'm wasted", "timeId": "17.740"}, + {"text": "Wasted CTA lovees wasted", "timeId": "20.790"}, + {"text": "Wasted I'm on these drugsI feel wasted", "timeId": "22.900"}, + {"text": "Wasted get her off my mind when I'm wasted", "timeId": "25.850"}, + {"text": "Wasted I'm waste all my time when I'm wasted", "timeId": "28.210"}, + {"text": "Wasted", "timeId": "30.830"}, + {"text": "Damn why is she so demonic", "timeId": "31.320"}, + {"text": "She medusa with a little pocahontas", "timeId": "33.510"}, + {"text": "She been lacin all my drugs or sosomethin", "timeId": "36.150"}, + { + "text": "Cause every time that we're together I'm unconscious", + "timeId": "38.560", + }, + {"text": "Hold upuhlet me be honest", "timeId": "41.100"}, + {"text": "I know l saw her put the percs in my chronic", "timeId": "43.760"}, + {"text": "Smokintil my eyes roll back like the omen", "timeId": "46.370"}, + {"text": "Just another funeral for hergod damn", "timeId": "48.370"}, + {"text": "Wasted CTA lovees wasted", "timeId": "61.320"}, + {"text": "Wasted I'm on these drugsI feel wasted", "timeId": "63.890"}, + {"text": "Wasted get her off my mind when I'm wasted", "timeId": "66.400"}, + {"text": "Wasted I'm waste all my time when I'm wasted", "timeId": "68.970"}, + {"text": "Wasted", "timeId": "71.160"}, + {"text": "She do cocaine in my basement", "timeId": "72.170"}, + {"text": "I'm a doctorsbut I'm runninout of patience", "timeId": "74.270"}, + {"text": "She told me that she tryna get closer to satan", "timeId": "76.760"}, + {"text": "She be talkin to him when she in the matrix", "timeId": "79.450"}, + { + "text": "Rockstarthat's our stylethere boys can't take it", + "timeId": "81.770", + }, + {"text": "Hatin but they're still tryna take our cadence", "timeId": "83.930"}, + {"text": "No basicbrand new rari when I'm racin", "timeId": "86.870"}, + { + "text": "Take itlet you roll my weedplease don't lace ityeah", + "timeId": "89.340", + }, + {"text": "That's a bum that you chasinayy", "timeId": "92.330"}, + {"text": "Foreign with meshe a dominatrix", "timeId": "95.220"}, + {"text": "I love that girls and I do like her body", "timeId": "97.270"}, + {"text": "I don't what the moneyI just want the molly", "timeId": "98.820"}, + { + "text": "That's what she say when she livesd in the valley", + "timeId": "100.160", + }, + {"text": "Lil boyI'm your fatherhakuna matata", "timeId": "101.380"}, + {"text": "I made that girl girls all of that top up", "timeId": "102.220"}, + { + "text": "Got dreadrs in my headused to pray for the lock up", + "timeId": "103.360", + }, + { + "text": "I htit from the back and my legs start to lock up", + "timeId": "104.850", + }, + {"text": "Jacuzzi thar bootyI gave that girl flakka", "timeId": "106.540"}, + {"text": "I'm talkinblue caps that keep tweakinmy chakra", "timeId": "107.520"}, + {"text": "Rose on my chainthere's no hint like no copper", "timeId": "108.860"}, + {"text": "Take in the middle my head like I'm avatar", "timeId": "110.190"}, + {"text": "That's the reason that I ride on my appas", "timeId": "111.510"}, + {"text": "Wasted", "timeId": "112.710"}, + {"text": "WastedGTA lovees wasted", "timeId": "122.290"}, + {"text": "WastedI'm on these drugsI feel wasted", "timeId": "124.800"}, + {"text": "Wastedget her off my mind when I'm wasted", "timeId": "127.380"}, + {"text": "WastedI waste all my time when I'm wasted", "timeId": "130.120"}, + {"text": "My eyes closedhopinthis ain't makebelieve", "timeId": "132.850"}, + { + "text": "And she don't know hate all her demons like in me", + "timeId": "135.150", + }, + {"text": "L don't know l don't know", "timeId": "137.730"}, + {"text": "Don't know what she been onI don't know", "timeId": "143.870"}, + {"text": "All that lean l ain't have to let her in", "timeId": "146.470"}, + { + "text": "She ain't take my heart,but she took my medicine", + "timeId": "148.580", + }, + {"text": "Least somebody gon'take lthate to waste it", "timeId": "151.330"}, + {"text": "WastedGTA lovees wasted", "timeId": "152.980"}, + {"text": "WastedI'm on these drugsI feel wasted", "timeId": "155.610"}, + {"text": "Wastedget her off my mind when I'm wasted", "timeId": "158.070"}, + {"text": "WastedI waste all my time when I'm wasted", "timeId": "160.820"}, +] + + +if __name__ == "__main__": + print(json_2_lrc(data)) diff --git a/docs/snippets/douyin/mix-id.py b/docs/snippets/douyin/mix-id.py new file mode 100644 index 00000000..8fc88b4e --- /dev/null +++ b/docs/snippets/douyin/mix-id.py @@ -0,0 +1,39 @@ +// #region single-mix-id-snippet +import asyncio +from f2.apps.douyin.utils import MixIdFetcher + + +async def main(): + raw_url = "https://www.douyin.com/collection/7360898383181809676" + return await MixIdFetcher.get_mix_id(raw_url) + + +if __name__ == "__main__": + print(asyncio.run(main())) + +// #endregion single-mix-id-snippet + + +// #region multi-mix-id-snippet +import asyncio +from f2.apps.douyin.utils import MixIdFetcher +from f2.utils.utils import extract_valid_urls + + +async def main(): + raw_urls = [ + "https://www.douyin.com/collection/7360898383181809676", + "https://www.douyin.com/collection/7270895771149404160", + ] + + # 提取有效URL + urls = extract_valid_urls(raw_urls) + + # 对于URL列表 + return await MixIdFetcher.get_all_mix_id(urls) + + +if __name__ == "__main__": + print(asyncio.run(main())) + +// #endregion multi-mix-id-snippet \ No newline at end of file diff --git a/docs/snippets/douyin/one-video.py b/docs/snippets/douyin/one-video.py index a608f413..918e427a 100644 --- a/docs/snippets/douyin/one-video.py +++ b/docs/snippets/douyin/one-video.py @@ -3,11 +3,11 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, "cookie": "YOUR_COOKIE_HERE", + "proxies": {"http://": None, "https://": None}, } diff --git a/docs/snippets/douyin/query-user.py b/docs/snippets/douyin/query-user.py new file mode 100644 index 00000000..923f3526 --- /dev/null +++ b/docs/snippets/douyin/query-user.py @@ -0,0 +1,26 @@ +import asyncio +from f2.apps.douyin.handler import DouyinHandler +from f2.apps.douyin.utils import TokenManager + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", + "Referer": "https://www.douyin.com/", + }, + "proxies": {"http://": None, "https://": None}, + "timeout": 10, + "cookie": f"ttwid={TokenManager.gen_ttwid()}", +} + + +async def main(): + user = await DouyinHandler(kwargs).fetch_query_user() + print("=================_to_raw================") + print(user._to_raw()) + # print("=================_to_dict===============") + # print(user._to_dict()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/snippets/douyin/sso-login.py b/docs/snippets/douyin/sso-login.py index 0110e214..d4a7a4ab 100644 --- a/docs/snippets/douyin/sso-login.py +++ b/docs/snippets/douyin/sso-login.py @@ -1,5 +1,5 @@ -import asyncio -from f2.apps.douyin.handler import handle_sso_login +# import asyncio +# from f2.apps.douyin.handler import handle_sso_login -if __name__ == "__main__": - asyncio.run(handle_sso_login()) +# if __name__ == "__main__": +# asyncio.run(handle_sso_login()) diff --git a/docs/snippets/douyin/user-collection.py b/docs/snippets/douyin/user-collection.py index a296c22b..2a135de9 100644 --- a/docs/snippets/douyin/user-collection.py +++ b/docs/snippets/douyin/user-collection.py @@ -1,13 +1,46 @@ +// #region user-collection-music-snippet import asyncio from f2.apps.douyin.handler import DouyinHandler + kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, "cookie": "YOUR_COOKIE_HERE", + "proxies": {"http://": None, "https://": None}, + "timeout": 10, +} + + +async def main(): + async for music_data_list in DouyinHandler(kwargs).fetch_user_music_collection(): + print("=================_to_raw================") + print(music_data_list._to_raw()) + # print("=================_to_dict===============") + # print(music_data_list._to_dict()) + # print("=================_to_list===============") + # print(music_data_list._to_list()) + + +if __name__ == "__main__": + asyncio.run(main()) + +// #endregion user-collection-music-snippet + + +// #region user-collection-video-snippet +import asyncio +from f2.apps.douyin.handler import DouyinHandler + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.douyin.com/", + }, + "cookie": "YOUR_COOKIE_HERE", + "proxies": {"http://": None, "https://": None}, "timeout": 10, } @@ -24,3 +57,5 @@ async def main(): if __name__ == "__main__": asyncio.run(main()) + +// #endregion user-collection-video-snippet \ No newline at end of file diff --git a/docs/snippets/douyin/user-collects.py b/docs/snippets/douyin/user-collects.py index fb609d1a..b73e5c7f 100644 --- a/docs/snippets/douyin/user-collects.py +++ b/docs/snippets/douyin/user-collects.py @@ -1,12 +1,44 @@ +// #region user-collects-snippet import asyncio from f2.apps.douyin.handler import DouyinHandler + kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, + "timeout": 10, + "cookie": "YOUR_COOKIE_HERE", +} + + +async def main(): + async for collects in DouyinHandler(kwargs).fetch_user_collects(0, 10, 20): + print("=================_to_raw================") + print(collects._to_raw()) + # print("=================_to_dict===============") + # print(collects._to_dict()) + + +if __name__ == "__main__": + asyncio.run(main()) + +// #endregion user-collects-snippet + + +// #region user-collects-videos-snippet +import asyncio +from f2.apps.douyin.handler import DouyinHandler + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.douyin.com/", + }, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } @@ -27,3 +59,5 @@ async def main(): if __name__ == "__main__": asyncio.run(main()) + +// #endregion user-collects-videos-snippet \ No newline at end of file diff --git a/docs/snippets/douyin/user-feed.py b/docs/snippets/douyin/user-feed.py new file mode 100644 index 00000000..01be14f8 --- /dev/null +++ b/docs/snippets/douyin/user-feed.py @@ -0,0 +1,30 @@ +import asyncio +from f2.apps.douyin.handler import DouyinHandler + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.douyin.com/", + }, + "proxies": {"http://": None, "https://": None}, + "timeout": 10, + "cookie": "YOUR_COOKIE_HERE", +} + + +async def main(): + sec_user_id = "" # 用户ID + async for feeds in DouyinHandler(kwargs).fetch_user_feed_videos( + sec_user_id, 0, 10, 20 + ): + print("=================_to_raw================") + print(feeds._to_raw()) + # print("=================_to_dict===============") + # print(feeds._to_dict()) + # print("=================_to_list===============") + # print(feeds._to_list()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/snippets/douyin/user-folder.py b/docs/snippets/douyin/user-folder.py index 7770db61..a9271c8f 100644 --- a/docs/snippets/douyin/user-folder.py +++ b/docs/snippets/douyin/user-folder.py @@ -4,7 +4,7 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, "proxies": {"http": None, "https": None}, @@ -31,10 +31,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", "path": "Download", "mode": "post", diff --git a/docs/snippets/douyin/user-follow-live.py b/docs/snippets/douyin/user-follow-live.py new file mode 100644 index 00000000..b76fefc4 --- /dev/null +++ b/docs/snippets/douyin/user-follow-live.py @@ -0,0 +1,26 @@ +import asyncio +from f2.apps.douyin.handler import DouyinHandler + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.douyin.com/", + }, + "proxies": {"http://": None, "https://": None}, + "cookie": "YOUR_COOKIE_HERE", +} + + +async def main(): + follow_live = await DouyinHandler(kwargs).fetch_following_live() + print("=================_to_raw================") + print(follow_live._to_raw()) + # print("=================_to_dict===============") + # print(follow_live._to_dict()) + # print("=================_to_list===============") + # print(follow_live._to_list()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/snippets/douyin/user-follower.py b/docs/snippets/douyin/user-follower.py index f0e6a665..fa615a71 100644 --- a/docs/snippets/douyin/user-follower.py +++ b/docs/snippets/douyin/user-follower.py @@ -2,25 +2,27 @@ from f2.log.logger import logger from f2.apps.douyin.handler import DouyinHandler + kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": { - "http": None, - "https": None, - }, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } async def main(): + user_id = "" # 公开粉丝的账号 sec_user_id = "" # 公开粉丝的账号 # sec_user_id = "MS4wLjABAAAAGPm-wPeGQuziCu5z6KerQA7WmSTnS99c8lU8WLToB0BsN02mqbPxPuxwDjKf7udZ" # 隐私设置的账号 + + # 至少提供 user_id 或 sec_user_id 中的一个参数 # 根据max_time 和 min_time 区间获取用户粉丝列表 async for follower in DouyinHandler(kwargs).fetch_user_follower( + user_id=user_id, sec_user_id=sec_user_id, # max_time=1668606509, # min_time=0, diff --git a/docs/snippets/douyin/user-following.py b/docs/snippets/douyin/user-following.py index 9fa39f1b..8ab56550 100644 --- a/docs/snippets/douyin/user-following.py +++ b/docs/snippets/douyin/user-following.py @@ -2,25 +2,30 @@ from f2.log.logger import logger from f2.apps.douyin.handler import DouyinHandler + kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": { - "http": None, - "https": None, - }, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } async def main(): + user_id = "" # 公开关注的账号 sec_user_id = "" # 公开关注的账号 # sec_user_id = "MS4wLjABAAAAGPm-wPeGQuziCu5z6KerQA7WmSTnS99c8lU8WLToB0BsN02mqbPxPuxwDjKf7udZ" # 隐私设置的账号 + + # 至少提供 user_id 或 sec_user_id 中的一个参数 + # 根据 max_time 和 min_time 区间获取关注用户列表 async for following in DouyinHandler(kwargs).fetch_user_following( - sec_user_id=sec_user_id + user_id=user_id, + sec_user_id=sec_user_id, + # max_time=1668606509, + # min_time=0, ): if following.status_code != 0: logger.error_("错误代码:{0} 错误消息:{1}").format( diff --git a/docs/snippets/douyin/user-friend.py b/docs/snippets/douyin/user-friend.py new file mode 100644 index 00000000..7ec23272 --- /dev/null +++ b/docs/snippets/douyin/user-friend.py @@ -0,0 +1,26 @@ +import asyncio +from f2.apps.douyin.handler import DouyinHandler + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.douyin.com/", + }, + "proxies": {"http://": None, "https://": None}, + "cookie": "YOUR_COOKIE_HERE", +} + + +async def main(): + friend_video_list = await DouyinHandler(kwargs).fetch_friend_feed_videos() + print("=================_to_raw================") + print(friend_video_list._to_raw()) + # print("=================_to_dict===============") + # print(friend_video_list._to_dict()) + # print("=================_to_list===============") + # print(friend_video_list._to_list()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/snippets/douyin/user-get-add.py b/docs/snippets/douyin/user-get-add.py index 13b4cb24..f75441b1 100644 --- a/docs/snippets/douyin/user-get-add.py +++ b/docs/snippets/douyin/user-get-add.py @@ -4,10 +4,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", "path": "Download", } diff --git a/docs/snippets/douyin/user-like.py b/docs/snippets/douyin/user-like.py index e48775db..40539098 100644 --- a/docs/snippets/douyin/user-like.py +++ b/docs/snippets/douyin/user-like.py @@ -3,12 +3,12 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, "cookie": "YOUR_COOKIE_HERE", "timeout": 10, + "proxies": {"http://": None, "https://": None}, } diff --git a/docs/snippets/douyin/user-live-im-fetch.py b/docs/snippets/douyin/user-live-im-fetch.py new file mode 100644 index 00000000..e94e087d --- /dev/null +++ b/docs/snippets/douyin/user-live-im-fetch.py @@ -0,0 +1,54 @@ +import asyncio +from f2.apps.douyin.handler import DouyinHandler + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.douyin.com/", + "Content-Type": "application/protobuffer;", + }, + "proxies": {"http://": None, "https://": None}, + "timeout": 10, + "cookie": "YOUR_COOKIE_HERE", +} + + +kwargs2 = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Upgrade": "websocket", + "Connection": "Upgrade", + }, + "proxies": {"http://": None, "https://": None}, + "timeout": 10, + "cookie": "DO_NOT_USE_COOKIE_HERE", +} + + +async def main(): + # 获取游客ttwid的user_unique_id,你可以通过TokenManager.gen_ttwid()生成新的游客ttwid + user = await DouyinHandler(kwargs).fetch_query_user() + # print("游客user_unique_id:", user.user_unique_id) + + # 通过此接口获取room_id,参数为live_id + room = await DouyinHandler(kwargs).fetch_user_live_videos("662122193366") + # print("直播间ID:", room.room_id) + + # 通过该接口获取wss所需的cursor和internal_ext + live_im = await DouyinHandler(kwargs).fetch_live_im( + room_id=room.room_id, unique_id=user.user_unique_id + ) + # print("直播间IM页码:", live_im.cursor, "直播间IM扩展:", live_im.internal_ext) + + # 获取直播弹幕 + await DouyinHandler(kwargs2).fetch_live_danmaku( + room_id=room.room_id, + user_unique_id=user.user_unique_id, + internal_ext=live_im.internal_ext, + cursor=live_im.cursor, + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/snippets/douyin/user-live-room-id.py b/docs/snippets/douyin/user-live-room-id.py index 5c427dcf..e6f5bb6e 100644 --- a/docs/snippets/douyin/user-live-room-id.py +++ b/docs/snippets/douyin/user-live-room-id.py @@ -3,10 +3,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/douyin/user-live.py b/docs/snippets/douyin/user-live.py index dba3acfd..2a5c802c 100644 --- a/docs/snippets/douyin/user-live.py +++ b/docs/snippets/douyin/user-live.py @@ -3,10 +3,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/douyin/user-mix.py b/docs/snippets/douyin/user-mix.py index 8e255205..24352f01 100644 --- a/docs/snippets/douyin/user-mix.py +++ b/docs/snippets/douyin/user-mix.py @@ -3,10 +3,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/douyin/user-nickname.py b/docs/snippets/douyin/user-nickname.py deleted file mode 100644 index 518f1c3f..00000000 --- a/docs/snippets/douyin/user-nickname.py +++ /dev/null @@ -1,26 +0,0 @@ -import asyncio -from f2.apps.douyin.handler import DouyinHandler -from f2.apps.douyin.db import AsyncUserDB - -kwargs = { - "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", - "Referer": "https://www.douyin.com/", - }, - "proxies": {"http": None, "https": None}, - "cookie": "YOUR_COOKIE_HERE", -} - - -async def main(): - async with AsyncUserDB("douyin_users.db") as audb: - sec_user_id = "MS4wLjABAAAANXSltcLCzDGmdNFI2Q_QixVTr67NiYzjKOIP5s03CAE" - print( - await DouyinHandler(kwargs).get_user_nickname( - sec_user_id=sec_user_id, db=audb - ) - ) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/docs/snippets/douyin/user-post.py b/docs/snippets/douyin/user-post.py index f4130e51..9bdb0856 100644 --- a/docs/snippets/douyin/user-post.py +++ b/docs/snippets/douyin/user-post.py @@ -3,10 +3,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/douyin/user-profile.py b/docs/snippets/douyin/user-profile.py index 5bfe045a..14968eb2 100644 --- a/docs/snippets/douyin/user-profile.py +++ b/docs/snippets/douyin/user-profile.py @@ -3,17 +3,17 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } async def main(): sec_user_id = "MS4wLjABAAAANXSltcLCzDGmdNFI2Q_QixVTr67NiYzjKOIP5s03CAE" - user = await DouyinHandler(kwargs).handler_user_profile(sec_user_id=sec_user_id) + user = await DouyinHandler(kwargs).fetch_user_profile(sec_user_id=sec_user_id) print("=================_to_raw================") print(user._to_raw()) # print("=================_to_dict===============") diff --git a/docs/snippets/douyin/video-get-add.py b/docs/snippets/douyin/video-get-add.py index 1829cbe8..44dcf26f 100644 --- a/docs/snippets/douyin/video-get-add.py +++ b/docs/snippets/douyin/video-get-add.py @@ -7,10 +7,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/douyin/webcast-id.py b/docs/snippets/douyin/webcast-id.py index c360ec30..b161de44 100644 --- a/docs/snippets/douyin/webcast-id.py +++ b/docs/snippets/douyin/webcast-id.py @@ -2,10 +2,12 @@ import asyncio from f2.apps.douyin.utils import WebCastIdFetcher + async def main(): raw_url = "https://live.douyin.com/775841227732" return await WebCastIdFetcher.get_webcast_id(raw_url) + if __name__ == "__main__": print(asyncio.run(main())) @@ -17,6 +19,7 @@ async def main(): from f2.apps.douyin.utils import WebCastIdFetcher from f2.utils.utils import extract_valid_urls + async def main(): raw_urls = [ "https://live.douyin.com/775841227732", @@ -32,6 +35,7 @@ async def main(): # 对于URL列表 return await WebCastIdFetcher.get_all_webcast_id(urls) + if __name__ == "__main__": print(asyncio.run(main())) diff --git a/docs/snippets/douyin/webcast-signature.py b/docs/snippets/douyin/webcast-signature.py new file mode 100644 index 00000000..22174085 --- /dev/null +++ b/docs/snippets/douyin/webcast-signature.py @@ -0,0 +1,36 @@ +// #region webcast-signature-snippet +from f2.apps.douyin.algorithm.webcast_signature import DouyinWebcastSignature + +if __name__ == "__main__": + room_id = "7383573503129258802" + user_unique_id = "7383588170770138661" + signature = DouyinWebcastSignature( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0" + ).get_signature(room_id, user_unique_id) + print(signature) + +// #endregion webcast-signature-snippet + + +// #region webcast-signature-manager-snippet +# fetch_live_danmaku + +import asyncio +from f2.apps.douyin.api import DouyinAPIEndpoints as dyendpoint +from f2.apps.douyin.model import LiveWebcast +from f2.apps.douyin.utils import WebcastSignatureManager, ClientConfManager + + +async def main(params: LiveWebcast): + final_endpoint = WebcastSignatureManager.model_2_endpoint( + user_agent=ClientConfManager.user_agent(), + base_endpoint=dyendpoint.LIVE_IM_WSS, + params=params.model_dump(), + ) + return final_endpoint + + +if __name__ == "__main__": + print(asyncio.run(main())) + +// #endregion webcast-signature-manager-snippet diff --git a/docs/snippets/douyin/webid.py b/docs/snippets/douyin/webid.py new file mode 100644 index 00000000..056ae598 --- /dev/null +++ b/docs/snippets/douyin/webid.py @@ -0,0 +1,4 @@ +from f2.apps.douyin.utils import TokenManager + +if __name__ == "__main__": + print("douyin webid:", TokenManager.gen_webid()) diff --git a/docs/snippets/douyin/xbogus.py b/docs/snippets/douyin/xbogus.py index d57f16f3..a4bcf9a4 100644 --- a/docs/snippets/douyin/xbogus.py +++ b/docs/snippets/douyin/xbogus.py @@ -1,11 +1,16 @@ // #region str-2-endpoint-snippet # 使用接口地址直接生成请求链接 import asyncio -from f2.apps.douyin.utils import XBogusManager +from f2.apps.douyin.utils import XBogusManager, ClientConfManager + async def main(): test_endpoint = "aweme_id=7196239141472980280&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333" - return XBogusManager.str_2_endpoint(test_endpoint) + return XBogusManager.str_2_endpoint( + ClientConfManager.user_agent(), + endpoint=test_endpoint, + ) + if __name__ == "__main__": print(asyncio.run(main())) @@ -18,18 +23,23 @@ async def main(): import asyncio from f2.apps.douyin.api import DouyinAPIEndpoints as dyendpoint from f2.apps.douyin.model import UserProfile -from f2.apps.douyin.utils import XBogusManager +from f2.apps.douyin.utils import XBogusManager, ClientConfManager + async def gen_user_profile(params: UserProfile): return XBogusManager.model_2_endpoint( - dyendpoint.USER_DETAIL, params.dict() + ClientConfManager.user_agent(), + base_endpoint=dyendpoint.USER_DETAIL, + params=params.model_dump(), ) + async def main(): sec_user_id="MS4wLjABAAAANXSltcLCzDGmdNFI2Q_QixVTr67NiYzjKOIP5s03CAE" params = UserProfile(sec_user_id=sec_user_id) return await gen_user_profile(params) + if __name__ == "__main__": print(asyncio.run(main())) @@ -43,15 +53,15 @@ async def main(): from f2.apps.douyin.crawler import DouyinCrawler from f2.apps.douyin.model import UserProfile from f2.apps.douyin.filter import UserProfileFilter -from f2.apps.douyin.utils import XBogusManager +from f2.apps.douyin.utils import XBogusManager, ClientConfManager kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/set-debug.py b/docs/snippets/set-debug.py index b73e8e7e..5a08cc28 100644 --- a/docs/snippets/set-debug.py +++ b/docs/snippets/set-debug.py @@ -15,10 +15,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } @@ -45,10 +45,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.douyin.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/tiktok/aweme-id.py b/docs/snippets/tiktok/aweme-id.py index 489b5e0b..94e1d838 100644 --- a/docs/snippets/tiktok/aweme-id.py +++ b/docs/snippets/tiktok/aweme-id.py @@ -2,11 +2,13 @@ import asyncio from f2.apps.tiktok.utils import AwemeIdFetcher + async def main(): raw_url = "https://www.tiktok.com/@vantoan___/video/7283528426256911649" - # 对于单个URL + # 支持短链解析但其具有时效性,故不举例 return await AwemeIdFetcher.get_aweme_id(raw_url) + if __name__ == "__main__": print(asyncio.run(main())) @@ -18,12 +20,12 @@ async def main(): from f2.apps.tiktok.utils import AwemeIdFetcher from f2.utils.utils import extract_valid_urls + async def main(): raw_urls = [ "https://www.tiktok.com/@vantoan___/video/7316948869764484384", "https://www.tiktok.com/@vantoan___/video/7316948869764484384?is_from_webapp=1&sender_device=pc&web_id=7306060721837852167", - # "https://vt.tiktok.com/xxxxxxxxxx/", - "https://www.tiktok.com/t/ZT8mxMcYh/" + # 支持短链解析但其具有时效性,故不举例 ] # 提取有效URL @@ -32,6 +34,7 @@ async def main(): # 对于URL列表 return await AwemeIdFetcher.get_all_aweme_id(urls) + if __name__ == "__main__": print(asyncio.run(main())) diff --git a/docs/snippets/tiktok/check-live-alive.py b/docs/snippets/tiktok/check-live-alive.py new file mode 100644 index 00000000..6da0b301 --- /dev/null +++ b/docs/snippets/tiktok/check-live-alive.py @@ -0,0 +1,33 @@ +import asyncio +from f2.apps.tiktok.handler import TiktokHandler + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", + "Referer": "https://www.tiktok.com/", + }, + "proxies": { + "http://": None, + "https://": None, + }, + "timeout": 10, + "cookie": "YOUR_COOKIE_HERE", +} + + +async def main(): + rooms = await TiktokHandler(kwargs).fetch_check_live_alive( + room_ids="7381444193462078214,7381457815116466949,7381456855157721863,7381439549143026438" + ) + + print("=================_to_raw================") + print(rooms._to_raw()) + # print("=================_to_dict===============") + # print(rooms._to_dict()) + # print("=================_to_list===============") + # print(rooms._to_list()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/snippets/tiktok/client-config.py b/docs/snippets/tiktok/client-config.py new file mode 100644 index 00000000..1a2446d0 --- /dev/null +++ b/docs/snippets/tiktok/client-config.py @@ -0,0 +1,11 @@ +from f2.apps.tiktok.utils import ClientConfManager + +if __name__ == "__main__": + print("Client Configuration:") + print(ClientConfManager.client()) + + print("Client Configuration version:") + print(ClientConfManager.conf_version()) + + print("Client Configuration user-agent:") + print(ClientConfManager.user_agent()) diff --git a/docs/snippets/tiktok/device-id.py b/docs/snippets/tiktok/device-id.py new file mode 100644 index 00000000..e43690d0 --- /dev/null +++ b/docs/snippets/tiktok/device-id.py @@ -0,0 +1,34 @@ +// #region device-id-snippet +import asyncio +from f2.apps.tiktok.utils import DeviceIdManager + + +async def main(): + device_id = await DeviceIdManager.gen_device_id() + print(device_id) + device_id = await DeviceIdManager.gen_device_id(full_cookie=True) + print(device_id) + + +if __name__ == "__main__": + asyncio.run(main()) + +// #endregion device-id-snippet + + +// #region device-ids-snippet +import asyncio +from f2.apps.tiktok.utils import DeviceIdManager + + +async def main(): + device_ids = await DeviceIdManager.gen_device_ids(3) + print(device_ids) + device_ids = await DeviceIdManager.gen_device_ids(3, full_cookie=True) + print(device_ids) + + +if __name__ == "__main__": + asyncio.run(main()) + +// #endregion device-ids-snippet \ No newline at end of file diff --git a/docs/snippets/tiktok/format-file-name.py b/docs/snippets/tiktok/format-file-name.py index 7ad5620c..2f5bb4c4 100644 --- a/docs/snippets/tiktok/format-file-name.py +++ b/docs/snippets/tiktok/format-file-name.py @@ -4,10 +4,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", "naming": "{create}_{desc}_{aweme_id}", } diff --git a/docs/snippets/tiktok/one-video.py b/docs/snippets/tiktok/one-video.py index fb264d2c..4b93b13f 100644 --- a/docs/snippets/tiktok/one-video.py +++ b/docs/snippets/tiktok/one-video.py @@ -3,10 +3,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/tiktok/sec-uid.py b/docs/snippets/tiktok/sec-uid.py index 953cf512..b9586c71 100644 --- a/docs/snippets/tiktok/sec-uid.py +++ b/docs/snippets/tiktok/sec-uid.py @@ -2,11 +2,13 @@ import asyncio from f2.apps.tiktok.utils import SecUserIdFetcher + async def main(): raw_url = "https://www.tiktok.com/@vantoan___" # 对于单个URL return await SecUserIdFetcher.get_secuid(raw_url) + if __name__ == "__main__": print(asyncio.run(main())) @@ -18,6 +20,7 @@ async def main(): from f2.apps.tiktok.utils import SecUserIdFetcher from f2.utils.utils import extract_valid_urls + async def main(): raw_urls = [ "https://www.tiktok.com/@vantoan___/", @@ -31,6 +34,7 @@ async def main(): # 对于URL列表 return await SecUserIdFetcher.get_all_secuid(urls) + if __name__ == "__main__": print(asyncio.run(main())) diff --git a/docs/snippets/tiktok/unique-id.py b/docs/snippets/tiktok/unique-id.py index db38226b..e859b597 100644 --- a/docs/snippets/tiktok/unique-id.py +++ b/docs/snippets/tiktok/unique-id.py @@ -2,11 +2,13 @@ import asyncio from f2.apps.tiktok.utils import SecUserIdFetcher + async def main(): raw_url = "https://www.tiktok.com/@vantoan___" # 对于单个URL return await SecUserIdFetcher.get_uniqueid(raw_url) + if __name__ == "__main__": print(asyncio.run(main())) @@ -18,6 +20,7 @@ async def main(): from f2.apps.tiktok.utils import SecUserIdFetcher from f2.utils.utils import extract_valid_urls + async def main(): raw_urls = [ "https://www.tiktok.com/@vantoan___/", @@ -31,6 +34,7 @@ async def main(): # 对于URL列表 return await SecUserIdFetcher.get_all_uniqueid(urls) + if __name__ == "__main__": print(asyncio.run(main())) diff --git a/docs/snippets/tiktok/user-collect.py b/docs/snippets/tiktok/user-collect.py index effeac21..837adafe 100644 --- a/docs/snippets/tiktok/user-collect.py +++ b/docs/snippets/tiktok/user-collect.py @@ -4,10 +4,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/tiktok/user-folder.py b/docs/snippets/tiktok/user-folder.py index 94cc72cb..e4bc5559 100644 --- a/docs/snippets/tiktok/user-folder.py +++ b/docs/snippets/tiktok/user-folder.py @@ -4,10 +4,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", "path": "Download", "mode": "post", @@ -31,10 +31,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", "path": "Download", "mode": "post", diff --git a/docs/snippets/tiktok/user-get-add.py b/docs/snippets/tiktok/user-get-add.py index 3b3ab49a..8c30c981 100644 --- a/docs/snippets/tiktok/user-get-add.py +++ b/docs/snippets/tiktok/user-get-add.py @@ -4,10 +4,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", "path": "Download", } diff --git a/docs/snippets/tiktok/user-like.py b/docs/snippets/tiktok/user-like.py index 4517fb67..6107eade 100644 --- a/docs/snippets/tiktok/user-like.py +++ b/docs/snippets/tiktok/user-like.py @@ -4,10 +4,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/tiktok/user-mix.py b/docs/snippets/tiktok/user-mix.py index bf486de9..81a881b9 100644 --- a/docs/snippets/tiktok/user-mix.py +++ b/docs/snippets/tiktok/user-mix.py @@ -6,10 +6,10 @@ kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } @@ -40,10 +40,10 @@ async def main(): kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/tiktok/user-nickname.py b/docs/snippets/tiktok/user-nickname.py deleted file mode 100644 index 31246a99..00000000 --- a/docs/snippets/tiktok/user-nickname.py +++ /dev/null @@ -1,24 +0,0 @@ -import asyncio -from f2.apps.tiktok.handler import TiktokHandler -from f2.apps.tiktok.db import AsyncUserDB - -kwargs = { - "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", - "Referer": "https://www.tiktok.com/", - }, - "proxies": {"http": None, "https": None}, - "cookie": "YOUR_COOKIE_HERE", -} - - -async def main(): - async with AsyncUserDB("tiktok_users.db") as audb: - secUid = "MS4wLjABAAAAQhcYf_TjRKUku-aF8oqngAfzrYksgGLRz8CKMciBFdfR54HQu3qGs-WoJ-KO7hO8" - print(await TiktokHandler(kwargs).get_user_nickname(secUid=secUid, db=audb)) - secUid = "MS4wLjABAAAAQeB9NnG9Sz6yDPlmm3zM891Qk4E66_CvHfSRGkDIX5Y" - print(await TiktokHandler(kwargs).get_user_nickname(secUid=secUid, db=audb)) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/docs/snippets/tiktok/user-playlist.py b/docs/snippets/tiktok/user-playlist.py index 8418e7ce..257a9d34 100644 --- a/docs/snippets/tiktok/user-playlist.py +++ b/docs/snippets/tiktok/user-playlist.py @@ -2,12 +2,13 @@ from f2.apps.tiktok.handler import TiktokHandler from f2.apps.tiktok.utils import SecUserIdFetcher + kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/tiktok/user-post.py b/docs/snippets/tiktok/user-post.py index 38cb3dcf..c829556f 100644 --- a/docs/snippets/tiktok/user-post.py +++ b/docs/snippets/tiktok/user-post.py @@ -2,12 +2,13 @@ from f2.apps.tiktok.handler import TiktokHandler from f2.apps.tiktok.utils import SecUserIdFetcher + kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "timeout": 10, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/tiktok/user-profile.py b/docs/snippets/tiktok/user-profile.py index 5819a3e6..7d82f0bd 100644 --- a/docs/snippets/tiktok/user-profile.py +++ b/docs/snippets/tiktok/user-profile.py @@ -1,13 +1,14 @@ import asyncio from f2.apps.tiktok.handler import TiktokHandler + kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, "cookie": "YOUR_COOKIE_HERE", + "proxies": {"http://": None, "https://": None}, } @@ -16,13 +17,13 @@ async def main(): "MS4wLjABAAAAQhcYf_TjRKUku-aF8oqngAfzrYksgGLRz8CKMciBFdfR54HQu3qGs-WoJ-KO7hO8" ) uniqueId = "vantoan___" - user = await TiktokHandler(kwargs).handler_user_profile(secUid=secUid) + user = await TiktokHandler(kwargs).fetch_user_profile(secUid=secUid) print("=================_to_raw================") print(user._to_raw()) # print("=================_to_dict===============") # print(user._to_dict()) - user = await TiktokHandler(kwargs).handler_user_profile(uniqueId=uniqueId) + user = await TiktokHandler(kwargs).fetch_user_profile(uniqueId=uniqueId) print("=================_to_raw================") print(user._to_raw()) # print("=================_to_dict===============") diff --git a/docs/snippets/tiktok/video-get-add.py b/docs/snippets/tiktok/video-get-add.py index 34c6d33e..a96a1b4b 100644 --- a/docs/snippets/tiktok/video-get-add.py +++ b/docs/snippets/tiktok/video-get-add.py @@ -2,15 +2,16 @@ from f2.apps.tiktok.handler import TiktokHandler from f2.apps.tiktok.db import AsyncVideoDB + # 需要忽略的字段(需过滤掉有时效性的字段) ignore_fields = ["video_play_addr", "images", "video_bit_rate", "cover"] kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/tiktok/xbogus.py b/docs/snippets/tiktok/xbogus.py index b8a6e144..69b0669b 100644 --- a/docs/snippets/tiktok/xbogus.py +++ b/docs/snippets/tiktok/xbogus.py @@ -3,6 +3,7 @@ import asyncio from f2.apps.tiktok.utils import XBogusManager + async def main(): test_endpoint = "aid=1988&app_language=zh-Hans&app_name=tiktok_web&browser_platform=Win32&browser_version=5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F120.0.0.0%20Safari%2F537.36&channel=tiktok_web&cookie_enabled=true&count=16&coverFormat=2&device_id=7306060721837852167&device_platform=web_pc&itemID=7294298719665622305" return XBogusManager.str_2_endpoint(test_endpoint) @@ -22,7 +23,7 @@ async def main(): async def gen_user_profile(params: UserProfile): return XBogusManager.model_2_endpoint( - tkendpoint.USER_DETAIL, params.dict() + tkendpoint.USER_DETAIL, params.model_dump() ) async def main(): @@ -47,10 +48,10 @@ async def main(): kwargs = { "headers": { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", "Referer": "https://www.tiktok.com/", }, - "proxies": {"http": None, "https": None}, + "proxies": {"http://": None, "https://": None}, "cookie": "YOUR_COOKIE_HERE", } diff --git a/docs/snippets/weibo/user-profile.py b/docs/snippets/weibo/user-profile.py new file mode 100644 index 00000000..c2a17b4a --- /dev/null +++ b/docs/snippets/weibo/user-profile.py @@ -0,0 +1,24 @@ +import asyncio +from f2.apps.weibo.handler import WeiboHandler +from f2.log.logger import logger + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.weibo.com/", + }, + "proxies": {"http://": None, "https://": None}, + "cookie": "YOUR_COOKIE_HERE", +} + + +async def main(): + user = await WeiboHandler(kwargs).fetch_user_info(uid="2265830070") + logger.info( + f"微博用户ID: {user.uid}, 昵称: {user.nickname}, 性别: {user.gender}, 地区: {user.location}, 关注数: {user.friends_count}, 粉丝数: {user.followers_count}, 微博数: {user.weibo_count}, 个人主页: {user.profile_url}" + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/snippets/weibo/user-weibo.py b/docs/snippets/weibo/user-weibo.py new file mode 100644 index 00000000..58790834 --- /dev/null +++ b/docs/snippets/weibo/user-weibo.py @@ -0,0 +1,24 @@ +import asyncio +from f2.apps.weibo.handler import WeiboHandler +from f2.log.logger import logger + + +kwargs = { + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + "Referer": "https://www.weibo.com/", + }, + "proxies": {"http://": None, "https://": None}, + "cookie": "YOUR_COOKIE_HERE", +} + + +async def main(): + weibo = await WeiboHandler(kwargs).fetch_one_weibo(weibo_id="O8DM0BLLm") + logger.info( + f"微博ID: {weibo.weibo_id}, 微博文案: {weibo.desc}, 作者昵称: {weibo.nickname}, 发布时间: {weibo.create_time}" + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/f2/__init__.py b/f2/__init__.py index e774a0ea..1dedceae 100644 --- a/f2/__init__.py +++ b/f2/__init__.py @@ -1,5 +1,7 @@ +# path: f2/__init__.py + __author__ = "JohnserfSeed " -__version__ = "0.0.1.5" +__version__ = "0.0.1.6" __description_cn__ = "基于[red]异步[/red]的[green]全平台下载工具." __description_en__ = "[yellow]Asynchronous based [/yellow]full-platform download tool." __reponame__ = "f2" @@ -32,6 +34,32 @@ "music", "mix", "live", + "related", + "friend", +] + +TIKTOK_MODE_LIST = [ + "one", + "post", + "like", + "collect", + "mix", + "search", + "live", +] + +WEIBO_MODE_LIST = [ + "one", + "post", + "like", +] + +TWITTER_MODE_LIST = [ + "one", + "post", + "retweet", + "like", + "bookmark", ] -TIKTOK_MODE_LIST = ["one", "post", "like", "collect", "mix"] +PYPI_URL = "https://pypi.org/pypi" diff --git a/f2/__main__.py b/f2/__main__.py index 8c15d876..cab518b0 100644 --- a/f2/__main__.py +++ b/f2/__main__.py @@ -1,4 +1,5 @@ +# path: f2/__main__.py from f2.cli.cli_commands import main -main() \ No newline at end of file +main() diff --git a/f2/apps/douyin/algorithm/webcast_signature.js b/f2/apps/douyin/algorithm/webcast_signature.js new file mode 100644 index 00000000..62d3600c --- /dev/null +++ b/f2/apps/douyin/algorithm/webcast_signature.js @@ -0,0 +1,7102 @@ +/*! + * File: webcast_signature.js + * Author: Johnserf-Seed + * Created: 2024-06-23 + * Update: 2024-06-23 + * Version: 1.0.0 + * Description: + * The source file is from douyin.com, with the necessary environment added and some of the obfuscation restored. + * Can be used to generate webcast_signature. + * + * Copyright (c) 2024 douyin.com + * Sdk Version: 1.0.0.53 + * Source: https://lf-c-flwb.bytetos.com/obj/rc-client-security/c-webmssdk/1.0.0.53/webmssdk.es5.js + * + * License: + * This file is part of the Douyin project. + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +// const md5 = require('md5'); + +_window = global; +_document = {}; +// _navigator = { +// userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0" +// } +_history = {}; +_screen = {}; +_location = {}; + +function handler(target_name, number) { + return { + get: function (target, prop) { + if (prop in target) { + console.log(`Getting ${prop} from ${target_name}`); + return target[prop]; + } else { + console.log(`${prop} not found on ${target_name}`); + return undefined; + } + }, + set: function (target, prop, value) { + console.log(`Setting ${prop} to ${value} on ${target_name}`); + target[prop] = value; + return true; + } + }; +} + +window = new Proxy(_window, handler("window", 10)); +document = new Proxy(_document, handler("document", 10)); +navigator = new Proxy(_navigator, handler("navigator", 10)); +history = new Proxy(_history, handler("history", 10)); + +if (!window.byted_acrawler) { + function w_0x25f3(e, r) { + var t = w_0x42f5(); + return w_0x25f3 = function (e, r) { + e -= 350; + var a = t[e]; + return a; + }, + w_0x25f3(e, r); + } + !function (e, r) { + var t = w_0x25f3; + var a = e(); + while (1) { + try { + var n = -parseInt(t(795)) / 1 * (-parseInt(t(618)) / 2) + parseInt(t(662)) / 3 + -parseInt(t(499)) / 4 * (parseInt(t(796)) / 5) + parseInt(t(839)) / 6 * (-parseInt(t(655)) / 7) + -parseInt(t(577)) / 8 * (-parseInt(t(697)) / 9) + -parseInt(t(661)) / 10 * (-parseInt(t(657)) / 11) + parseInt(t(775)) / 12 * (-parseInt(t(925)) / 13); + if (n === r) { + break; + } + a.push(a.shift()); + } catch (e) { + console.log(e); + a.push(a.shift()); + } + } + } + (w_0x42f5, 732770); + function w_0x42f5() { + var e = [" can't have a .", "484e4f4a403f5243001f3009ad9ffc90000000dc0b1204fb00000477110001033f2e17000135491102004a120000110001110001031a2747000503414500201100010334274700050347450012110001033e2747000603041d45000303111d184301421101021400020211000211000103182c43010211000211000103122c43011802110002110001030c2c4301180211000211000103062c43011802110002110001430118421100011401010211010311000103022c430142110101031c2b11000103042d2f1400021100011401010211010311000243014202110103110101031a2b11000103062d2f4301021101021100014301184205000000003b0114000205000000473b01140003050000008b3b01140004050000009e3b0114000505000000be3b0114000603001400010300140007030014000811010144004a12000143000403e81b03002d14000911010212000232330033021101030211010303001100090700031843021101041200044a12000511010412000612000703021843014302050000fff11c140008110009110008050000fff11a3103002d4a1200080302430114000a11000a14000b11000a12000703202947001811000a4a12000511000a120007032019430114000b45004511000a12000703202747003907000314000c030014000d11000d032011000a120007192747001411000c0700091817000c354917000d214945ffdc11000c11000b1814000b07000a11000b18140007021101051100070302430214000702110103030011000707000318430214000e02110106430014000f11011807000b25470004014500010011000f07000c1607000314001011011612000d3300131101074a12000e11011612000d430107000f2647006503001400111101161200104700290211010803001101074a12000e0211010911011612000d1101161200104302430143021400114500200211010803001101074a12000e0211010a11011612000d43014301430214001107001111001118070012181400100211010b110116120013430114001211011612001447001511010c4a12001511001211011612001443024500031100121400121100100211010d110012430118140010110010070016180211010e1101161200134301180700121814001011001007001718070018181400100211010f11000f4301140013110102120002323300060211011043001400141101021200023233001811011112001934000f021101120211011307001a430143011400150211000411000743010211000511000706001b1b03002d4301180211000611001411000731430118021100040211010311000e1101021200023233000611011412001c4a12000843004302050000fff11c03102b0211010311000e110010070003184302050000fff11c2f4301180211000511001303082b11010212001d03042b2f110007314301180211000311000843011814001602110006030043014911001547000a1100161100151814001607001e1100161814001702110108030011001743024a120008031043011400181100184a12001f1100181200070302191100181200074302140019110017110019181400171100174200200c6b7f62604e656c7f4e626968076a6879596460680b6962604362795b6c6164690004657f686b097e786f7e797f64636a087d7f6279626e6261066168636a79650879625e797f64636a013d0e3c3d3d3d3d3d3d3d3c3c3d3d3d3d076b627f7f686c610a69647f686e795e646a63046f626974097e797f64636a646b740276700b6f6269745b6c613f7e797f0a6f62697452656c7e6530012b03787f61057c78687f740a6c7e626169527e646a63097d6c7965636c606830097979527a686f646930062b78786469300e526f74796869527e686e52696469077979527e6e64690a393f3439343b3a3f343b09787e687f4c6a686379096b685b687f7e6462630e523d3f4f39573b7a623d3d3d3d3c057e61646e68", "484e4f4a403f5243003c01321067d4bc00000824ebfd74540000087f0211010311000111000243024a12000505000000213b0105000001533b014302421100011200064701251100011200073300191100011200074a12000811030112000912000a430103011d2634000c0211030211000112000743014700f111000112000b4a12000c07000d43011400021100024700d902110303110001120007430114000311000311030412000e2547005511000211030515000f1100031103051500100211030607000f110002430249021103071100024301491100031101032947001f1103051200111200120300294700100211030811030903020403e81a4302494500161101031103051200102a47000911000211030515000f1101031103041200132533000c110305120011120012030a274700361103051200114a120014110002430149110305120011120012030125470017021103071100024301490211030607000f110002430249110001421100014008421100023400010d14000211020a33000711000111020b3714000307001514000407001614000507001514000611020c33000711000111020d374701c411000112000a14000411000212001747000f1100021200174a12001843004500030700161400050211020e1100044301330011110005070016253400071100050700192547017d11020512001014000711020512001a1400081100080700152347000f07000f11020512000f0c000245001207000f11020512000f07001a1100080c00041400090211020f021102101100044301110009430214000a0211021111000a430114000b0211021211000b11000212001b430214000c0211020f11000a11010111000c0c0002430214000d07001514000e11021312001c47000911000d14000e4500b10d021102140211000d43020e000714000f110005070019254700710211021511000111000243024a12001d07001e43010300134a12001f430014000602110216110006430147003b0211021711000f11000611000212001b4303490211021811000f0807002043031400100211020f11000d1101021100100c0002430214000e45000611000d14000e4500250211021811000f0807002043031400110211020f11000d1101021100110c0002430214000e1102131200214700130211021a430011000212000b110219120022160211010411000e1100021100074303421100034701e91100011200071400041100011200174700091100011200174500030700161400050211020e1100044301330011110005070016253400071100050700192547019811020512001014001211020512001a1400131100130700152347000f07000f11020512000f0c000245001207000f11020512000f07001a1100130c00041400140211020f021102101100044301110014430214001502110211110015430114001611000112000b1400171102131200214700161100174a1200231102191200220211021a4300430249110005070019254700480211021511000111000243024a12001d07001e43010300134a12001f43001400061100014a12002443004a12002543004a12000505000007293b01050000081e3b014302424500bd021102121100160243021400180211020f1100151101011100180c000243021400190d021102140211001943020e000714001a0211021811001a08070020430314001b0211020f11001911010211001b0c0002430214001c11020b11001c0d1100170e000b080e001b1100011200260e00261100011200270e00271100011200280e00281100011200290e002911000112002a0e002a11000112002b0e002b11000112002c0e002c440214001d0211010411001d110002110012430342021101031100011100024302424501df11000212000b324700070d11000215000b11000114000411000212001747000f1100021200174a12001843004500030700161400050211020e1100044301330011110005070016253400071100050700192547017d11020512001014001e11020512001a14001f11001f0700152347000f07000f11020512000f0c000245001207000f11020512000f07001a11001f0c00041400200211020f02110210110004430111002043021400210211021111002143011400220211021211002211000212001b43021400230211020f1100211101011100230c0002430214002407001514002511021312001c4700091100241400254500b10d021102140211002443020e0007140026110005070019254700710211021511000111000243024a12001d07001e43010300134a12001f430014000602110216110006430147003b0211021711002611000611000212001b430349021102181100260807002043031400270211020f1100241101021100270c00024302140025450006110024140025450025021102181100260807002043031400280211020f1100241101021100280c000243021400251102131200214700130211021a430011000212000b110219120022160211010411002511000211001e4303420211010311000111000243024208420700151400020211031211011611000143021400030211030f1101151102011100030c000243021400040211031611010643014700490d021103140211000443020e000714000502110317110005110106110001430349021103181100050807002043031400060211030f1100041102021100060c0002430214000245000611000414000211030b1100020d1101011200170e00171101170e000b1100010e001b1101011200260e00261101011200270e00271101011200280e00281101011200290e002911010112002a0e002a11010112002b0e002b11010112002c0e002c44021400070211020411000711010211011243034211000140084205000000003b0314000405000001593b021400050700001400010700011400020211010043003247000208421101011200024700020842001101011500021101011200031400031100031101011500041100051101011500030842002d0754214e636b797f0a537f656b626d78797e691653536d6f53656278697e6f697c786968536a69786f64056a69786f6406536a69786f64047864696202636703797e60076562686974436a0860636f6d7865636204647e696a0764696d68697e7f036b69780a7421617f217863676962037f696f07617f586367696208617f5f786d78797f0e617f42697b586367696240657f78066069626b78640465626578047c797f6400034b4958066169786463680b7863597c7c697e4f6d7f69045c435f580b53536d6f5378697f786568046e636875017a057f7c60657801370b786340637b697e4f6d7f69076a637e7e696d60037f68650d7f696f45626a6344696d68697e037f6978056f606362690478697478087e696a697e7e697e0e7e696a697e7e697e5c6360656f7504616368690b6f7e6968696278656d607f056f6d6f6469087e6968657e696f7809656278696b7e657875", "own", "Super expression must either be null or a function", "getTimezoneOffset", "array", "toDataURL", "enumerable", "setPrototypeOf", "field", "WEBGL", "wID", "illegal catch attempt", "TouchEvent", "3160hBpQVk", "484e4f4a403f5243000d0a13c08652000000000f3be74070000003930211021611010111000143024908421101003300031101013300031101023247000208420d0700000e000103040e00021101181200000e00030d0700040e000103030e00021101030e00050d0700060e000103030e00021101040e00050d0700070e000103030e00021101050e00050d0700080e000103030e00021101030e00050d0700090e000103000e00020d07000a0e000103000e00020d07000b0e000103000e00020d07000c0e000103000e00020d07000d0e000103000e00020d07000e0e000103030e00021101060e00050d07000f0e000103030e00021101070e00050d0700100e000103010e00020d0700110e000103010e00020d0700120e000103010e00020d0700130e000103000e00020d0700140e000103030e00021101080e000503010e00150d0700160e000103030e00021101090e00050d0700170e000103030e000211010a0e00050d0700180e000103030e00021101030e00050d0700190e000103030e000211010b0e00050d07001a0e000103030e000211010c0e00050d07001b0e000103030e000211010d0e00050d07001c0e000103030e00021101030e00050d07001d0e000103000e00020d07001e0e000103030e000211010e0e000507001f0e00200d0700210e000103030e000211010f0e00050d0700220e000103030e00021101100e00050d0700230e000103030e00021101110e000503010e00150d0700240e000103010e00020d0700250e000103040e00021101121200260e00030d0700270e000103030e00021101130e00050d0700280e000103030e00021101030e00050d0700290e000103040e00020c00221400010c0000140002030014000311000311000112002a274700eb110001110003131200020300480013030148002f0302480045030348005b494500be0211011411010011000111000313120001134301110001110003131500034500a011010111000111000313120001131100011100031315000345008511010211000111000313120001131100011100031315000345006a110001110003131200154700321101153a07002b264700241100024a12002c110001110003131200054a12002d110001110003131200204301430149450025110001110003131200054a12002d0211000111000313120020430211000111000313150003450003450000170003214945ff081101153a07002b2647001d1101154a12002e11000243014a12002f05000000003b0143014945000a02110116110001430149084200300349414c0146014e015a095b5c495a5c7c41454d015c09494a4144415c414d5b064b49465e495b0a5c41454d5b5c49455819085844495c4e475a451340495a4c5f495a4d6b47464b5d5a5a4d464b510c4c4d5e414b4d654d45475a51084449464f5d494f4d094449464f5d494f4d5b0a5a4d5b47445d5c4147460f495e4941447a4d5b47445d5c414746095b4b5a4d4d467c47580a5b4b5a4d4d46644d4e5c104c4d5e414b4d7841504d447a495c41470a585a474c5d4b5c7b5d4a074a495c5c4d5a510158095c475d4b4061464e47085c41454d5247464d0a5c41454d5b5c4945581a074f585d61464e470b425b6e47465c5b64415b5c0b58445d4f41465b64415b5c0a5c41454d5b5c4945581b095d5b4d5a694f4d465c0a4d5e4d5a6b474743414d075c5c775b4b414c01450b5b51465c49506d5a5a475a0c46495c415e4d644d464f5c40055a5c4b61780844474b495c414746094e587e4d5a5b4147460b77775e4d5a5b4147467777084b44414d465c614c0a5c41454d5b5c4945581c0b4d505c4d464c6e414d444c06444d464f5c40095d464c4d4e41464d4c04585d5b40044b49444403494444045c404d46", "rewriteUrl ", "setTTWid", "height", "product", "Vrinda", "X-Mssdk-Info", "arrayBuffer", "vibrate", "sendBeacon", "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "canvas", "Character outside valid Unicode range: 0x", "join", "throw", "iterator result is not an object", "DEPTH_BITS", "compatMode", "forEach", "Attempted to access private element on non-instance", "unsupport type", "attempted to call addInitializer after decoration was finished", "pageXOffset", "length", "method", "bytes", "charAt", "Sylfaen", "toElementDescriptor", "base64", "createElement", "private", "ttcid", "msStatus", "MAX_VARYING_VECTORS", "elements", "484e4f4a403f524300232a0d2ebaf9a00000000042410a740000019d110100002347000200421101011200004700020042070001110102364700351101024a12000111010143011400011100014a120002070000430103002a34000f1100014a120002070003430103002a470002004211010333000611010312000433000911010312000412000533000c1101031200041200051200064700213e000414000c413d00171101031200041200054a1200064300082547000200424107000707000807000907000a07000b07000c07000d07000e07000f0700100700110c000b1400020700120700130700140c000314000303001400041100041100031200152747001e11000311000413140005110103110005134700020042170004214945ffd503001400061100061100021200152747002111000211000613140007110103120016110007134700020042170006214945ffd21101024a1200171101031200164301140008030014000911000814000a11000911000a1200152747003911000a1100091314000b11000b4a1200181101050700194401430133000e11010312001611000b1307001a134700020042170009214945ffba0142001b096d7f787e68736c7f68137d7f6e556d744a68756a7f686e63547b777f690773747e7f62557c09767b747d6f7b7d7f690679726875777f07686f746e73777f07797574747f796e1445456d7f787e68736c7f68457f6c7b766f7b6e7f134545697f767f74736f77457f6c7b766f7b6e7f1b45456d7f787e68736c7f6845697968736a6e457c6f74796e7375741745456d7f787e68736c7f6845697968736a6e457c6f74791545456d7f787e68736c7f6845697968736a6e457c741345457c627e68736c7f68457f6c7b766f7b6e7f1245457e68736c7f68456f746d687b6a6a7f7e1545456d7f787e68736c7f68456f746d687b6a6a7f7e1145457e68736c7f68457f6c7b766f7b6e7f144545697f767f74736f77456f746d687b6a6a7f7e1445457c627e68736c7f68456f746d687b6a6a7f7e0945697f767f74736f770c797b7676497f767f74736f771645497f767f74736f7745535e5f45487f7975687e7f6806767f747d6e72087e75796f777f746e04717f636905777b6e79720a463e417b3760477e794506797b79727f45", "pop", "showOffsetX", "MAX_TEXTURE_IMAGE_UNITS", "2571598OPFKfY", "webgl", "appendChild", "outerHeight", "screenX", "kind", "navigator", "attempted to use private field on non-instance", "cookie", "AsyncIterator", "crypto", "buffer", "availHeight", "The property descriptor of a field descriptor", "resolve", "react.element", "close", "monospace", "stun:stun.l.google.com:19302", "acc", "BLUE_BITS", "Arguments", "style", "nextLoc", "pageYOffset", "72px", "webkitRequestAnimationFrame", "hidden", "MAX_FRAGMENT_UNIFORM_VECTORS", "node", "Parchment", "kWebsocket", "xmst", "number", "altKey", "A method descriptor", "Leelawadee", "6300OtYFrs", "href", "1155KnHwvu", "storage", "sTm", "setMonth", "20690wmYtVy", "266076LNZfld", "Malformed string", "setConfig", "touchmove", "rval", "unload", "from", "Image", "tryLoc", "ActiveXObject", "assign", " property.", "_sent", "mmmmmmmmmmlli", "offsetHeight", "slice", "getOwnPropertyDescriptor", "mhe", "utf8", "Object is not async iterable", "reset", "raw", "constructor", "attempted to ", "Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.", "isArray", "chargingTime", "__await", "484e4f4a403f524300071336bc6677450000001613b112b6000003090b4a12000911021607000a07000b4402070001430242070000140001110115082633000511011502263300071101150700012647001d3e000a140029070002140001413d000d021101001101154301140001411101013234000611010212000347000b001401010211010343004902110104430049110105120004140002110106120005140003030214000411000414000503401400060211010011011443011400071101074a120006021101001101074a1200061100074301430143011400081101074a120006021101001101074a1200061100014301430143011400091101081200071200083247001005000000003b0011010812000715000811010912000c14000a11000a33000811000a3a07000d2547000c11000a4a120008430014000a0211010a110003110002430214000b0211010b11000b11000a430214000c0211010c11000c07000e430214000d1101074a1200060211010011000d4301430114000e11010d44004a12000f43000403e81b14000f0211010e43001400101100061400111100030401001b1400121100030401001c140013110002140014110008030e13140015110008030f13140016110009030e13140017110009030f1314001811000e030e1314001911000e030f1314001a11000f03182c0400ff2e14001b11000f03102c0400ff2e14001c11000f03082c0400ff2e14001d11000f03002c0400ff2e14001e11001003182c0400ff2e14001f11001003102c0400ff2e14002011001003082c0400ff2e14002111001003002c0400ff2e140022110011110012311100133111001431110015311100163111001731110018311100193111001a3111001b3111001c3111001d3111001e3111001f311100203111002131110022311400230400ff1400240211010f11001111001311001511001711001911001b11001d11001f11002111002311001211001411001611001811001a11001c11001e11002011002243131400250211010b0211011011002443011100254302140026021101111100051100241100264303140027021101121100270700104302140028110028420011201c4c491c401b1c41401e48481a4a484c1d414048484141401d1b1e404c4a4f1d00201e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e010e060d1a1b171c1d071d160e1b171c1d061c1d1b171c1d09080a170c170c01081d040c0a1115070a1d0814191b1d212623240b240d3e3d3e3e2400394825530423240b240d3e3d3e3e2400394825535c011f090d0b1d0a391f1d160c060b0c0a11161f020b48071f1d0c2c11151d020b4a", "Arial Hebrew", "msToken", 'An element descriptor\'s .placement property must be one of "static", "prototype" or "own", but a decorator created an element descriptor with .placement "', "visible", "decode", "concat", "30762fvQkGV", "hash", "STENCIL_BITS", "configurable", " private field on non-instance", "T_MOVE", "clientX", "images", "version", "renderer", "removeItem", "catchLoc", "indexOf", "msHidden", "isView", "toLocaleString", "https://mssdk.bytedance.com", "B4Z6wo", "dispatchException", "msvisibilitychange", " decorators must return a function or void 0", "Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe", "Cannot convert undefined or null to object", "initializer", "awrap", "continue", "mousedown", "mousemove", "update", "__ac_blank", "0123456789abcdef", "getItem", "Futura", "MAX_RENDERBUFFER_SIZE", "GPUINFO", "host", "484e4f4a403f524300010a1106afb0650000000079a66ec20000008c1101001200004a12000143001400011100014a120002070003430103002a470002014211010307000444011400021101013300061101011200053300091101011200051200064700411101011200051200061400031100034a120002070007430103002534000f1100034a120002070008430103002534000c1100024a120009110003430147000200420142000a093b3d2b3c0f292b203a0b3a210221392b3c0d2f3d2b0727202a2b360128082b222b2d3a3c21204a10263a3a3e3d71741261126166157e637713357f627d33661260157e637713357f627d3367357d3332152f63287e637713357f627a336674152f63287e637713357f627a3367357933670822212d2f3a27212004263c2b28042827222b10263a3a3e74616122212d2f2226213d3a043a2b3d3a", "BluetoothUUID", "decorateClass", "mozRTCPeerConnection", "defineClassElement", "credentials", "writable", "value", "WEBKIT_EXT_texture_filter_anisotropic", "JS_MD5_NO_ARRAY_BUFFER", "Metadata keys must be symbols, received: ", "getReferer", "indexDB", "__proto__", "Object", "splice", "symbol", "offsetWidth", "executing", "mozBattery", "normal", "kFakeOperations", "reverse", "finisher", "createOffer", "addEventListener", "Cannot call a class as a function", " must be a function", "getContext", "referrer", "afterLoc", "has", "return", "kNoMove", "netscape", "MAX_CUBE_MAP_TEXTURE_SIZE", "bind", "width", "valueOf", "off", "JS_MD5_NO_ARRAY_BUFFER_IS_VIEW", "innerWidth", "257232gkndOM", "null", "isSecureContext", "pixelDepth", ".initializer has been renamed to .init as of March 2022", "/web/report", "removeChild", "[object Boolean]", "getSupportedExtensions", "error", "GeneratorFunction", "message", "initializeInstanceElements", "systemLanguage", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", "tt_webid", "sqrt", "__ac_referer", "blocks", "MOZ_EXT_texture_filter_anisotropic", "1hcbEHL", "308350OyvEoV", "484e4f4a403f5243003a20169967f185000000000b49c93c000000761101001200004a12000143001400011100014a120002070003430103002a47000201421101013a070004263300191101021200051200064a12000711010112000843010700092534002b1101033a0700042547000607000445000902110104110103430107000a2533000a11010312000b07000c2542000d09282e382f1c3a3833290b293211322a382f1e3c2e38073433393825123b083831383e292f323309283339383b34333839092d2f32293229242d380829320e292f34333a043e3c3131072d2f323e382e2e1006323f37383e297d2d2f323e382e2e0006323f37383e290529342931380433323938", "for", "break", "construct", "Constantia", "webkitvisibilitychange", "activeState", "toClassDescriptor", "layers", "bogusIndex", "arg", "screen", "buffer8", "getOwnPropertyNames", "get", "prev", "buildID", "lastByteIndex", "' method", "Bad UTF-8 encoding 0x", "[object Array]", "add", "484e4f4a403f5243000027194f9666590000000044a16fed000000270700001400013e000a140002070001140001413d000d0211010011010243011400014111000142000200200d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d", "call", "characterSet", "bodyVal2str", "tryEntries", '" is read-only', "tt_webid_v2", "locationbar", "maxTouchPoints", "string", "addElementPlacement", "try statement without catch or finally", "mozVisibilityState", "showColor", "name", "split", "xmstr", "prototype", "AcroPDF.PDF.1", "Tunga", "4218tDAtHd", "AVENIR", "getMetadata", "track", "touchEvent", "decorateConstructor", "now", "failed to set property", "484e4f4a403f524300263203ec75a3740000000817718683000003051100011100022e4211011507000013002547000a070001110115070002160d03000e0003000e00040c00000e00050c00000e0006010e0007010e00000700080e0002010e00090d0305033c1a0e000a03020e000b0305033c1a0e000c0e000d0700080e000e000e000f03030e00101400011101004a1200111100011101154302491100011200030300253400161101014a120012110001120003430111000112000326470009110102070013440140110001120014330007110001120015324700091101020700164401401101031200174a12001811000112000343014911010412000303002547000c1100011200031101041500031100011200043247009c110001120002070008254700091101020700194401401100011200020700012447000911010207001a44014011000112000211010415000202110105110001430111010415001b021101061101071100011200100403e81a43024911000112001c0826330005110001022647002e11010412001d4a12001811000112001c43014911010412001d4a12001e05000000003b024301323211010415001c11000112000d4700a60011010415001f11010847006411000112000d12000a33001311000112000d12000a11010412000d12000a2947003f021101091101084301491101004a1200110d11010412000d11000112000d430311010415000d0211010a11010b11010412000d12000a0403e81a43021401084500351101004a1200110d11010412000d11000112000d430311010415000d0211010a11010b11010412000d12000a0403e81a430214010811000112002047001c1100011200201101041500200211010611010c03050403e81a4302491100010b1500210211010d4300490211010e1100011200054301490211010f1100011200064301490211011043004911011132330006110001120007470020001401111100011200071101041500070211010611011203050403e81a43024911000112000f4700241101041200223247001a0011010415002202110106110113030a0403e81a11000143034900110104150023084200240350524402575a064651535d5b5a03555d50055d4767707f0e515a555658516455405c785d47400f414658665143465d405166415851470347505d0003505142035246510a415a5d4075595b415a4008415a5d40605d595105404655575f04595b5051044c4c56530450504640065547475d535a0552585b5b461e5b44405d5b5a14555d501c7d5a40515351461d145d47145a51515051501503565b5107565b517c5b474024565b517c5b474014594147401456511444465b425d505150145d5a14565b5114595b505107555d50785d4740044441475c0f4651535d5b5a145d47145a41585815124651535d5b5a145d47145d5a4255585d50150a4651535d5b5a775b5a520142106b515a55565851675d535a5540414651064651504157510b515a55565851604655575f0444514652075b44405d5b5a47046b5052440b5d5a5d405d55585d4e5150", "appMinorVersion", "Attempted to decorate a public method/accessor that has the same name as a previously decorated public method/accessor. This is not currently supported by the decorators plugin. Property name was: ", "hex", "dischargingTime", "PLUGIN", "T_KEYBOARD", "ret_code", '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.', "@@toPrimitive must return a primitive value.", "battery", "Generator is already running", "headers", "md5", "setter", "=; expires=Mon, 20 Sep 2010 00:00:00 UTC; path=/;", "_raw_sec_did", "hardwareConcurrency", "script", "fontSize", "_byted_sec_did", "keydown", "__private_", "screenY", "Duplicated element (", "484e4f4a403f524300211209597bcccc0000053aae77fba6000005e50b1200093247004e0b12000a4a12000b0d0700050e000c1100000e000d43014911021607000e07000f44024a120010110001430147001f1100024a12001143004a12001243004a12001307001443010300130b1500151101054a1200160b1100004302421100000b1500171101074a1200160b1100004302420c00000b15000a0b12000a4a12000b0d0700040e000c1100000e000d4301491100014a12001843000b1500191100020b15001a1101044a1200160b1100004302421101094a1200240b120019430103011d26140002021102010b12001a43013300031100024702fe0b12001a4a120024070025430103011d2947000e1101064a1200160b1100004302421100010b1500260b1200271400030b12001b1400040b12001c1400050b12001d1400060b12001e1400070b12001f1400080b1200201400090b12002114000a0d14000b030014000c11000c1101081200282747001f0b12002911010811000c131311000b11010811000c131617000c214945ffd411020212002a14000d11020212002b14000e11000e07002c2347000f07002d11020212002d0c000245001207002d11020212002d07002b11000e0c000414000f02110203021102040b12001a430111000f4302140010021102051100104301140011021102061100110b1200264302140012021102031100101101011100120c0002430214001307002c14001411020712002e4700091100131400144500910d021102080211001343020e002f1400150b12001907002325470050021102090b120015430147003a0211020a1100150b1200150b1200264303490211020b110015080700304303140016021102031100131101021100160c000243021400144500061100131400144500250211020b110015080700304303140017021102031100131101021100170c000243021400140b12000a33000f0b12000a03001307000c130700042647000202420b12000a14001803001400191100191100181200282747005d11001903002547002d1100141100181100191312000d030116000b1500091101044a1200160b1100181100191312000d43024945001f0b1100181100191307000c13134a1200160b1100181100191312000d430249170019214945ff960b1200174700100b1200074a1200160b0b1200174302490b07000a39491102071200314700140b4a12000511020c1200320211020d43004302491100030b1500271100040b15001b1100050b15001c05000003ed3b010b15001d1100070b15001e1100080b15001f1100090b15002011000a0b150021030014001a11001a1101081200282747001f11000b11010811001a13130b12002911010811001a131617001a214945ffd41101064a1200160b11000043024203001400020b1200333400040b12001a34000307002c1400030211030e110003430147000503011400021100034a120024110300120034120035430103011d2647000503021400021100020300294700ea0b4a12003607003743011400041100044700d70211030f0b12001a43011400051100051103101200382547005511000411030215002d11000511030215002a0211031107002d1100044302490211031211000443014911000511010d2947001f1103021200391200280300294700100211031311031403020403e81a43024945001611010d11030212002a2a47000911000411030215002d11010d11031012003a2533000c110302120039120028030a274700361103021200394a12000b110004430149110302120039120028030125470017021103121100044301490211031107002d11000443024911010647000a02110106110001430149084207000014000107000114000211010012000212000314000311000312000414000411000312000514000511000312000614000611000312000714000711000312000847000208420011000315000805000000003b0211000315000505000000643b0011000315000705000000793b0211000315000407001b07001c07001d07001e07001f0700200700210c00071400080700220700230c000214000905000000ba3b011100031500060842003b0755204f626a787e0a527e646a636c79787f680e5540414579797d5f687c78687e79097d7f62796279747d6804627d6863107e68795f687c78687e7945686c69687f047e68636910627b687f7f6469684064606859747d680f526c6e52646379687f6e687d79686905527e68636915526f7479686952646379687f6e687d795261647e79047d787e65046b78636e096c7f6a78606863797e0e536e6263796863792079747d682901640479687e790879625e797f64636a0b796241627a687f4e6c7e68057e7d61647901360e526f74796869526e626379686379056c7d7d61741552627b687f7f6469684064606859747d684c7f6a7e0b7962587d7d687f4e6c7e680d526f74796869526068796562690a526f7479686952787f610762636c6f627f79076263687f7f627f06626361626c6909626361626c696863690b626361626c697e796c7f790a62637d7f626a7f687e7e09626379646068627879034a4859045d425e59076463696875426b0b527e646a636c79787f68300b526f74796869526f6269741262637f686c69747e796c79686e656c636a68066168636a796506787d61626c6908607e5e796c79787e0b52526c6e5279687e7964690007607e5962666863017b03787f61076b627f7f686c61037e69640d7e686e44636b6245686c69687f0b7f687e7d62637e68585f410861626e6c796462630465627e79116a68795f687e7d62637e6845686c69687f0a7520607e207962666863037e686e0e607e43687a596266686341647e790464636479", "round", "innerHTML", "appCodeName", "defineProperties", 'Could not dynamically require "', "push", "__destrObj", "toElementDescriptors", "defaultProps", "fromElementDescriptor", "documentMode", "Object.keys called on non-object", "WEBGL_debug_renderer_info", "reduce", "replace", "setUserMode", "changedTouches", "JS_MD5_NO_WINDOW", "[object Object]", "item", "beforeunload", "%27", "enableTrack", "envcode", "gpu", "ubcode", "getParameter", "undefined", "start", "isGeneratorFunction", "completion", "getOwnPropertySymbols", "next", "availWidth", "values", "oscpu", "Tw Cen MT", "sort", "bluetooth", "requestMediaKeySystemAccess", "keyboardList", "key", "window", "Unfinished UTF-8 octet sequence", "setMetadata", "static", "debug", "languages", "toolbar", "msDoNotTrack", "reject", "finishers", "208XZrBOJ", "UNMASKED_RENDERER_WEBGL", "external", "shadowBlur", "POST", "484e4f4a403f5243003e0d23e5c579310000006248d1745c000002cc0d140001110200070000131400021100020700012447000a11000211000107000016110200070002131400031100030700012447000a11000311000107000316110200070004131400041100040700012447000a110004110001070005161100014205000000003b001400010114000211010f3247000911010112000614010f11010f110101120007254700040014000211010244004a12000843001400030d1101001200094a12000a030043010e000b11010012000c4a12000a030043010e000d11010012000e4a12000a030043010e000f1101001200104a12000a030043010e001114000411000412000b12001203002533000c11000412000d12001203002533000c11000412000f12001203002533000c110004120011120012030025470002084211000412000b12001203101a11000412000d120012030c1a1811000412000f12001203041a1811000412001112001203081a181400051100031101031200131101041200141200150403e81a182747003a1101031200161101041200141200170404001a27470020110103120016110005181101030700163549021101054300490014000245000045001d11000311010315001311000511010315001602110105430049001400021100024700f703021400060d1100040e00181100060e00191400070d11000707001a1611010412001b11000707001a1307001b1607000111010244004a12000843001811000707001a1307001c1611010012001d11000707001a1307001d16030011000707001a1307001e160d11000707001f161101064a12002011000707001f13021100014300430249021101071101081200210211010911010a4a120022110007430111010b120023430243021400081101041200240700251314000911000932470002084211010f110101120026254700190211010c110009110008430214000a11000a3247000045000f0211010d1100091100080d0043044908420027052121223c31000821210a2230373c310721210230371c310b21210a2230373c310a23670921210230373c3103670727203b3b3c3b3205333920263d07323021013c383008383a2330193c2621062625393c3630063730183a23300936393c363e193c262107373016393c363e0c3e302c373a342731193c26210a37301e302c373a3427310b3436213c233006213421300b223c3b313a2206213421300639303b32213d0326013805212734363e08203b3c21013c3830033436360a203b3c2114383a203b210837303d34233c3a2707382632012c253003221c1103343c3109213c3830262134382507343c31193c26210b25273c2334362c183a313006362026213a38063426263c323b0f0210170a1110031c16100a1c1b131a092621273c3b323c332c043f263a3b0a2730323c3a3b163a3b33092730253a272100273904302d3c21", "metadata", "productSub", "mark", " is not an object.", "substr", "Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.", "Gulim", "experimental-webgl", "setRequestHeader", "charging", "484e4f4a403f52430031032581faca6c0000000093505d60000001c60700001400010d1400020700011100020700021607000311000207000416070005110002070006161100021101021314000307000714000403001400061101011200081100060303182a4700b11101014a1200091700062143010400ff2e03102b1101014a1200091700062143010400ff2e03082b2f1101014a1200091700062143010400ff2e2f1400051100041100034a12000a1100050500fc00002e03122c43011817000435491100041100034a12000a110005050003f0002e030c2c43011817000435491100041100034a12000a110005040fc02e03062c43011817000435491100041100034a12000a110005033f2e430118170004354945ff3f110101120008110006190300294700b41101014a1200091700062143010400ff2e03102b110101120008110006294700161101014a12000911000643010400ff2e03082b45000203002f1400051100041100034a12000a1100050500fc00002e03122c43011817000435491100041100034a12000a110005050003f0002e030c2c4301181700043549110004110101120008110006294700161100034a12000a110005040fc02e03062c430145000311000118170004354911000411000118170004354911000442000b011441686b6a6d6c6f6e616063626564676679787b7a7d7c7f7e717073484b4a4d4c4f4e414043424544474659585b5a5d5c5f5e51505319181b1a1d1c1f1e1110020614025a19416d424d594e411d73625a786b111906644f5f5e1a1f7160187b1b1c027e7c68456c401e67654b4658707d66795c53446f4363475b505110617f6e4a487a5d6a4c14025a18416d424d594e411d73625a786b111906644f5f5e1a1f7160187b1b1c047e7c68456c401e67654b4658707d66795c53446f4363475b505110617f6e4a487a5d6a4c14025a1b0006454c474e5d410a4a41485b6a464d4c685d064a41485b685d", "toString", "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx", "warn", "parse", "sent", "iterator", "PDF.PdfCtrl.", "__ac_testid", "clickList", "create", "hasOwnProperty", "init", "An initializer", "clientHeight", "extras", "__esModule", "sessionStorage", "kKeyboardFast", "devicePixelRatio", "; path=/;", "location", "GREEN_BITS", "484e4f4a403f524300192d11257a6fc000000000d4cf00750000006703011400011101004a12000011010603062b1100012f43011400021101004a1200001101014a1200011101014a12000243000401001a4301430114000302110102110003110105430214000411000211000318110004181400050211010311000507000343024200040c5f4b56547a51584b7a565d5c055f5556564b064b58575d5654024a08", "Castellar", "toGMTString", "mozHidden", "Set", "[native code]", "complete", "484e4f4a403f52430012270f0b8aa329000000005cddda270000008a0211010043003247007e1101014a12000007000143011400011100011200024a12000343004a120004110104070005070006440207000743024a120008070009430103002734002c1101021200034a12000343004a120004110104070005070006440207000743024a120008070009430103002734001011010212000a4a120003430007000b26420142000c0d18091e1a0f1e3e171e161e150f06181a150d1a08090f143f1a0f1a2e2937080f14280f0912151c07091e0b171a181e03270851011c000712151f1e03341d0a151a0f120d1e18141f1e070b170e1c12150814201419111e180f5b2b170e1c12153a09091a0226", "filter", "runClassFinishers", "webkitVisibilityState", "2.11.0", " after decoration was finished", "suffixes", "completed", "src", "Cannot instantiate an arrow function", "getTime", "resultName", "children", "accessor.get", "484e4f4a403f52430024153c4037f00000000009d722c1e5000000d200110208150002084202110100430047001c1101014a120000070001430114000105000000003b001100011500030211010243004700553e002b140002110002120004110104070005132533000c11010312000612000703002547000700110108150002413d00241101031200064a12000807000907000a4302491101031200064a12000b0700094301494102110105430047002311010312000c3233000f11010312000d34000611010312000e4700070011010815000211010612000f11010812000203022b2f11010607000f35490842001004637c69620478697f780965626f636b62657863076362697e7e637e046f636869125d5943584d5349544f494948494853495e5e0e7f697f7f6563625f78637e6d6b69066069626b7864077f697845786961107f63616947697544697e694e75786968000a7e6961637a69457869610965626869746968484e0c5c63656278697e497a6962780e415f5c63656278697e497a6962780769627a6f636869", "https://mssdk.bytedance.com/websdk/v1/getInfo", "?q=", "484e4f4a403f5243003e19390bcd41790000000084fc29f10000006111010012000033000d1101001200001200010700022347000303014211010112000311010112000412000326470003030142110101120005110101120006264700030301421101011200071200081101021200071200082447000303014203024200090c3125363a32123b323a3239230723363019363a32061e1105161a12083b383436233e3839062736253239230424323b3103233827063125363a3224063b323930233f", "defineProperty", "[object Generator]", "Wingdings", "hasInstance", "SHADING_LANGUAGE_VERSION", "platform", "484e4f4a403f52430007152cc2c1a53c00000061235466970000007f1100010700022534000711000107000325340007110001070004253400071100010700052547000200423e0004140002413d002b1102021100011333001b11020211000113120006082634000c11020211000113120007082647000200424108421101014a12000011010243014a12000105000000003b0143011401000842000813717362596178466479667364626f58777b73650465797b7308757370457e77646608557370457e77646605737977667f16737941737454647961657364527f65667762757e73640f747f787259747c73756257656f78750e7f65535941737454647961657364", "asyncIterator", "object", "colorDepth", "keys", "wrapped", "getter", "finalized", "all", "appName", "regionConf", "application/x-www-form-urlencoded", "attempted to get private field on non-instance", "createEvent", "body", "toElementFinisherExtras", "open", "displayName", "_urlRewriteRules", "random", "font", "484e4f4a403f52430031033191576c4000000000940c5eb50000005302110100430032470047070000110101363234000b110101120000110102373234000707000111010336340007070002110103363400070700031101033634000f0700041101033607000511010336274201420006077d61786a64637e08527d656c637962600b6e6c61615d656c637962600b525263646a6579606c7f68054c78696462184e6c637b6c7e5f686369687f64636a4e6263796875793f49", "discharingTime", "SimSun-ExtB", "fromClassDescriptor", "versions", "charCodeAt", "fillText", "fetch", "freeze", "Create WebSocket", "webkitRTCPeerConnection", "digest", "class", "MAX_VERTEX_UNIFORM_VECTORS", "filename", "first", "visibilitychange", "A class descriptor", "clientWidth", "frontierSign", "msVisibilityState", "antialias", "484e4f4a403f5243002a3d04fa03273900000000b93145d7000004061101001200004a12000143001400011101001200024a120001430014000203001400030301140004030214000503031400060304140007030514000811000814000907000314000a07000414000b07000514000c07000614000d07000714000e07000814000f07000914001007000a1400111100014a12000b07000c430103002a34000f1100014a12000b07000d430103002a4700091100071400094500de1100014a12000b11000a430103002a4700091100031400094500c31100014a12000b11000c430103002a4700091100041400094500a81100014a12000b11000d430103002a34000f1100014a12000b07000e430103002a34000f1100014a12000b07000f430103002a4700091100051400094500691100014a12000b11000e430103002a34000f1100014a12000b11000f430103002a34000f1100014a12000b110010430103002a34000f1100014a12000b070010430103002a34000f1100014a12000b070011430103002a4700091100061400094500061100081400091100024a12000b11000b430103002a33000711000911000326470005004245012c1100024a12000b11000d430103002a34000f1100024a12000b11000c430103002a34000f1100024a12000b070012430103002a330007110009110005263300071100091100042647000500424500dd1100024a12000b110011430103002a34000f1100024a12000b11000f430103002a34000f1100024a12000b110010430103002a34000f1100024a12000b11000e430103002a3300071100091100072633000711000911000626470005004245007c1100024a12000b11000b430103002733000f1100024a12000b11000d430103002733000f1100024a12000b110011430103002733000f1100024a12000b11000e430103002733000f1100024a12000b11000f430103002733000f1100024a12000b1100104301030027140012110012110009110008252647000200420300140013030114001403021400150303140016030414001703051400181100181400191100014a12000b070013430103002a47000911001514001945008a1100014a12000b070014430103002a34000f1100014a12000b070015430103002a34000c1100014a12000b070016430147000911001414001945004e1100014a12000b070017430103002a4700091100131400194500331100014a12000b070018430103002a34000f1100014a12000b070019430103002a4700091100171400194500061100181400190211010143004a120001430014001a110019110013243300071100191100142433002111010212001a34001811010012001b4a12001c43004a12000b07001d430103002a4700020042110019110013243300071100191100142433000f11001a4a12000b07001a430103002a47000200420142001e090b0d1b0c3f191b100a0b0a113211091b0c3d1f0d1b080e121f0a18110c13070917101a11090d03091710071f101a0c11171a051217100b0606170e1611101b04170e1f1a04170e111a03131f1d0717101a1b06311809131f1d17100a110d160c131f1d210e11091b0c0e1d57041d0c110d03064f4f051d0c17110d05180617110d040e17151b0818170c1b1811065106110e1b0c1f51055e110e0c51055e110e0a51071d160c11131b51080a0c171a1b100a5104130d171b061d160c11131b06081b101a110c080a112d0a0c1710190639111119121b", "setTTWebid", "Buffer", "_enablePathListRegex", "outerWidth", "type", "decorateElement", "484e4f4a403f524300040e131c85d3950000064d665eaab9000007ca05000001d03b0014000105000003953b00140002050000046a3b001400031102084400140004021100024300490211000343004907004b07004c07004d07004e07004f07005007005107005207005307005407005507005607005707005807005907005a07005b07005c0c001214000702110209110200110007030043031400051100050211020911020007005d1307005e0c000111000712005f43032f17000535490700600c00011400080211020911020607006113110008030043031400060d1400090211020a4300110009070062160211020b4300110009070063160211020c43001100090700641607001811020844004a1200654300181100090700661611020d4a1200671100044a12006843001d033c1b4301110009070069160211020e430011000907006a160211020f43004a120008430011000907001d1611000511000907006b1611000611000907006c1602110210430011000907006d1602110001430011000907006e1602110211430011000907006f16030114000a11021212007011000907007016021102130700714301110009070072160211021307007343011100090700741611000a1100090700751603001100090700761611021412007711000907007716110009423e000714000a030042413d01b6030014000111030007000013340014110301070001134a07000213070003430103002a47000607000445000203001400020700051103023a24470006070006450002030014000311030307000713070008134a0700091311030007000a1343014a0700021307000b430103002934002e11030007000c1333000b11030007000c1307000d1333001607000e11030007000c1307000d134a0700081343002534000711030007000f131400041100044700060700104500020300140004110004330011110301070001134a0700111307001243014700060700134500020300140005110300070014133300041100023247000607001545000203001400060211030443001400071100073233000711030007001613470006070017450002030014000807001814000911000247000b11000103012f170001354911000347000e110001030103012b2f170001354911000847000e110001030103022b2f170001354911000747000e110001030103032b2f170001354911000647000e110001030103042b2f170001354911000547000e110001030103052b2f170001354911000447000e110001030103062b2f17000135491100014241084211030512001907001a133247000c030011030512001907001a163e0010140003030111030512001907001a16413d004911030007001b1347003e11030007001b1344001400011103064a07001c1307001d43014a07001e1307001f430114000205000004103b0011000107002316070024110001070025164108423e0010140002030111040512001907001a16413d00421101024a070020131101010300030043034903001101024a0700211303000300030103014304070022130303132514000103021100011811040512001907001a164108420c000014000107002607002707002807002907002a07002b07002c07002d07002e07002f0700300700310700320700330700340700350700360700370700380700390c001414000211030107003a133247000e07003b11030512001907003c35423e001214000507003d11030512001907003c3542413d003b05000005203c021400031100024a0700481305000005c63b0243011400041103074a0700491311000443014a0700401305000005d33b0043014941084211050107003a134a07003e130d1100010e003f43014a0700401305000005523b0143014a07004513050000059e3b014301421100010700411307004248001007004348001607004448001c49450024030111030111010216450021030211030111010216450015030011030111010216450009030511030111010216084203011d110001070046134a0700021307004743012647000503044500020303110301110102160842021101031100011100024302421101014a07004a13070018430111040512001907003c35420d140001110214070078131400021100020700182447000a11000211000107007816110214070079131400031100030700182447000a11000311000107007a1611021407007b131400041100040700182447000a11000411000107007c161100014205000000003b0014000105000005eb3b0014000202110115430049021101164300490211011743004902110118430049021101194300491101034a12007d1101051200190211000143004302491101034a12007d11010512007e0211011a43004302491101034a12007d11010512007f0211011b43004302491101034a12007d1101051200800211000243004302491101141200814a120082030043011400030d1100030e00831400040700841400050211011c0211011d1100054301030a430214000611000647000e110006030118170006354945000503011400060211011e11000511000643024911000611010507001913070085161101034a12007d1100041101054302490211011f1101204a1200861100044301110121120087430214000702110122110123120088110007430214000811011212008907008a1314000911000932470002084211012447001b1101244a120040021101251100091100080d00430443014945000f021101251100091100080d004304490842008b051b0411061509010711063513111a00071d1a10110c3b1205543b24265b053b0411061509011a1011121d1a111007321d0611121b0c0904061b001b000d041108001b2700061d1a1304171518180b3c20393831181119111a000b371b1a0700060117001b060607151215061d100401071c3a1b001d121d1715001d1b1a212f1b161e1117005427151215061d2611191b00113a1b001d121d1715001d1b1a290f350404181124150d271107071d1b1a0627151215061d05191500171c0537061d3b270a371c061b1911543d3b2706171c061b191106371c061b19110a27000d18113911101d1504311013110003033d3004181b1510053d191513110d17061115001131181119111a000617151a0215070a131100371b1a00110c0002461009100615033d191513110c1311003d19151311301500150410150015061b1a181b15104e101500154e1d191513115b131d124f16150711424058264418333b30181c35253536353d35353535353535245b5b5b0d3c41363531353535353538353535353535363535313535353d3626353543030706170b13111b181b1715001d1b1a0d1a1b001d121d1715001d1b1a07040401071c04191d101d061715191106150a191d17061b041c1b1a1107070411151f11060b1011021d1711591d1a121b0f1615171f13061b011a1059070d1a170916180111001b1b001c12041106071d0700111a005907001b06151311141519161d111a0059181d131c005907111a071b060d151717111811061b191100110609130d061b07171b04110c1915131a11001b19110011060917181d04161b150610141517171107071d161d181d000d591102111a00070e17181d04161b15061059061115100f17181d04161b1506105903061d00110f04150d19111a00591c151a101811060b041106191d07071d1b1a070142031a1504014305050111060d041a15191104001c111a0507001500110604061b190400071306151a0011100610111a1d111005171500171c0719110707151311301d07541a1b005415540215181d1054111a0119540215180111541b1254000d041154241106191d07071d1b1a3a1519110319150403151818041e1b1d1a0e2c301b19151d1a261105011107000b170611150011241b040104130611191b02113102111a00381d0700111a11060d13181b16151827001b061513110c1b04111a3015001516150711091d1a10110c111030360b15000015171c3102111a000d3517001d02112c3b161e1117000d101d07041500171c3102111a000b15101036111c15021d1b06101510103102111a00381d0700111a11060b10110015171c3102111a0009121d06113102111a001039010015001d1b1a3b16071106021106133c20393839111a013d00111931181119111a00093d1a004c350606150d0b041b0700391107071513110d050111060d2711181117001b060b041106121b0619151a1711031a1b030618111a13001c0b171b1a00110c0039111a010f101b170119111a0031181119111a000c1a15001d021138111a13001c0b1e07321b1a0007381d07000b070d1a00150c3106061b0607131100201d191109001d191107001519040512181b1b0611131100201d19110e1b1a113b121207110008001d19110e1b1a11051915131d17060324061b0407061024061b0407031e07020b16061b03071106200d0411061d120615191103151d10050000171d100617181d111a000700002b07171d1005001b1f111a07190713200d04110b04061d0215170d391b101107151d10381d0700050000031d100800002b0311161d100700002311163d100b00002b0311161d102b02460900002311161d102246061507071d131a07041801131d1a070607170611111a06170107001b190e19073a1103201b1f111a381d0700060704181d171109001b1f111a381d0700040c19071d051d1a10110c090700061d1a131d120d041e071b1a0f2331362b3031223d37312b3d3a323b0a0611131d1b1a371b1a12090611041b0600210618", "dev", "[object Function]", "substring", "_invoke", "getBattery", "boeHost", "asgw", "Generator", "boe", "decorators", "exports", "accessor", "plugins", "this hasn't been initialised - super() hasn't been called", "delegate", "attempted to set read only static private field", "callback=", "MAX_TEXTURE_MAX_ANISOTROPY_EXT", "toStringTag", "; expires=", "match", "root", "setItem", "getContextAttributes", "withCredentials", 'Class "', "mozvisibilitychange", "userLanguage", "createHash", "map", "Cannot destructure ", "hBytes", "[object Number]", "floor", "access", "484e4f4a403f5243001a3309b621c6a00000000048c0ec7f000000650d14000111010012000047000c1101001200001400014500090211010143001400011101024a1200014300110001150002021101030304430114000211000202110104021101051101064a12000311000143011100024302070004430218140003110003420005077563656f6860690348495109524f4b435552474b56095552544f48414f405f40676465626360616e6f6c6d6a6b686976777475727370717e7f7c474445424340414e4f4c4d4a4b484956575455525350515e5f5c16171415121310111e1f0b08", "MAX_COMBINED_TEXTURE_IMAGE_UNITS", "default", "public", "vendorSub", "touchstart", "getExtension", "Aparajita", "finalize", "propertyIsEnumerable", "end", "localStorage", "@@iterator", "deviceMemory", "kHttp", "@@toStringTag", "cookieEnabled", "fromCharCode", "done", "set", "createDataChannel", "stringify", "async", "Descriptor", "hashed", "ontouchstart", "onicegatheringstatechange", "getOwnPropertyDescriptors", "then", "function", "CordiaUPC", "T_CLICK", "EXT_texture_filter_anisotropic", "MS Outlook", "80pSJSjK", "Jokerman", "byted_acrawler", "visibilityState", "span", "isWebmssdk", "__web_idontknowwhyiwriteit__", "cpuClass", "serif", "initialized", "accessor decorators must return an object with get, set, or init properties or void 0", "[object HTMLAllCollection]", "Decorating class property failed. Please ensure that proposal-class-properties is enabled and runs after the decorators transform.", "' to be a function", "VERSION", "toLowerCase", "MAX_TEXTURE_SIZE", "triggerUnload", "MAX_VERTEX_TEXTURE_IMAGE_UNITS", "element", "apply", "document", "exec", "send", ") can't be decorated.", "min", "484e4f4a403f5243002814122ddd79950000009eb285a1cb000000e811000114000402110201110001430147007c1102021200041400051100050700052347000f0700061102021200060c00024500120700061102021200060700041100050c0004140006021102030211020411000143011100064302140007021102051100074301140008021102061100080700054302140009021102031100071101011100090c000243021400040211010211000411000211000343034205000000003b03140003070000140001110100120001082334000611010012000247000208421101001200011400021100021101001500030011010015000211000311010015000108420007070d78173a322026043a25303b150a0a34360a3c3b2130273630252130310a3a25303b050a3a25303b0b0a0a34360a213026213c3100073826013a3e303b", "vivobrowser", "descriptor", "language", "484e4f4a403f524300023a25866a0150000000002b0aa01b000001541101001200004a12000143001400011100014a120002070003430103002a47000201420700041400021101013a070004254700060700044500090211010211010143011100022534000d1101014a1200054300070006263400161101031200071200054a12000811010143010700062634001e1101043a07000425470006070004450009021101021101044301110002253400151101044a12000543004a120002070009430103002734001e1101003a070004254700060700044500090211010211010043011100022534000d1101004a120005430007000a263400121101001200004a12000207000b430103002a34001e1101053a07000425470006070004450009021101021101054301110002254700020042021101064300324700331101073a070004254700060700044500090211010211010743011100022534000d1101074a120005430007000c2647000200420142000d096f697f685b7d7f746e0b6e7556756d7f68597b697f0773747e7f62557c087f767f796e687574096f747e7f7c73747f7e086e75496e6873747d0f417578707f796e3a4d73747e756d47096a68756e756e636a7f04797b7676085e75796f777f746e12417578707f796e3a547b6c737d7b6e7568470570697e757710417578707f796e3a5273696e75686347", "vendor", "level", "attempted to call ", "placement", "JS_MD5_NO_NODE_JS", "initializeClassElements", "indexedDB", "perf", "disallowProperty", "lime", "484e4f4a403f524300341b3e336a785800000000dbd5951f000001b50114000111010012000000254700070014000145001b1101001200000125470007011400014500090211010143001400010d010e0001010e0002010e00031100010e0004010e0005010e0006010e0007010e0008010e0009010e000a010e000b000e000c1400020211010243001100021500051100021200053247005c021101031100024301490211010411000243014902110105430011000215000702110106430011000215000802110107430011000215000902110108430011000215000b0211010943001100021500030211010a4300110002150002030014000311000303012f170003354911000311000212000b03012b2f170003354911000311000212000a03022b2f170003354911000311000212000903032b2f170003354911000311000212000803042b2f170003354911000311000212000703052b2f17000335491100031100020700061303062b2f170003354911000311000212000503072b2f17000335491100031100020700041303082b2f170003354911000311000212000303092b2f1700033549110003110002120002030a2b2f170003354911010b12000d1100032f11010b07000d354911000242000e0e547b6a796a66587c627f686344650a6f62796e687f58626c650a6864657862787f6e657f086764686a7f62646506787c627f6863036f6466086f6e697e6c6c6e790465646f6e077b636a657f6466097c6e696f79627d6e7909626568646c65627f640463646460047f6e787f076e657d68646f6e", "moveList", "MYRIAD PRO", "An element descriptor", "finallyLoc", "head", "innerHeight", "ORIGIN: ", "region", "addInitializer", "[object SafariRemoteNotification]", "candidate", "content-type", "userAgent", "webkitHidden", "test", "getPrototypeOf", "setDate", "shiftKey", "accessor.init", "@@asyncIterator", "accessor.set"]; + w_0x42f5 = function () { + return e; + }; + return w_0x42f5(); + } + function w_0x5c3140(e, r, t) { + var a = w_0x25f3; + function n(e, r) { + var t = w_0x25f3; + var a = parseInt(e[t(677)](r, r + 2), 16); + return a >>> 7 == 0 ? [1, a] : a >>> 6 == 2 ? (a = (63 & a) << 8, [2, a += parseInt(e.slice(r + 2, r + 4), 16)]) : (a = (63 & a) << 16, + [3, a += parseInt(e[t(677)](r + 2, r + 6), 16)]); + } + var f; + var i = 0; + var c = []; + var o = []; + var d = parseInt(e[a(677)](0, 8), 16); + var _ = parseInt(e[a(677)](8, 16), 16); + if (1213091658 !== d || 1077891651 !== _) { + throw new Error(a(679)); + } + if (0 !== parseInt(e[a(677)](16, 18), 16)) { + throw new Error("ve"); + } + for (f = 0; f < 4; ++f) { + i += (3 & parseInt(e[a(677)](24 + 2 * f, 26 + 2 * f), 16)) << 2 * f; + } + var x = parseInt(e[a(677)](32, 40), 16); + var u = 2 * parseInt(e[a(677)](48, 56), 16); + for (f = 56; f < u + 56; f += 2) { + c[a(878)](parseInt(e[a(677)](f, f + 2), 16)); + } + var b = u + 56; + var v = parseInt(e[a(677)](b, b + 4), 16); + for (b += 4, f = 0; f < v; ++f) { + var s = n(e, b); + b += 2 * s[0]; + for (var l = "", h = 0; h < s[1]; ++h) { + var w = n(e, b); + l += String[a(482)](i ^ w[1]); + b += 2 * w[0]; + } + o.push(l); + } + return r.p = null, + function e(r, t, n, f, i) { + var d = a; + var _; + var x; + var u; + var b; + var v; + var s = -1; + var l = []; + var h = [0, null]; + var w = null; + var g = [t]; + for (x = Math.min(t[d(601)], n), u = 0; u < x; ++u) { + g[d(878)](t[u]); + } + g.p = f; + for (var p = []; ;) { + try { + var y = c[r++]; + if (y < 39) { + if (y < 19) { + if (y < 7) { + if (y < 3) { + l[++s] = y < 1 || 1 !== y && null; + } else if (y < 5) { + if (3 === y) { + _ = c[r++]; + l[++s] = _ << 24 >> 24; + } else { + _ = (c[r] << 8) + c[r + 1]; + r += 2; + l[++s] = _ << 16 >> 16; + } + } else if (5 === y) { + _ = ((_ = ((_ = c[r++]) << 8) + c[r++]) << 8) + c[r++]; + l[++s] = (_ << 8) + c[r++]; + } else { + _ = (c[r] << 8) + c[r + 1]; + r += 2; + l[++s] = +o[_]; + } + } else if (y < 13) { + if (y < 11) { + if (7 === y) { + _ = (c[r] << 8) + c[r + 1]; + r += 2; + l[++s] = o[_]; + } else { + l[++s] = void 0; + } + } else if (11 === y) { + l[++s] = i; + } else { + _ = (c[r] << 8) + c[r + 1]; + r += 2; + s = s - _ + 1; + x = l[d(677)](s, s + _); + l[s] = x; + } + } else if (y < 17) { + if (13 === y) { + l[++s] = {}; + } else { + _ = (c[r] << 8) + c[r + 1]; + r += 2; + x = o[_]; + u = l[s--]; + l[s][x] = u; + } + } else if (17 === y) { + for (x = c[r++], u = c[r++], b = g; x > 0; --x) { + b = b.p; + } + l[++s] = b[u]; + } else { + _ = (c[r] << 8) + c[r + 1]; + r += 2; + x = o[_]; + l[s] = l[s][x]; + } + } else if (y < 27) { + if (y < 23) { + if (y < 21) { + if (19 === y) { + x = l[s--]; + l[s] = l[s][x]; + } else { + for (x = c[r++], u = c[r++], b = g; x > 0; --x) { + b = b.p; + } + b[u] = l[s--]; + } + } else if (21 === y) { + _ = (c[r] << 8) + c[r + 1]; + r += 2; + x = o[_]; + u = l[s--]; + b = l[s--]; + u[x] = b; + } else { + x = l[s--]; + u = l[s--]; + b = l[s--]; + u[x] = b; + } + } else if (y < 25) { + if (23 === y) { + for (x = c[r++], u = c[r++], b = g, b = g; x > 0; --x) { + b = b.p; + } + l[++s] = b; + l[++s] = u; + } else { + x = l[s--]; + l[s] += x; + } + } else if (25 === y) { + x = l[s--]; + l[s] -= x; + } else { + x = l[s--]; + l[s] *= x; + } + } else if (y < 35) { + if (y < 29) { + if (27 === y) { + x = l[s--]; + l[s] /= x; + } else { + x = l[s--]; + l[s] %= x; + } + } else if (29 === y) { + l[s] = -l[s]; + } else { + x = l[s--]; + u = l[s--]; + l[++s] = u[x]++; + } + } else if (y < 37) { + if (35 === y) { + x = l[s--]; + l[s] = l[s] == x; + } else { + x = l[s--]; + l[s] = l[s] != x; + } + } else if (37 === y) { + x = l[s--]; + l[s] = l[s] === x; + } else { + x = l[s--]; + l[s] = l[s] !== x; + } + } else if (y < 57) { + if (y < 47) { + if (y < 43) { + if (y < 41) { + x = l[s--]; + l[s] = l[s] < x; + } else if (41 === y) { + x = l[s--]; + l[s] = l[s] > x; + } else { + x = l[s--]; + l[s] = l[s] >= x; + } + } else if (y < 45) { + if (43 === y) { + x = l[s--]; + l[s] = l[s] << x; + } else { + x = l[s--]; + l[s] = l[s] >> x; + } + } else if (45 === y) { + x = l[s--]; + l[s] = l[s] >>> x; + } else { + x = l[s--]; + l[s] = l[s] & x; + } + } else if (y < 52) { + if (y < 50) { + if (47 === y) { + x = l[s--]; + l[s] = l[s] | x; + } else { + x = l[s--]; + l[s] = l[s] ^ x; + } + } else if (50 === y) { + l[s] = !l[s]; + } else { + _ = (_ = (c[r] << 8) + c[r + 1]) << 16 >> 16; + r += 2; + l[s] ? --s : r += _; + } + } else if (y < 54) { + if (52 === y) { + _ = (_ = (c[r] << 8) + c[r + 1]) << 16 >> 16; + r += 2; + l[s] ? r += _ : --s; + } else { + x = l[s--]; + (u = l[s--])[x] = l[s]; + } + } else if (54 === y) { + x = l[s--]; + l[s] = l[s] in x; + } else { + x = l[s--]; + l[s] = l[s] instanceof x; + } + } else if (y < 66) { + if (y < 61) { + if (y < 59) { + if (57 === y) { + x = l[s--]; + u = l[s--]; + l[++s] = delete u[x]; + } else { + l[s] = typeof l[s]; + } + } else if (59 === y) { + _ = c[r++]; + x = l[s--]; + (u = function e() { + var r = e._u; + var t = e._v; + return r(t[0], arguments, t[1], t[2], this); + })._v = [x, _, g]; + u._u = e; + l[++s] = u; + } else { + _ = c[r++]; + x = l[s--]; + (b = [u = function e() { + var r = e._u; + var t = e._v; + return r(t[0], arguments, t[1], t[2], this); + } + ]).p = g; + u._v = [x, _, b]; + u._u = e; + l[++s] = u; + } + } else if (y < 64) { + if (61 === y) { + _ = (_ = (c[r] << 8) + c[r + 1]) << 16 >> 16; + r += 2; + (x = p[p.length - 1])[1] = r + _; + } else { + _ = (_ = (c[r] << 8) + c[r + 1]) << 16 >> 16; + r += 2; + (x = p[p.length - 1]) && !x[1] ? (x[0] = 3, x[d(878)](r)) : p[d(878)]([1, 0, r]); + r += _; + } + } else { + if (64 === y) { + throw x = l[s--]; + } + if (u = (x = p.pop())[0], b = h[0], 1 === u) { + r = x[1]; + } else if (0 === u) { + if (0 === b) { + r = x[1]; + } else { + if (1 !== b) { + throw h[1]; + } + if (!w) { + return h[1]; + } + r = w[1]; + i = w[2]; + g = w[3]; + p = w[4]; + l[++s] = h[1]; + h = [0, null]; + w = w[0]; + } + } else { + r = x[2]; + x[0] = 0; + p[d(878)](x); + } + } + } else if (y < 71) { + if (y < 68) { + if (66 === y) { + for (x = l[s--], u = null; b = p[d(615)]();) { + if (2 === b[0] || 3 === b[0]) { + u = b; + break; + } + } + if (u) { + h = [1, x]; + r = u[2]; + u[0] = 0; + p[d(878)](u); + } else { + if (!w) { + return x; + } + r = w[1]; + i = w[2]; + g = w[3]; + p = w[4]; + l[++s] = x; + h = [0, null]; + w = w[0]; + } + } else { + s -= _ = c[r++]; + u = l.slice(s + 1, s + _ + 1); + x = l[s--]; + b = l[s--]; + x._u === e ? (x = x._v, w = [w, r, i, g, p], r = x[0], null == b && (b = function () { + return this; + } + ()), i = b, (g = [u].concat(u))[d(601)] = Math[d(524)](x[1], _) + 1, g.p = x[2], + p = []) : l[++s] = x[d(519)](b, u); + } + } else if (68 === y) { + for (_ = c[r++], b = [void 0], v = _; v > 0; --v) { + b[v] = l[s--]; + } + u = l[s--]; + x = Function.bind[d(519)](u, b); + l[++s] = new x(); + } else { + r += 2 + (_ = (_ = (c[r] << 8) + c[r + 1]) << 16 >> 16); + } + } else if (y < 73) { + if (71 === y) { + _ = (_ = (c[r] << 8) + c[r + 1]) << 16 >> 16; + r += 2; + (x = l[s--]) || (r += _); + } else { + _ = (_ = (c[r] << 8) + c[r + 1]) << 16 >> 16; + r += 2; + x = l[s--]; + l[s] === x && (--s, r += _); + } + } else if (73 === y) { + --s; + } else { + x = l[s]; + l[++s] = x; + } + } catch (e) { + console.log(e); + for (h = [0, null]; (_ = p[d(615)]()) && !_[0];) { } + if (!_) { + e: for (; w;) { + for (x = w[4]; _ = x[d(615)]();) { + if (_[0]) { + break e; + } + } + w = w[0]; + } + if (!w) { + throw e; + } + r = w[1]; + i = w[2]; + g = w[3]; + p = w[4]; + w = w[0]; + } + if (1 === (x = _[0])) { + r = _[2]; + _[0] = 0; + p.push(_); + l[++s] = e; + } else if (2 === x) { + r = _[2]; + _[0] = 0; + p.push(_); + h = [3, e]; + } else { + r = _[3]; + _[0] = 2; + p[d(878)](_); + l[++s] = e; + } + } + } + } + (x, [], 0, r, t); + } + !function (e, r) { + //var t = w_0x25f3; + //"object" == typeof exports && t(900) != typeof module ? r(exports) : t(494) == typeof define && define.amd ? define([t(440)], r) : r((e = t(900) != typeof globalThis ? globalThis : e || self)[t(501)] = {}); + r(window.byted_acrawler = {}); + } + (this, function (_0x1d18f2) { + "use strict"; + var _0x5612de = w_0x25f3; + function _0x137ba2(e) { + var r = w_0x25f3; + var t; + var a; + function n(r, t) { + var a = w_0x25f3; + try { + var i = e[r](t); + var c = i[a(740)]; + var o = c instanceof _0x59d886; + Promise[a(632)](o ? c.v : c)[a(493)](function (t) { + var d = a; + if (o) { + if ("return" === r) { + var _ = d(765); + } else { + _ = d(905); + } + if (!c.k || t.done) { + return n(_, t); + } + t = e[_](t)[d(740)]; + } + f(i[d(483)] ? d(765) : d(753), t); + }, function (e) { + var r = a; + n(r(592), e); + }); + } catch (e) { + console.log(e); + f(a(592), e); + } + } + function f(e, r) { + var f = w_0x25f3; + switch (e) { + case f(765): + t[f(632)]({ + value: r, + done: !0 + }); + break; + + case "throw": + t[f(923)](r); + break; + + default: + t[f(632)]({ + value: r, + done: !1 + }); + } + (t = t.next) ? n(t[f(914)], t[f(807)]) : a = null; + } + this._invoke = function (e, r) { + return new Promise(function (f, i) { + var c = w_0x25f3; + var o = { + key: e, + arg: r, + resolve: f, + reject: i, + next: null + }; + if (a) { + a = a[c(905)] = o; + } else { + t = a = o; + n(e, r); + } + }); + }; + r(494) != typeof e[r(765)] && (this[r(765)] = void 0); + } + function _0x59d886(e, r) { + this.v = e; + this.k = r; + } + function _0x1d9867(e, r, t, a) { + return { + getMetadata: function (n) { + var f = w_0x25f3; + _0x1fca4f(a, f(841)); + _0x525dc3(n); + var i = e[n]; + if (void 0 !== i) { + if (1 === r) { + var c = i[f(468)]; + if (void 0 !== c) { + return c[t]; + } + } else if (2 === r) { + var o = i[f(609)]; + if (void 0 !== o) { + return o[f(811)](t); + } + } else if (Object[f(952)][f(820)](i, f(684))) { + return i[f(684)]; + } + } + }, + setMetadata: function (n, f) { + var i = w_0x25f3; + _0x1fca4f(a, i(917)); + _0x525dc3(n); + var c = e[n]; + if (void 0 === c && (c = e[n] = {}), 1 === r) { + var o = c[i(468)]; + void 0 === o && (o = c[i(468)] = {}); + o[t] = f; + } else if (2 === r) { + var d = c.priv; + void 0 === d && (d = c[i(609)] = new Map()); + d[i(484)](t, f); + } else { + c[i(684)] = f; + } + } + }; + } + function _0x43bce4(e, r) { + var t = w_0x25f3; + var a = e[Symbol.metadata || Symbol[t(798)]("Symbol.metadata")]; + var n = Object[t(904)](r); + if (0 !== n[t(601)]) { + for (var f = 0; f < n[t(601)]; f++) { + var i = n[f]; + var c = r[i]; + if (a) { + var o = a[i]; + } else { + o = null; + } + var d = c[t(468)]; + if (o) { + var _ = o[t(468)]; + } else { + _ = null; + } + d && _ && Object.setPrototypeOf(d, _); + var x = c[t(609)]; + if (x) { + var u = Array[t(668)](x.values()); + var b = o ? o[t(609)] : null; + b && (u = u[t(696)](b)); + c[t(609)] = u; + } + o && Object[t(571)](c, o); + } + a && Object.setPrototypeOf(r, a); + e[Symbol[t(931)] || Symbol.for("Symbol.metadata")] = r; + } + } + function _0x26f5a8(e, r) { + return function (t) { + var a = w_0x25f3; + _0x1fca4f(r, "addInitializer"); + _0x3ccc16(t, a(954)); + e[a(878)](t); + }; + } + function _0x2bb58f(e, r, t, a, n, f, i, c, o) { + var d = w_0x25f3; + var _; + switch (f) { + case 1: + _ = d(441); + break; + + case 2: + _ = d(602); + break; + + case 3: + _ = d(385); + break; + + case 4: + _ = d(861); + break; + + default: + _ = d(572); + } + var x; + var u; + var b = { + kind: _, + name: c ? "#" + r : r, + isStatic: i, + isPrivate: c + }; + var v = { + v: !1 + }; + if (0 !== f && (b.addInitializer = _0x26f5a8(n, v)), c) { + x = 2; + u = Symbol(r); + var s = {}; + 0 === f ? (s[d(811)] = t[d(811)], s.set = t.set) : 2 === f ? s[d(811)] = function () { + var e = d; + return t[e(740)]; + } + : (1 !== f && 3 !== f || (s[d(811)] = function () { + var e = d; + return t[e(811)][e(820)](this); + }), 1 !== f && 4 !== f || (s[d(484)] = function (e) { + var r = d; + t.set[r(820)](this, e); + })); + b[d(464)] = s; + } else { + x = 1; + u = r; + } + try { + return e(o, Object[d(672)](b, _0x1d9867(a, x, u, v))); + } finally { + v.v = !0; + } + } + function _0x1fca4f(e, r) { + var t = w_0x25f3; + if (e.v) { + throw new Error(t(532) + r + t(360)); + } + } + function _0x525dc3(e) { + var r = w_0x25f3; + if (r(749) != typeof e) { + throw new TypeError(r(743) + e); + } + } + function _0x3ccc16(e, r) { + var t = w_0x25f3; + if ("function" != typeof e) { + throw new TypeError(r + t(760)); + } + } + function _0x32dfd4(e, r) { + var t = w_0x25f3; + var a = typeof r; + if (1 === e) { + if (t(381) !== a || null === r) { + throw new TypeError(t(509)); + } + void 0 !== r[t(811)] && _0x3ccc16(r[t(811)], t(368)); + void 0 !== r[t(484)] && _0x3ccc16(r[t(484)], t(561)); + void 0 !== r.init && _0x3ccc16(r.init, t(559)); + void 0 !== r[t(720)] && _0x3ccc16(r[t(720)], "accessor.initializer"); + } else if (t(494) !== a) { + throw new TypeError(t(0 === e ? 572 : 10 === e ? 412 : 602) + t(717)); + } + } + function _0x372120(e) { + var r = w_0x25f3; + var t; + return null == (t = e.init) && (t = e[r(720)]) && r(900) != typeof console && console[r(944)](r(779)), + t; + } + function _0x481bfe(e, r, t, a, n, f, i, c, o) { + var d = w_0x25f3; + var _; + var x; + var u; + var b; + var v; + var s; + var l = t[0]; + if (i ? _ = 0 === n || 1 === n ? { + get: t[3], + set: t[4] + } + : 3 === n ? { + get: t[3] + } + : 4 === n ? { + set: t[3] + } + : { + value: t[3] + } + : 0 !== n && (_ = Object[d(678)](r, a)), 1 === n ? u = { + get: _.get, + set: _[d(484)] + } + : 2 === n ? u = _[d(740)] : 3 === n ? u = _.get : 4 === n && (u = _[d(484)]), + d(494) == typeof l) { + if (void 0 !== (b = _0x2bb58f(l, a, _, c, o, n, f, i, u))) { + _0x32dfd4(n, b); + 0 === n ? x = b : 1 === n ? (x = _0x372120(b), v = b[d(811)] || u[d(811)], s = b.set || u.set, + u = { + get: v, + set: s + }) : u = b; + } + } else { + for (var h = l.length - 1; h >= 0; h--) { + var w; + if (void 0 !== (b = _0x2bb58f(l[h], a, _, c, o, n, f, i, u))) { + _0x32dfd4(n, b); + 0 === n ? w = b : 1 === n ? (w = _0x372120(b), v = b[d(811)] || u[d(811)], s = b[d(484)] || u[d(484)], + u = { + get: v, + set: s + }) : u = b; + void 0 !== w && (void 0 === x ? x = w : d(494) == typeof x ? x = [x, w] : x.push(w)); + } + } + } + if (0 === n || 1 === n) { + if (void 0 === x) { + x = function (e, r) { + return r; + }; + } else if (d(494) != typeof x) { + var g = x; + x = function (e, r) { + var t = d; + for (var a = r, n = 0; n < g.length; n++) { + a = g[n][t(820)](e, a); + } + return a; + }; + } else { + var p = x; + x = function (e, r) { + return p.call(e, r); + }; + } + e[d(878)](x); + } + if (0 !== n) { + 1 === n ? (_[d(811)] = u[d(811)], _[d(484)] = u[d(484)]) : 2 === n ? _[d(740)] = u : 3 === n ? _.get = u : 4 === n && (_[d(484)] = u); + i ? 1 === n ? (e[d(878)](function (e, r) { + var t = d; + return u[t(811)][t(820)](e, r); + }), e[d(878)](function (e, r) { + var t = d; + return u[t(484)][t(820)](e, r); + })) : 2 === n ? e.push(u) : e[d(878)](function (e, r) { + var t = d; + return u[t(820)](e, r); + }) : Object[d(373)](r, a, _); + } + } + function _0xa6bc9e(e, r, t, a, n) { + var f = w_0x25f3; + for (var i, c, o = new Map(), d = new Map(), _ = 0; _ < n[f(601)]; _++) { + var x = n[_]; + if (Array.isArray(x)) { + var u; + var b; + var v; + var s = x[1]; + var l = x[2]; + var h = x[f(601)] > 3; + var w = s >= 5; + if (w ? (u = r, b = a, 0 != (s -= 5) && (v = c = c || [])) : (u = r[f(836)], b = t, + 0 !== s && (v = i = i || [])), 0 !== s && !h) { + if (w) { + var g = d; + } else { + g = o; + } + var p = g[f(811)](l) || 0; + if (!0 === p || 3 === p && 4 !== s || 4 === p && 3 !== s) { + throw new Error(f(849) + l); + } + !p && s > 2 ? g[f(484)](l, s) : g.set(l, !0); + } + _0x481bfe(e, u, x, l, s, w, h, b, v); + } + } + _0x3657e2(e, i); + _0x3657e2(e, c); + } + function _0x3657e2(e, r) { + var t = w_0x25f3; + r && e[t(878)](function (e) { + var a = t; + for (var n = 0; n < r.length; n++) { + r[n][a(820)](e); + } + return e; + }); + } + function _0x5f3676(e, r, t, a) { + var n = w_0x25f3; + if (a.length > 0) { + for (var f = [], i = r, c = r.name, o = a[n(601)] - 1; o >= 0; o--) { + var d = { + v: !1 + }; + try { + var _ = Object.assign({ + kind: n(412), + name: c, + addInitializer: _0x26f5a8(f, d) + }, _0x1d9867(t, 0, c, d)); + var x = a[o](i, _); + } finally { + d.v = !0; + } + if (void 0 !== x) { + _0x32dfd4(10, x); + i = x; + } + } + e[n(878)](i, function () { + var e = n; + for (var r = 0; r < f[e(601)]; r++) { + f[r][e(820)](i); + } + }); + } + } + function _0x51be3f(e, r, t) { + var a = w_0x25f3; + var n = []; + var f = {}; + var i = {}; + return _0xa6bc9e(n, e, i, f, r), + _0x43bce4(e[a(836)], i), + _0x5f3676(n, e, f, t), + _0x43bce4(e, f), + n; + } + function _0x281b3b() { + function e(e, r) { + return function (a) { + var n = w_0x25f3; + !function (e, r) { + var t = w_0x25f3; + if (e.v) { + throw new Error(t(599)); + } + } + (r); + t(a, n(954)); + e[n(878)](a); + }; + } + function r(r, t, a, n, f, i, c, o) { + var d = w_0x25f3; + var _; + switch (f) { + case 1: + _ = "accessor"; + break; + + case 2: + _ = d(602); + break; + + case 3: + _ = "getter"; + break; + + case 4: + _ = "setter"; + break; + + default: + _ = d(572); + } + var x; + var u; + var b = { + kind: _, + name: c ? "#" + t : t, + static: i, + private: c + }; + var v = { + v: !1 + }; + 0 !== f && (b.addInitializer = e(n, v)); + 0 === f ? c ? (x = a.get, u = a[d(484)]) : (x = function () { + return this[t]; + }, u = function (e) { + this[t] = e; + }) : 2 === f ? x = function () { + var e = d; + return a[e(740)]; + } + : (1 !== f && 3 !== f || (x = function () { + var e = d; + return a[e(811)][e(820)](this); + }), 1 !== f && 4 !== f || (u = function (e) { + var r = d; + a[r(484)].call(this, e); + })); + b[d(464)] = x && u ? { + get: x, + set: u + } + : x ? { + get: x + } + : { + set: u + }; + try { + return r(o, b); + } finally { + v.v = !0; + } + } + function t(e, r) { + var t = w_0x25f3; + if (t(494) != typeof e) { + throw new TypeError(r + " must be a function"); + } + } + function a(e, r) { + var a = w_0x25f3; + var n = typeof r; + if (1 === e) { + if (a(381) !== n || null === r) { + throw new TypeError(a(509)); + } + void 0 !== r[a(811)] && t(r[a(811)], a(368)); + void 0 !== r[a(484)] && t(r[a(484)], a(561)); + void 0 !== r[a(953)] && t(r[a(953)], a(559)); + } else if ("function" !== n) { + throw new TypeError(a(0 === e ? 572 : 10 === e ? 412 : 602) + " decorators must return a function or void 0"); + } + } + function n(e, t, n, f, i, c, o, d) { + var _ = w_0x25f3; + var x; + var u; + var b; + var v; + var s; + var l; + var h = n[0]; + if (o ? x = 0 === i || 1 === i ? { + get: n[3], + set: n[4] + } + : 3 === i ? { + get: n[3] + } + : 4 === i ? { + set: n[3] + } + : { + value: n[3] + } + : 0 !== i && (x = Object[_(678)](t, f)), 1 === i ? b = { + get: x[_(811)], + set: x.set + } + : 2 === i ? b = x[_(740)] : 3 === i ? b = x.get : 4 === i && (b = x[_(484)]), + "function" == typeof h) { + if (void 0 !== (v = r(h, f, x, d, i, c, o, b))) { + a(i, v); + 0 === i ? u = v : 1 === i ? (u = v.init, s = v[_(811)] || b[_(811)], l = v[_(484)] || b[_(484)], + b = { + get: s, + set: l + }) : b = v; + } + } else { + for (var w = h[_(601)] - 1; w >= 0; w--) { + var g; + if (void 0 !== (v = r(h[w], f, x, d, i, c, o, b))) { + a(i, v); + 0 === i ? g = v : 1 === i ? (g = v[_(953)], s = v[_(811)] || b[_(811)], l = v[_(484)] || b[_(484)], + b = { + get: s, + set: l + }) : b = v; + void 0 !== g && (void 0 === u ? u = g : _(494) == typeof u ? u = [u, g] : u.push(g)); + } + } + } + if (0 === i || 1 === i) { + if (void 0 === u) { + u = function (e, r) { + return r; + }; + } else if (_(494) != typeof u) { + var p = u; + u = function (e, r) { + var t = _; + for (var a = r, n = 0; n < p[t(601)]; n++) { + a = p[n].call(e, a); + } + return a; + }; + } else { + var y = u; + u = function (e, r) { + var t = _; + return y[t(820)](e, r); + }; + } + e.push(u); + } + if (0 !== i) { + 1 === i ? (x[_(811)] = b.get, x[_(484)] = b.set) : 2 === i ? x[_(740)] = b : 3 === i ? x[_(811)] = b : 4 === i && (x[_(484)] = b); + o ? 1 === i ? (e[_(878)](function (e, r) { + var t = _; + return b.get[t(820)](e, r); + }), e[_(878)](function (e, r) { + var t = _; + return b[t(484)][t(820)](e, r); + })) : 2 === i ? e.push(b) : e[_(878)](function (e, r) { + return b.call(e, r); + }) : Object[_(373)](t, f, x); + } + } + function f(e, r) { + var t = w_0x25f3; + r && e[t(878)](function (e) { + var a = t; + for (var n = 0; n < r[a(601)]; n++) { + r[n][a(820)](e); + } + return e; + }); + } + return function (r, t, i) { + var c = []; + return function (e, r, t) { + var a = w_0x25f3; + for (var i, c, o = new Map(), d = new Map(), _ = 0; _ < t[a(601)]; _++) { + var x = t[_]; + if (Array.isArray(x)) { + var u; + var b; + var v = x[1]; + var s = x[2]; + var l = x[a(601)] > 3; + var h = v >= 5; + if (h ? (u = r, 0 != (v -= 5) && (b = c = c || [])) : (u = r.prototype, 0 !== v && (b = i = i || [])), + 0 !== v && !l) { + if (h) { + var w = d; + } else { + w = o; + } + var g = w[a(811)](s) || 0; + if (!0 === g || 3 === g && 4 !== v || 4 === g && 3 !== v) { + throw new Error("Attempted to decorate a public method/accessor that has the same name as a previously decorated public method/accessor. This is not currently supported by the decorators plugin. Property name was: " + s); + } + !g && v > 2 ? w[a(484)](s, v) : w[a(484)](s, !0); + } + n(e, u, x, s, v, h, l, b); + } + } + f(e, i); + f(e, c); + } + (c, r, t), + function (r, t, n) { + var f = w_0x25f3; + if (n[f(601)] > 0) { + for (var i = [], c = t, o = t[f(833)], d = n[f(601)] - 1; d >= 0; d--) { + var _ = { + v: !1 + }; + try { + var x = n[d](c, { + kind: f(412), + name: o, + addInitializer: e(i, _) + }); + } finally { + _.v = !0; + } + if (void 0 !== x) { + a(10, x); + c = x; + } + } + r.push(c, function () { + var e = f; + for (var r = 0; r < i[e(601)]; r++) { + i[r].call(c); + } + }); + } + } + (c, r, i), + c; + }; + } + var _0x15b960; + var _0x500e9f; + function _0x4ab133(e, r, t) { + return (_0x15b960 = _0x15b960 || _0x281b3b())(e, r, t); + } + function _0x4591cd() { + function e(e, r) { + return function (a) { + var n = w_0x25f3; + !function (e, r) { + var t = w_0x25f3; + if (e.v) { + throw new Error(t(599)); + } + } + (r); + t(a, n(954)); + e[n(878)](a); + }; + } + function r(r, t, a, n, f, i, c, o) { + var d = w_0x25f3; + var _; + switch (f) { + case 1: + _ = d(441); + break; + + case 2: + _ = d(602); + break; + + case 3: + _ = d(385); + break; + + case 4: + _ = d(861); + break; + + default: + _ = d(572); + } + var x; + var u; + var b = { + kind: _, + name: c ? "#" + t : t, + static: i, + private: c + }; + var v = { + v: !1 + }; + 0 !== f && (b[d(549)] = e(n, v)); + 0 === f ? c ? (x = a[d(811)], u = a.set) : (x = function () { + return this[t]; + }, u = function (e) { + this[t] = e; + }) : 2 === f ? x = function () { + var e = d; + return a[e(740)]; + } + : (1 !== f && 3 !== f || (x = function () { + var e = d; + return a.get[e(820)](this); + }), 1 !== f && 4 !== f || (u = function (e) { + a.set.call(this, e); + })); + b[d(464)] = x && u ? { + get: x, + set: u + } + : x ? { + get: x + } + : { + set: u + }; + try { + return r(o, b); + } finally { + v.v = !0; + } + } + function t(e, r) { + var t = w_0x25f3; + if (t(494) != typeof e) { + throw new TypeError(r + t(760)); + } + } + function a(e, r) { + var a = w_0x25f3; + var n = typeof r; + if (1 === e) { + if (a(381) !== n || null === r) { + throw new TypeError(a(509)); + } + void 0 !== r[a(811)] && t(r[a(811)], "accessor.get"); + void 0 !== r[a(484)] && t(r[a(484)], a(561)); + void 0 !== r[a(953)] && t(r.init, a(559)); + } else if (a(494) !== n) { + throw new TypeError(a(0 === e ? 572 : 10 === e ? 412 : 602) + a(717)); + } + } + function n(e, t, n, f, i, c, o, d) { + var _ = w_0x25f3; + var x; + var u; + var b; + var v; + var s; + var l; + var h = n[0]; + if (o ? x = 0 === i || 1 === i ? { + get: n[3], + set: n[4] + } + : 3 === i ? { + get: n[3] + } + : 4 === i ? { + set: n[3] + } + : { + value: n[3] + } + : 0 !== i && (x = Object[_(678)](t, f)), 1 === i ? b = { + get: x[_(811)], + set: x.set + } + : 2 === i ? b = x[_(740)] : 3 === i ? b = x[_(811)] : 4 === i && (b = x.set), + _(494) == typeof h) { + if (void 0 !== (v = r(h, f, x, d, i, c, o, b))) { + a(i, v); + 0 === i ? u = v : 1 === i ? (u = v[_(953)], s = v[_(811)] || b.get, l = v[_(484)] || b[_(484)], + b = { + get: s, + set: l + }) : b = v; + } + } else { + for (var w = h[_(601)] - 1; w >= 0; w--) { + var g; + if (void 0 !== (v = r(h[w], f, x, d, i, c, o, b))) { + a(i, v); + 0 === i ? g = v : 1 === i ? (g = v.init, s = v[_(811)] || b.get, l = v[_(484)] || b[_(484)], + b = { + get: s, + set: l + }) : b = v; + void 0 !== g && (void 0 === u ? u = g : _(494) == typeof u ? u = [u, g] : u[_(878)](g)); + } + } + } + if (0 === i || 1 === i) { + if (void 0 === u) { + u = function (e, r) { + return r; + }; + } else if ("function" != typeof u) { + var p = u; + u = function (e, r) { + var t = _; + for (var a = r, n = 0; n < p[t(601)]; n++) { + a = p[n].call(e, a); + } + return a; + }; + } else { + var y = u; + u = function (e, r) { + return y.call(e, r); + }; + } + e[_(878)](u); + } + if (0 !== i) { + 1 === i ? (x[_(811)] = b[_(811)], x[_(484)] = b[_(484)]) : 2 === i ? x[_(740)] = b : 3 === i ? x[_(811)] = b : 4 === i && (x[_(484)] = b); + o ? 1 === i ? (e[_(878)](function (e, r) { + var t = _; + return b[t(811)][t(820)](e, r); + }), e[_(878)](function (e, r) { + var t = _; + return b[t(484)][t(820)](e, r); + })) : 2 === i ? e.push(b) : e[_(878)](function (e, r) { + var t = _; + return b[t(820)](e, r); + }) : Object[_(373)](t, f, x); + } + } + function f(e, r) { + var t = w_0x25f3; + for (var a, f, c = [], o = new Map(), d = new Map(), _ = 0; _ < r[t(601)]; _++) { + var x = r[_]; + if (Array[t(687)](x)) { + var u; + var b; + var v = x[1]; + var s = x[2]; + var l = x.length > 3; + var h = v >= 5; + if (h ? (u = e, 0 != (v -= 5) && (b = f = f || [])) : (u = e[t(836)], 0 !== v && (b = a = a || [])), + 0 !== v && !l) { + if (h) { + var w = d; + } else { + w = o; + } + var g = w[t(811)](s) || 0; + if (!0 === g || 3 === g && 4 !== v || 4 === g && 3 !== v) { + throw new Error("Attempted to decorate a public method/accessor that has the same name as a previously decorated public method/accessor. This is not currently supported by the decorators plugin. Property name was: " + s); + } + !g && v > 2 ? w[t(484)](s, v) : w.set(s, !0); + } + n(c, u, x, s, v, h, l, b); + } + } + return i(c, a), + i(c, f), + c; + } + function i(e, r) { + var t = w_0x25f3; + r && e[t(878)](function (e) { + var a = t; + for (var n = 0; n < r.length; n++) { + r[n][a(820)](e); + } + return e; + }); + } + return function (r, t, n) { + return { + e: f(r, t), + get c() { + return function (r, t) { + var n = w_0x25f3; + if (t.length > 0) { + for (var f = [], i = r, c = r[n(833)], o = t[n(601)] - 1; o >= 0; o--) { + var d = { + v: !1 + }; + try { + var _ = t[o](i, { + kind: n(412), + name: c, + addInitializer: e(f, d) + }); + } finally { + d.v = !0; + } + if (void 0 !== _) { + a(10, _); + i = _; + } + } + return [i, function () { + var e = n; + for (var r = 0; r < f.length; r++) { + f[r][e(820)](i); + } + } + ]; + } + } + (r, n); + } + }; + }; + } + function _0x49af1f(e, r, t) { + return (_0x49af1f = _0x4591cd())(e, r, t); + } + function _0x3e4ff5(e, r) { + return function (t) { + var a = w_0x25f3; + _0x15eb90(r, a(549)); + _0x45d8b1(t, a(954)); + e[a(878)](t); + }; + } + function _0x19207d(e, r) { + var t = w_0x25f3; + if (!e(r)) { + throw new TypeError(t(597)); + } + } + function _0x151762(e, r, t, a, n, f, i, c, o) { + var d = w_0x25f3; + var _; + switch (n) { + case 1: + _ = d(441); + break; + + case 2: + _ = d(602); + break; + + case 3: + _ = d(385); + break; + + case 4: + _ = d(861); + break; + + default: + _ = d(572); + } + var x; + var u; + var b = { + kind: _, + name: i ? "#" + r : r, + static: f, + private: i + }; + var v = { + v: !1 + }; + if (0 !== n && (b[d(549)] = _0x3e4ff5(a, v)), i || 0 !== n && 2 !== n) { + if (2 === n) { + x = function (e) { + var r = d; + return _0x19207d(o, e), + t[r(740)]; + }; + } else { + var s = 0 === n || 1 === n; + (s || 3 === n) && (x = i ? function (e) { + var r = d; + return _0x19207d(o, e), + t.get[r(820)](e); + } + : function (e) { + var r = d; + return t[r(811)][r(820)](e); + }); + (s || 4 === n) && (u = i ? function (e, r) { + var a = d; + _0x19207d(o, e); + t[a(484)][a(820)](e, r); + } + : function (e, r) { + var a = d; + t[a(484)][a(820)](e, r); + }); + } + } else { + x = function (e) { + return e[r]; + }; + 0 === n && (u = function (e, t) { + e[r] = t; + }); + } + if (i) { + var l = o[d(769)](); + } else { + l = function (e) { + return r in e; + }; + } + b[d(464)] = x && u ? { + get: x, + set: u, + has: l + } + : x ? { + get: x, + has: l + } + : { + set: u, + has: l + }; + try { + return e(c, b); + } finally { + v.v = !0; + } + } + function _0x15eb90(e, r) { + var t = w_0x25f3; + if (e.v) { + throw new Error("attempted to call " + r + t(360)); + } + } + function _0x45d8b1(e, r) { + var t = w_0x25f3; + if (t(494) != typeof e) { + throw new TypeError(r + t(760)); + } + } + function _0xed525b(e, r) { + var t = w_0x25f3; + var a = typeof r; + if (1 === e) { + if (t(381) !== a || null === r) { + throw new TypeError(t(509)); + } + void 0 !== r[t(811)] && _0x45d8b1(r[t(811)], t(368)); + void 0 !== r.set && _0x45d8b1(r[t(484)], t(561)); + void 0 !== r.init && _0x45d8b1(r[t(953)], t(559)); + } else if (t(494) !== a) { + throw new TypeError((0 === e ? t(572) : 10 === e ? "class" : "method") + " decorators must return a function or void 0"); + } + } + function _0x244c39(e) { + return function () { + return e(this); + }; + } + function _0x48532c(e) { + return function (r) { + e(this, r); + }; + } + function _0x23e6b(e, r, t, a, n, f, i, c, o) { + var d = w_0x25f3; + var _; + var x; + var u; + var b; + var v; + var s; + var l = t[0]; + if (i ? _ = 0 === n || 1 === n ? { + get: _0x244c39(t[3]), + set: _0x48532c(t[4]) + } + : 3 === n ? { + get: t[3] + } + : 4 === n ? { + set: t[3] + } + : { + value: t[3] + } + : 0 !== n && (_ = Object[d(678)](r, a)), 1 === n ? u = { + get: _[d(811)], + set: _[d(484)] + } + : 2 === n ? u = _.value : 3 === n ? u = _[d(811)] : 4 === n && (u = _.set), "function" == typeof l) { + if (void 0 !== (b = _0x151762(l, a, _, c, n, f, i, u, o))) { + _0xed525b(n, b); + 0 === n ? x = b : 1 === n ? (x = b[d(953)], v = b[d(811)] || u[d(811)], s = b.set || u[d(484)], + u = { + get: v, + set: s + }) : u = b; + } + } else { + for (var h = l.length - 1; h >= 0; h--) { + var w; + if (void 0 !== (b = _0x151762(l[h], a, _, c, n, f, i, u, o))) { + _0xed525b(n, b); + 0 === n ? w = b : 1 === n ? (w = b[d(953)], v = b[d(811)] || u[d(811)], s = b[d(484)] || u.set, + u = { + get: v, + set: s + }) : u = b; + void 0 !== w && (void 0 === x ? x = w : d(494) == typeof x ? x = [x, w] : x[d(878)](w)); + } + } + } + if (0 === n || 1 === n) { + if (void 0 === x) { + x = function (e, r) { + return r; + }; + } else if (d(494) != typeof x) { + var g = x; + x = function (e, r) { + var t = d; + for (var a = r, n = 0; n < g[t(601)]; n++) { + a = g[n][t(820)](e, a); + } + return a; + }; + } else { + var p = x; + x = function (e, r) { + var t = d; + return p[t(820)](e, r); + }; + } + e.push(x); + } + if (0 !== n) { + 1 === n ? (_[d(811)] = u[d(811)], _.set = u.set) : 2 === n ? _[d(740)] = u : 3 === n ? _[d(811)] = u : 4 === n && (_[d(484)] = u); + i ? 1 === n ? (e.push(function (e, r) { + var t = d; + return u.get[t(820)](e, r); + }), e[d(878)](function (e, r) { + var t = d; + return u.set[t(820)](e, r); + })) : 2 === n ? e[d(878)](u) : e[d(878)](function (e, r) { + var t = d; + return u[t(820)](e, r); + }) : Object.defineProperty(r, a, _); + } + } + function _0x754898(e, r, t) { + var a = w_0x25f3; + for (var n, f, i, c = [], o = new Map(), d = new Map(), _ = 0; _ < r[a(601)]; _++) { + var x = r[_]; + if (Array[a(687)](x)) { + var u; + var b; + var v = x[1]; + var s = x[2]; + var l = x[a(601)] > 3; + var h = v >= 5; + var w = t; + if (h ? (u = e, 0 != (v -= 5) && (b = f = f || []), l && !i && (i = function (r) { + return _0x37e1e2(r) === e; + }), w = i) : (u = e.prototype, 0 !== v && (b = n = n || [])), 0 !== v && !l) { + if (h) { + var g = d; + } else { + g = o; + } + var p = g[a(811)](s) || 0; + if (!0 === p || 3 === p && 4 !== v || 4 === p && 3 !== v) { + throw new Error("Attempted to decorate a public method/accessor that has the same name as a previously decorated public method/accessor. This is not currently supported by the decorators plugin. Property name was: " + s); + } + !p && v > 2 ? g.set(s, v) : g.set(s, !0); + } + _0x23e6b(c, u, x, s, v, h, l, b, w); + } + } + return _0xdfc9aa(c, n), + _0xdfc9aa(c, f), + c; + } + function _0xdfc9aa(e, r) { + var t = w_0x25f3; + r && e[t(878)](function (e) { + var a = t; + for (var n = 0; n < r.length; n++) { + r[n][a(820)](e); + } + return e; + }); + } + function _0x2258b9(e, r) { + var t = w_0x25f3; + if (r[t(601)] > 0) { + for (var a = [], n = e, f = e.name, i = r[t(601)] - 1; i >= 0; i--) { + var c = { + v: !1 + }; + try { + var o = r[i](n, { + kind: t(412), + name: f, + addInitializer: _0x3e4ff5(a, c) + }); + } finally { + c.v = !0; + } + if (void 0 !== o) { + _0xed525b(10, o); + n = o; + } + } + return [n, function () { + var e = t; + for (var r = 0; r < a[e(601)]; r++) { + a[r][e(820)](n); + } + } + ]; + } + } + function _0x499d65(e, r, t, a) { + return { + e: _0x754898(e, r, a), + get c() { + return _0x2258b9(e, t); + } + }; + } + function _0x63f01f(e) { + var r = w_0x25f3; + var t = {}; + var a = !1; + function n(r, t) { + return a = !0, { + done: !1, + value: new _0x59d886(t = new Promise(function (a) { + a(e[r](t)); + }), 1) + }; + } + return t[r(900) != typeof Symbol && Symbol.iterator || r(477)] = function () { + return this; + }, + t[r(905)] = function (e) { + return a ? (a = !1, e) : n("next", e); + }, + r(494) == typeof e[r(592)] && (t[r(592)] = function (e) { + var t = r; + if (a) { + throw a = !1, + e; + } + return n(t(592), e); + }), + r(494) == typeof e[r(765)] && (t[r(765)] = function (e) { + return a ? (a = !1, e) : n("return", e); + }), + t; + } + function _0x278b9f(e) { + var r = w_0x25f3; + var t; + var a; + var n; + var f = 2; + for ("undefined" != typeof Symbol && (a = Symbol[r(380)], n = Symbol[r(947)]); f--;) { + if (a && null != (t = e[a])) { + return t[r(820)](e); + } + if (n && null != (t = e[n])) { + return new _0x3d6fc9(t[r(820)](e)); + } + a = r(560); + n = r(477); + } + throw new TypeError(r(681)); + } + function _0x3d6fc9(e) { + var r = w_0x25f3; + function t(e) { + var r = w_0x25f3; + if (Object(e) !== e) { + return Promise.reject(new TypeError(e + r(934))); + } + var t = e[r(483)]; + return Promise.resolve(e[r(740)]).then(function (e) { + return { + value: e, + done: t + }; + }); + } + return (_0x3d6fc9 = function (e) { + var r = w_0x25f3; + this.s = e; + this.n = e[r(905)]; + })[r(836)] = { + s: null, + n: null, + next: function () { + var e = r; + return t(this.n[e(519)](this.s, arguments)); + }, + return: function (e) { + var a = r; + var n = this.s[a(765)]; + return void 0 === n ? Promise[a(632)]({ + value: e, + done: !0 + }) : t(n[a(519)](this.s, arguments)); + }, + throw: function (e) { + var a = r; + var n = this.s[a(765)]; + return void 0 === n ? Promise[a(923)](e) : t(n.apply(this.s, arguments)); + } + }, + new _0x3d6fc9(e); + } + function _0x81c36b(e) { + return new _0x59d886(e, 0); + } + function _0x37e1e2(e) { + var r = w_0x25f3; + if (Object(e) !== e) { + throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : r(776))); + } + return e; + } + function _0x564673(e, r, t, a) { + var n = w_0x25f3; + var f = { + configurable: !0, + enumerable: !0 + }; + return f[e] = a, + Object[n(373)](r, t, f); + } + function _0x19d66a(e, r) { + var t = w_0x25f3; + if (null == e) { + var a = null; + } else { + a = t(900) != typeof Symbol && e[Symbol[t(947)]] || e[t(477)]; + } + if (null != a) { + var n; + var f; + var i; + var c; + var o = []; + var d = !0; + var _ = !1; + try { + if (i = (a = a[t(820)](e)).next, 0 === r) { + if (Object(a) !== a) { + return; + } + d = !1; + } else { + for (; !(d = (n = i.call(a))[t(483)]) && (o[t(878)](n[t(740)]), o[t(601)] !== r); d = !0) { } + } + } catch (e) { + console.log(e); + _ = !0; + f = e; + } finally { + try { + if (!d && null != a[t(765)] && (c = a.return(), Object(c) !== c)) { + return; + } + } finally { + if (_) { + throw f; + } + } + } + return o; + } + } + function _0x376fd5(e, r) { + var t = w_0x25f3; + var a = e && (t(900) != typeof Symbol && e[Symbol[t(947)]] || e["@@iterator"]); + if (null != a) { + var n; + var f = []; + for (a = a.call(e); e[t(601)] < r && !(n = a.next())[t(483)];) { + f[t(878)](n.value); + } + return f; + } + } + function _0x2e4b86(e, r, t, a) { + var n = w_0x25f3; + _0x500e9f || (_0x500e9f = n(494) == typeof Symbol && Symbol[n(798)] && Symbol[n(798)](n(633)) || 60103); + var f = e && e[n(881)]; + var i = arguments[n(601)] - 3; + if (r || 0 === i || (r = { + children: void 0 + }), 1 === i) { + r[n(367)] = a; + } else if (i > 1) { + for (var c = new Array(i), o = 0; o < i; o++) { + c[o] = arguments[o + 3]; + } + r[n(367)] = c; + } + if (r && f) { + for (var d in f) { + void 0 === r[d] && (r[d] = f[d]); + } + } else { + r || (r = f || {}); + } + return { + $$typeof: _0x500e9f, + type: e, + key: void 0 === t ? null : "" + t, + ref: null, + props: r, + _owner: null + }; + } + function _0x3a3eb5(e, r) { + var t = w_0x25f3; + var a = Object[t(383)](e); + if (Object[t(904)]) { + var n = Object.getOwnPropertySymbols(e); + r && (n = n[t(356)](function (r) { + var a = t; + return Object[a(678)](e, r)[a(570)]; + })); + a.push[t(519)](a, n); + } + return a; + } + function _0x4ee2dd(e) { + var r = w_0x25f3; + for (var t = 1; t < arguments[r(601)]; t++) { + if (null != arguments[t]) { + var a = arguments[t]; + } else { + a = {}; + } + t % 2 ? _0x3a3eb5(Object(a), !0)[r(596)](function (r) { + _0x4a7824(e, r, a[r]); + }) : Object[r(492)] ? Object[r(876)](e, Object[r(492)](a)) : _0x3a3eb5(Object(a))[r(596)](function (t) { + var n = r; + Object[n(373)](e, t, Object[n(678)](a, t)); + }); + } + return e; + } + function _0x385d19() { + var e = w_0x25f3; + _0x385d19 = function () { + return r; + }; + var r = {}; + var t = Object[e(836)]; + var a = t[e(952)]; + var n = Object.defineProperty || function (r, t, a) { + var n = e; + r[t] = a[n(740)]; + }; + if (e(494) == typeof Symbol) { + var f = Symbol; + } else { + f = {}; + } + var i = f[e(947)] || e(477); + var c = f.asyncIterator || e(560); + var o = f[e(448)] || e(480); + function d(r, t, a) { + var n = e; + return Object[n(373)](r, t, { + value: a, + enumerable: !0, + configurable: !0, + writable: !0 + }), + r[t]; + } + try { + d({}, ""); + } catch (e) { + console.log(e); + d = function (e, r, t) { + return e[r] = t; + }; + } + function _(r, t, a, f) { + var i = e; + var c = t && t.prototype instanceof b ? t : b; + var o = Object[i(951)](c[i(836)]); + var d = new S(f || []); + return n(o, i(433), { + value: m(r, a, d) + }), + o; + } + function x(r, t, a) { + var n = e; + try { + return { + type: n(753), + arg: r[n(820)](t, a) + }; + } catch (e) { + console.log(e); + return { + type: "throw", + arg: e + }; + } + } + r.wrap = _; + var u = {}; + function b() { } + function v() { } + function s() { } + var l = {}; + d(l, i, function () { + return this; + }); + var h = Object[e(556)]; + var w = h && h(h(j([]))); + w && w !== t && a[e(820)](w, i) && (l = w); + var g = s[e(836)] = b.prototype = Object[e(951)](l); + function p(r) { + var t = e; + [t(905), t(592), t(765)][t(596)](function (e) { + d(r, e, function (r) { + var t = w_0x25f3; + return this[t(433)](e, r); + }); + }); + } + function y(e, r) { + var t; + n(this, "_invoke", { + value: function (n, f) { + var i = w_0x25f3; + function c() { + return new r(function (t, i) { + !function t(n, f, i, c) { + var o = w_0x25f3; + var d = x(e[n], e, f); + if (o(592) !== d[o(427)]) { + var _ = d[o(807)]; + var u = _[o(740)]; + return u && "object" == typeof u && a[o(820)](u, o(689)) ? r[o(632)](u[o(689)])[o(493)](function (e) { + var r = o; + t(r(905), e, i, c); + }, function (e) { + t("throw", e, i, c); + }) : r[o(632)](u)[o(493)](function (e) { + var r = o; + _[r(740)] = e; + i(_); + }, function (e) { + var r = o; + return t(r(592), e, i, c); + }); + } + c(d[o(807)]); + } + (n, f, t, i); + }); + } + return t = t ? t[i(493)](c, c) : c(); + } + }); + } + function m(e, r, t) { + var a = "suspendedStart"; + return function (n, f) { + var i = w_0x25f3; + if (i(751) === a) { + throw new Error(i(858)); + } + if ("completed" === a) { + if ("throw" === n) { + throw f; + } + return k(); + } + for (t[i(602)] = n, t[i(807)] = f; ;) { + var c = t[i(444)]; + if (c) { + var o = O(c, t); + if (o) { + if (o === u) { + continue; + } + return o; + } + } + if (i(905) === t[i(602)]) { + t[i(946)] = t[i(674)] = t[i(807)]; + } else if ("throw" === t[i(602)]) { + if ("suspendedStart" === a) { + throw a = i(362), + t.arg; + } + t[i(715)](t[i(807)]); + } else { + i(765) === t[i(602)] && t.abrupt(i(765), t.arg); + } + a = i(751); + var d = x(e, r, t); + if ("normal" === d.type) { + if (a = t[i(483)] ? i(362) : "suspendedYield", d[i(807)] === u) { + continue; + } + return { + value: d[i(807)], + done: t[i(483)] + }; + } + if (i(592) === d[i(427)]) { + a = i(362); + t[i(602)] = i(592); + t[i(807)] = d[i(807)]; + } + } + }; + } + function O(r, t) { + var a = e; + var n = t.method; + var f = r[a(947)][n]; + if (void 0 === f) { + return t[a(444)] = null, + a(592) === n && r[a(947)][a(765)] && (t.method = "return", + t[a(807)] = void 0, O(r, t), a(592) === t[a(602)]) || "return" !== n && (t[a(602)] = a(592), + t[a(807)] = new TypeError("The iterator does not provide a '" + n + a(815))), + u; + } + var i = x(f, r[a(947)], t[a(807)]); + if (a(592) === i[a(427)]) { + return t[a(602)] = a(592), + t.arg = i[a(807)], + t[a(444)] = null, + u; + } + var c = i[a(807)]; + return c ? c.done ? (t[r[a(366)]] = c[a(740)], t.next = r[a(641)], a(765) !== t[a(602)] && (t[a(602)] = a(905), + t.arg = void 0), t[a(444)] = null, u) : c : (t[a(602)] = a(592), t.arg = new TypeError(a(593)), + t[a(444)] = null, u); + } + function E(r) { + var t = e; + var a = { + tryLoc: r[0] + }; + 1 in r && (a.catchLoc = r[1]); + 2 in r && (a[t(544)] = r[2], a[t(763)] = r[3]); + this.tryEntries[t(878)](a); + } + function T(r) { + var t = e; + var a = r[t(903)] || {}; + a.type = t(753); + delete a[t(807)]; + r.completion = a; + } + function S(r) { + var t = e; + this[t(823)] = [{ + tryLoc: t(451) + } + ]; + r[t(596)](E, this); + this[t(682)](!0); + } + function j(r) { + var t = e; + if (r) { + var n = r[i]; + if (n) { + return n.call(r); + } + if (t(494) == typeof r[t(905)]) { + return r; + } + if (!isNaN(r.length)) { + var f = -1; + var c = function e() { + var n = t; + for (; ++f < r[n(601)];) { + if (a[n(820)](r, f)) { + return e[n(740)] = r[f], + e[n(483)] = !1, + e; + } + } + return e.value = void 0, + e[n(483)] = !0, + e; + }; + return c[t(905)] = c; + } + } + return { + next: k + }; + } + function k() { + return { + value: void 0, + done: !0 + }; + } + return v[e(836)] = s, + n(g, e(684), { + value: s, + configurable: !0 + }), + n(s, e(684), { + value: v, + configurable: !0 + }), + v[e(396)] = d(s, o, e(785)), + r[e(902)] = function (r) { + var t = e; + var a = t(494) == typeof r && r[t(684)]; + return !!a && (a === v || t(785) === (a.displayName || a.name)); + }, + r[e(933)] = function (r) { + var t = e; + return Object[t(571)] ? Object[t(571)](r, s) : (r[t(746)] = s, d(r, o, "GeneratorFunction")), + r[t(836)] = Object.create(g), + r; + }, + r[e(721)] = function (e) { + return { + __await: e + }; + }, + p(y[e(836)]), + d(y[e(836)], c, function () { + return this; + }), + r[e(627)] = y, + r[e(487)] = function (t, a, n, f, i) { + var c = e; + void 0 === i && (i = Promise); + var o = new y(_(t, a, n, f), i); + return r[c(902)](a) ? o : o[c(905)]()[c(493)](function (e) { + var r = c; + return e[r(483)] ? e[r(740)] : o[r(905)](); + }); + }, + p(g), + d(g, o, e(437)), + d(g, i, function () { + return this; + }), + d(g, e(942), function () { + var r = e; + return r(374); + }), + r.keys = function (r) { + var t = e; + var a = Object(r); + var n = []; + for (var f in a) { + n[t(878)](f); + } + return n[t(755)](), + function e() { + var r = t; + for (; n[r(601)];) { + var f = n[r(615)](); + if (f in a) { + return e[r(740)] = f, + e[r(483)] = !1, + e; + } + } + return e.done = !0, + e; + }; + }, + r[e(907)] = j, + S.prototype = { + constructor: S, + reset: function (r) { + var t = e; + if (this[t(812)] = 0, this[t(905)] = 0, this[t(946)] = this[t(674)] = void 0, this[t(483)] = !1, + this[t(444)] = null, this[t(602)] = t(905), this[t(807)] = void 0, this[t(823)][t(596)](T), + !r) { + for (var n in this) { + "t" === n.charAt(0) && a[t(820)](this, n) && !isNaN(+n.slice(1)) && (this[n] = void 0); + } + } + }, + stop: function () { + var r = e; + this[r(483)] = !0; + var t = this[r(823)][0][r(903)]; + if (r(592) === t[r(427)]) { + throw t.arg; + } + return this[r(666)]; + }, + dispatchException: function (r) { + var t = e; + if (this[t(483)]) { + throw r; + } + var n = this; + function f(e, a) { + var f = t; + return o[f(427)] = f(592), + o[f(807)] = r, + n[f(905)] = e, + a && (n[f(602)] = f(905), + n[f(807)] = void 0), + !!a; + } + for (var i = this[t(823)][t(601)] - 1; i >= 0; --i) { + var c = this[t(823)][i]; + var o = c[t(903)]; + if (t(451) === c.tryLoc) { + return f(t(475)); + } + if (c[t(670)] <= this[t(812)]) { + var d = a.call(c, "catchLoc"); + var _ = a[t(820)](c, t(544)); + if (d && _) { + if (this[t(812)] < c[t(708)]) { + return f(c.catchLoc, !0); + } + if (this.prev < c[t(544)]) { + return f(c[t(544)]); + } + } else if (d) { + if (this.prev < c[t(708)]) { + return f(c[t(708)], !0); + } + } else { + if (!_) { + throw new Error(t(830)); + } + if (this[t(812)] < c.finallyLoc) { + return f(c.finallyLoc); + } + } + } + } + }, + abrupt: function (r, t) { + var n = e; + for (var f = this[n(823)][n(601)] - 1; f >= 0; --f) { + var i = this[n(823)][f]; + if (i[n(670)] <= this[n(812)] && a[n(820)](i, n(544)) && this[n(812)] < i[n(544)]) { + var c = i; + break; + } + } + c && (n(799) === r || n(722) === r) && c[n(670)] <= t && t <= c[n(544)] && (c = null); + var o = c ? c[n(903)] : {}; + return o.type = r, + o[n(807)] = t, + c ? (this[n(602)] = n(905), this[n(905)] = c[n(544)], + u) : this[n(354)](o); + }, + complete: function (r, t) { + var a = e; + if (a(592) === r[a(427)]) { + throw r[a(807)]; + } + return a(799) === r[a(427)] || a(722) === r[a(427)] ? this[a(905)] = r[a(807)] : a(765) === r.type ? (this[a(666)] = this[a(807)] = r[a(807)], + this[a(602)] = a(765), this[a(905)] = "end") : a(753) === r[a(427)] && t && (this[a(905)] = t), + u; + }, + finish: function (r) { + var t = e; + for (var a = this[t(823)][t(601)] - 1; a >= 0; --a) { + var n = this[t(823)][a]; + if (n[t(544)] === r) { + return this.complete(n[t(903)], n[t(763)]), + T(n), + u; + } + } + }, + catch: function (r) { + var t = e; + for (var a = this.tryEntries[t(601)] - 1; a >= 0; --a) { + var n = this.tryEntries[a]; + if (n[t(670)] === r) { + var f = n[t(903)]; + if (t(592) === f[t(427)]) { + var i = f[t(807)]; + T(n); + } + return i; + } + } + throw new Error(t(575)); + }, + delegateYield: function (r, t, a) { + var n = e; + return this[n(444)] = { + iterator: j(r), + resultName: t, + nextLoc: a + }, + "next" === this.method && (this.arg = void 0), + u; + } + }, + r; + } + function _0x1db123(e) { + var r = w_0x25f3; + return (_0x1db123 = r(494) == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (e) { + return typeof e; + } + : function (e) { + var t = r; + return e && t(494) == typeof Symbol && e[t(684)] === Symbol && e !== Symbol.prototype ? t(749) : typeof e; + })(e); + } + function _0x34d29a() { + var e = w_0x25f3; + _0x34d29a = function (e, r) { + return new a(e, void 0, r); + }; + var r = RegExp[e(836)]; + var t = new WeakMap(); + function a(r, n, f) { + var i = e; + var c = new RegExp(r, n); + return t[i(484)](c, f || t[i(811)](r)), + _0x2e2f47(c, a[i(836)]); + } + function n(r, a) { + var n = e; + var f = t[n(811)](a); + return Object[n(383)](f)[n(886)](function (e, t) { + var a = n; + var i = f[t]; + if (a(651) == typeof i) { + e[t] = r[i]; + } else { + for (var c = 0; void 0 === r[i[c]] && c + 1 < i.length;) { + c++; + } + e[t] = r[i[c]]; + } + return e; + }, Object[n(951)](null)); + } + return _0x5dda7d(a, RegExp), + a.prototype.exec = function (t) { + var a = e; + var f = r[a(521)].call(this, t); + if (f) { + f.groups = n(f, this); + var i = f.indices; + i && (i.groups = n(i, this)); + } + return f; + }, + a[e(836)][Symbol[e(887)]] = function (a, f) { + var i = e; + if (i(828) == typeof f) { + var c = t[i(811)](this); + return r[Symbol[i(887)]].call(this, a, f[i(887)](/\$<([^>]+)>/g, function (e, r) { + var t = i; + var a = c[r]; + return "$" + (Array[t(687)](a) ? a[t(591)]("$") : a); + })); + } + if (i(494) == typeof f) { + var o = this; + return r[Symbol[i(887)]][i(820)](this, a, function () { + var e = i; + var r = arguments; + return e(381) != typeof r[r[e(601)] - 1] && (r = [].slice[e(820)](r))[e(878)](n(r, o)), + f[e(519)](this, r); + }); + } + return r[Symbol[i(887)]].call(this, a, f); + }, + _0x34d29a[e(519)](this, arguments); + } + function _0x2772ab(e) { + var r = w_0x25f3; + this[r(384)] = e; + } + function _0x125e01(e) { + return function () { + var r = w_0x25f3; + return new _0x137ba2(e[r(519)](this, arguments)); + }; + } + function _0x8a0370(e, r, t, a, n, f, i) { + var c = w_0x25f3; + try { + var o = e[f](i); + var d = o[c(740)]; + } catch (e) { + console.log(e); + return void t(e); + } + o[c(483)] ? r(d) : Promise[c(632)](d)[c(493)](a, n); + } + function _0x50daaa(e) { + return function () { + var r = this; + var t = arguments; + return new Promise(function (a, n) { + var f = e.apply(r, t); + function i(e) { + var r = w_0x25f3; + _0x8a0370(f, a, n, i, c, r(905), e); + } + function c(e) { + var r = w_0x25f3; + _0x8a0370(f, a, n, i, c, r(592), e); + } + i(void 0); + }); + }; + } + function _0x297d3d(e, r) { + var t = w_0x25f3; + if (!(e instanceof r)) { + throw new TypeError(t(759)); + } + } + function _0x96f358(e, r) { + var t = w_0x25f3; + for (var a = 0; a < r[t(601)]; a++) { + var n = r[a]; + n[t(570)] = n[t(570)] || !1; + n[t(700)] = !0; + t(740) in n && (n.writable = !0); + Object[t(373)](e, _0x32e885(n[t(914)]), n); + } + } + function _0x55cd8a(e, r, t) { + var a = w_0x25f3; + return r && _0x96f358(e[a(836)], r), + t && _0x96f358(e, t), + Object[a(373)](e, a(836), { + writable: !1 + }), + e; + } + function _0x58d8c2(e, r) { + var t = w_0x25f3; + for (var a in r) { + (i = r[a])[t(700)] = i[t(570)] = !0; + "value" in i && (i[t(739)] = !0); + Object[t(373)](e, a, i); + } + if (Object[t(904)]) { + for (var n = Object[t(904)](r), f = 0; f < n[t(601)]; f++) { + var i; + var c = n[f]; + (i = r[c]).configurable = i[t(570)] = !0; + t(740) in i && (i[t(739)] = !0); + Object[t(373)](e, c, i); + } + } + return e; + } + function _0x40ff51(e, r) { + var t = w_0x25f3; + for (var a = Object[t(810)](r), n = 0; n < a[t(601)]; n++) { + var f = a[n]; + var i = Object[t(678)](r, f); + i && i[t(700)] && void 0 === e[f] && Object.defineProperty(e, f, i); + } + return e; + } + function _0x4a7824(e, r, t) { + var a = w_0x25f3; + return (r = _0x32e885(r)) in e ? Object[a(373)](e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0 + }) : e[r] = t, + e; + } + function _0x36f54f() { + var e = w_0x25f3; + return (_0x36f54f = Object.assign ? Object[e(672)].bind() : function (r) { + var t = e; + for (var a = 1; a < arguments[t(601)]; a++) { + var n = arguments[a]; + for (var f in n) { + Object[t(836)][t(952)][t(820)](n, f) && (r[f] = n[f]); + } + } + return r; + }).apply(this, arguments); + } + function _0x5f346f(e) { + var r = w_0x25f3; + for (var t = 1; t < arguments[r(601)]; t++) { + if (null != arguments[t]) { + var a = Object(arguments[t]); + } else { + a = {}; + } + var n = Object[r(383)](a); + r(494) == typeof Object[r(904)] && n[r(878)][r(519)](n, Object[r(904)](a)[r(356)](function (e) { + var t = r; + return Object[t(678)](a, e)[t(570)]; + })); + n.forEach(function (r) { + _0x4a7824(e, r, a[r]); + }); + } + return e; + } + function _0x5dda7d(e, r) { + var t = w_0x25f3; + if ("function" != typeof r && null !== r) { + throw new TypeError(t(566)); + } + e[t(836)] = Object.create(r && r[t(836)], { + constructor: { + value: e, + writable: !0, + configurable: !0 + } + }); + Object[t(373)](e, t(836), { + writable: !1 + }); + r && _0x2e2f47(e, r); + } + function _0x3879ac(e, r) { + var t = w_0x25f3; + e[t(836)] = Object.create(r[t(836)]); + e[t(836)][t(684)] = e; + _0x2e2f47(e, r); + } + function _0x22af63(e) { + var r = w_0x25f3; + return (_0x22af63 = Object[r(571)] ? Object.getPrototypeOf[r(769)]() : function (e) { + var t = r; + return e.__proto__ || Object[t(556)](e); + })(e); + } + function _0x2e2f47(e, r) { + var t = w_0x25f3; + return (_0x2e2f47 = Object.setPrototypeOf ? Object[t(571)][t(769)]() : function (e, r) { + var a = t; + return e[a(746)] = r, + e; + })(e, r); + } + function _0x3390dc() { + var e = w_0x25f3; + if (e(900) == typeof Reflect || !Reflect[e(800)]) { + return !1; + } + if (Reflect[e(800)].sham) { + return !1; + } + if (e(494) == typeof Proxy) { + return !0; + } + try { + return Boolean[e(836)][e(771)][e(820)](Reflect[e(800)](Boolean, [], function () { })), + !0; + } catch (e) { + console.log(e); + return !1; + } + } + function _0x920a3d(e, r, t) { + var a = w_0x25f3; + return (_0x920a3d = _0x3390dc() ? Reflect.construct[a(769)]() : function (e, r, t) { + var n = a; + var f = [null]; + f.push[n(519)](f, r); + var i = new (Function[n(769)].apply(e, f))(); + return t && _0x2e2f47(i, t[n(836)]), + i; + }).apply(null, arguments); + } + function _0x5cb0d7(e) { + var r = w_0x25f3; + return -1 !== Function[r(942)].call(e)[r(709)](r(353)); + } + function _0x16f283(e) { + var r = w_0x25f3; + if (r(494) == typeof Map) { + var t = new Map(); + } else { + t = void 0; + } + return (_0x16f283 = function (e) { + var a = r; + if (null === e || !_0x5cb0d7(e)) { + return e; + } + if (a(494) != typeof e) { + throw new TypeError(a(566)); + } + if (void 0 !== t) { + if (t[a(764)](e)) { + return t[a(811)](e); + } + t[a(484)](e, n); + } + function n() { + var r = a; + return _0x920a3d(e, arguments, _0x22af63(this)[r(684)]); + } + return n.prototype = Object[a(951)](e[a(836)], { + constructor: { + value: n, + enumerable: !1, + writable: !0, + configurable: !0 + } + }), + _0x2e2f47(n, e); + })(e); + } + function _0x8f6e33(e, r) { + var t = w_0x25f3; + return null != r && t(900) != typeof Symbol && r[Symbol[t(376)]] ? !!r[Symbol[t(376)]](e) : e instanceof r; + } + function _0x2ea366(e) { + var r = w_0x25f3; + return e && e[r(957)] ? e : { + default: + e + }; + } + function _0x1629e6(e) { + var r = w_0x25f3; + if (r(494) != typeof WeakMap) { + return null; + } + var t = new WeakMap(); + var a = new WeakMap(); + return (_0x1629e6 = function (e) { + return e ? a : t; + })(e); + } + function _0x2c9158(e, r) { + var t = w_0x25f3; + if (!r && e && e.__esModule) { + return e; + } + if (null === e || t(381) != typeof e && "function" != typeof e) { + return { + default: + e + }; + } + var a = _0x1629e6(r); + if (a && a[t(764)](e)) { + return a[t(811)](e); + } + var n = {}; + var f = Object[t(373)] && Object[t(678)]; + for (var i in e) { + if (t(467) !== i && Object[t(836)].hasOwnProperty[t(820)](e, i)) { + if (f) { + var c = Object[t(678)](e, i); + } else { + c = null; + } + c && (c[t(811)] || c[t(484)]) ? Object.defineProperty(n, i, c) : n[i] = e[i]; + } + } + return n.default = e, + a && a[t(484)](e, n), + n; + } + function _0x374736(e, r) { + var t = w_0x25f3; + if (e !== r) { + throw new TypeError(t(364)); + } + } + function _0x206199(e) { + var r = w_0x25f3; + if (null == e) { + throw new TypeError(r(460) + e); + } + } + function _0x2bfa3e(e, r) { + var t = w_0x25f3; + if (null == e) { + return {}; + } + var a; + var n; + var f = {}; + var i = Object.keys(e); + for (n = 0; n < i[t(601)]; n++) { + a = i[n]; + r[t(709)](a) >= 0 || (f[a] = e[a]); + } + return f; + } + function _0x2a28e9(e, r) { + var t = w_0x25f3; + if (null == e) { + return {}; + } + var a; + var n; + var f = _0x2bfa3e(e, r); + if (Object.getOwnPropertySymbols) { + var i = Object[t(904)](e); + for (n = 0; n < i[t(601)]; n++) { + a = i[n]; + r[t(709)](a) >= 0 || Object[t(836)][t(474)][t(820)](e, a) && (f[a] = e[a]); + } + } + return f; + } + function _0x58f550(e) { + var r = w_0x25f3; + if (void 0 === e) { + throw new ReferenceError(r(443)); + } + return e; + } + function _0xf3b17(e, r) { + var t = w_0x25f3; + if (r && (t(381) == typeof r || t(494) == typeof r)) { + return r; + } + if (void 0 !== r) { + throw new TypeError("Derived constructors may only return object or undefined"); + } + return _0x58f550(e); + } + function _0x17b234(e) { + var r = _0x3390dc(); + return function () { + var t = w_0x25f3; + var a; + var n = _0x22af63(e); + if (r) { + var f = _0x22af63(this)[t(684)]; + a = Reflect[t(800)](n, arguments, f); + } else { + a = n[t(519)](this, arguments); + } + return _0xf3b17(this, a); + }; + } + function _0x2b3a8b(e, r) { + for (; !Object.prototype.hasOwnProperty.call(e, r) && null !== (e = _0x22af63(e));) { } + return e; + } + function _0x33bfa4() { + var e = w_0x25f3; + return (_0x33bfa4 = e(900) != typeof Reflect && Reflect[e(811)] ? Reflect[e(811)][e(769)]() : function (r, t, a) { + var n = e; + var f = _0x2b3a8b(r, t); + if (f) { + var i = Object[n(678)](f, t); + return i[n(811)] ? i[n(811)][n(820)](arguments[n(601)] < 3 ? r : a) : i[n(740)]; + } + }).apply(this, arguments); + } + function _0x5b010c(e, r, t, a) { + var n = w_0x25f3; + return (_0x5b010c = "undefined" != typeof Reflect && Reflect[n(484)] ? Reflect[n(484)] : function (e, r, t, a) { + var f = n; + var i; + var c = _0x2b3a8b(e, r); + if (c) { + if ((i = Object[f(678)](c, r))[f(484)]) { + return i.set[f(820)](a, t), + !0; + } + if (!i[f(739)]) { + return !1; + } + } + if (i = Object.getOwnPropertyDescriptor(a, r)) { + if (!i[f(739)]) { + return !1; + } + i[f(740)] = t; + Object[f(373)](a, r, i); + } else { + _0x4a7824(a, r, t); + } + return !0; + })(e, r, t, a); + } + function _0x2732a4(e, r, t, a, n) { + var f = w_0x25f3; + if (!_0x5b010c(e, r, t, a || e) && n) { + throw new TypeError(f(846)); + } + return t; + } + function _0x281bf3(e, r) { + var t = w_0x25f3; + return r || (r = e[t(677)](0)), + Object[t(408)](Object[t(876)](e, { + raw: { + value: Object[t(408)](r) + } + })); + } + function _0x2d5f0c(e, r) { + var t = w_0x25f3; + return r || (r = e.slice(0)), + e[t(683)] = r, + e; + } + function _0x246229(e) { + var r = w_0x25f3; + throw new TypeError('"' + e + r(824)); + } + function _0x5db7bf(e) { + throw new TypeError('"' + e + '" is write-only'); + } + function _0x2275f4(e) { + var r = w_0x25f3; + throw new ReferenceError(r(455) + e + '" cannot be referenced in computed property keys.'); + } + function _0x516c2b() { } + function _0xab4ac9(e) { + throw new ReferenceError(e + " is not defined - temporal dead zone"); + } + function _0x5b377b(e, r) { + return e === _0x516c2b ? _0xab4ac9(r) : e; + } + function _0x5096de(e, r) { + return _0x1ae312(e) || _0x19d66a(e, r) || _0x525331(e, r) || _0x25b697(); + } + function _0x1dff6c(e, r) { + return _0x1ae312(e) || _0x376fd5(e, r) || _0x525331(e, r) || _0x25b697(); + } + function _0x5c885(e) { + return _0x1ae312(e) || _0x1853c6(e) || _0x525331(e) || _0x25b697(); + } + function _0x534083(e) { + return _0x1ccd19(e) || _0x1853c6(e) || _0x525331(e) || _0x542337(); + } + function _0x1ccd19(e) { + var r = w_0x25f3; + if (Array[r(687)](e)) { + return _0x537c83(e); + } + } + function _0x1ae312(e) { + var r = w_0x25f3; + if (Array[r(687)](e)) { + return e; + } + } + function _0x4bbf4b(e, r, t) { + var a = w_0x25f3; + if (r && !Array[a(687)](r) && a(651) == typeof r[a(601)]) { + var n = r[a(601)]; + return _0x537c83(r, void 0 !== t && t < n ? t : n); + } + return e(r, t); + } + function _0x1853c6(e) { + var r = w_0x25f3; + if (r(900) != typeof Symbol && null != e[Symbol[r(947)]] || null != e[r(477)]) { + return Array.from(e); + } + } + function _0x525331(e, r) { + var t = w_0x25f3; + if (e) { + if (t(828) == typeof e) { + return _0x537c83(e, r); + } + var a = Object[t(836)][t(942)][t(820)](e).slice(8, -1); + return t(747) === a && e[t(684)] && (a = e[t(684)].name), + "Map" === a || t(352) === a ? Array[t(668)](e) : t(639) === a || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/[t(555)](a) ? _0x537c83(e, r) : void 0; + } + } + function _0x537c83(e, r) { + var t = w_0x25f3; + (null == r || r > e[t(601)]) && (r = e[t(601)]); + for (var a = 0, n = new Array(r); a < r; a++) { + n[a] = e[a]; + } + return n; + } + function _0x542337() { + var e = w_0x25f3; + throw new TypeError(e(936)); + } + function _0x25b697() { + throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + function _0x350075(e, r) { + var t = w_0x25f3; + var a = t(900) != typeof Symbol && e[Symbol[t(947)]] || e[t(477)]; + if (!a) { + if (Array.isArray(e) || (a = _0x525331(e)) || r && e && t(651) == typeof e[t(601)]) { + a && (e = a); + var n = 0; + var f = function () { }; + return { + s: f, + n: function () { + var r = t; + return n >= e[r(601)] ? { + done: !0 + } + : { + done: !1, + value: e[n++] + }; + }, + e: function (e) { + throw e; + }, + f: f + }; + } + throw new TypeError(t(686)); + } + var i; + var c = !0; + var o = !1; + return { + s: function () { + a = a.call(e); + }, + n: function () { + var e = t; + var r = a.next(); + return c = r[e(483)], + r; + }, + e: function (e) { + o = !0; + i = e; + }, + f: function () { + var e = t; + try { + c || null == a[e(765)] || a[e(765)](); + } finally { + if (o) { + throw i; + } + } + } + }; + } + function _0x37af30(e, r) { + var t = w_0x25f3; + var a = "undefined" != typeof Symbol && e[Symbol[t(947)]] || e["@@iterator"]; + if (a) { + return (a = a.call(e))[t(905)][t(769)](a); + } + if (Array[t(687)](e) || (a = _0x525331(e)) || r && e && "number" == typeof e[t(601)]) { + a && (e = a); + var n = 0; + return function () { + var r = t; + return n >= e[r(601)] ? { + done: !0 + } + : { + done: !1, + value: e[n++] + }; + }; + } + throw new TypeError(t(686)); + } + function _0x5362e1(e) { + return function () { + var r = w_0x25f3; + var t = e.apply(this, arguments); + return t[r(905)](), + t; + }; + } + function _0x4ad3b0(e, r) { + var t = w_0x25f3; + if (t(381) != typeof e || null === e) { + return e; + } + var a = e[Symbol.toPrimitive]; + if (void 0 !== a) { + var n = a.call(e, r || t(467)); + if ("object" != typeof n) { + return n; + } + throw new TypeError(t(856)); + } + return ("string" === r ? String : Number)(e); + } + function _0x32e885(e) { + var r = w_0x25f3; + var t = _0x4ad3b0(e, r(828)); + return r(749) == typeof t ? t : String(t); + } + function _0xf47dc3(e, r) { + var t = w_0x25f3; + throw new Error(t(511)); + } + function _0x9d8dd5(e, r, t, a) { + var n = w_0x25f3; + t && Object[n(373)](e, r, { + enumerable: t.enumerable, + configurable: t[n(700)], + writable: t[n(739)], + value: t[n(720)] ? t.initializer[n(820)](a) : void 0 + }); + } + function _0x271739(e, r, t, a, n) { + var f = w_0x25f3; + var i = {}; + return Object.keys(a)[f(596)](function (e) { + i[e] = a[e]; + }), + i[f(570)] = !!i[f(570)], + i[f(700)] = !!i[f(700)], + (f(740) in i || i.initializer) && (i[f(739)] = !0), + i = t[f(677)]()[f(755)]()[f(886)](function (t, a) { + return a(e, r, t) || t; + }, i), + n && void 0 !== i[f(720)] && (i[f(720)] ? i[f(740)] = i[f(720)][f(820)](n) : i[f(740)] = void 0, + i[f(720)] = void 0), + void 0 === i[f(720)] && (Object[f(373)](e, r, i), i = null), + i; + } + _0x137ba2.prototype[_0x5612de(494) == typeof Symbol && Symbol[_0x5612de(380)] || _0x5612de(560)] = function () { + return this; + }; + _0x137ba2[_0x5612de(836)].next = function (e) { + var r = _0x5612de; + return this[r(433)](r(905), e); + }; + _0x137ba2[_0x5612de(836)].throw = function (e) { + var r = _0x5612de; + return this[r(433)]("throw", e); + }; + _0x137ba2[_0x5612de(836)][_0x5612de(765)] = function (e) { + var r = _0x5612de; + return this[r(433)](r(765), e); + }; + var _0x371360 = 0; + var _0x4d6c78; + var _0x312e19; + var _0x2a32a1; + var _0x371ac2; + function _0x2ca065(e) { + var r = _0x5612de; + return r(869) + _0x371360++ + "_" + e; + } + function _0x33f649(e, r) { + var t = _0x5612de; + if (!Object.prototype[t(952)][t(820)](e, r)) { + throw new TypeError(t(625)); + } + return e; + } + function _0x4c53b8(e, r) { + var t = _0x5612de; + return _0x1de0ff(e, _0x3feef3(e, r, t(811))); + } + function _0xf01d58(e, r, t) { + var a = _0x5612de; + return _0x30760d(e, _0x3feef3(e, r, a(484)), t), + t; + } + function _0x3ff428(e, r) { + var t = _0x5612de; + return _0x1af7ca(e, _0x3feef3(e, r, t(484))); + } + function _0x3feef3(e, r, t) { + var a = _0x5612de; + if (!r[a(764)](e)) { + throw new TypeError(a(685) + t + a(701)); + } + return r[a(811)](e); + } + function _0x46f318(e, r, t) { + var a = _0x5612de; + return _0x3fbf28(e, r), + _0x234ff5(t, a(811)), + _0x1de0ff(e, t); + } + function _0x3d490a(e, r, t, a) { + return _0x3fbf28(e, r), + _0x234ff5(t, "set"), + _0x30760d(e, t, a), + a; + } + function _0x1f2159(e, r, t) { + return _0x3fbf28(e, r), + t; + } + function _0x2fcc7b() { + var e = _0x5612de; + throw new TypeError(e(445)); + } + function _0x1de0ff(e, r) { + var t = _0x5612de; + return r[t(811)] ? r[t(811)][t(820)](e) : r[t(740)]; + } + function _0x30760d(e, r, t) { + var a = _0x5612de; + if (r.set) { + r.set[a(820)](e, t); + } else { + if (!r.writable) { + throw new TypeError("attempted to set read only private field"); + } + r.value = t; + } + } + function _0x1af7ca(e, r) { + var t = _0x5612de; + if (r[t(484)]) { + return t(879) in r || (r.__destrObj = { + set value(a) { + var n = t; + r[n(484)][n(820)](e, a); + } + }), + r.__destrObj; + } + if (!r[t(739)]) { + throw new TypeError("attempted to set read only private field"); + } + return r; + } + function _0x153ad5(e, r, t) { + var a = _0x5612de; + return _0x3fbf28(e, r), + _0x234ff5(t, a(484)), + _0x1af7ca(e, t); + } + function _0x3fbf28(e, r) { + if (e !== r) { + throw new TypeError("Private static access of wrong provenance"); + } + } + function _0x234ff5(e, r) { + var t = _0x5612de; + if (void 0 === e) { + throw new TypeError(t(685) + r + " private static field before its declaration"); + } + } + function _0x3cd893(e, r, t, a) { + var n = _0x5612de; + var f = _0xdaf8f6(); + if (a) { + for (var i = 0; i < a[n(601)]; i++) { + f = a[i](f); + } + } + var c = r(function (e) { + var r = n; + f[r(787)](e, o[r(613)]); + }, t); + var o = f[n(735)](_0x872852(c.d[n(459)](_0x210308)), e); + return f[n(535)](c.F, o[n(613)]), + f[n(357)](c.F, o[n(924)]); + } + function _0xdaf8f6() { + var e = _0x5612de; + _0xdaf8f6 = function () { + return r; + }; + var r = { + elementsDefinitionOrder: [[e(602)], [e(572)]], + initializeInstanceElements: function (r, t) { + var a = e; + [a(602), a(572)][a(596)](function (e) { + var n = a; + t[n(596)](function (t) { + var a = n; + t.kind === e && a(565) === t[a(533)] && this[a(737)](r, t); + }, this); + }, this); + }, + initializeClassElements: function (r, t) { + var a = e; + var n = r[a(836)]; + [a(602), a(572)].forEach(function (e) { + t.forEach(function (t) { + var a = w_0x25f3; + var f = t[a(533)]; + if (t[a(623)] === e && (a(918) === f || a(836) === f)) { + if (a(918) === f) { + var i = r; + } else { + i = n; + } + this[a(737)](i, t); + } + }, this); + }, this); + }, + defineClassElement: function (r, t) { + var a = e; + var n = t.descriptor; + if (a(572) === t[a(623)]) { + var f = t[a(720)]; + n = { + enumerable: n[a(570)], + writable: n[a(739)], + configurable: n[a(700)], + value: void 0 === f ? void 0 : f[a(820)](r) + }; + } + Object[a(373)](r, t.key, n); + }, + decorateClass: function (r, t) { + var a = e; + var n = []; + var f = []; + var i = { + static: [], + prototype: [], + own: [] + }; + if (r[a(596)](function (e) { + var r = a; + this[r(829)](e, i); + }, this), r[a(596)](function (e) { + var r = a; + if (!_0x51f3b5(e)) { + return n[r(878)](e); + } + var t = this[r(428)](e, i); + n[r(878)](t[r(518)]); + n[r(878)].apply(n, t[r(956)]); + f[r(878)].apply(f, t[r(924)]); + }, this), !t) { + return { + elements: n, + finishers: f + }; + } + var c = this[a(844)](n, t); + return f.push[a(519)](f, c[a(924)]), + c[a(924)] = f, + c; + }, + addElementPlacement: function (r, t, a) { + var n = e; + var f = t[r[n(533)]]; + if (!a && -1 !== f.indexOf(r.key)) { + throw new TypeError("Duplicated element (" + r[n(914)] + ")"); + } + f[n(878)](r[n(914)]); + }, + decorateElement: function (r, t) { + var a = e; + for (var n = [], f = [], i = r.decorators, c = i[a(601)] - 1; c >= 0; c--) { + var o = t[r[a(533)]]; + o[a(748)](o[a(709)](r[a(914)]), 1); + var d = this[a(882)](r); + var _ = this[a(394)]((0, i[c])(d) || d); + r = _.element; + this.addElementPlacement(r, t); + _[a(756)] && f[a(878)](_[a(756)]); + var x = _[a(956)]; + if (x) { + for (var u = 0; u < x[a(601)]; u++) { + this[a(829)](x[u], t); + } + n[a(878)].apply(n, x); + } + } + return { + element: r, + finishers: f, + extras: n + }; + }, + decorateConstructor: function (r, t) { + var a = e; + for (var n = [], f = t[a(601)] - 1; f >= 0; f--) { + var i = this[a(403)](r); + var c = this[a(804)]((0, t[f])(i) || i); + if (void 0 !== c[a(756)] && n[a(878)](c[a(756)]), void 0 !== c.elements) { + r = c[a(613)]; + for (var o = 0; o < r[a(601)] - 1; o++) { + for (var d = o + 1; d < r.length; d++) { + if (r[o][a(914)] === r[d][a(914)] && r[o][a(533)] === r[d][a(533)]) { + throw new TypeError(a(871) + r[o][a(914)] + ")"); + } + } + } + } + } + return { + elements: r, + finishers: n + }; + }, + fromElementDescriptor: function (r) { + var t = e; + var a = { + kind: r[t(623)], + key: r.key, + placement: r.placement, + descriptor: r[t(527)] + }; + return Object[t(373)](a, Symbol[t(448)], { + value: t(488), + configurable: !0 + }), + t(572) === r.kind && (a[t(720)] = r.initializer), + a; + }, + toElementDescriptors: function (e) { + if (void 0 !== e) { + return _0x5c885(e).map(function (e) { + var r = w_0x25f3; + var t = this[r(606)](e); + return this.disallowProperty(e, r(756), r(543)), + this[r(538)](e, r(956), r(543)), + t; + }, this); + } + }, + toElementDescriptor: function (r) { + var t = e; + var a = String(r[t(623)]); + if (t(602) !== a && t(572) !== a) { + throw new TypeError('An element descriptor\'s .kind property must be either "method" or "field", but a decorator created an element descriptor with .kind "' + a + '"'); + } + var n = _0x32e885(r[t(914)]); + var f = String(r[t(533)]); + if (t(918) !== f && t(836) !== f && t(565) !== f) { + throw new TypeError(t(693) + f + '"'); + } + var i = r[t(527)]; + this[t(538)](r, "elements", t(543)); + var c = { + kind: a, + key: n, + placement: f, + descriptor: Object[t(672)]({}, i) + }; + return t(572) !== a ? this[t(538)](r, t(720), t(653)) : (this.disallowProperty(i, t(811), t(631)), + this.disallowProperty(i, "set", t(631)), this.disallowProperty(i, t(740), t(631)), + c.initializer = r[t(720)]), + c; + }, + toElementFinisherExtras: function (r) { + var t = e; + return { + element: this[t(606)](r), + finisher: _0x22df7b(r, t(756)), + extras: this[t(880)](r.extras) + }; + }, + fromClassDescriptor: function (r) { + var t = e; + var a = { + kind: t(412), + elements: r.map(this[t(882)], this) + }; + return Object[t(373)](a, Symbol[t(448)], { + value: t(488), + configurable: !0 + }), + a; + }, + toClassDescriptor: function (r) { + var t = e; + var a = String(r[t(623)]); + if ("class" !== a) { + throw new TypeError('A class descriptor\'s .kind property must be "class", but a decorator created a class descriptor with .kind "' + a + '"'); + } + this.disallowProperty(r, t(914), "A class descriptor"); + this[t(538)](r, t(533), "A class descriptor"); + this[t(538)](r, t(527), t(417)); + this.disallowProperty(r, "initializer", t(417)); + this[t(538)](r, t(956), "A class descriptor"); + var n = _0x22df7b(r, t(756)); + return { + elements: this.toElementDescriptors(r.elements), + finisher: n + }; + }, + runClassFinishers: function (r, t) { + var a = e; + for (var n = 0; n < t[a(601)]; n++) { + var f = (0, t[n])(r); + if (void 0 !== f) { + if (a(494) != typeof f) { + throw new TypeError("Finishers must return a constructor."); + } + r = f; + } + } + return r; + }, + disallowProperty: function (r, t, a) { + var n = e; + if (void 0 !== r[t]) { + throw new TypeError(a + n(562) + t + n(673)); + } + } + }; + return r; + } + function _0x210308(e) { + var r = _0x5612de; + var t; + var a = _0x32e885(e.key); + r(602) === e[r(623)] ? t = { + value: e[r(740)], + writable: !0, + configurable: !0, + enumerable: !1 + } + : r(811) === e[r(623)] ? t = { + get: e.value, + configurable: !0, + enumerable: !1 + } + : "set" === e[r(623)] ? t = { + set: e[r(740)], + configurable: !0, + enumerable: !1 + } + : r(572) === e.kind && (t = { + configurable: !0, + writable: !0, + enumerable: !0 + }); + var n = { + kind: r(572) === e[r(623)] ? r(572) : "method", + key: a, + placement: e[r(918)] ? r(918) : "field" === e[r(623)] ? "own" : r(836), + descriptor: t + }; + return e.decorators && (n[r(439)] = e[r(439)]), + r(572) === e.kind && (n.initializer = e[r(740)]), + n; + } + function _0x3b2c5d(e, r) { + var t = _0x5612de; + void 0 !== e[t(527)][t(811)] ? r.descriptor[t(811)] = e.descriptor[t(811)] : r.descriptor.set = e[t(527)][t(484)]; + } + function _0x872852(e) { + var r = _0x5612de; + for (var t = [], a = function (e) { + var r = w_0x25f3; + return r(602) === e[r(623)] && e[r(914)] === i.key && e[r(533)] === i[r(533)]; + }, n = 0; n < e[r(601)]; n++) { + var f; + var i = e[n]; + if (r(602) === i[r(623)] && (f = t.find(a))) { + if (_0x3f9244(i[r(527)]) || _0x3f9244(f[r(527)])) { + if (_0x51f3b5(i) || _0x51f3b5(f)) { + throw new ReferenceError("Duplicated methods (" + i[r(914)] + r(523)); + } + f[r(527)] = i[r(527)]; + } else { + if (_0x51f3b5(i)) { + if (_0x51f3b5(f)) { + throw new ReferenceError("Decorators can't be placed on different accessors with for the same property (" + i.key + ")."); + } + f[r(439)] = i[r(439)]; + } + _0x3b2c5d(i, f); + } + } else { + t[r(878)](i); + } + } + return t; + } + function _0x51f3b5(e) { + var r = _0x5612de; + return e[r(439)] && e.decorators.length; + } + function _0x3f9244(e) { + var r = _0x5612de; + return void 0 !== e && !(void 0 === e[r(740)] && void 0 === e[r(739)]); + } + function _0x22df7b(e, r) { + var t = _0x5612de; + var a = e[r]; + if (void 0 !== a && t(494) != typeof a) { + throw new TypeError("Expected '" + r + t(512)); + } + return a; + } + function _0x44a779(e, r, t) { + var a = _0x5612de; + if (!r[a(764)](e)) { + throw new TypeError(a(391)); + } + return t; + } + function _0x227cd9(e, r) { + var t = _0x5612de; + if (r[t(764)](e)) { + throw new TypeError("Cannot initialize the same private elements twice on an object"); + } + } + function _0x41d2eb(e, r, t) { + _0x227cd9(e, r); + r.set(e, t); + } + function _0x4fd04a(e, r) { + var t = _0x5612de; + _0x227cd9(e, r); + r[t(818)](e); + } + function _0x4fb313() { + throw new TypeError("attempted to reassign private method"); + } + function _0x4e4f66(e) { + return e; + } + _0x5612de(494) != typeof Object[_0x5612de(672)] && Object.defineProperty(Object, _0x5612de(672), { + value: function (e, r) { + var t = _0x5612de; + if (null == e) { + throw new TypeError(t(719)); + } + for (var a = Object(e), n = 1; n < arguments[t(601)]; n++) { + var f = arguments[n]; + if (null != f) { + for (var i in f) { + Object[t(836)][t(952)][t(820)](f, i) && (a[i] = f[i]); + } + } + } + return a; + }, + writable: !0, + configurable: !0 + }); + Object[_0x5612de(383)] || (Object.keys = (_0x4d6c78 = Object[_0x5612de(836)][_0x5612de(952)], + _0x312e19 = !{ + toString: null + } + [_0x5612de(474)]("toString"), _0x2a32a1 = [_0x5612de(942), _0x5612de(712), _0x5612de(771), _0x5612de(952), "isPrototypeOf", _0x5612de(474), "constructor"], + _0x371ac2 = _0x2a32a1[_0x5612de(601)], function (e) { + var r = _0x5612de; + if (r(494) != typeof e && (r(381) !== _0x1db123(e) || null === e)) { + throw new TypeError(r(884)); + } + var t; + var a; + var n = []; + for (t in e) { + _0x4d6c78[r(820)](e, t) && n[r(878)](t); + } + if (_0x312e19) { + for (a = 0; a < _0x371ac2; a++) { + _0x4d6c78.call(e, _0x2a32a1[a]) && n[r(878)](_0x2a32a1[a]); + } + } + return n; + })); + var _0x45b94b = { + __version__: _0x5612de(359), + feVersion: 2, + domNotValid: !1, + refererKey: _0x5612de(792), + pushVersion: _0x5612de(714), + secInfoHeader: _0x5612de(584) + }; + function _0x598972(e, r) { + var t = _0x5612de; + if (t(828) == typeof r) { + for (var a, n = e + "=", f = r[t(834)](/[;&]/), i = 0; i < f[t(601)]; i++) { + for (a = f[i]; " " === a.charAt(0);) { + a = a[t(432)](1, a.length); + } + if (0 === a[t(709)](n)) { + return a[t(432)](n[t(601)], a[t(601)]); + } + } + } + } + function _0x24dc34(e) { + var r = _0x5612de; + try { + var t = ""; + return window.sessionStorage && (t = window.sessionStorage[r(728)](e)) || window[r(476)] && (t = window[r(476)][r(728)](e)) ? t : t = _0x598972(e, document.cookie); + } catch (e) { + console.log(e); + return ""; + } + } + function _0x1f42cb(e, r) { + var t = _0x5612de; + try { + window[t(958)] && window.sessionStorage.setItem(e, r); + window[t(476)] && window[t(476)][t(452)](e, r); + document[t(626)] = e + t(862); + document[t(626)] = e + "=" + r + t(449) + new Date(new Date().getTime() + 6048e5)[t(350)]() + t(961); + } catch (e) { + console.log(e); + } + } + function _0x2ecc5a(e) { + var r = _0x5612de; + try { + window[r(958)] && window.sessionStorage.removeItem(e); + window[r(476)] && window[r(476)][r(707)](e); + document[r(626)] = e + r(862); + } catch (e) { + console.log(e); + } + } + for (var _0x462335 = { + 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, + _enableSignature: [], + perf: !1, + xxbg: !0 + }, _0x3d40ff = { + debug: function (e, r) { + var t = _0x5612de; + _0x462335[t(438)]; + } + }, _0x100715 = _0x5612de(727)[_0x5612de(834)](""), _0x1f510e = [], _0x37750d = [], _0x1cb307 = 0; _0x1cb307 < 256; _0x1cb307++) { + _0x1f510e[_0x1cb307] = _0x100715[_0x1cb307 >> 4 & 15] + _0x100715[15 & _0x1cb307]; + _0x1cb307 < 16 && (_0x1cb307 < 10 ? _0x37750d[48 + _0x1cb307] = _0x1cb307 : _0x37750d[87 + _0x1cb307] = _0x1cb307); + } + var _0x55de18 = function (e) { + var r = _0x5612de; + for (var t = e[r(601)], a = "", n = 0; n < t;) { + a += _0x1f510e[e[n++]]; + } + return a; + }; + var _0x655940 = function (e) { + for (var r = e.length >> 1, t = r << 1, a = new Uint8Array(r), n = 0, f = 0; f < t;) { + a[n++] = _0x37750d[e.charCodeAt(f++)] << 4 | _0x37750d[e.charCodeAt(f++)]; + } + return a; + }; + var _0x42e709 = { + encode: _0x55de18, + decode: _0x655940 + }; + if (_0x5612de(900) != typeof globalThis) { + var _0x1e7721 = globalThis; + } else { + var _0x1e7721 = _0x5612de(900) != typeof window ? window : _0x5612de(900) != typeof global ? global : "undefined" != typeof self ? self : {}; + } + function _0x3bc8d3(e) { + var r = _0x5612de; + return e && e[r(957)] && Object[r(836)][r(952)][r(820)](e, r(467)) ? e[r(467)] : e; + } + function _0x3abc0b(e) { + var r = _0x5612de; + return e && Object[r(836)].hasOwnProperty[r(820)](e, r(467)) ? e[r(467)] : e; + } + function _0x41ace1(e) { + var r = _0x5612de; + return e && Object[r(836)][r(952)].call(e, r(467)) && 1 === Object[r(383)](e)[r(601)] ? e[r(467)] : e; + } + function _0x3e9554(e) { + var r = _0x5612de; + if (e[r(957)]) { + return e; + } + var t = Object[r(373)]({}, r(957), { + value: !0 + }); + return Object[r(383)](e).forEach(function (a) { + var n = r; + var f = Object[n(678)](e, a); + Object.defineProperty(t, a, f[n(811)] ? f : { + enumerable: !0, + get: function () { + return e[a]; + } + }); + }), + t; + } + function _0x56409e(e) { + var r = _0x5612de; + var t = { + exports: {} + }; + return e(t, t[r(440)]), + t.exports; + } + function _0x171dc9(e) { + var r = _0x5612de; + throw new Error(r(877) + e + r(855)); + } + var _0x90795 = _0x56409e(function (_0xb9c7d1) { + !function () { + var _0x2db296 = w_0x25f3; + var _0x1f977a = "input is invalid type"; + var _0x5a87b4 = _0x2db296(381) == typeof window; + var _0x11d177 = _0x5a87b4 ? window : {}; + _0x11d177[_0x2db296(890)] && (_0x5a87b4 = !1); + var _0x387fd8 = !_0x5a87b4 && _0x2db296(381) == typeof self; + var _0x2e1226 = !_0x11d177[_0x2db296(534)] && _0x2db296(381) == typeof process && process[_0x2db296(404)] && process[_0x2db296(404)][_0x2db296(647)]; + _0x2e1226 ? _0x11d177 = _0x1e7721 : _0x387fd8 && (_0x11d177 = self); + var _0x299a21 = !_0x11d177.JS_MD5_NO_COMMON_JS && _0xb9c7d1[_0x2db296(440)]; + var _0x5efc8f = !1; + var _0x1edc95 = !_0x11d177[_0x2db296(742)] && "undefined" != typeof ArrayBuffer; + var _0x142491 = _0x2db296(727)[_0x2db296(834)](""); + var _0x146907 = [128, 32768, 8388608, -2147483648]; + var _0x4d13bd = [0, 8, 16, 24]; + var _0x3f581b = [_0x2db296(850), _0x2db296(568), _0x2db296(411), _0x2db296(629), "arrayBuffer", _0x2db296(607)]; + var _0xcb4d61 = _0x2db296(789)[_0x2db296(834)](""); + var _0x5c5e45 = []; + var _0x54b265; + if (_0x1edc95) { + var _0x171107 = new ArrayBuffer(68); + _0x54b265 = new Uint8Array(_0x171107); + _0x5c5e45 = new Uint32Array(_0x171107); + } + !_0x11d177[_0x2db296(534)] && Array[_0x2db296(687)] || (Array.isArray = function (e) { + var r = _0x2db296; + return r(817) === Object.prototype.toString.call(e); + }); + !_0x1edc95 || !_0x11d177[_0x2db296(773)] && ArrayBuffer.isView || (ArrayBuffer.isView = function (e) { + var r = _0x2db296; + return r(381) == typeof e && e[r(629)] && e.buffer[r(684)] === ArrayBuffer; + }); + var _0x2e393f = function (e) { + return function (r) { + var t = w_0x25f3; + return new _0x22f902(!0)[t(725)](r)[e](); + }; + }; + var _0x1edabb = function () { + var e = _0x2db296; + var r = _0x2e393f(e(850)); + _0x2e1226 && (r = _0x4d7e2d(r)); + r[e(951)] = function () { + return new _0x22f902(); + }; + r.update = function (e) { + return r.create().update(e); + }; + for (var t = 0; t < _0x3f581b[e(601)]; ++t) { + var a = _0x3f581b[t]; + r[a] = _0x2e393f(a); + } + return r; + }; + var _0x4d7e2d = function (_0x1afe1b) { + var _0x3e0a6e = eval("var _0x13db80 = w_0x25f3;require(_0x13db80(628));"); + var _0x5bfe7e = eval("var _0x20dc8b = w_0x25f3;require('buffer')[_0x20dc8b(424)];"); + var _0x3d7396 = function (e) { + var r = w_0x25f3; + if (r(828) == typeof e) { + return _0x3e0a6e[r(458)](r(860))[r(725)](e, r(680))[r(411)](r(850)); + } + if (null == e) { + throw _0x1f977a; + } + return e[r(684)] === ArrayBuffer && (e = new Uint8Array(e)), + Array[r(687)](e) || ArrayBuffer[r(711)](e) || e[r(684)] === _0x5bfe7e ? _0x3e0a6e[r(458)](r(860)).update(new _0x5bfe7e(e))[r(411)](r(850)) : _0x1afe1b(e); + }; + return _0x3d7396; + }; + function _0x22f902(e) { + var r = _0x2db296; + if (e) { + _0x5c5e45[0] = _0x5c5e45[16] = _0x5c5e45[1] = _0x5c5e45[2] = _0x5c5e45[3] = _0x5c5e45[4] = _0x5c5e45[5] = _0x5c5e45[6] = _0x5c5e45[7] = _0x5c5e45[8] = _0x5c5e45[9] = _0x5c5e45[10] = _0x5c5e45[11] = _0x5c5e45[12] = _0x5c5e45[13] = _0x5c5e45[14] = _0x5c5e45[15] = 0; + this.blocks = _0x5c5e45; + this[r(809)] = _0x54b265; + } else if (_0x1edc95) { + var t = new ArrayBuffer(68); + this[r(809)] = new Uint8Array(t); + this[r(793)] = new Uint32Array(t); + } else { + this[r(793)] = [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[r(901)] = this[r(603)] = this[r(461)] = 0; + this[r(386)] = this[r(489)] = !1; + this[r(415)] = !0; + } + _0x22f902[_0x2db296(836)][_0x2db296(725)] = function (e) { + var r = _0x2db296; + if (!this.finalized) { + var t; + var a = typeof e; + if (r(828) !== a) { + if ("object" !== a) { + throw _0x1f977a; + } + if (null === e) { + throw _0x1f977a; + } + if (_0x1edc95 && e.constructor === ArrayBuffer) { + e = new Uint8Array(e); + } else if (!(Array[r(687)](e) || _0x1edc95 && ArrayBuffer.isView(e))) { + throw _0x1f977a; + } + t = !0; + } + for (var n, f, i = 0, c = e[r(601)], o = this[r(793)], d = this[r(809)]; i < c;) { + if (this[r(489)] && (this[r(489)] = !1, o[0] = o[16], o[16] = o[1] = o[2] = o[3] = o[4] = o[5] = o[6] = o[7] = o[8] = o[9] = o[10] = o[11] = o[12] = o[13] = o[14] = o[15] = 0), + t) { + if (_0x1edc95) { + for (f = this[r(901)]; i < c && f < 64; ++i) { + d[f++] = e[i]; + } + } else { + for (f = this[r(901)]; i < c && f < 64; ++i) { + o[f >> 2] |= e[i] << _0x4d13bd[3 & f++]; + } + } + } else if (_0x1edc95) { + for (f = this[r(901)]; i < c && f < 64; ++i) { + if ((n = e[r(405)](i)) < 128) { + d[f++] = n; + } else if (n < 2048) { + d[f++] = 192 | n >> 6; + d[f++] = 128 | 63 & n; + } else if (n < 55296 || n >= 57344) { + d[f++] = 224 | n >> 12; + d[f++] = 128 | n >> 6 & 63; + d[f++] = 128 | 63 & n; + } else { + n = 65536 + ((1023 & n) << 10 | 1023 & e[r(405)](++i)); + d[f++] = 240 | n >> 18; + d[f++] = 128 | n >> 12 & 63; + d[f++] = 128 | n >> 6 & 63; + d[f++] = 128 | 63 & n; + } + } + } else { + for (f = this[r(901)]; i < c && f < 64; ++i) { + if ((n = e[r(405)](i)) < 128) { + o[f >> 2] |= n << _0x4d13bd[3 & f++]; + } else if (n < 2048) { + o[f >> 2] |= (192 | n >> 6) << _0x4d13bd[3 & f++]; + o[f >> 2] |= (128 | 63 & n) << _0x4d13bd[3 & f++]; + } else if (n < 55296 || n >= 57344) { + o[f >> 2] |= (224 | n >> 12) << _0x4d13bd[3 & f++]; + o[f >> 2] |= (128 | n >> 6 & 63) << _0x4d13bd[3 & f++]; + o[f >> 2] |= (128 | 63 & n) << _0x4d13bd[3 & f++]; + } else { + n = 65536 + ((1023 & n) << 10 | 1023 & e[r(405)](++i)); + o[f >> 2] |= (240 | n >> 18) << _0x4d13bd[3 & f++]; + o[f >> 2] |= (128 | n >> 12 & 63) << _0x4d13bd[3 & f++]; + o[f >> 2] |= (128 | n >> 6 & 63) << _0x4d13bd[3 & f++]; + o[f >> 2] |= (128 | 63 & n) << _0x4d13bd[3 & f++]; + } + } + } + this.lastByteIndex = f; + this[r(603)] += f - this.start; + f >= 64 ? (this[r(901)] = f - 64, this[r(698)](), this[r(489)] = !0) : this[r(901)] = f; + } + return this[r(603)] > 4294967295 && (this.hBytes += this.bytes / 4294967296 << 0, + this[r(603)] = this[r(603)] % 4294967296), + this; + } + }; + _0x22f902[_0x2db296(836)][_0x2db296(473)] = function () { + var e = _0x2db296; + if (!this[e(386)]) { + this[e(386)] = !0; + var r = this[e(793)]; + var t = this[e(814)]; + r[t >> 2] |= _0x146907[3 & t]; + t >= 56 && (this.hashed || this.hash(), r[0] = r[16], r[16] = r[1] = r[2] = r[3] = r[4] = r[5] = r[6] = r[7] = r[8] = r[9] = r[10] = r[11] = r[12] = r[13] = r[14] = r[15] = 0); + r[14] = this[e(603)] << 3; + r[15] = this[e(461)] << 3 | this[e(603)] >>> 29; + this.hash(); + } + }; + _0x22f902[_0x2db296(836)][_0x2db296(698)] = function () { + var e = _0x2db296; + var r; + var t; + var a; + var n; + var f; + var i; + var c = this[e(793)]; + this.first ? t = ((t = ((r = ((r = c[0] - 680876937) << 7 | r >>> 25) - 271733879 << 0) ^ (a = ((a = (-271733879 ^ (n = ((n = (-1732584194 ^ 2004318071 & r) + c[1] - 117830708) << 12 | n >>> 20) + r << 0) & (-271733879 ^ r)) + c[2] - 1126478375) << 17 | a >>> 15) + n << 0) & (n ^ r)) + c[3] - 1316259209) << 22 | t >>> 10) + a << 0 : (r = this.h0, + t = this.h1, a = this.h2, t = ((t += ((r = ((r += ((n = this.h3) ^ t & (a ^ n)) + c[0] - 680876936) << 7 | r >>> 25) + t << 0) ^ (a = ((a += (t ^ (n = ((n += (a ^ r & (t ^ a)) + c[1] - 389564586) << 12 | n >>> 20) + r << 0) & (r ^ t)) + c[2] + 606105819) << 17 | a >>> 15) + n << 0) & (n ^ r)) + c[3] - 1044525330) << 22 | t >>> 10) + a << 0); + t = ((t += ((r = ((r += (n ^ t & (a ^ n)) + c[4] - 176418897) << 7 | r >>> 25) + t << 0) ^ (a = ((a += (t ^ (n = ((n += (a ^ r & (t ^ a)) + c[5] + 1200080426) << 12 | n >>> 20) + r << 0) & (r ^ t)) + c[6] - 1473231341) << 17 | a >>> 15) + n << 0) & (n ^ r)) + c[7] - 45705983) << 22 | t >>> 10) + a << 0; + t = ((t += ((r = ((r += (n ^ t & (a ^ n)) + c[8] + 1770035416) << 7 | r >>> 25) + t << 0) ^ (a = ((a += (t ^ (n = ((n += (a ^ r & (t ^ a)) + c[9] - 1958414417) << 12 | n >>> 20) + r << 0) & (r ^ t)) + c[10] - 42063) << 17 | a >>> 15) + n << 0) & (n ^ r)) + c[11] - 1990404162) << 22 | t >>> 10) + a << 0; + t = ((t += ((r = ((r += (n ^ t & (a ^ n)) + c[12] + 1804603682) << 7 | r >>> 25) + t << 0) ^ (a = ((a += (t ^ (n = ((n += (a ^ r & (t ^ a)) + c[13] - 40341101) << 12 | n >>> 20) + r << 0) & (r ^ t)) + c[14] - 1502002290) << 17 | a >>> 15) + n << 0) & (n ^ r)) + c[15] + 1236535329) << 22 | t >>> 10) + a << 0; + t = ((t += ((n = ((n += (t ^ a & ((r = ((r += (a ^ n & (t ^ a)) + c[1] - 165796510) << 5 | r >>> 27) + t << 0) ^ t)) + c[6] - 1069501632) << 9 | n >>> 23) + r << 0) ^ r & ((a = ((a += (r ^ t & (n ^ r)) + c[11] + 643717713) << 14 | a >>> 18) + n << 0) ^ n)) + c[0] - 373897302) << 20 | t >>> 12) + a << 0; + t = ((t += ((n = ((n += (t ^ a & ((r = ((r += (a ^ n & (t ^ a)) + c[5] - 701558691) << 5 | r >>> 27) + t << 0) ^ t)) + c[10] + 38016083) << 9 | n >>> 23) + r << 0) ^ r & ((a = ((a += (r ^ t & (n ^ r)) + c[15] - 660478335) << 14 | a >>> 18) + n << 0) ^ n)) + c[4] - 405537848) << 20 | t >>> 12) + a << 0; + t = ((t += ((n = ((n += (t ^ a & ((r = ((r += (a ^ n & (t ^ a)) + c[9] + 568446438) << 5 | r >>> 27) + t << 0) ^ t)) + c[14] - 1019803690) << 9 | n >>> 23) + r << 0) ^ r & ((a = ((a += (r ^ t & (n ^ r)) + c[3] - 187363961) << 14 | a >>> 18) + n << 0) ^ n)) + c[8] + 1163531501) << 20 | t >>> 12) + a << 0; + t = ((t += ((n = ((n += (t ^ a & ((r = ((r += (a ^ n & (t ^ a)) + c[13] - 1444681467) << 5 | r >>> 27) + t << 0) ^ t)) + c[2] - 51403784) << 9 | n >>> 23) + r << 0) ^ r & ((a = ((a += (r ^ t & (n ^ r)) + c[7] + 1735328473) << 14 | a >>> 18) + n << 0) ^ n)) + c[12] - 1926607734) << 20 | t >>> 12) + a << 0; + t = ((t += ((i = (n = ((n += ((f = t ^ a) ^ (r = ((r += (f ^ n) + c[5] - 378558) << 4 | r >>> 28) + t << 0)) + c[8] - 2022574463) << 11 | n >>> 21) + r << 0) ^ r) ^ (a = ((a += (i ^ t) + c[11] + 1839030562) << 16 | a >>> 16) + n << 0)) + c[14] - 35309556) << 23 | t >>> 9) + a << 0; + t = ((t += ((i = (n = ((n += ((f = t ^ a) ^ (r = ((r += (f ^ n) + c[1] - 1530992060) << 4 | r >>> 28) + t << 0)) + c[4] + 1272893353) << 11 | n >>> 21) + r << 0) ^ r) ^ (a = ((a += (i ^ t) + c[7] - 155497632) << 16 | a >>> 16) + n << 0)) + c[10] - 1094730640) << 23 | t >>> 9) + a << 0; + t = ((t += ((i = (n = ((n += ((f = t ^ a) ^ (r = ((r += (f ^ n) + c[13] + 681279174) << 4 | r >>> 28) + t << 0)) + c[0] - 358537222) << 11 | n >>> 21) + r << 0) ^ r) ^ (a = ((a += (i ^ t) + c[3] - 722521979) << 16 | a >>> 16) + n << 0)) + c[6] + 76029189) << 23 | t >>> 9) + a << 0; + t = ((t += ((i = (n = ((n += ((f = t ^ a) ^ (r = ((r += (f ^ n) + c[9] - 640364487) << 4 | r >>> 28) + t << 0)) + c[12] - 421815835) << 11 | n >>> 21) + r << 0) ^ r) ^ (a = ((a += (i ^ t) + c[15] + 530742520) << 16 | a >>> 16) + n << 0)) + c[2] - 995338651) << 23 | t >>> 9) + a << 0; + t = ((t += ((n = ((n += (t ^ ((r = ((r += (a ^ (t | ~n)) + c[0] - 198630844) << 6 | r >>> 26) + t << 0) | ~a)) + c[7] + 1126891415) << 10 | n >>> 22) + r << 0) ^ ((a = ((a += (r ^ (n | ~t)) + c[14] - 1416354905) << 15 | a >>> 17) + n << 0) | ~r)) + c[5] - 57434055) << 21 | t >>> 11) + a << 0; + t = ((t += ((n = ((n += (t ^ ((r = ((r += (a ^ (t | ~n)) + c[12] + 1700485571) << 6 | r >>> 26) + t << 0) | ~a)) + c[3] - 1894986606) << 10 | n >>> 22) + r << 0) ^ ((a = ((a += (r ^ (n | ~t)) + c[10] - 1051523) << 15 | a >>> 17) + n << 0) | ~r)) + c[1] - 2054922799) << 21 | t >>> 11) + a << 0; + t = ((t += ((n = ((n += (t ^ ((r = ((r += (a ^ (t | ~n)) + c[8] + 1873313359) << 6 | r >>> 26) + t << 0) | ~a)) + c[15] - 30611744) << 10 | n >>> 22) + r << 0) ^ ((a = ((a += (r ^ (n | ~t)) + c[6] - 1560198380) << 15 | a >>> 17) + n << 0) | ~r)) + c[13] + 1309151649) << 21 | t >>> 11) + a << 0; + t = ((t += ((n = ((n += (t ^ ((r = ((r += (a ^ (t | ~n)) + c[4] - 145523070) << 6 | r >>> 26) + t << 0) | ~a)) + c[11] - 1120210379) << 10 | n >>> 22) + r << 0) ^ ((a = ((a += (r ^ (n | ~t)) + c[2] + 718787259) << 15 | a >>> 17) + n << 0) | ~r)) + c[9] - 343485551) << 21 | t >>> 11) + a << 0; + this.first ? (this.h0 = r + 1732584193 << 0, this.h1 = t - 271733879 << 0, this.h2 = a - 1732584194 << 0, + this.h3 = n + 271733878 << 0, this[e(415)] = !1) : (this.h0 = this.h0 + r << 0, + this.h1 = this.h1 + t << 0, this.h2 = this.h2 + a << 0, this.h3 = this.h3 + n << 0); + }; + _0x22f902[_0x2db296(836)].hex = function () { + this.finalize(); + var e = this.h0; + var r = this.h1; + var t = this.h2; + var a = this.h3; + return _0x142491[e >> 4 & 15] + _0x142491[15 & e] + _0x142491[e >> 12 & 15] + _0x142491[e >> 8 & 15] + _0x142491[e >> 20 & 15] + _0x142491[e >> 16 & 15] + _0x142491[e >> 28 & 15] + _0x142491[e >> 24 & 15] + _0x142491[r >> 4 & 15] + _0x142491[15 & r] + _0x142491[r >> 12 & 15] + _0x142491[r >> 8 & 15] + _0x142491[r >> 20 & 15] + _0x142491[r >> 16 & 15] + _0x142491[r >> 28 & 15] + _0x142491[r >> 24 & 15] + _0x142491[t >> 4 & 15] + _0x142491[15 & t] + _0x142491[t >> 12 & 15] + _0x142491[t >> 8 & 15] + _0x142491[t >> 20 & 15] + _0x142491[t >> 16 & 15] + _0x142491[t >> 28 & 15] + _0x142491[t >> 24 & 15] + _0x142491[a >> 4 & 15] + _0x142491[15 & a] + _0x142491[a >> 12 & 15] + _0x142491[a >> 8 & 15] + _0x142491[a >> 20 & 15] + _0x142491[a >> 16 & 15] + _0x142491[a >> 28 & 15] + _0x142491[a >> 24 & 15]; + }; + _0x22f902.prototype[_0x2db296(942)] = _0x22f902[_0x2db296(836)].hex; + _0x22f902[_0x2db296(836)].digest = function () { + var e = _0x2db296; + this[e(473)](); + var r = this.h0; + var t = this.h1; + var a = this.h2; + var n = this.h3; + return [255 & r, r >> 8 & 255, r >> 16 & 255, r >> 24 & 255, 255 & t, t >> 8 & 255, t >> 16 & 255, t >> 24 & 255, 255 & a, a >> 8 & 255, a >> 16 & 255, a >> 24 & 255, 255 & n, n >> 8 & 255, n >> 16 & 255, n >> 24 & 255]; + }; + _0x22f902[_0x2db296(836)][_0x2db296(568)] = _0x22f902.prototype[_0x2db296(411)]; + _0x22f902.prototype[_0x2db296(585)] = function () { + var e = _0x2db296; + this[e(473)](); + var r = new ArrayBuffer(16); + var t = new Uint32Array(r); + return t[0] = this.h0, + t[1] = this.h1, + t[2] = this.h2, + t[3] = this.h3, + r; + }; + _0x22f902.prototype.buffer = _0x22f902[_0x2db296(836)][_0x2db296(585)]; + _0x22f902[_0x2db296(836)][_0x2db296(607)] = function () { + var e = _0x2db296; + for (var r, t, a, n = "", f = this[e(568)](), i = 0; i < 15;) { + r = f[i++]; + t = f[i++]; + a = f[i++]; + n += _0xcb4d61[r >>> 2] + _0xcb4d61[63 & (r << 4 | t >>> 4)] + _0xcb4d61[63 & (t << 2 | a >>> 6)] + _0xcb4d61[63 & a]; + } + return r = f[i], + n + (_0xcb4d61[r >>> 2] + _0xcb4d61[r << 4 & 63] + "=="); + }; + var _0x336469 = _0x1edabb(); + if (_0x299a21) { + _0xb9c7d1[_0x2db296(440)] = _0x336469; + } else { + _0x11d177[_0x2db296(860)] = _0x336469; + _0x5efc8f && (void 0)(function () { + return _0x336469; + }); + } + } + (); + }); + function _0x5dd467(e) { + var r = _0x5612de; + return w_0x5c3140(r(819), { + get 0() { + return _0x90795; + }, + 1: arguments, + 2: e + }, this); + } + function _0x176a57() { + var e = _0x5612de; + return !!document[e(883)]; + } + function _0x1230e7() { + return "undefined" != typeof InstallTrigger; + } + function _0xf8ccf1() { + var e = _0x5612de; + return /constructor/i[e(555)](window.HTMLElement) || e(550) === (!window.safari || e(900) != typeof safari && safari.pushNotification)[e(942)](); + } + function _0x30c916() { + var e = _0x5612de; + return new Date()[e(365)](); + } + function _0x5af46a(e) { + return null == e ? "" : "boolean" == typeof e ? e ? "1" : "0" : e; + } + function _0x325f58(e, r) { + var t = _0x5612de; + r || (r = t(588)); + for (var a = "", n = e; n > 0; --n) { + a += r[Math[t(463)](Math.random() * r[t(601)])]; + } + return a; + } + var _0x39693d = { + sec: 9, + asgw: 5, + init: 0 + }; + var _0x6caf = { + bogusIndex: 0, + msNewTokenList: [], + moveList: [], + clickList: [], + keyboardList: [], + activeState: [], + aidList: [] + }; + function _0x53b77d(e) { + return w_0x5c3140("484e4f4a403f5243001714366d6da13c00000025f8c25369000000ee00110307070002161103021200031103070700021303062b2f11030207000335490700044211010044001400011101014a1200001100010700010d05000000003c000e00054303491101034a12000607000711000143024911010433000611010412000833000911010412000812000947002100110107070002161101021200031101070700021303062b2f110102070003354902110105430047004f11010433002511010412000a11010412000b190400962934001111010412000c11010412000d190400962947002100110107070002161101021200031101070700021303062b2f11010207000335490842000e0e7170737c7b7045677a657067616c027c7108717077607272706707707b63767a71700003727061047c7b737a02307607767a7b667a797007737c67707760720a7a60617067427c71617d0a7c7b7b7067427c71617d0b7a606170675d707c727d610b7c7b7b70675d707c727d61", { + get 0() { + return Image; + }, + 1: Object, + get 2() { + return _0x6caf; + }, + get 3() { + return console; + }, + get 4() { + return window; + }, + get 5() { + return _0x1230e7; + }, + 6: arguments, + 7: e + }, this); + } + function _0x3ed707() { + var e = _0x5612de; + return w_0x5c3140(e(797), { + get 0() { + return navigator; + }, + get 1() { + var r = e; + return r(900) != typeof global ? global : void 0; + }, + 2: Object, + get 3() { + var r = e; + return r(900) != typeof process ? process : void 0; + }, + get 4() { + return _0x1db123; + }, + 5: arguments + }, this); + } + function _0x328bde(e, r, t) { + var a = _0x5612de; + var n = a(718); + var f = "="; + t && (f = ""); + r && (n = r); + for (var i, c = "", o = 0; e[a(601)] >= o + 3;) { + i = (255 & e[a(405)](o++)) << 16 | (255 & e[a(405)](o++)) << 8 | 255 & e[a(405)](o++); + c += n[a(604)]((16515072 & i) >> 18); + c += n.charAt((258048 & i) >> 12); + c += n.charAt((4032 & i) >> 6); + c += n[a(604)](63 & i); + } + return e[a(601)] - o > 0 && (i = (255 & e.charCodeAt(o++)) << 16 | (e.length > o ? (255 & e[a(405)](o)) << 8 : 0), + c += n[a(604)]((16515072 & i) >> 18), c += n.charAt((258048 & i) >> 12), c += e.length > o ? n[a(604)]((4032 & i) >> 6) : f, + c += f), + c; + } + function _0x389396(e, r) { + var t = _0x5612de; + return w_0x5c3140(t(941), { + 0: arguments, + 1: e, + 2: r + }, this); + } + function _0x3262d3(e) { + var r = _0x5612de; + return r(789)[r(709)](e); + } + function _0xb5350b(e) { + var r = _0x5612de; + var t; + var a; + var n; + var f; + var i; + var c = ""; + for (t = 0; t < e[r(601)] - 3; t += 4) { + a = _0x3262d3(e[r(604)](t)); + n = _0x3262d3(e[r(604)](t + 1)); + f = _0x3262d3(e[r(604)](t + 2)); + i = _0x3262d3(e.charAt(t + 3)); + c += String[r(482)](a << 2 | n >>> 4); + "=" !== e[r(604)](t + 2) && (c += String.fromCharCode(n << 4 & 240 | f >>> 2 & 15)); + "=" !== e[r(604)](t + 3) && (c += String[r(482)](f << 6 & 192 | i)); + } + return c; + } + _0x6caf[_0x5612de(896)] = 0; + _0x6caf.msToken = ""; + _0x6caf[_0x5612de(611)] = _0x39693d[_0x5612de(953)]; + _0x6caf[_0x5612de(949)] = ""; + _0x6caf.ttwid = ""; + _0x6caf[_0x5612de(790)] = ""; + _0x6caf[_0x5612de(825)] = ""; + var _0x28d239 = 0; + var _0x2d6b72; + var _0x1e9bba; + var _0xdc7355; + var _0xf08186; + function _0x33f406(e) { + var r = _0x5612de; + return e &= 63, + String[r(482)](e + (e < 26 ? 65 : e < 52 ? 71 : e < 62 ? -4 : -17)); + } + function _0x4da136(e) { + var r = _0x33f406; + return r(e >> 24) + r(e >> 18) + r(e >> 12) + r(e >> 6) + r(e); + } + _0x2d6b72 = _0x1e9bba = function (e) { + return _0x2d6b72 = _0xdc7355, + _0x28d239 = e, + _0x4da136(e >> 2); + }; + _0xdc7355 = function (e) { + _0x2d6b72 = _0xf08186; + var r = _0x28d239 << 28 | e >>> 4; + return _0x28d239 = e, + _0x4da136(r); + }; + _0xf08186 = function (e) { + return _0x2d6b72 = _0x1e9bba, + _0x4da136(_0x28d239 << 26 | e >>> 6) + _0x33f406(e); + }; + var _0x539097 = 2654435769; + var _0x12ada0; + function _0x75d5b3(e, r) { + var t = _0x5612de; + var a = e.length; + var n = a << 2; + if (r) { + var f = e[a - 1]; + if (f < (n -= 4) - 3 || f > n) { + return null; + } + n = f; + } + for (var i = 0; i < a; i++) { + e[i] = String.fromCharCode(255 & e[i], e[i] >>> 8 & 255, e[i] >>> 16 & 255, e[i] >>> 24 & 255); + } + var c = e[t(591)](""); + return r ? c.substring(0, n) : c; + } + function _0x183951(e, r) { + var t = _0x5612de; + var a; + var n = e[t(601)]; + var f = n >> 2; + 0 != (3 & n) && ++f; + r ? (a = new Array(f + 1))[f] = n : a = new Array(f); + for (var i = 0; i < n; ++i) { + a[i >> 2] |= e.charCodeAt(i) << ((3 & i) << 3); + } + return a; + } + function _0x25c901(e) { + return 4294967295 & e; + } + function _0x38d33e(e, r, t, a, n, f) { + return (t >>> 5 ^ r << 2) + (r >>> 3 ^ t << 4) ^ (e ^ r) + (f[3 & a ^ n] ^ t); + } + function _0x231484(e) { + var r = _0x5612de; + return e[r(601)] < 4 && (e[r(601)] = 4), + e; + } + function _0x3d58e7(e, r) { + var t = _0x5612de; + var a; + var n; + var f; + var i; + var c; + var o; + var d = e[t(601)]; + var _ = d - 1; + for (n = e[_], f = 0, o = 0 | Math[t(463)](6 + 52 / d); o > 0; --o) { + for (i = (f = _0x25c901(f + _0x539097)) >>> 2 & 3, c = 0; c < _; ++c) { + a = e[c + 1]; + n = e[c] = _0x25c901(e[c] + _0x38d33e(f, a, n, c, i, r)); + } + a = e[0]; + n = e[_] = _0x25c901(e[_] + _0x38d33e(f, a, n, _, i, r)); + } + return e; + } + function _0x4aaf04(e, r) { + var t = _0x5612de; + var a; + var n; + var f; + var i; + var c; + var o = e[t(601)]; + var d = o - 1; + for (a = e[0], f = _0x25c901(Math[t(463)](6 + 52 / o) * _0x539097); 0 !== f; f = _0x25c901(f - _0x539097)) { + for (i = f >>> 2 & 3, c = d; c > 0; --c) { + n = e[c - 1]; + a = e[c] = _0x25c901(e[c] - _0x38d33e(f, a, n, c, i, r)); + } + n = e[d]; + a = e[0] = _0x25c901(e[0] - _0x38d33e(f, a, n, 0, i, r)); + } + return e; + } + function _0x532596(e) { + var r = _0x5612de; + if (/^[\x00-\x7f]*$/[r(555)](e)) { + return e; + } + for (var t = [], a = e[r(601)], n = 0, f = 0; n < a; ++n, ++f) { + var i = e[r(405)](n); + if (i < 128) { + t[f] = e[r(604)](n); + } else if (i < 2048) { + t[f] = String.fromCharCode(192 | i >> 6, 128 | 63 & i); + } else { + if (!(i < 55296 || i > 57343)) { + if (n + 1 < a) { + var c = e[r(405)](n + 1); + if (i < 56320 && 56320 <= c && c <= 57343) { + var o = 65536 + ((1023 & i) << 10 | 1023 & c); + t[f] = String[r(482)](240 | o >> 18 & 63, 128 | o >> 12 & 63, 128 | o >> 6 & 63, 128 | 63 & o); + ++n; + continue; + } + } + throw new Error(r(663)); + } + t[f] = String[r(482)](224 | i >> 12, 128 | i >> 6 & 63, 128 | 63 & i); + } + } + return t[r(591)](""); + } + function _0x45fbef(e, r) { + var t = _0x5612de; + for (var a = new Array(r), n = 0, f = 0, i = e[t(601)]; n < r && f < i; n++) { + var c = e[t(405)](f++); + switch (c >> 4) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + a[n] = c; + break; + + case 12: + case 13: + if (!(f < i)) { + throw new Error("Unfinished UTF-8 octet sequence"); + } + a[n] = (31 & c) << 6 | 63 & e[t(405)](f++); + break; + + case 14: + if (!(f + 1 < i)) { + throw new Error(t(916)); + } + a[n] = (15 & c) << 12 | (63 & e[t(405)](f++)) << 6 | 63 & e[t(405)](f++); + break; + + case 15: + if (!(f + 2 < i)) { + throw new Error(t(916)); + } + var o = ((7 & c) << 18 | (63 & e[t(405)](f++)) << 12 | (63 & e[t(405)](f++)) << 6 | 63 & e[t(405)](f++)) - 65536; + if (!(0 <= o && o <= 1048575)) { + throw new Error(t(590) + o.toString(16)); + } + a[n++] = o >> 10 & 1023 | 55296; + a[n] = 1023 & o | 56320; + break; + + default: + throw new Error(t(816) + c[t(942)](16)); + } + } + return n < r && (a[t(601)] = n), + String[t(482)].apply(String, a); + } + function _0x25bfe1(e, r) { + var t = _0x5612de; + for (var a = [], n = new Array(32768), f = 0, i = 0, c = e[t(601)]; f < r && i < c; f++) { + var o = e.charCodeAt(i++); + switch (o >> 4) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + n[f] = o; + break; + + case 12: + case 13: + if (!(i < c)) { + throw new Error(t(916)); + } + n[f] = (31 & o) << 6 | 63 & e.charCodeAt(i++); + break; + + case 14: + if (!(i + 1 < c)) { + throw new Error(t(916)); + } + n[f] = (15 & o) << 12 | (63 & e[t(405)](i++)) << 6 | 63 & e.charCodeAt(i++); + break; + + case 15: + if (!(i + 2 < c)) { + throw new Error(t(916)); + } + var d = ((7 & o) << 18 | (63 & e[t(405)](i++)) << 12 | (63 & e[t(405)](i++)) << 6 | 63 & e[t(405)](i++)) - 65536; + if (!(0 <= d && d <= 1048575)) { + throw new Error("Character outside valid Unicode range: 0x" + d.toString(16)); + } + n[f++] = d >> 10 & 1023 | 55296; + n[f] = 1023 & d | 56320; + break; + + default: + throw new Error("Bad UTF-8 encoding 0x" + o[t(942)](16)); + } + if (f >= 32766) { + var _ = f + 1; + n[t(601)] = _; + a[a[t(601)]] = String[t(482)][t(519)](String, n); + r -= _; + f = -1; + } + } + return f > 0 && (n.length = f, a[a[t(601)]] = String.fromCharCode[t(519)](String, n)), + a.join(""); + } + function _0x31dfb5(e, r) { + var t = _0x5612de; + return (null == r || r < 0) && (r = e[t(601)]), + 0 === r ? "" : /^[\x00-\x7f]*$/[t(555)](e) || !/^[\x00-\xff]*$/[t(555)](e) ? r === e[t(601)] ? e : e[t(935)](0, r) : r < 65535 ? _0x45fbef(e, r) : _0x25bfe1(e, r); + } + function _0xdda738(e, r) { + var t = _0x5612de; + return null == e || 0 === e[t(601)] ? e : (e = _0x532596(e), r = _0x532596(r), _0x75d5b3(_0x3d58e7(_0x183951(e, !0), _0x231484(_0x183951(r, !1))), !1)); + } + function _0x4bb829(e, r) { + var t = _0x5612de; + return null == e || 0 === e[t(601)] ? e : (r = _0x532596(r), _0x31dfb5(_0x75d5b3(_0x4aaf04(_0x183951(e, !1), _0x231484(_0x183951(r, !1))), !0))); + } + function _0x39dfe4() { + var e = _0x5612de; + var r = ""; + try { + window[e(958)] && (r = window[e(958)].getItem("_byted_param_sw")); + r && !window[e(476)] || (r = window.localStorage[e(728)]("_byted_param_sw")); + } catch (e) { + console.log(e); + } + if (r) { + try { + var t = _0x4bb829(_0xb5350b(r[e(677)](8)), r[e(677)](0, 8)); + if ("on" === t) { + return !0; + } + if (e(772) === t) { + return !1; + } + } catch (e) { + console.log(e); + } + } + return !1; + } + function _0x1b4bf1() { + var e = _0x5612de; + return w_0x5c3140(e(529), { + get 0() { + var r = e; + return r(900) != typeof navigator ? navigator : void 0; + }, + get 1() { + var r = e; + return r(900) != typeof window ? window : void 0; + }, + get 2() { + return _0x1db123; + }, + 3: Object, + get 4() { + return "undefined" != typeof document ? document : void 0; + }, + get 5() { + var r = e; + return r(900) != typeof location ? location : void 0; + }, + get 6() { + return _0x176a57; + }, + get 7() { + return "undefined" != typeof history ? history : void 0; + }, + 8: arguments + }, this); + } + function _0x532cd9() { + var e = _0x5612de; + return w_0x5c3140(e(400), { + get 0() { + return _0x176a57; + }, + get 1() { + return navigator; + }, + get 2() { + return PluginArray; + }, + get 3() { + return window; + }, + 4: arguments + }, this); + } + function _0x3391fc() { + var e = _0x5612de; + return w_0x5c3140(e(614), { + get 0() { + return _0x12ada0; + }, + get 1() { + return navigator; + }, + 2: Object, + get 3() { + return window; + }, + 4: arguments, + 5: RegExp + }, this); + } + function _0x21fa28() { + var e = _0x5612de; + return w_0x5c3140(e(379), { + set 0(e) { + _0x12ada0 = e; + }, + 1: Object, + get 2() { + return window; + }, + 3: arguments + }, this); + } + function _0x49b1d7(e) { + var r = _0x5612de; + return w_0x5c3140(r(369), { + get 0() { + return _0x1230e7; + }, + get 1() { + return indexedDB; + }, + get 2() { + return _0xf8ccf1; + }, + get 3() { + return window; + }, + get 4() { + return DOMException; + }, + get 5() { + return _0x176a57; + }, + get 6() { + return _0x6caf; + }, + 7: arguments, + 8: e + }, this); + } + function _0x462256() { + var e = _0x5612de; + return w_0x5c3140(e(355), { + get 0() { + return _0x176a57; + }, + get 1() { + return document; + }, + get 2() { + return navigator; + }, + 3: arguments, + 4: RegExp + }, this); + } + function _0x3cee0e() { + var e = _0x5612de; + return w_0x5c3140(e(733), { + get 0() { + return navigator; + }, + get 1() { + return window; + }, + 2: arguments, + 3: RegExp + }, this); + } + function _0x1f9824() { + var e = _0x5612de; + var r = ""; + if (_0x6caf.PLUGIN) { + r = _0x6caf[e(852)]; + } else { + for (var t = [], a = navigator[e(442)] || [], n = 0; n < 5; n++) { + try { + for (var f = a[n], i = [], c = 0; c < f[e(601)]; c++) { + f[e(892)](c) && i[e(878)](f.item(c)[e(427)]); + } + var o = f[e(833)] + ""; + f.version && (o += f[e(705)] + ""); + o += f.filename + ""; + o += i[e(591)](""); + t.push(o); + } catch (e) { + console.log(e); + } + } + r = t[e(591)]("##"); + _0x6caf[e(852)] = r; + } + return r[e(677)](0, 1024); + } + function _0x30412e() { + var e = _0x5612de; + var r = []; + try { + var t = navigator[e(442)]; + if (t) { + for (var a = 0; a < t[e(601)]; a++) { + for (var n = 0; n < t[a][e(601)]; n++) { + var f = [t[a][e(414)], t[a][n][e(427)], t[a][n][e(361)]][e(591)]("|"); + r.push(f); + } + } + } + } catch (e) { + console.log(e); + } + return r; + } + function _0x28e2ec() { + var e = _0x5612de; + return w_0x5c3140(e(422), { + get 0() { + return navigator; + }, + get 1() { + return _0x1f9824; + }, + get 2() { + return window; + }, + 3: arguments + }, this); + } + function _0x5863d1() { + var e = _0x5612de; + var e540 = e(540); + return w_0x5c3140(e540, { + get 0() { + return _0x462335; + }, + get 1() { + return _0x39dfe4; + }, + get 2() { + return _0x1b4bf1; + }, + get 3() { + return _0x53b77d; + }, + get 4() { + return _0x49b1d7; + }, + get 5() { + return _0x3ed707; + }, + get 6() { + return _0x532cd9; + }, + get 7() { + return _0x3391fc; + }, + get 8() { + return _0x462256; + }, + get 9() { + return _0x3cee0e; + }, + get 10() { + return _0x28e2ec; + }, + get 11() { + return _0x6caf; + }, + 12: arguments + }, this); + } + function _0x2a900b(e) { + var r = _0x5612de; + for (var t = Object[r(383)](e), a = 0, n = t[r(601)] - 1; n >= 0; n--) { + a = (e[t[n]] ? 1 : 0) << t[r(601)] - n - 1 | a; + } + return a; + } + function _0x246aeb(e, r) { + var t = _0x5612de; + for (var a = 0; a < r[t(601)]; a++) { + e = 65599 * e + r[t(405)](a) >>> 0; + } + return e; + } + function _0x184783(e, r) { + var t = _0x5612de; + for (var a = 0; a < r[t(601)]; a++) { + e = 65599 * (e ^ r.charCodeAt(a)) >>> 0; + } + return e; + } + function _0xf119da(e, r) { + var t = _0x5612de; + for (var a = 0; a < r[t(601)]; a++) { + var n = r[t(405)](a); + if (n >= 55296 && n <= 56319 && a < r[t(601)]) { + var f = r.charCodeAt(a + 1); + if (56320 == (64512 & f)) { + n = ((1023 & n) << 10) + (1023 & f) + 65536; + a += 1; + } + } + e = 65599 * e + n >>> 0; + } + return e; + } + function _0x53f850(e) { + var r = _0x5612de; + var t = e || ""; + return (t = -1 !== (t = t[r(887)](/(http:\/\/|https:\/\/|\/\/)?[^\/]*/, "")).indexOf("?") ? t[r(935)](0, t[r(709)]("?")) : t) || "/"; + } + function _0x446110(e) { + var r = _0x5612de; + var t = e || ""; + var a = t[r(450)](/[?](\w+=.*&?)*/); + var n = (t = a ? a[0][r(935)](1) : "") ? t[r(834)]("&") : null; + var f = {}; + if (n) { + for (var i = 0; i < n[r(601)]; i++) { + f[n[i].split("=")[0]] = n[i][r(834)]("=")[1]; + } + } + return f; + } + function _0x3a1cf3(e, r) { + var t = _0x5612de; + if (!e || "{}" === JSON[t(486)](e)) { + return {}; + } + for (var a = Object[t(383)](e).sort(), n = {}, f = 0; f < a[t(601)]; f++) { + n[a[f]] = r ? e[a[f]] + "" : e[a[f]]; + } + return n; + } + function _0x20b77a(e) { + var r = _0x5612de; + return Array[r(687)](e) ? e.map(_0x20b77a) : e instanceof Object ? Object[r(383)](e)[r(910)]()[r(886)](function (r, t) { + return r[t] = _0x20b77a(e[t]), + r; + }, {}) : e; + } + function _0x483e03(e) { + var r = _0x5612de; + if (!e || "{}" === JSON[r(486)](e)) { + return ""; + } + for (var t = Object.keys(e).sort(), a = "", n = 0; n < t[r(601)]; n++) { + a += [t[n]] + "=" + e[t[n]] + "&"; + } + return a; + } + function _0x4bae98() { + var e = _0x5612de; + try { + return !!window[e(958)]; + } catch (e) { + console.log(e); + return !0; + } + } + function _0x275e5a() { + try { + return !!window.localStorage; + } catch (e) { + console.log(e); + return !0; + } + } + function _0x4fdb47() { + var e = _0x5612de; + try { + return !!window[e(536)]; + } catch (e) { + console.log(e); + return !0; + } + } + function _0x1afbc2() { + return _0x5af46a(_0x4fdb47()) + _0x5af46a(_0x275e5a()) + _0x5af46a(_0x4bae98()); + } + function _0xc6f828(e) { + var r = _0x5612de; + var t; + var a = document[r(608)](r(589)); + a[r(770)] = 48; + a[r(581)] = 16; + var n = a.getContext("2d"); + n[r(399)] = "14px serif"; + n[r(406)]("龘ฑภ경", 2, 12); + n[r(928)] = 2; + n[r(616)] = 1; + n[r(832)] = r(539); + n.arc(8, 8, 8, 0, 2); + n.stroke(); + t = a[r(569)](); + for (var f = 0; f < 32; f++) { + e = 65599 * e + t.charCodeAt(e % t[r(601)]) >>> 0; + } + return e; + } + var _0x37a93a = 0; + function _0x18b4be() { + var e = _0x5612de; + try { + return _0x37a93a || (_0x462335[e(537)] ? -1 : _0x37a93a = _0xc6f828(3735928559)); + } catch (e) { + console.log(e); + return -1; + } + } + function _0x1a39c4() { + if (_0x37a93a) { + return _0x37a93a; + } + _0x37a93a = _0xc6f828(3735928559); + } + var _0x45ece5 = { + fpProfileUrl: _0x5612de(370) + }; + function _0x130155() { + var e = _0x5612de; + var r = window[e(808)]; + return r[e(770)] + "_" + r[e(581)] + "_" + r[e(382)]; + } + function _0x2a76f8() { + var e = _0x5612de; + var r = window.screen; + return r[e(906)] + "_" + r[e(630)]; + } + function _0x3da279() { + return new Promise(function (e) { + var r = w_0x25f3; + if (r(434) in navigator) { + try { + navigator.getBattery()[r(493)](function (t) { + var a = r; + e(t[a(940)] + "_" + t[a(688)] + "_" + t[a(851)] + "_" + t[a(531)]); + }); + } catch (r) { + console.log(r); + e(""); + } + } else { + e(""); + } + }); + } + var _0x47ec2a = {}; + function _0x299f3a() { + var e = _0x5612de; + var r; + var t = e(827); + var a = 0; + void 0 !== navigator[t] && (a = navigator[t]); + try { + document[e(392)](e(576)); + r = !0; + } catch (e) { + console.log(e); + r = !1; + } + var n = e(490) in window; + return Object[e(672)](_0x47ec2a, { + maxTouchPoints: a, + touchEvent: r, + touchStart: n + }), + a + "_" + r + "_" + n; + } + function _0xbe842b() { + return _0x47ec2a; + } + function _0x4649a1() { + var e = _0x5612de; + var r = new Date(); + r[e(557)](1); + r[e(660)](5); + var t = -r[e(567)](); + r[e(660)](11); + var a = -r[e(567)](); + return Math.min(t, a); + } + function _0x487576() { + var e = _0x5612de; + if (_0x6caf[e(731)]) { + return _0x6caf[e(731)]; + } + try { + var r = document[e(608)](e(589))[e(761)]("webgl"); + var t = r.getExtension(e(885)); + var a = r[e(899)](t.UNMASKED_VENDOR_WEBGL) + "/" + r.getParameter(t[e(926)]); + return _0x6caf[e(731)] = a, + a; + } catch (e) { + console.log(e); + return ""; + } + } + function _0x4f323e() { + var e = _0x5612de; + var r = [e(635), "sans-serif", e(507)]; + var t = {}; + var a = {}; + if (!document[e(393)]) { + return "0"; + } + for (var n = 0, f = r; n < f.length; n++) { + var i = f[n]; + var c = document[e(608)](e(503)); + c.innerHTML = e(675); + c.style[e(866)] = "72px"; + c.style.fontFamily = i; + document[e(393)][e(620)](c); + t[i] = c[e(750)]; + a[i] = c.offsetHeight; + document[e(393)][e(781)](c); + } + var o; + var d = ["Trebuchet MS", e(375), e(605), "Segoe UI", e(801), e(402), "MT Extra", e(937), e(654), e(838), "Meiryo", e(583), e(495), e(472), "IrisUPC", "Palatino", "Colonna MT", "Playbill", e(500), e(648), e(498), e(909), "OPTIMA", e(729), e(840), e(691), "Savoye LET", e(965), e(542)]; + o = 0; + for (var _ = 0; _ < d[e(601)]; _++) { + var x; + var u = _0x350075(r); + try { + for (u.s(); !(x = u.n())[e(483)];) { + var b = x.value; + var v = document[e(608)]("span"); + v[e(874)] = e(675); + v.style[e(866)] = e(643); + v[e(640)].fontFamily = d[_] + "," + b; + document.body[e(620)](v); + var s = v[e(750)] !== t[b] || v[e(676)] !== a[b]; + if (document[e(393)][e(781)](v), s) { + _ < 30 && (o |= 1 << _); + break; + } + } + } catch (e) { + console.log(e); + u.e(e); + } finally { + u.f(); + } + } + return o[e(942)](16); + } + function _0x5090f5() { + var e = _0x5612de; + try { + new WebSocket(e(409)); + } catch (r) { + console.log(r); + return r[e(786)]; + } + } + function _0x468d57() { + var e = _0x5612de; + return eval[e(942)]()[e(601)]; + } + function _0x5bbaf0() { + var e = _0x5612de; + var r = window.RTCPeerConnection || window[e(736)] || window[e(410)]; + var t = []; + return new Promise(function (a) { + var n = e; + (_0x176a57() || navigator[n(553)][n(514)]()[n(709)](n(526)) > 0) && a(""); + try { + if (r && "function" == typeof r) { + var f = new r({ + iceServers: [{ + urls: n(636) + } + ] + }); + var i = function () { }; + var c = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/; + f[n(491)] = function () { + var e = n; + if (e(354) === f.iceGatheringState) { + f[e(634)](); + f = null; + } + }; + f.onicecandidate = function (e) { + var r = n; + if (e && e[r(551)] && e[r(551)][r(551)]) { + if ("" === e.candidate[r(551)]) { + return; + } + var f = c.exec(e[r(551)][r(551)]); + if (null !== f && f[r(601)] > 1) { + var i = f[1]; + -1 === t[r(709)](i) && t[r(878)](i); + } + } else { + a(t[r(591)]()); + } + }; + f[n(485)](""); + setTimeout(function () { + var e = n; + a(t[e(591)]()); + }, 500); + var o = f[n(757)](); + o instanceof Promise ? o[n(493)](function (e) { + return f.setLocalDescription(e); + })[n(493)](i) : f.createOffer(function (e) { + f.setLocalDescription(e, i, i); + }, i); + } else { + a(""); + } + } catch (e) { + console.log(e); + a(""); + } + }); + } + function _0x2e02ca() { + var e = _0x5612de; + return e(943)[e(887)](/[xy]/g, function (r) { + var t = e; + var a = 16 * Math[t(398)]() | 0; + return ("x" == r ? a : 3 & a | 8)[t(942)](16); + }); + } + function _0x2e2fa0(e) { + var r = _0x5612de; + return 34 === e[r(601)] && _0x246aeb(0, e[r(432)](0, 32))[r(942)]().substring(0, 2) === e[r(432)](32, 34); + } + function _0x178d7c() { + var e = _0x5612de; + var r = _0x24dc34(e(610)); + return r && _0x2e2fa0(r) || _0x1f42cb(e(610), r = ((r = _0x2e02ca()) + _0x246aeb(0, r))[e(432)](0, 34)), + r; + } + function _0x3c0a68(e, r) { + var t = _0x5612de; + var a = null; + try { + a = document.getElementsByTagName(t(545))[0]; + } catch (e) { + console.log(e); + a = document.body; + } + if (null !== a) { + var n = document[t(608)](t(865)); + var f = "_" + parseInt(1e4 * Math.random(), 10) + "_" + new Date()[t(365)](); + e += t(446) + f; + n[t(363)] = e; + window[f] = function (e) { + var i = t; + try { + r(e); + a[i(781)](n); + delete window[f]; + } catch (e) { + console.log(e); + } + }; + a[t(620)](n); + } + } + function _0x4072ad(e) { + return w_0x5c3140("484e4f4a403f524300022c395eafc0a4000000004a04440d00000030110104324700040700004202110100030443011400011100010211010102110102110104110001430207000143021842000200404f4c4d4a4b484946474445424340415e5f5c5d5a5b58595657546f6c6d6a6b686966676465626360617e7f7c7d7a7b78797677743e3f3c3d3a3b383936372320", { + get 0() { + return _0x325f58; + }, + get 1() { + return _0x328bde; + }, + get 2() { + return _0xdda738; + }, + 3: arguments, + 4: e + }, this); + } + function _0x5c0cdd(e, r) { + var t = _0x5612de; + if (r) { + for (var a = 0, n = 0; n < e[t(601)]; n++) { + e[n].p && (e[n].r = r[a++]); + } + } + var f = ""; + e[t(596)](function (e) { + f += _0x5af46a(e.r) + "^^"; + }); + f += _0x30c916(); + var i = _0x2e02ca(); + var c = Math[t(463)](i[t(405)](3) / 8) + i[t(405)](3) % 8; + var o = i.substring(4, 4 + c); + f = _0x328bde(_0xdda738(f, o) + i); + var d = _0x45ece5.fpProfileUrl; + _0x3c0a68(d += t(371) + encodeURIComponent(f) + "&", function (e) { + var r = t; + if (0 == e[r(854)] && e.fp) { + _0x462335[r(863)] = e.fp; + _0x462335[r(867)] = _0x4072ad(e.fp); + _0x1f42cb("tt_scid", e.fp); + } + }); + } + function _0x1c3b6d(e) { + var r = _0x5612de; + return w_0x5c3140(r(578), { + get 0() { + return navigator; + }, + get 1() { + return window; + }, + get 2() { + return document; + }, + get 3() { + return _0x30c916; + }, + get 4() { + return _0x1afbc2; + }, + get 5() { + return _0x18b4be; + }, + get 6() { + return _0x130155; + }, + get 7() { + return _0x2a76f8; + }, + get 8() { + return _0x3da279; + }, + get 9() { + return _0x299f3a; + }, + get 10() { + return _0x4649a1; + }, + get 11() { + return _0x487576; + }, + get 12() { + return _0x4f323e; + }, + get 13() { + return _0x1f9824; + }, + get 14() { + return _0x24dc34; + }, + get 15() { + return _0x5090f5; + }, + get 16() { + return _0x468d57; + }, + get 17() { + return _0x5bbaf0; + }, + get 18() { + return _0x45b94b; + }, + get 19() { + return _0x178d7c; + }, + get 20() { + return _0x5af46a; + }, + 21: Promise, + get 22() { + return _0x5c0cdd; + }, + 23: arguments, + 24: e + }, this); + } + function _0x20cbf3(e, r, t) { + var a = _0x5612de; + return w_0x5c3140(a(563), { + 0: String, + 1: Date, + get 2() { + return _0x45b94b; + }, + get 3() { + return _0x184783; + }, + get 4() { + return location; + }, + 5: parseInt, + get 6() { + return _0x5863d1; + }, + 7: JSON, + get 8() { + return _0xf119da; + }, + get 9() { + return _0x3a1cf3; + }, + get 10() { + return _0x20b77a; + }, + get 11() { + return _0x446110; + }, + 12: Object, + get 13() { + return _0x483e03; + }, + get 14() { + return _0x53f850; + }, + get 15() { + return _0x2a900b; + }, + get 16() { + return _0x18b4be; + }, + get 17() { + return _0x462335; + }, + get 18() { + return _0x4072ad; + }, + get 19() { + return _0x24dc34; + }, + get 20() { + return navigator; + }, + 21: arguments, + 22: e, + 23: r, + 24: t + }, this); + } + function _0x5e5a64(e, r) { + var t = _0x5612de; + for (var a = {}, n = 0; n < r.length; n++) { + var f = r[n]; + var i = e[f]; + null == i && (i = !1); + null === i || "function" != typeof i && t(381) !== _0x1db123(i) || (i = !0); + a[f] = i; + } + return a; + } + function _0x2a6ac2() { + var e = _0x5612de; + return _0x5e5a64(navigator, [e(875), "appName", e(378), e(582), e(932), e(864), e(506), e(827), e(908), e(530), "vendorSub", "doNotTrack", e(586), e(738), e(658), e(912), e(911)]); + } + function _0x226933() { + var e = _0x5612de; + return _0x5e5a64(window, [e(669), e(546), e(774), e(622), e(870), "isSecureContext", e(960), e(921), "locationbar", e(671), e(927), e(736), "postMessage", "webkitRequestAnimationFrame", e(734), "netscape"]); + } + function _0x1e5a7f() { + var e = _0x5612de; + return _0x5e5a64(document, [e(821), e(595), e(883), "layers", e(704)]); + } + function _0x11a1d6() { + var e = _0x5612de; + var r = document[e(608)](e(589)); + var t = null; + try { + t = r[e(761)](e(619)) || r.getContext(e(938)); + } catch (e) { + console.log(e); + } + return t || (t = null), + t; + } + function _0x39c3d8(e) { + var r = _0x5612de; + var t = e[r(471)](r(497)) || e[r(471)](r(741)) || e[r(471)](r(794)); + if (t) { + var a = e[r(899)](t[r(447)]); + return 0 === a && (a = 2), + a; + } + return null; + } + function _0x425568() { + var e = _0x5612de; + if (_0x6caf[e(573)]) { + return _0x6caf[e(573)]; + } + var r = _0x11a1d6(); + if (!r) { + return {}; + } + var t = { + supportedExtensions: r[e(783)]() || [], + antialias: r[e(453)]()[e(421)], + blueBits: r[e(899)](r[e(638)]), + depthBits: r.getParameter(r[e(594)]), + greenBits: r[e(899)](r[e(963)]), + maxAnisotropy: _0x39c3d8(r), + maxCombinedTextureImageUnits: r.getParameter(r[e(466)]), + maxCubeMapTextureSize: r[e(899)](r[e(768)]), + maxFragmentUniformVectors: r.getParameter(r.MAX_FRAGMENT_UNIFORM_VECTORS), + maxRenderbufferSize: r[e(899)](r[e(730)]), + maxTextureImageUnits: r[e(899)](r[e(617)]), + maxTextureSize: r[e(899)](r[e(515)]), + maxVaryingVectors: r[e(899)](r[e(612)]), + maxVertexAttribs: r[e(899)](r.MAX_VERTEX_ATTRIBS), + maxVertexTextureImageUnits: r[e(899)](r[e(517)]), + maxVertexUniformVectors: r[e(899)](r[e(413)]), + shadingLanguageVersion: r.getParameter(r.SHADING_LANGUAGE_VERSION), + stencilBits: r.getParameter(r.STENCIL_BITS), + version: r[e(899)](r.VERSION) + }; + return _0x6caf[e(573)] = t, + t; + } + function _0x18707d() { + var e = _0x5612de; + var r = {}; + return r[e(624)] = _0x2a6ac2(), + r[e(915)] = _0x226933(), + r[e(520)] = _0x1e5a7f(), + r[e(619)] = _0x425568(), + r[e(897)] = _0x487576(), + r.plugins = _0x1f9824(), + _0x6caf.SECINFO = r, + r; + } + function _0x572e48() { + var e = _0x5612de; + return w_0x5c3140(e(465), { + get 0() { + return _0x6caf; + }, + get 1() { + return _0x18707d; + }, + 2: Date, + get 3() { + return _0x325f58; + }, + get 4() { + return _0x328bde; + }, + get 5() { + return _0xdda738; + }, + 6: JSON, + 7: arguments + }, this); + } + var _0x522d20 = { + kCallTypeDirect: 0, + kCallTypeInterceptor: 1 + }; + var _0x2e10da = { + kHttp: 0, + kWebsocket: 1 + }; + var _0x3f742e = _0x45b94b; + function _0x5646fa(e) { + var r = _0x5612de; + for (var t, a, n = [], f = 0; f < e[r(601)]; f++) { + t = e[r(405)](f); + a = []; + do { + a[r(878)](255 & t); + t >>= 8; + } while (t); + n = n[r(696)](a.reverse()); + } + return n; + } + function _0x6bc4ae(e) { } + function _0x5b890d(e) { } + function _0x16f345(e) { } + function _0x361214(e) { } + function _0xc35122(e, r, t) { } + var _0x5dde58 = { + WEB_DEVICE_INFO: 8 + }; + function _0x4df596(e, r) { + var t = _0x5612de; + return JSON[t(486)]({ + magic: 538969122, + version: 1, + dataType: e, + strData: r, + tspFromClient: new Date()[t(365)]() + }); + } + function _0x29a5ac(e, r, t, a) { + var n = _0x5612de; + return _0x32af0e(n(929), e, r, t, a); + } + function _0x32af0e(e, r, t, a, n) { + var f = _0x5612de; + var i = new XMLHttpRequest(); + if (i[f(395)](e, r, !0), n && (i[f(454)] = !0), a) { + for (var c = 0, o = Object.keys(a); c < o[f(601)]; c++) { + var d = o[c]; + var _ = a[d]; + i[f(939)](d, _); + } + } + i[f(522)](t); + } + function _0x2cd488(e, r) { + var t = _0x5612de; + return r || (r = null), + !!navigator.sendBeacon && (navigator[t(587)](e, r), !0); + } + function _0x4a2daf(e, r) { + var t = _0x5612de; + try { + window[t(476)] && window[t(476)][t(452)](e, r); + } catch (e) { + console.log(e); + } + } + function _0x34f60a(e) { + var r = _0x5612de; + try { + window[r(476)] && window[r(476)][r(707)](e); + } catch (e) { + console.log(e); + } + } + function _0x3d13cf(e) { + var r = _0x5612de; + try { + return window[r(476)] ? window.localStorage.getItem(e) : null; + } catch (e) { + console.log(e); + return null; + } + } + function _0x21db29(e, r) { + var t = _0x5612de; + for (var a, n = [], f = 0, i = "", c = 0; c < 256; c++) { + n[c] = c; + } + for (var o = 0; o < 256; o++) { + f = (f + n[o] + e[t(405)](o % e[t(601)])) % 256; + a = n[o]; + n[o] = n[f]; + n[f] = a; + } + var d = 0; + f = 0; + for (var _ = 0; _ < r[t(601)]; _++) { + f = (f + n[d = (d + 1) % 256]) % 256; + a = n[d]; + n[d] = n[f]; + n[f] = a; + i += String[t(482)](r[t(405)](_) ^ n[(n[d] + n[f]) % 256]); + } + return i; + } + var _0x45bf15 = _0x21db29; + var _0x48a082 = {}; + function _0x641e3d(e, r) { + var t = _0x5612de; + return w_0x5c3140(t(964), { + 0: String, + 1: Math, + get 2() { + return _0x45bf15; + }, + get 3() { + return _0x389396; + }, + 4: arguments, + 5: e, + 6: r + }, this); + } + _0x48a082.pb = 2; + _0x48a082.json = 1; + var _0x216650 = { + kNoMove: 2, + kNoClickTouch: 4, + kNoKeyboardEvent: 8, + kMoveFast: 16, + kKeyboardFast: 32, + kFakeOperations: 64 + }; + var _0x5dc9cc = { + sTm: 0, + acc: 0 + }; + function _0x18a9f7() { + var e = _0x5612de; + try { + var r = _0x3d13cf(e(835)); + if (r) { + Object[e(672)](_0x5dc9cc, JSON[e(945)](r)); + } else { + _0x5dc9cc[e(659)] = new Date()[e(365)](); + _0x5dc9cc[e(637)] = 0; + } + } catch (r) { + console.log(r); + _0x5dc9cc[e(659)] = new Date()[e(365)](); + _0x5dc9cc[e(637)] = 0; + _0x26e186(); + } + } + function _0x26e186() { + var e = _0x5612de; + _0x4a2daf(e(835), JSON.stringify(_0x5dc9cc)); + } + var _0xf63a81 = { + T_MOVE: 1, + T_CLICK: 2, + T_KEYBOARD: 3 + }; + var _0x2786f5 = !1; + var _0x9fb121 = []; + var _0x207cc5 = []; + var _0x191fa5 = []; + var _0xe06992 = { + ubcode: 0 + }; + var _0x388856 = function (e, r) { + return e + r; + }; + var _0x20a3d9 = function (e) { + return e * e; + }; + function _0x9c0be2(e, r) { + var t = _0x5612de; + if (e[t(601)] > 200 && e.splice(0, 100), e[t(601)] > 0) { + var a = e[e.length - 1]; + if (r.d - a.d <= 0 || "y" in r && r.x === a.x && r.y === a.y) { + return; + } + } + e[t(878)](r); + } + function _0x1d26db(e, r, t) { + var a = _0x5612de; + if (_0x462335.enableTrack) { + if (t !== _0xf63a81.T_MOVE) { + return t === _0xf63a81[a(496)] ? (e[a(601)] >= 500 && _0x450b73(), void e[a(878)](r)) : t === _0xf63a81[a(853)] ? (e[a(601)] > 500 && _0x450b73(), + void e.push(r)) : void 0; + } + if (e.length >= 500 && _0x450b73(), e[a(601)] > 0) { + var n = e[e[a(601)] - 1]; + var f = n.x; + var i = n.y; + var c = n.ts; + if (f === r.x && i === r.y) { + return; + } + if (r.ts - c < 500) { + return; + } + } + e.push(r); + } + } + var _0x19ffaf = { + init: 0, + running: 1, + exit: 2, + flush: 3 + }; + function _0x450b73(e) { + var r = _0x5612de; + return w_0x5c3140(r(930), { + get 0() { + return _0x6caf; + }, + get 1() { + return _0x19ffaf; + }, + 2: Date, + get 3() { + return _0x5dc9cc; + }, + get 4() { + return _0x462335; + }, + get 5() { + return _0x26e186; + }, + 6: Object, + get 7() { + return _0x4df596; + }, + get 8() { + return _0x5dde58; + }, + get 9() { + return _0x641e3d; + }, + 10: JSON, + get 11() { + return _0x48a082; + }, + get 12() { + return _0x2cd488; + }, + get 13() { + return _0x29a5ac; + }, + 14: arguments, + 15: e + }, this); + } + function _0x58c311() { + var e = _0x5612de; + _0x462335[e(895)] && _0x450b73(_0x19ffaf.exit); + } + var _0x1a755e = {}; + _0x1a755e[_0x5612de(724)] = _0x3dcfd5; + _0x1a755e.touchmove = _0x3dcfd5; + _0x1a755e[_0x5612de(868)] = _0x56114b; + _0x1a755e[_0x5612de(470)] = _0x19d89b; + _0x1a755e[_0x5612de(723)] = _0x19d89b; + var _0x18582a = !1; + function _0x1dbe74() { + var e = _0x5612de; + if (document && document[e(758)] && !_0x18582a) { + for (var r = 0, t = Object[e(383)](_0x1a755e); r < t[e(601)]; r++) { + var a = t[r]; + document[e(758)](a, _0x1a755e[a]); + } + _0x18582a = !0; + } + } + function _0x3dcfd5(e) { + var r = _0x5612de; + var t = e; + var a = e.type; + if (e[r(889)] && r(665) === a) { + t = e.touches[0]; + _0x2786f5 = !0; + } + var n = { + x: Math[r(463)](t.clientX), + y: Math[r(463)](t.clientY), + d: Date[r(845)]() + }; + _0x9c0be2(_0x9fb121, n); + _0x1d26db(_0x6caf[r(541)], { + ts: n.d, + x: n.x, + y: n.y + }, _0xf63a81[r(702)]); + } + function _0x56114b(e) { + var r = _0x5612de; + var t = 0; + (e[r(652)] || e.ctrlKey || e.metaKey || e[r(558)]) && (t = 1); + var a = { + x: t, + d: Date.now() + }; + _0x9c0be2(_0x191fa5, a); + _0x1d26db(_0x6caf[r(913)], { + ts: a.d + }, _0xf63a81[r(853)]); + } + function _0x19d89b(e) { + var r = _0x5612de; + var t = e; + var a = e.type; + if (e[r(889)] && "touchstart" === a) { + t = e.touches[0]; + _0x2786f5 = !0; + } + var n = { + x: Math.floor(t[r(703)]), + y: Math[r(463)](t.clientY), + d: Date.now() + }; + _0x9c0be2(_0x207cc5, n); + _0x1d26db(_0x6caf[r(950)], { + ts: n.d, + x: n.x, + y: n.y + }, _0xf63a81[r(496)]); + } + function _0x42fe9b(e) { + var r = _0x5612de; + return e.reduce(_0x388856) / e[r(601)]; + } + function _0x3ea7d6(e) { + var r = _0x5612de; + if (e[r(601)] <= 1) { + return 0; + } + var t = _0x42fe9b(e); + var a = e.map(function (e) { + return e - t; + }); + return Math[r(791)](a.map(_0x20a3d9)[r(886)](_0x388856) / (e[r(601)] - 1)); + } + function _0x52f064(e, r, t) { + var a = _0x5612de; + var n = 0; + var f = 0; + if (e[a(601)] > r) { + for (var i = [], c = 0; c < e[a(601)] - 1; c++) { + var o = e[c + 1]; + var d = e[c]; + var _ = o.d - d.d; + _ && (t ? i[a(878)](1 / _) : i[a(878)](Math[a(791)](_0x20a3d9(o.x - d.x) + _0x20a3d9(o.y - d.y)) / _)); + } + n = _0x42fe9b(i); + 0 === (f = _0x3ea7d6(i)) && (f = .01); + } + return [n, f]; + } + function _0x26d461() { + var e = _0x5612de; + var r = !1; + var t = 0; + try { + if (document && document.createEvent) { + document[e(392)](e(576)); + r = !0; + } + } catch (e) { + console.log(e); + } + var a = _0x52f064(_0x9fb121, 1); + var n = _0x52f064(_0x191fa5, 5, !0); + var f = 1; + !r && _0x2786f5 && (f |= 64, t |= _0x216650[e(754)]); + 0 === _0x9fb121.length ? (f |= 2, t |= _0x216650[e(766)]) : a[0] > 50 && (f |= 16, + t |= _0x216650.kMoveFast); + 0 === _0x207cc5.length && (f |= 4, t |= _0x216650.kNoClickTouch); + 0 === _0x191fa5[e(601)] ? (f |= 8, t |= _0x216650.kNoKeyboardEvent) : n[0] > .5 && (f |= 32, + t |= _0x216650[e(959)]); + _0xe06992.ubcode = t; + var i = f[e(942)](32); + return 1 === i.length ? i = "00" + i : 2 === i[e(601)] && (i = "0" + i), + i; + } + function _0x5047d8() { + _0x450b73(3); + } + function _0x4145f8(e, r) { + var t = _0x5612de; + for (var a = r[t(601)], n = new ArrayBuffer(a + 1), f = new Uint8Array(n), i = 0, c = 0; c < a; c++) { + f[c] = r[c]; + i ^= r[c]; + } + f[a] = i; + var o = 255 & Math[t(463)](255 * Math[t(398)]()); + var d = String[t(482)][t(519)](null, f); + var _ = _0x21db29(String[t(482)](o), d); + var x = ""; + return x += String[t(482)](e), + x += String[t(482)](o), + _0x389396(x += _, "s1"); + } + function _0x1633f2(e, r, t, a, n) { + var f = _0x5612de; + console.log(e, r, t, a, n); + // _0x5863d1(); + // _0x26d461(); + void 0 !== a && "" !== a && (a = ""); + var i = _0x5dd467(a); + n || (n = "00000000000000000000000000000000"); + var c = new ArrayBuffer(9); + var o = new Uint8Array(c); + var f398 = f(398); + var f463 = f(463); + var f806 = f(806); + var ra = Math[f463](100 * Math[f398]()); + var ra2 = Math.floor(100 * Math.random()); + var d = 0 | e << 6 | r << 5 | (1 & ra) << 4 | 0; + _0x6caf[f806]++; + var _ = 63 & _0x6caf[f806]; + o[0] = t << 6 | _; + o[1] = 0// _0x6caf.envcode >> 8 & 255; // 1 o[1]=0 + o[2] = 255 & _0x6caf[f(896)]; + o[3] = _0xe06992[f(898)]; + var x = _0x42e709[f(695)](_0x5dd467(_0x42e709[f(695)](i))); + o[4] = x[14]; + o[5] = x[15]; + var ttt = _0x42e709.decode(n); + console.log(ttt); + var tttt = _0x5dd467(ttt); + console.log(tttt); + var u = _0x42e709.decode(tttt); + return o[6] = u[14], + o[7] = u[15], + o[8] = 255 & Math[f(463)](255 * Math[f(398)]()), + _0x4145f8(d, o); + } + function _0x34c70a(e, r, t) { + var a = _0x5612de; + var a1 = _0x2e10da[a(649)]; + var a2 = _0x462335.initialized; + var a3 = e; + var a4 = null; + var a5 = t; + var xb = _0x1633f2(a1, a2, a3, a4, a5); + return { + "X-Bogus": xb + }; + } + // function _0x11233a(e, r, t) { + // var a = _0x5612de; + // return { + // "X-Bogus": _0x1633f2(_0x2e10da[a(479)], _0x462335[a(508)], e, r, t) + // }; + // } + function _0x5c2014(e) { + return w_0x5c3140("484e4f4a403f524300362d0a5f00233c0000000029b6a730000000630214000103001400020700001400030700011400041101031100031347000d11010311000313140001450023110103110004134700130211010011010311000413430114000145000607000214000102110101110002021100014303140005110005420003096b1e7e601e606766710c6b1e7e601e63726a7f7c7277200303030303030303030303030303030303030303030303030303030303030303", { + get 0() { + return _0x5dd467; + }, + get 1() { + return _0x34c70a; + }, + 2: arguments, + 3: e + }, this); + } + function _0x3c875d(e, r) { + var t = _0x5612de; + var a = new Uint8Array(3); + return a[0] = e / 256, + a[1] = e % 256, + a[2] = r % 256, + String[t(482)][t(519)](null, a); + } + function _0x4b49f3(e) { + var r = _0x5612de; + return String[r(482)](e); + } + function _0x26151b(e, r, t) { + return _0x4b49f3(e) + _0x4b49f3(r) + t; + } + function _0xc38697(e, r) { + return _0x389396(e, r); + } + function _0x538c80(e, r, t, a, n, f, i, c, o, d, _, x, u, b, v, s, l, h, w) { + var g = _0x5612de; + var p = new Uint8Array(19); + return p[0] = e, + p[1] = _, + p[2] = r, + p[3] = x, + p[4] = t, + p[5] = u, + p[6] = a, + p[7] = b, + p[8] = n, + p[9] = v, + p[10] = f, + p[11] = s, + p[12] = i, + p[13] = l, + p[14] = c, + p[15] = h, + p[16] = o, + p[17] = w, + p[18] = d, + String.fromCharCode[g(519)](null, p); + } + var _0x3c4305 = !1; + function _0x8edc3d(e, r) { + var t = _0x5612de; + return w_0x5c3140(t(690), { + get 0() { + return _0x5dd467; + }, + get 1() { + return _0x3c4305; + }, + set 1(e) { + _0x3c4305 = e; + }, + get 2() { + return _0x462335; + }, + get 3() { + return _0x5863d1; + }, + get 4() { + return _0x26d461; + }, + get 5() { + return _0xe06992; + }, + get 6() { + return _0x6caf; + }, + get 7() { + return _0x42e709; + }, + 8: String, + get 9() { + return navigator; + }, + get 10() { + return _0x3c875d; + }, + get 11() { + return _0x21db29; + }, + get 12() { + return _0xc38697; + }, + 13: Date, + get 14() { + return _0x18b4be; + }, + get 15() { + return _0x538c80; + }, + get 16() { + return _0x4b49f3; + }, + get 17() { + return _0x26151b; + }, + get 18() { + return _0x389396; + }, + 19: arguments, + 20: e, + 21: r, + 22: RegExp + }, this); + } + function _0x556182(e) { + var r = _0x5612de; + _0x4a2daf(r(650), e); + } + function _0x5141ac() { + var e = _0x3d13cf("xmst"); + return e || ""; + } + function _0x50686a(e) { + var r = _0x5612de; + return r(817) === Object[r(836)][r(942)][r(820)](e); + } + function _0x2195cd(e, r) { + var t = _0x5612de; + if (e) { + var a = e[r]; + if (a) { + var n = _0x1db123(a); + return t(381) === n || t(494) === n ? 1 : "string" === n ? n[t(601)] > 0 ? 1 : 2 : _0x50686a(a) ? 1 : 2; + } + } + return 2; + } + function _0xdc4d4c(e) { + var r = _0x5612de; + try { + var t = Object[r(836)][r(942)][r(820)](e); + return r(782) === t ? !0 === e ? 1 : 2 : r(431) === t ? 3 : "[object Undefined]" === t ? 4 : r(462) === t ? 5 : "[object String]" === t ? "" === e ? 7 : 8 : r(817) === t ? 0 === e.length ? 9 : 10 : r(891) === t ? 11 : r(510) === t ? 12 : r(381) === _0x1db123(e) ? 99 : -1; + } catch (e) { + console.log(e); + return -2; + } + } + var _0x2d8bb4 = {}; + function _0x241339() { + var e = _0x5612de; + return document[e(883)] ? "IE" : 0; + } + function _0x5011f8() { + var e = _0x5612de; + return eval.toString()[e(601)]; + } + function _0x58210c(e, r, t) { + var a = _0x5612de; + for (var n = {}, f = 0; f < r[a(601)]; f++) { + var i = void 0; + var c = void 0; + var o = r[f]; + try { + e && (i = e[o]); + } catch (e) { + console.log(e); + } + if ("string" === t) { + c = "" + i; + } else if (a(651) === t) { + c = i ? Math[a(463)](i) : -1; + } else { + if (a(427) !== t) { + throw Error(a(598)); + } + c = _0xdc4d4c(i); + } + n[o] = c; + } + return n; + } + function _0x3a2b92() { + var e = _0x5612de; + var r; + Object[e(672)](_0x2d8bb4[e(624)], _0x58210c(navigator, [e(875), e(848), e(388), "appVersion", e(813), "doNotTrack", e(922), e(908), e(378), e(582), "productSub", "cpuClass", e(530), e(469), e(478), e(528), e(788), e(457), "webdriver"], "string")); + Object.assign(_0x2d8bb4[e(624)], _0x58210c(navigator, [e(481), "vibrate", e(738), e(658), e(912), e(911)], e(427))); + Object[e(672)](_0x2d8bb4[e(624)], _0x58210c(navigator, [e(864), e(827)], e(651))); + _0x2d8bb4.navigator[e(920)] = "" + navigator[e(920)]; + try { + document[e(392)](e(576)); + r = 1; + } catch (e) { + console.log(e); + r = 2; + } + _0x2d8bb4.navigator[e(843)] = r; + var t = e(490) in window ? 1 : 2; + _0x2d8bb4[e(624)][e(470)] = t; + } + function _0x5abc93() { + var e = _0x5612de; + Object.assign(_0x2d8bb4.window, _0x58210c(window, ["Image", e(777), e(671), e(921), e(826), "external", "mozRTCPeerConnection", "postMessage", e(644), e(734), e(767), e(476), e(958), e(745)], e(427))); + Object[e(672)](_0x2d8bb4[e(915)], _0x58210c(window, [e(960)], e(651))); + _0x2d8bb4[e(915)][e(962)] = window[e(962)][e(656)]; + } + function _0x5d3845() { + var e = _0x5612de; + try { + var r = document; + var t = window[e(808)]; + var a = window[e(774)] >>> 0; + var n = window[e(546)] >>> 0; + var f = window[e(426)] >>> 0; + var i = window[e(621)] >>> 0; + var c = Math[e(463)](window.screenX); + var o = Math.floor(window[e(870)]); + var d = window[e(600)] >>> 0; + var _ = window[e(642)] >>> 0; + var x = t.availWidth >>> 0; + var u = t[e(630)] >>> 0; + var b = t.width >>> 0; + var v = t[e(581)] >>> 0; + return { + innerWidth: void 0 !== a ? a : -1, + innerHeight: void 0 !== n ? n : -1, + outerWidth: void 0 !== f ? f : -1, + outerHeight: void 0 !== i ? i : -1, + screenX: void 0 !== c ? c : -1, + screenY: void 0 !== o ? o : -1, + pageXOffset: void 0 !== d ? d : -1, + pageYOffset: void 0 !== _ ? _ : -1, + availWidth: void 0 !== x ? x : -1, + availHeight: void 0 !== u ? u : -1, + sizeWidth: void 0 !== b ? b : -1, + sizeHeight: void 0 !== v ? v : -1, + clientWidth: r.body ? r[e(393)][e(418)] >>> 0 : -1, + clientHeight: r[e(393)] ? r[e(393)][e(955)] >>> 0 : -1, + colorDepth: t[e(382)] >>> 0, + pixelDepth: t[e(778)] >>> 0 + }; + } catch (e) { + console.log(e); + return {}; + } + } + function _0x573065() { + var e = _0x5612de; + Object[e(672)](_0x2d8bb4[e(520)], _0x58210c(document, [e(821), e(595), "documentMode"], e(828))); + Object.assign(_0x2d8bb4[e(520)], _0x58210c(document, [e(805), e(387), "images"], e(427))); + } + function _0x47f354() { + var e = _0x5612de; + var r = {}; + try { + var t = document[e(608)](e(589))[e(761)](e(619)); + var a = t[e(471)](e(885)); + var n = t.getParameter(a.UNMASKED_VENDOR_WEBGL); + var f = t[e(899)](a[e(926)]); + r.vendor = n; + r[e(706)] = f; + } catch (e) { + console.log(e); + } + return r; + } + function _0x58bcf8() { + var e = _0x5612de; + var r = _0x11a1d6(); + if (r) { + var t = { + antialias: r[e(453)]()[e(421)] ? 1 : 2, + blueBits: r[e(899)](r[e(638)]), + depthBits: r.getParameter(r[e(594)]), + greenBits: r[e(899)](r.GREEN_BITS), + maxAnisotropy: _0x39c3d8(r), + maxCombinedTextureImageUnits: r[e(899)](r.MAX_COMBINED_TEXTURE_IMAGE_UNITS), + maxCubeMapTextureSize: r[e(899)](r[e(768)]), + maxFragmentUniformVectors: r[e(899)](r[e(646)]), + maxRenderbufferSize: r[e(899)](r[e(730)]), + maxTextureImageUnits: r[e(899)](r.MAX_TEXTURE_IMAGE_UNITS), + maxTextureSize: r[e(899)](r[e(515)]), + maxVaryingVectors: r.getParameter(r[e(612)]), + maxVertexAttribs: r[e(899)](r.MAX_VERTEX_ATTRIBS), + maxVertexTextureImageUnits: r.getParameter(r[e(517)]), + maxVertexUniformVectors: r.getParameter(r[e(413)]), + shadingLanguageVersion: r[e(899)](r[e(377)]), + stencilBits: r.getParameter(r[e(699)]), + version: r[e(899)](r[e(513)]) + }; + Object.assign(_0x2d8bb4.webgl, t); + } + Object[e(672)](_0x2d8bb4[e(619)], _0x47f354()); + } + function _0x75957() { + var e = _0x5612de; + if (window[e(671)]) { + for (var r = 2; r < 10; r++) { + try { + return !!new (window[e(671)])(e(948) + r) && r[e(942)](); + } catch (e) { + console.log(e); + } + } + try { + return !!new (window[e(671)])("PDF.PdfCtrl.1") && "4"; + } catch (e) { + console.log(e); + } + try { + return !!new (window[e(671)])(e(837)) && "7"; + } catch (e) { + console.log(e); + } + } + return "0"; + } + function _0x1555d9() { + return { + plugin: _0x30412e(), + pv: _0x75957() + }; + } + function _0x1f01ce(e) { + var r = _0x5612de; + try { + var t = window[e]; + var a = r(505); + return t[r(452)](a, a), + t[r(707)](a), + !0; + } catch (e) { + console.log(e); + return !1; + } + } + function _0x3ffe15() { + return w_0x5c3140("484e4f4a403f5243003c20117d3adeac000000004e770f390000003a030014000102110100070000430147000b11000103012f170001354902110100070001430147000e110001030103012b2f17000135491100014200020c45464a48457a5d465b484e4c0e5a4c5a5a4046477a5d465b484e4c", { + get 0() { + return _0x1f01ce; + }, + 1: arguments + }, this); + } + function _0x252788(e, r, t) { + var a = _0x5612de; + for (var n = 0, f = 0; f < r.length; f++) { + var i = _0x2195cd(e, r[f]); + if (i && (n |= i << t + f, t + f >= 32)) { + console[a(784)]("abort 32"); + break; + } + } + return n; + } + function _0x484054() { + return w_0x5c3140("484e4f4a403f5243002c3b0a6f4f88290000000044c410000000011f1101001400010700000700010700020700030700040700050700060700070700080700090c000a14000207000a14000307000b14000407000a110101110004163e000414000a413d00d11100014a07000c1307000d43010300131400050c0000140006030014000711000711000207000e13274700691100014a07000f130700104301140008110002110007131400091100084a0700111307001207001311000918430249110004070014181100091807001518110008070016161100054a070017131100084301491100064a07001813110008430149170007214945ff891101011100041317000335490300170007354911000711000207000e132747001a1100054a0700191311000611000713430149170007214945ffd84111000342001a037e617e037e617d037e617c037e617b037e617a037e6179037e6178037e6177037e6176037d617f0014262b20213b242120382138272e3b263c3b27263c14282a3b0a232a222a213b3c0d361b2e28012e222a04272a2e2b06232a21283b270d2c3d2a2e3b2a0a232a222a213b063c2c3d263f3b0c3c2a3b0e3b3b3d262d3a3b2a08232e21283a2e282a0a052e392e1c2c3d263f3b02726d016d043b2a373b0b2e3f3f2a212b0c2726232b043f3a3c270b3d2a2220392a0c2726232b", { + get 0() { + return document; + }, + get 1() { + return window; + }, + 2: arguments + }, this); + } + _0x2d8bb4[_0x5612de(624)] = {}; + _0x2d8bb4[_0x5612de(574)] = {}; + _0x2d8bb4[_0x5612de(915)] = {}; + _0x2d8bb4[_0x5612de(619)] = {}; + _0x2d8bb4.document = {}; + _0x2d8bb4[_0x5612de(808)] = {}; + _0x2d8bb4[_0x5612de(442)] = {}; + _0x2d8bb4.custom = {}; + var _0x548676 = null; + function _0x491716() { + var e = _0x5612de; + return w_0x5c3140(e(372), { + get 0() { + return self; + }, + get 1() { + return window; + }, + get 2() { + return parent; + }, + 3: arguments + }, this); + } + function _0x2d2578() { + !function () { + var e = w_0x25f3; + var r = {}; + var t = navigator[e(857)] || navigator[e(752)]; + if (t) { + try { + t[e(940)] ? r[e(940)] = 1 : r[e(940)] = 2; + r[e(531)] = Math[e(873)](100 * t[e(531)]); + r[e(688)] = "" + t[e(688)]; + r[e(401)] = "" + t.dischargingTime; + } catch (e) { + console.log(e); + } + _0x2d8bb4[e(857)] = {}; + Object[e(672)](_0x2d8bb4[e(857)], r); + } else if (e(900) != typeof navigator && navigator[e(434)]) { + try { + navigator[e(434)]()[e(493)](function (t) { + var a = e; + try { + t[a(940)] ? r[a(940)] = 1 : r[a(940)] = 2; + r[a(531)] = Math[a(873)](100 * t[a(531)]); + r[a(688)] = "" + t[a(688)]; + r[a(401)] = "" + t.dischargingTime; + } catch (e) { + console.log(e); + } + _0x2d8bb4.battery = {}; + Object[a(672)](_0x2d8bb4[a(857)], r); + }); + } catch (e) { + console.log(e); + } + } + } + (); + "undefined" != typeof Promise && (_0x548676 = new Promise(function (e) { + try { + _0x5bbaf0().then(function (e) { + var r = w_0x25f3; + Object[r(672)](_0x2d8bb4.wID, { + rtcIP: e + }); + }); + } catch (e) { + console.log(e); + } + e(""); + })); + } + function _0x5c328e() { + var e = _0x5612de; + return w_0x5c3140(e(429), { + get 0() { + return window; + }, + get 1() { + return navigator; + }, + get 2() { + var r = e; + return r(900) != typeof InstallTrigger ? InstallTrigger : void 0; + }, + 3: Object, + get 4() { + return _0x241339; + }, + get 5() { + return _0x2d8bb4; + }, + get 6() { + return document; + }, + 7: Promise, + 8: Date, + get 9() { + return _0x252788; + }, + get 10() { + return _0x5011f8; + }, + get 11() { + return _0x4f323e; + }, + get 12() { + return _0x5090f5; + }, + 13: Math, + get 14() { + return _0x3ffe15; + }, + get 15() { + return _0x18b4be; + }, + get 16() { + return _0x484054; + }, + get 17() { + return _0x491716; + }, + get 18() { + return _0x462335; + }, + get 19() { + return _0x24dc34; + }, + get 20() { + return _0x6caf; + }, + get 21() { + return _0x2d2578; + }, + get 22() { + return _0x3a2b92; + }, + get 23() { + return _0x5abc93; + }, + get 24() { + return _0x573065; + }, + get 25() { + return _0x58bcf8; + }, + get 26() { + return _0x1555d9; + }, + get 27() { + return _0x5d3845; + }, + 28: parseInt, + get 29() { + return _0x3d13cf; + }, + get 30() { + return _0x4a2daf; + }, + get 31() { + return _0x641e3d; + }, + 32: JSON, + get 33() { + return _0x48a082; + }, + get 34() { + return _0x4df596; + }, + get 35() { + return _0x5dde58; + }, + get 36() { + return _0x548676; + }, + get 37() { + return _0x29a5ac; + }, + 38: arguments + }, this); + } + function _0x25a792(e) { + var r = _0x5612de; + return _0x462335[r(389)] && _0x462335[r(389)][r(732)] && -1 !== e[r(709)](_0x462335[r(389)][r(732)]) ? _0x39693d.sec : _0x39693d[r(436)]; + } + function _0xd287a1(e) { + var r = _0x5612de; + var t = _0x462335[r(389)][r(732)]; + return !(!t || !e || -1 === e[r(709)](t)); + } + function _0x2b13af(e) { + var r = _0x5612de; + var t = e; + decodeURIComponent(e) === e && (t = encodeURI(e)); + var a = t[r(709)]("?"); + if (a > 0) { + var n = t[r(935)](0, a + 1); + var f = t[r(935)](a + 1); + t = n + f[r(834)]("'").join(r(894)); + } + return t; + } + function _0x1958a5(e, r) { + var t = _0x5612de; + for (var a = "", n = "", f = 0; f < r[t(601)]; f++) { + f % 2 == 0 ? n = r[f] : a += "&" + n + "=" + r[f]; + } + var i = e; + if (a[t(601)] > 0) { + var c = -1 === e.indexOf("?") ? "?" : "&"; + i = e + c + a[t(935)](1); + } + return i; + } + function _0x288415(e) { + var r = _0x5612de; + var t = e.indexOf("?"); + return -1 !== t ? e[r(935)](t + 1) : ""; + } + function _0x6a7375(e) { + var r = _0x5612de; + for (var t = 0; t < _0x462335._enablePathListRegex.length; t++) { + if (_0x462335[r(425)][t].test(e)) { + return !0; + } + } + return !1; + } + function _0x7d8404(e) { + var r = _0x5612de; + return r(390) === e || "application/json" === e; + } + function _0x3af1be() { + var e = _0x5612de; + return w_0x5c3140(e(872), { + get 0() { + return window; + }, + get 1() { + return _0x6a7375; + }, + get 2() { + return _0x6caf; + }, + get 3() { + return _0x1958a5; + }, + get 4() { + return _0x2b13af; + }, + get 5() { + return _0x288415; + }, + get 6() { + return _0x8edc3d; + }, + get 7() { + return _0x462335; + }, + get 8() { + return _0x45e0e9; + }, + get 9() { + return _0x7d8404; + }, + get 10() { + return _0x1294ff; + }, + get 11() { + return _0x20cbf3; + }, + get 12() { + return _0x45b94b; + }, + get 13() { + return _0x572e48; + }, + get 14() { + return _0xd287a1; + }, + get 15() { + return _0x25a792; + }, + get 16() { + return _0x39693d; + }, + get 17() { + return _0x1f42cb; + }, + get 18() { + return _0x556182; + }, + get 19() { + return setTimeout; + }, + get 20() { + return _0x5c328e; + }, + 21: arguments, + 22: RegExp + }, this); + } + var _0x3c4266 = _0x5612de(900) != typeof URL && URL instanceof Object; + var _0x3311d7 = "undefined" != typeof Request && Request instanceof Object; + var _0x4f1fa4 = "undefined" != typeof Headers && Headers instanceof Object; + function _0x415adb() { + var e = _0x5612de; + return window[e(407)]; + } + function _0x1d82ac() { + var e = _0x5612de; + return w_0x5c3140(e(564), { + get 0() { + return _0x415adb; + }, + get 1() { + return window; + }, + get 2() { + return _0xd287a1; + }, + get 3() { + return _0x25a792; + }, + get 4() { + return _0x39693d; + }, + get 5() { + return _0x6caf; + }, + get 6() { + return _0x1f42cb; + }, + get 7() { + return _0x556182; + }, + get 8() { + return setTimeout; + }, + get 9() { + return _0x5c328e; + }, + get 10() { + return _0x3311d7; + }, + get 11() { + return Request; + }, + get 12() { + return _0x3c4266; + }, + get 13() { + return URL; + }, + get 14() { + return _0x6a7375; + }, + get 15() { + return _0x1958a5; + }, + get 16() { + return _0x2b13af; + }, + get 17() { + return _0x288415; + }, + get 18() { + return _0x8edc3d; + }, + get 19() { + return _0x462335; + }, + get 20() { + return _0x45e0e9; + }, + get 21() { + return _0xb48e77; + }, + get 22() { + return _0x7d8404; + }, + get 23() { + return _0x1294ff; + }, + get 24() { + return _0x20cbf3; + }, + get 25() { + return _0x45b94b; + }, + get 26() { + return _0x572e48; + }, + 27: arguments + }, this); + } + function _0xb48e77(e, r) { + var t = _0x5612de; + var a = ""; + if (_0x3311d7 && e instanceof Request) { + var n = e[t(859)][t(811)](t(552)); + return n && (a = n), + a; + } + if (r && r[t(859)]) { + if (_0x4f1fa4 && r[t(859)] instanceof Headers) { + var f = r[t(859)][t(811)](t(552)); + return f && (a = f), + a; + } + if (r.headers instanceof Array) { + for (var i = 0; i < r.headers[t(601)]; i++) { + if (t(552) == r[t(859)][i][0].toLowerCase()) { + return r[t(859)][i][1]; + } + } + } + if (r[t(859)] instanceof Object) { + for (var c = 0, o = Object.keys(r.headers); c < o[t(601)]; c++) { + var d = o[c]; + if ("content-type" === d.toLowerCase()) { + return r[t(859)][d]; + } + } + return a; + } + } + } + function _0x1294ff(e, r, t) { + var a = _0x5612de; + if (null == t || "" === t) { + return e; + } + if (t = t[a(942)](), a(390) === r) { + e[a(822)] = !0; + var n = t[a(834)]("&"); + var f = {}; + if (n) { + for (var i = 0; i < n.length; i++) { + f[n[i][a(834)]("=")[0]] = decodeURIComponent(n[i][a(834)]("=")[1]); + } + } + e[a(393)] = f; + } else { + e[a(393)] = JSON.parse(t); + } + return e; + } + function _0x45e0e9(e, r) { + var t = _0x5612de; + var a = r; + if (_0x462335[t(397)][t(601)] > 0) { + for (var n = 0; n < _0x462335[t(397)][t(601)]; n++) { + var f = _0x462335[t(397)][n][0]; + if (f[t(555)](r)) { + a = r[t(887)](f, _0x462335[t(397)][n][1]); + e && _0x3d40ff[t(919)].call(e, t(579), t(547) + r + "\nREWRITED: " + a); + break; + } + } + } + return _0x2b13af(a); + } + function _0x344a4d() { + var e = _0x5612de; + return w_0x5c3140(e(525), { + get 0() { + return window; + }, + get 1() { + return _0x6a7375; + }, + get 2() { + return _0x6caf; + }, + get 3() { + return _0x1958a5; + }, + get 4() { + return _0x2b13af; + }, + get 5() { + return _0x288415; + }, + get 6() { + return _0x8edc3d; + }, + 7: arguments + }, this); + } + function _0x3f720d() { + _0x3af1be(); + _0x1d82ac(); + _0x344a4d(); + } + function _0x3bfecb(e) { + var r = _0x5612de; + this[r(833)] = "ConfigException"; + this[r(786)] = e; + } + var _0x589057 = { + cn: { + host: _0x5612de(713) + } + }; + var _0x2bbf08 = [_0x5612de(780)]; + var _0x3d70a4; + function _0x43f5a3(e) { + var r = _0x5612de; + var t = ""; + return { + host: t = e[r(438)] || e[r(430)] ? e[r(435)] : _0x589057[e[r(548)]][r(732)], + pathList: _0x2bbf08, + reportUrl: t + _0x2bbf08[0] + }; + } + var _0x383bd7 = !1; + var _0xd39ee2; + var _0x9e520d; + function _0x53ee31(e) { + var r = _0x5612de; + return w_0x5c3140(r(847), { + 0: Object, + 1: Math, + get 2() { + return _0x3bfecb; + }, + get 3() { + return _0x6caf; + }, + get 4() { + return _0x462335; + }, + get 5() { + return _0x43f5a3; + }, + get 6() { + return setTimeout; + }, + get 7() { + return _0x5c328e; + }, + get 8() { + return _0x3d70a4; + }, + set 8(e) { + _0x3d70a4 = e; + }, + get 9() { + return clearInterval; + }, + get 10() { + return setInterval; + }, + get 11() { + return _0x450b73; + }, + get 12() { + return _0x1a39c4; + }, + get 13() { + return _0x3f720d; + }, + get 14() { + return _0x59992f; + }, + get 15() { + return _0x39d569; + }, + get 16() { + return _0x1dbe74; + }, + get 17() { + return _0x383bd7; + }, + set 17(e) { + _0x383bd7 = e; + }, + get 18() { + return _0x18707d; + }, + get 19() { + return _0x1c3b6d; + }, + 20: arguments, + 21: e + }, this); + } + function _0x3498af(e) { } + function _0x59992f(e) { + var r = _0x5612de; + for (var t = 0; t < e[r(601)]; t++) { + e[t] && _0x462335._enablePathListRegex.push(new RegExp(e[t])); + } + } + function _0x39d569(e) { + var r = _0x5612de; + if (void 0 !== e) { + for (var t = 0; t < e[r(601)]; t++) { + _0x462335[r(397)][r(878)]([new RegExp(e[t][0]), e[t][1]]); + } + } + } + function _0x32e4a6() { + var e = _0x5612de; + return window[e(792)] || ""; + } + function _0x1be1e1(e) { + var r = _0x5612de; + var t = _0x6caf[r(803)]; + var a = 9; + r(694) === e && (a = 1); + r(645) === e && (a = 2); + var n = { + ts: new Date()[r(365)](), + v: a + }; + t[r(878)](n); + } + function _0x4de7ef() { + var e = _0x5612de; + var r; + var t; + void 0 !== document[e(645)] ? (e(645), t = e(416), r = e(502)) : void 0 !== document[e(351)] ? (e(351), + t = e(456), r = e(831)) : void 0 !== document[e(710)] ? (e(710), t = e(716), r = e(420)) : void 0 !== document[e(554)] && (e(554), + t = e(802), r = e(358)); + document[e(758)](t, function () { + _0x1be1e1(document[r]); + }, !1); + _0x1be1e1(document[r]); + } + function _0x3ff3f1() { + _0x58c311(); + } + function _0x4c727e() { + var e = _0x5612de; + function r(e) { + var r = w_0x25f3; + _0x462335.triggerUnload || (_0x462335[r(516)] = !0, _0x3ff3f1()); + } + if (window && window[e(758)]) { + window[e(758)](e(893), r); + window[e(758)](e(667), r); + } + } + function _0x5b850b() { + var e = _0x5612de; + for (var r = document[e(626)][e(834)](";"), t = [], a = 0; a < r[e(601)]; a++) { + if (e(949) == (t = r[a][e(834)]("="))[0].trim()) { + _0x6caf[e(949)] = t[1]; + break; + } + } + } + function _0x498349(e) { + return new _0x53ee31(e); + } + function _0x475194(e) { + 0 === e ? setTimeout(_0x5047d8, 100) : 1 === e && setTimeout(_0x5c328e, 100); + } + function _0x4a4111(e, r) { + var t = _0x5612de; + 1 === e && (_0x462335[t(842)] = Object.assign({}, _0x462335[t(842)], r)); + } + function _0x271dea(e) { + void 0 !== e && "" != e && (_0x6caf.ttwid = e); + } + function _0x3a4a1a(e) { + var r = _0x5612de; + void 0 !== e && "" != e && (_0x6caf[r(790)] = e); + } + function _0x3f0a66(e) { + var r = _0x5612de; + void 0 !== e && "" != e && (_0x6caf[r(825)] = e); + } + _0x53ee31[_0x5612de(836)][_0x5612de(419)] = _0x5c2014; + _0x53ee31[_0x5612de(836)].getReferer = _0x32e4a6; + _0x53ee31[_0x5612de(836)].setUserMode = _0x3498af; + _0xd39ee2 = _0x24dc34(_0x45b94b.refererKey) || ""; + _0x2ecc5a(_0x45b94b.refererKey); + _0x5612de(726) === _0xd39ee2 ? _0xd39ee2 = "" : "" === _0xd39ee2 && (_0xd39ee2 = document[_0x5612de(762)]); + _0xd39ee2 && (window[_0x5612de(792)] = _0xd39ee2); + _0x9e520d = _0x5141ac(); + _0x9e520d && (_0x6caf[_0x5612de(692)] = _0x9e520d, _0x6caf.msStatus = _0x39693d.asgw); + // setTimeout(function () { + // _0x18a9f7(); + // _0x1dbe74(); + // _0x4de7ef(); + // _0x4c727e(); + // _0x21fa28(); + // }, 3e3); + _0x5b850b(); + _0x59992f([_0x5612de(780)]); + var _0x1649bc = !0; + _0x1d18f2.frontierSign = _0x5c2014; + _0x1d18f2[_0x5612de(744)] = _0x32e4a6; + _0x1d18f2[_0x5612de(953)] = _0x498349; + _0x1d18f2[_0x5612de(504)] = _0x1649bc; + _0x1d18f2.report = _0x475194; + _0x1d18f2[_0x5612de(664)] = _0x4a4111; + _0x1d18f2[_0x5612de(423)] = _0x3a4a1a; + _0x1d18f2.setTTWebidV2 = _0x3f0a66; + _0x1d18f2[_0x5612de(580)] = _0x271dea; + _0x1d18f2[_0x5612de(888)] = _0x3498af; + Object.defineProperty(_0x1d18f2, _0x5612de(957), { + value: !0 + }); + }); +} + +// function get_signature(room_id, user_unique_id) { +// return window.byted_acrawler.frontierSign({ +// "X-MS-STUB": md5(`live_id=1,aid=6383,version_code=180800,webcast_sdk_version=1.0.14-beta.0,room_id=${room_id},sub_room_id=,sub_channel_id=,did_rule=3,user_unique_id=${user_unique_id},device_platform=web,device_type=,ac=,identity=audience`) +// }) +// } +// console.log(get_signature("7382517534467115826","7382524529011246630")) + +function get_signature(x_ms_stub) { + return window.byted_acrawler.frontierSign(x_ms_stub) +} diff --git a/f2/apps/douyin/algorithm/webcast_signature.py b/f2/apps/douyin/algorithm/webcast_signature.py new file mode 100644 index 00000000..e573176e --- /dev/null +++ b/f2/apps/douyin/algorithm/webcast_signature.py @@ -0,0 +1,64 @@ +# path: f2/apps/douyin/algorithm/webcast_signature.py + +import execjs +import hashlib +from pathlib import Path +from f2.utils.utils import get_resource_path + + +class DouyinWebcastSignature: + def __init__(self, user_agent: str = None): + self.user_agent = ( + user_agent + if user_agent is not None and user_agent != "" + else "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0" + ) # 自定义 ua,为空则设置一个默认 ua + + def get_signature(self, room_id: str, user_unique_id: str) -> str: + """ + 获取直播间签名 + + Args: + room_id: (str) 直播间 ID + user_unique_id: (str) 用户唯一 ID + + Returns: + signature: (str) 签名 + """ + # 使用 importlib_resources 读取库中的 js 文件 + js_path = get_resource_path("apps/douyin/algorithm/webcast_signature.js") + # 读取 js 文件,确保使用 utf-8 编码 + js_code = Path(js_path).read_text() + + # 在 js_code 中动态设置 user_agent + js_code = f""" + _navigator = {{ + userAgent: "{self.user_agent}" + }}; + {js_code} + """ + + # 创建 execjs 运行环境 + ctx = execjs.compile(js_code) + + # 构造待 signature 的字符串 + raw_string = f"live_id=1,aid=6383,version_code=180800,webcast_sdk_version=1.0.12,room_id={room_id},sub_room_id=,sub_channel_id=,did_rule=3,user_unique_id={user_unique_id},device_platform=web,device_type=,ac=,identity=audience" + + # md5 计算 X-MS-STUB + x_ms_stub = {"X-MS-STUB": hashlib.md5(raw_string.encode("utf-8")).hexdigest()} + + # 调用 js 函数计算 signature + result = ctx.call("get_signature", x_ms_stub) + + # 加密参数的 key 为 X-Bogus + return result.get("X-Bogus") + + +if __name__ == "__main__": + signature_handler = DouyinWebcastSignature( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0" + ) + signature = signature_handler.get_signature( + "7382517534467115826", "7382524529011246630" + ) + print(signature) diff --git a/f2/apps/douyin/api.py b/f2/apps/douyin/api.py index b8a30f18..12df7313 100644 --- a/f2/apps/douyin/api.py +++ b/f2/apps/douyin/api.py @@ -22,7 +22,10 @@ class DouyinAPIEndpoints: SSO_DOMAIN = "https://sso.douyin.com" # WSS域名 (WSS Domain) - WEBCAST_WSS_DOMAIN = "wss://webcast5-ws-web-lf.douyin.com" + WEBCAST_WSS_DOMAIN = "wss://webcast5-ws-web-hl.douyin.com" + + # 直播弹幕(WSS) (Live Danmaku WSS) + LIVE_IM_WSS = f"{WEBCAST_WSS_DOMAIN}/webcast/im/push/v2/" # 首页Feed (Home Feed) TAB_FEED = f"{DOUYIN_DOMAIN}/aweme/v1/web/tab/feed/" @@ -99,6 +102,9 @@ class DouyinAPIEndpoints: # 直播用户信息 (Live User Info) LIVE_USER_INFO = f"{LIVE_DOMAIN}/webcast/user/me/" + # 直播弹幕初始化 (Live Danmaku Init) + LIVE_IM_FETCH = f"{LIVE_DOMAIN}/webcast/im/fetch/" + # 推荐搜索词 (Suggest Words) SUGGEST_WORDS = f"{DOUYIN_DOMAIN}/aweme/v1/web/api/suggest_words/" @@ -128,3 +134,6 @@ class DouyinAPIEndpoints: # 点赞评论 (Like Comment) POST_COMMENT_DIGG = f"{DOUYIN_DOMAIN}/aweme/v1/web/comment/digg" + + # 查询用户 (Query User) + QUERY_USER = f"{DOUYIN_DOMAIN}/aweme/v1/web/query/user/" diff --git a/f2/apps/douyin/cli.py b/f2/apps/douyin/cli.py index 688b71a1..4fc6bc87 100644 --- a/f2/apps/douyin/cli.py +++ b/f2/apps/douyin/cli.py @@ -3,7 +3,8 @@ import f2 import click import typing -import asyncio + +# import asyncio from pathlib import Path @@ -19,7 +20,9 @@ ) from f2.utils.conf_manager import ConfigManager from f2.i18n.translator import TranslationManager, _ -from f2.apps.douyin.handler import handle_sso_login + +# from f2.apps.douyin.handler import handle_sso_login +from f2.apps.douyin.utils import ClientConfManager def handler_help( @@ -80,6 +83,8 @@ def handler_auto_cookie( except Exception as e: logger.error(_("自动获取Cookie失败:{0}").format(str(e))) ctx.abort() + finally: + ctx.exit(0) def handler_language( @@ -142,39 +147,39 @@ def handler_naming( return value -def handler_sso_login( - ctx: click.Context, - param: typing.Union[click.Option, click.Parameter], - value: typing.Any, -) -> None: - """处理SSO登录 (Handle SSO login) +# def handler_sso_login( +# ctx: click.Context, +# param: typing.Union[click.Option, click.Parameter], +# value: typing.Any, +# ) -> None: +# """处理SSO登录 (Handle SSO login) - Args: - ctx (click.Context): click的上下文对象 (Click's context object) - param (typing.Union[click.Option, click.Parameter]): 提供的参数或选项 (The provided parameter or option) - value (typing.Any): 参数或选项的值 (The value of the parameter or option) +# Args: +# ctx (click.Context): click的上下文对象 (Click's context object) +# param (typing.Union[click.Option, click.Parameter]): 提供的参数或选项 (The provided parameter or option) +# value (typing.Any): 参数或选项的值 (The value of the parameter or option) - Raises: - click.UsageError: 如果SSO登录失败 (If SSO login failed) +# Raises: +# click.UsageError: 如果SSO登录失败 (If SSO login failed) - Returns: - 更新配置文件 (Update the configuration file) - """ - if not value or ctx.resilient_parsing: - return +# Returns: +# 更新配置文件 (Update the configuration file) +# """ +# if not value or ctx.resilient_parsing: +# return - if ctx.params.get("cookie"): - return +# if ctx.params.get("cookie"): +# return - is_login, login_cookie = asyncio.run(handle_sso_login()) +# is_login, login_cookie = asyncio.run(handle_sso_login()) - if is_login: - manager = ConfigManager( - ctx.params.get("config", get_resource_path(f2.APP_CONFIG_FILE_PATH)) - ) - manager.update_config_with_args("douyin", cookie=login_cookie) - else: - raise click.UsageError(_("SSO登录失败,请重试!")) +# if is_login: +# manager = ConfigManager( +# ctx.params.get("config", get_resource_path(f2.APP_CONFIG_FILE_PATH)) +# ) +# manager.update_config_with_args("douyin", cookie=login_cookie) +# else: +# raise click.UsageError(_("SSO登录失败,请重试!")) @click.command(name="douyin", help=_("抖音无水印解析")) @@ -190,7 +195,7 @@ def handler_sso_login( type=str, # default="", help=_( - "根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合辑与直播同上" + "根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合集与直播同上" ), ) @click.option( @@ -235,7 +240,7 @@ def handler_sso_login( # default="post", # required=True, help=_( - "下载模式:单个作品(one),主页作品(post),点赞作品(like),收藏作品(collection),收藏夹作品(collects),收藏音乐(music),合辑(mix),直播(live)" + "下载模式:单个作品(one),主页作品(post),点赞作品(like),收藏作品(collection),收藏夹作品(collects),收藏音乐(music),合集(mix),直播(live)" ), ) @click.option( @@ -300,7 +305,7 @@ def handler_sso_login( "-s", type=int, # default=20, - help=_("从接口每页可获取作品数,不建议超过20"), + help=_("从接口每页可获取作品数,不建议超过 20"), ) @click.option( "--languages", @@ -316,7 +321,7 @@ def handler_sso_login( type=str, nargs=2, help=_( - "代理服务器,最多 2 个参数,http与https。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" + "代理服务器,最多 2 个参数,http://与https://。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" ), ) @click.option("--lyric", "-L", type=bool, help=_("是否保存原声歌词")) @@ -336,12 +341,12 @@ def handler_sso_login( help=_("自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器"), callback=handler_auto_cookie, ) -@click.option( - "--sso-login", - is_flag=True, - help=_("使用SSO扫码登录获取cookie,保存低频主配置文件"), - callback=handler_sso_login, -) +# @click.option( +# "--sso-login", +# is_flag=True, +# help=_("使用SSO扫码登录获取cookie,保存低频主配置文件(暂时弃用)"), +# callback=handler_sso_login, +# ) @click.option( "-h", is_flag=True, @@ -375,27 +380,18 @@ def douyin( main_conf_path = get_resource_path(f2.APP_CONFIG_FILE_PATH) main_conf = main_manager.get_config("douyin") - # 读取f2低频配置文件 - f2_manager = ConfigManager(f2.F2_CONFIG_FILE_PATH) - - f2_conf = f2_manager.get_config("f2").get("douyin") - f2_proxies = f2_conf.get("proxies") - # 更新主配置文件中的代理参数 - main_conf["proxies"] = { - "http": f2_proxies.get("http"), - "https": f2_proxies.get("https"), - } + main_conf["proxies"] = ClientConfManager.proxies() # 更新主配置文件中的headers参数 kwargs.setdefault("headers", {}) - kwargs["headers"]["User-Agent"] = f2_conf["headers"].get("User-Agent", "") - kwargs["headers"]["Referer"] = f2_conf["headers"].get("Referer", "") + kwargs["headers"]["User-Agent"] = ClientConfManager.user_agent() + kwargs["headers"]["Referer"] = ClientConfManager.referer() # 如果初始化配置文件,则与更新配置文件互斥 if init_config and not update_config: main_manager.generate_config("douyin", init_config) - # return + return elif init_config: raise click.UsageError(_("不能同时初始化和更新配置文件")) # 如果没有初始化配置文件,但是更新配置文件,则需要提供配置文件路径 @@ -416,17 +412,19 @@ def douyin( if update_config: # 如果指定了 update_config,更新配置文件 update_manger = ConfigManager(config) update_manger.update_config_with_args("douyin", **kwargs) + return # 将kwargs["proxies"]中的tuple转换为dict if kwargs.get("proxies"): kwargs["proxies"] = { - "http": kwargs["proxies"][0], - "https": kwargs["proxies"][1], + "http://": kwargs["proxies"][0], + "https://": kwargs["proxies"][1], } # 从低频配置开始到高频配置再到cli参数,逐级覆盖,如果键值不存在使用父级的键值 kwargs = merge_config(main_conf, custom_conf, **kwargs) + logger.info(_("模式:{0}").format(kwargs.get("mode"))) logger.info(_("主配置路径:{0}").format(main_conf_path)) logger.info(_("自定义配置路径:{0}").format(Path.cwd() / config)) logger.debug(_("主配置参数:{0}").format(main_conf)) @@ -435,7 +433,7 @@ def douyin( # 尝试从命令行参数或kwargs中获取URL if not kwargs.get("url"): - logger.error("缺乏URL参数,详情看命令帮助") + logger.error(_("缺乏URL参数,详情看命令帮助")) handler_help(ctx, None, True) # 添加app_name到kwargs diff --git a/f2/apps/douyin/crawler.py b/f2/apps/douyin/crawler.py index da37917e..ca7c641b 100644 --- a/f2/apps/douyin/crawler.py +++ b/f2/apps/douyin/crawler.py @@ -1,11 +1,14 @@ # path: f2/apps/douyin/crawler.py -import f2 +import gzip +import traceback + +from google.protobuf import json_format from f2.log.logger import logger from f2.i18n.translator import _ -from f2.utils.conf_manager import ConfigManager -from f2.crawlers.base_crawler import BaseCrawler +from f2.crawlers.base_crawler import BaseCrawler, WebSocketCrawler +from f2.utils.utils import BaseEndpointManager from f2.apps.douyin.api import DouyinAPIEndpoints as dyendpoint from f2.apps.douyin.model import ( UserProfile, @@ -19,13 +22,36 @@ UserMix, UserLive, UserLive2, - FollowUserLive, + FollowingUserLive, LoginGetQr, LoginCheckQr, UserFollowing, UserFollower, + LiveWebcast, + LiveImFetch, + QueryUser, +) +from f2.apps.douyin.utils import ( + XBogusManager, + ABogusManager, + ClientConfManager, + TokenManager, +) +from f2.apps.douyin.proto.douyin_webcast_pb2 import ( + PushFrame, + Response, + RoomMessage, + LikeMessage, + MemberMessage, + ChatMessage, + GiftMessage, + SocialMessage, + RoomUserSeqMessage, + UpdateFanTicketMessage, + CommonTextMessage, + MatchAgainstScoreMessage, + FansClubMessage, ) -from f2.apps.douyin.utils import XBogusManager class DouyinCrawler(BaseCrawler): @@ -33,153 +59,146 @@ def __init__( self, kwargs: dict = ..., ): - f2_manager = ConfigManager(f2.F2_CONFIG_FILE_PATH) - f2_conf = f2_manager.get_config("f2").get("douyin") - proxies_conf = kwargs.get("proxies", {"http": None, "https": None}) - proxies = { - "http://": proxies_conf.get("http", None), - "https://": proxies_conf.get("https", None), - } - - self.headers = { - "User-Agent": f2_conf["headers"]["User-Agent"], - "Referer": f2_conf["headers"]["Referer"], - "Cookie": kwargs["cookie"], - } - + # 需要与cli同步 + proxies = kwargs.get("proxies", {"http://": None, "https://": None}) + self.headers = kwargs.get("headers", {}) | {"Cookie": kwargs["cookie"]} + if ClientConfManager.encryption() == "ab": + self.bogus_manager = ABogusManager + else: + self.bogus_manager = XBogusManager super().__init__(proxies=proxies, crawler_headers=self.headers) async def fetch_user_profile(self, params: UserProfile): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.USER_DETAIL, - params.dict(), + params.model_dump(), ) logger.debug(_("用户信息接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_user_post(self, params: UserPost): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.USER_POST, - params.dict(), + params.model_dump(), ) logger.debug(_("主页作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_user_like(self, params: UserLike): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.USER_FAVORITE_A, - params.dict(), + params.model_dump(), ) logger.debug(_("主页喜欢作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_user_collection(self, params: UserCollection): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.USER_COLLECTION, - params.dict(), + params.model_dump(), ) logger.debug(_("主页收藏作品接口地址:{0}").format(endpoint)) - return await self._fetch_post_json(endpoint, params.dict()) + return await self._fetch_post_json(endpoint, params.model_dump()) async def fetch_user_collects(self, params: UserCollects): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.USER_COLLECTS, - params.dict(), + params.model_dump(), ) logger.debug(_("收藏夹接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_user_collects_video(self, params: UserCollectsVideo): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.USER_COLLECTS_VIDEO, - params.dict(), + params.model_dump(), ) logger.debug(_("收藏夹作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_user_music_collection(self, params: UserMusicCollection): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.USER_MUSIC_COLLECTION, - params.dict(), + params.model_dump(), ) logger.debug(_("音乐收藏接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_user_mix(self, params: UserMix): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.MIX_AWEME, - params.dict(), + params.model_dump(), ) logger.debug(_("合集作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_post_detail(self, params: PostDetail): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.POST_DETAIL, - params.dict(), + params.model_dump(), ) logger.debug(_("作品详情接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_post_comment(self, params: PostDetail): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.POST_COMMENT, - params.dict(), + params.model_dump(), ) logger.debug(_("作品评论接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_post_feed(self, params: PostDetail): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.TAB_FEED, - params.dict(), + params.model_dump(), ) logger.debug(_("首页推荐作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_follow_feed(self, params: PostDetail): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.FOLLOW_FEED, - params.dict(), + params.model_dump(), ) logger.debug(_("关注作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_friend_feed(self, params: PostDetail): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.FRIEND_FEED, - params.dict(), + params.model_dump(), ) logger.debug(_("朋友作品接口地址:{0}").format(endpoint)) - return await self._fetch_get_json(endpoint) + return await self._fetch_post_json(endpoint) async def fetch_post_related(self, params: PostDetail): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.POST_RELATED, - params.dict(), + params.model_dump(), ) logger.debug(_("相关推荐作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_live(self, params: UserLive): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.LIVE_INFO, - params.dict(), + params.model_dump(), ) logger.debug(_("直播接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) @@ -189,81 +208,359 @@ async def fetch_live_room_id(self, params: UserLive2): try: # 避免invalid session self.aclient.headers.update({"Cookie": ""}) - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.LIVE_INFO_ROOM_ID, - params.dict(), + params.model_dump(), ) logger.debug(_("直播接口地址(room_id):{0}").format(endpoint)) return await self._fetch_get_json(endpoint) finally: self.aclient.headers = original_headers - async def fetch_follow_live(self, params: FollowUserLive): - endpoint = XBogusManager.model_2_endpoint( + async def fetch_following_live(self, params: FollowingUserLive): + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.FOLLOW_USER_LIVE, - params.dict(), + params.model_dump(), ) logger.debug(_("关注用户直播接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_locate_post(self, params: UserPost): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.LOCATE_POST, - params.dict(), + params.model_dump(), ) logger.debug(_("定位上一次作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_login_qrcode(self, parms: LoginGetQr): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.SSO_LOGIN_GET_QR, - parms.dict(), + parms.model_dump(), ) logger.debug(_("SSO获取二维码接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_check_qrcode(self, parms: LoginCheckQr): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.SSO_LOGIN_CHECK_QR, - parms.dict(), + parms.model_dump(), ) logger.debug(_("SSO检查扫码状态接口地址:{0}").format(endpoint)) return await self._fetch_response(endpoint) async def fetch_check_login(self, parms: LoginCheckQr): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.SSO_LOGIN_CHECK_LOGIN, - parms.dict(), + parms.model_dump(), ) logger.debug(_("SSO检查登录状态接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_user_following(self, params: UserFollowing): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.USER_FOLLOWING, - params.dict(), + params.model_dump(), ) logger.debug(_("用户关注列表接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_user_follower(self, params: UserFollower): - endpoint = XBogusManager.model_2_endpoint( + endpoint = self.bogus_manager.model_2_endpoint( self.headers.get("User-Agent"), dyendpoint.USER_FOLLOWER, - params.dict(), + params.model_dump(), ) logger.debug(_("用户粉丝列表接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) + async def fetch_live_im_fetch(self, params: LiveImFetch): + endpoint = self.bogus_manager.model_2_endpoint( + self.headers.get("User-Agent"), + dyendpoint.LIVE_IM_FETCH, + params.model_dump(), + ) + logger.debug(_("直播弹幕初始化接口地址:{0}").format(endpoint)) + return await self._fetch_get_json(endpoint) + + async def fetch_query_user(self, params: QueryUser): + endpoint = self.bogus_manager.model_2_endpoint( + self.headers.get("User-Agent"), + dyendpoint.QUERY_USER, + params.model_dump(), + ) + logger.debug(_("查询用户接口地址:{0}").format(endpoint)) + return await self._fetch_get_json(endpoint) + async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() + + +class DouyinWebSocketCrawler(WebSocketCrawler): + def __init__(self, kwargs: dict = ..., callbacks: dict = None): + # 需要与cli同步 + self.headers = kwargs.get("headers", {}) | { + "Cookie": f"ttwid={TokenManager.gen_ttwid()};" + } + self.callbacks = callbacks or {} + self.timeout = kwargs.get("timeout", 10) + super().__init__( + wss_headers=self.headers, callbacks=self.callbacks, timeout=self.timeout + ) + + async def fetch_live_danmaku(self, params: LiveWebcast): + endpoint = BaseEndpointManager.model_2_endpoint( + dyendpoint.LIVE_IM_WSS, + params.model_dump(), + ) + logger.debug(_("直播弹幕接口地址:{0}").format(endpoint)) + await self.connect_websocket(endpoint) + return await self.receive_messages() + + async def handle_wss_message(self, message: bytes): + """ + 处理 WebSocket 消息 + + Args: + message (bytes): WebSocket 消息的字节数据 + """ + try: + wss_package = PushFrame() + wss_package.ParseFromString(message) + log_id = wss_package.logId + decompressed = gzip.decompress(wss_package.payload) + payload_package = Response() + payload_package.ParseFromString(decompressed) + + # 发送 ack 包 + if payload_package.needAck: + await self.send_ack(log_id, payload_package.internalExt) + + # 处理每个消息 + for msg in payload_package.messagesList: + method = msg.method + payload = msg.payload + + # 调用对应的回调函数处理消息 + if method in self.callbacks: + await self.callbacks[method](data=payload) + else: + logger.warning( + _("未找到对应的回调函数处理消息:{0}").format(method) + ) + + except Exception: + logger.error(traceback.format_exc()) + + async def send_ack(self, log_id: str, internal_ext: str): + """ + 发送 ack 包 + + Args: + log_id: 日志ID + internal_ext: 内部扩展信息 + """ + ack = PushFrame() + ack.logId = log_id + ack.payloadType = internal_ext + data = ack.SerializeToString() + logger.debug(_("[SendAck] [💓发送ack包]")) + await self.websocket.send(data) + + async def send_ping(self): + """ + 发送 ping 包 + """ + ping = PushFrame() + ping.payloadType = "hb" + data = ping.SerializeToString() + logger.debug(_("[SendPing] [📤发送ping包]")) + await self.websocket.ping(data) + + async def on_message(self, message): + await self.handle_wss_message(message) + + async def on_error(self, message): + return await super().on_error(message) + + async def on_close(self, message): + return await super().on_close(message) + + async def on_open(self): + return await super().on_open() + + # 定义所有的回调消息函数 + @classmethod + async def WebcastRoomMessage(self, data: bytes): + roomMessage = RoomMessage() + roomMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + roomMessage, preserving_proto_field_name=True + ) + logger.info( + _("[WebcastRoomMessage] [🏠房间消息] | {0}").format(data_dict.get("room")) + ) + return data_dict + + @classmethod + async def WebcastLikeMessage(self, data: bytes): + likeMessage = LikeMessage() + likeMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + likeMessage, preserving_proto_field_name=True + ) + logger.info( + "[WebcastLikeMessage] [👍点赞消息] | " + + "[用户Id:{0}] [当前用户点赞:{1}] [总点赞:{2}]".format( + data_dict.get("user").get("id"), + data_dict.get("count"), + data_dict.get("total"), + ) + ) + return data_dict + + @classmethod + async def WebcastMemberMessage(self, data: bytes): + memberMessage = MemberMessage() + memberMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + memberMessage, preserving_proto_field_name=True + ) + logger.info( + f"[WebcastMemberMessage] [🚺观众加入消息] | [用户Id:{data_dict.get('user').get('id')} 用户名:{data_dict.get('user').get('nickname')}]" + ) + return data_dict + + @classmethod + async def WebcastChatMessage(self, data: bytes): + chatMessage = ChatMessage() + chatMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + chatMessage, preserving_proto_field_name=True + ) + logger.info( + _("[WebcastChatMessage] [💬聊天消息] | {0}").format( + data_dict.get("content") + ) + ) + return data + + @classmethod + async def WebcastGiftMessage(self, data: bytes): + giftMessage = GiftMessage() + giftMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + giftMessage, preserving_proto_field_name=True + ) + logger.info( + _("[WebcastGiftMessage] [🎁礼物消息] | [{0}]").format( + data_dict.get("common").get("describe") + ) + ) + return data_dict + + @classmethod + async def WebcastSocialMessage(self, data: bytes): + socialMessage = SocialMessage() + socialMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + socialMessage, preserving_proto_field_name=True + ) + logger.info( + _("[WebcastSocialMessage] [➕用户关注消息] | [{0}]").format( + data_dict.get("user").get("id") + ) + ) + return data_dict + + @classmethod + async def WebcastRoomUserSeqMessage(self, data: bytes): + roomUserSeqMessage = RoomUserSeqMessage() + roomUserSeqMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + roomUserSeqMessage, preserving_proto_field_name=True + ) + + logger.info( + _("[WebcastRoomUserSeqMessage] [👥在线观众排行榜] | [{0} {1} {2}]").format( + data_dict.get("ranksList")[0].get("user").get("id"), + data_dict.get("ranksList")[1].get("user").get("id"), + data_dict.get("ranksList")[2].get("user").get("id"), + ) + ) + return data_dict + + @classmethod + async def WebcastUpdateFanTicketMessage(self, data: bytes): + updateFanTicketMessage = UpdateFanTicketMessage() + updateFanTicketMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + updateFanTicketMessage, preserving_proto_field_name=True + ) + + logger.info( + _("[WebcastUpdateFanTicketMessage] [🎟️粉丝票更新消息] | [{0}]").format( + data_dict.get("roomFanTicketCount") + ) + ) + return data_dict + + @classmethod + async def WebcastCommonTextMessage(self, data: bytes): + commonTextMessage = CommonTextMessage() + commonTextMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + commonTextMessage, preserving_proto_field_name=True + ) + + logger.info( + _("[WebcastCommonTextMessage] [📝文本消息] | [{0}]").format(data_dict) + ) + return data_dict + + @classmethod + async def WebcastMatchAgainstScoreMessage(self, data: bytes): + matchAgainstScoreMessage = MatchAgainstScoreMessage() + matchAgainstScoreMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + matchAgainstScoreMessage, preserving_proto_field_name=True + ) + + logger.info( + _("[WebcastMatchAgainstScoreMessage] [🏆对战积分消息] | [{0}]").format( + data_dict + ) + ) + return data_dict + + @classmethod + async def WebcastFansclubMessage(self, data: bytes): + fansClubMessage = FansClubMessage() + fansClubMessage.ParseFromString(data) + data_dict = json_format.MessageToDict( + fansClubMessage, preserving_proto_field_name=True + ) + + logger.info( + _("[WebcastFansclubMessage] [🎉粉丝团消息] | [{0}]").format( + data_dict.get("content") + ) + ) + return data_dict + + async def __aenter__(self): + await super().__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await super().__aexit__(exc_type, exc_val, exc_tb) diff --git a/f2/apps/douyin/dl.py b/f2/apps/douyin/dl.py index e2224dd4..ee88879c 100644 --- a/f2/apps/douyin/dl.py +++ b/f2/apps/douyin/dl.py @@ -237,7 +237,7 @@ async def handler_download( ) # 处理不同类型的作品下载任务 - if aweme_type in [0, 55, 61, 109]: + if aweme_type in [0, 55, 61, 109, 201]: video_name = ( format_file_name( kwargs.get("naming", "{create}_{desc}"), aweme_data_dict @@ -404,5 +404,5 @@ async def handler_stream( webcast_url = webcast_data_dict.get("m3u8_pull_url").get("FULL_HD1") await self.initiate_m3u8_download( - _("直播"), webcast_url, base_path, webcast_name, ".mp4" + _("直播"), webcast_url, base_path, webcast_name, ".flv" ) diff --git a/f2/apps/douyin/filter.py b/f2/apps/douyin/filter.py index 1465ee3c..6b109267 100644 --- a/f2/apps/douyin/filter.py +++ b/f2/apps/douyin/filter.py @@ -131,6 +131,11 @@ def _to_dict(self) -> dict: class UserPostFilter(JSONModel): + + @property + def status_code(self): + return self._get_attr_value("$.status_code") + @property def has_aweme(self) -> bool: return bool( @@ -234,24 +239,25 @@ def cover(self): @property def video_play_addr(self): - return self._get_list_attr_value("$.aweme_list[*].video.play_addr.url_list[0]") + return self._get_list_attr_value("$.aweme_list[*].video.play_addr.url_list") @property def video_bit_rate(self): bit_rate_data = self._get_list_attr_value("$.aweme_list[*].video.bit_rate") - return [ - ( - [aweme["bit_rate"]] - if isinstance(aweme, dict) - else ( - [aweme[0]["bit_rate"]] - if len(aweme) == 1 - else [item["bit_rate"] for item in aweme] - ) - ) - for aweme in bit_rate_data - ] + def extract_bit_rate(aweme): + if not aweme: + return [] + + if isinstance(aweme, dict): + return [aweme.get("bit_rate", 0)] + + if isinstance(aweme, list): + return [item.get("bit_rate", 0) for item in aweme] + + return [] + + return [extract_bit_rate(aweme) for aweme in bit_rate_data] @property def video_duration(self): @@ -317,6 +323,7 @@ def _to_dict(self) -> dict: def _to_list(self): exclude_list = [ + "status_code", "has_more", "max_cursor", "min_cursor", @@ -337,6 +344,7 @@ def _to_list(self): list_dicts = [] for entry in aweme_entries: d = { + "status_code": self.status_code, "has_more": self.has_more, "max_cursor": self.max_cursor, "min_cursor": self.min_cursor, @@ -369,7 +377,7 @@ def status_code(self): return self._get_attr_value("$.status_code") @property - def total_number(self): + def collects_total_number(self): return self._get_attr_value("$.total_number") @property @@ -1381,22 +1389,21 @@ def cover(self): @property def video_bit_rate(self): - bit_rate_data = self._get_list_attr_value( - "$.aweme_detail.video.bit_rate", - ) + bit_rate_data = self._get_list_attr_value("$.aweme_detail.video.bit_rate") - return [ - ( - [aweme["bit_rate"]] - if isinstance(aweme, dict) - else ( - [aweme[0]["bit_rate"]] - if len(aweme) == 1 - else [item["bit_rate"] for item in aweme] - ) - ) - for aweme in bit_rate_data - ] + def extract_bit_rate(aweme): + if not aweme: + return [] + + if isinstance(aweme, dict): + return [aweme.get("bit_rate", 0)] + + if isinstance(aweme, list): + return [item.get("bit_rate", 0) for item in aweme] + + return [] + + return [extract_bit_rate(aweme) for aweme in bit_rate_data] @property def video_play_addr(self): @@ -1658,6 +1665,289 @@ def _to_dict(self) -> dict: } +class PostRelatedFilter(UserPostFilter): + def __init__(self, data): + super().__init__(data) + + +class FriendFeedFilter(JSONModel): + # 8 login_expired + @property + def status_code(self): + return self._get_attr_value("$.status_code") + + @property + def status_msg(self): + return self._get_attr_value("$.status_msg") + + @property + def toast(self): + return self._get_attr_value("$.toast") + + @property + def has_more(self): + return bool(self._get_attr_value("$.has_more")) + + @property + def has_aweme(self): + return bool(self._get_attr_value("$.data")) + + @property + def friend_update_count(self): + return self._get_attr_value("$.friend_update_count") + + @property + def cursor(self): + return self._get_attr_value("$.cursor") + + @property + def level(self): + return self._get_attr_value("$.level") + + @property + def friend_feed_type(self): + return self._get_list_attr_value("$.data[*].feed_type") + + @property + def friend_feed_source(self): + return self._get_list_attr_value("$.data[*].source") + + # user + @property + def avatar_larger(self): + return self._get_list_attr_value( + "$.data[*].aweme.author.avatar_larger.url_list[0]" + ) + + @property + def nickname(self): + return replaceT(self._get_list_attr_value("$.data[*].aweme.author.nickname")) + + @property + def nickname_raw(self): + return self._get_list_attr_value("$.data[*].aweme.author.nickname") + + @property + def sec_uid(self): + return self._get_list_attr_value("$.data[*].aweme.author.sec_uid") + + @property + def uid(self): + return self._get_list_attr_value("$.data[*].aweme.author.uid") + + # aweme + @property + def aweme_id(self): + return self._get_list_attr_value("$.data[*].aweme.aweme_id") + + @property + def aweme_type(self): + return self._get_list_attr_value("$.data[*].aweme.aweme_type") + + @property + def desc(self): + return replaceT(self._get_list_attr_value("$.data[*].aweme.desc")) + + @property + def desc_raw(self): + return self._get_list_attr_value("$.data[*].aweme.desc") + + @property + def recommend_reason(self): + return self._get_list_attr_value( + "$.data[*].aweme.fall_card_struct.recommend_reason" + ) + + @property + def create_time(self): + create_times = self._get_list_attr_value("$.data[*].aweme.create_time") + return ( + [timestamp_2_str(str(ct)) for ct in create_times] + if isinstance(create_times, list) + else timestamp_2_str(str(create_times)) + ) + + @property + def is_24_story(self): # 是否是24小时动态 + return self._get_list_attr_value("$.data[*].aweme.is_24_story") + + @property + def media_type(self): + return self._get_list_attr_value("$.data[*].aweme.media_type") + + @property + def collect_count(self): + return self._get_list_attr_value("$.data[*].aweme.statistics.collect_count") + + @property + def comment_count(self): + return self._get_list_attr_value("$.data[*].aweme.statistics.comment_count") + + @property + def digg_count(self): + return self._get_list_attr_value("$.data[*].aweme.statistics.digg_count") + + @property + def exposure_count(self): + return self._get_list_attr_value("$.data[*].aweme.statistics.exposure_count") + + @property + def live_watch_count(self): + return self._get_list_attr_value("$.data[*].aweme.statistics.live_watch_count") + + @property + def play_count(self): + return self._get_list_attr_value("$.data[*].aweme.statistics.play_count") + + @property + def share_count(self): + return self._get_list_attr_value("$.data[*].aweme.statistics.share_count") + + @property + def allow_share(self): + return self._get_list_attr_value("$.data[*].aweme.status.allow_share") + + @property + def private_status(self): + return self._get_list_attr_value("$.data[*].aweme.status.private_status") + + @property + def is_prohibited(self): + return self._get_list_attr_value("$.data[*].aweme.status.is_prohibited") + + @property + def part_see(self): + return self._get_list_attr_value("$.data[*].aweme.status.part_see") + + # video + @property + def animated_cover(self): + # 获取所有视频 + videos = self._get_list_attr_value("$.data[*].aweme.video") + + # 逐个视频判断是否存在animated_cover + animated_covers = [ + ( + video.get("animated_cover", {}).get("url_list", [None])[0] + if video.get("animated_cover") + else None + ) + for video in videos + ] + + return animated_covers + + @property + def cover(self): + return self._get_list_attr_value("$.data[*].aweme.video.cover.url_list[0]") + + @property + def images(self): + images_list = self._get_list_attr_value("$.data[*].aweme.images") + return [ + ( + [ + img["url_list"][0] + for img in images + if isinstance(img, dict) and "url_list" in img and img["url_list"] + ] + if images + else None + ) + for images in images_list + ] + + @property + def video_play_addr(self): + return self._get_list_attr_value("$.data[*].aweme.video.play_addr.url_list") + + # music + @property + def music_id(self): + return self._get_list_attr_value("$.data[*].aweme.music.id") + + @property + def music_mid(self): + return self._get_list_attr_value("$.data[*].aweme.music.mid") + + @property + def music_duration(self): + return self._get_list_attr_value("$.data[*].aweme.music.duration") + + @property + def music_play_url(self): + return self._get_list_attr_value("$.data[*].aweme.music.play_url.url_list[0]") + + @property + def music_owner_nickname(self): + return replaceT( + self._get_list_attr_value("$.data[*].aweme.music.owner_nickname") + ) + + @property + def music_owner_nickname_raw(self): + return self._get_list_attr_value("$.data[*].aweme.music.owner_nickname") + + @property + def music_sec_uid(self): + return self._get_list_attr_value("$.data[*].aweme.music.sec_uid") + + @property + def music_title(self): + return replaceT(self._get_list_attr_value("$.data[*].aweme.music.title")) + + @property + def music_title_raw(self): + return self._get_list_attr_value("$.data[*].aweme.music.title") + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + def _to_list(self): + exclude_list = [ + "status_code", + "status_msg", + "has_more", + "has_aweme", + "friend_update_count", + "cursor", + "level", + ] + + keys = [ + prop_name + for prop_name in dir(self) + if not prop_name.startswith("__") + and not prop_name.startswith("_") + and prop_name not in exclude_list + ] + + friend_feed_entries = self._get_attr_value("$.data") or [] + + list_dicts = [] + for entry in friend_feed_entries: + d = { + "has_more": self.has_more, + "has_aweme": self.has_aweme, + "friend_update_count": self.friend_update_count, + "cursor": self.cursor, + "level": self.level, + } + for key in keys: + attr_values = getattr(self, key) + index = friend_feed_entries.index(entry) + d[key] = attr_values[index] if index < len(attr_values) else None + list_dicts.append(d) + return list_dicts + + class GetQrcodeFilter(JSONModel): @property def app_name(self): @@ -1764,3 +2054,247 @@ def _to_dict(self) -> dict: for prop_name in dir(self) if not prop_name.startswith("__") and not prop_name.startswith("_") } + + +class LiveImFetchFilter(JSONModel): + @property + def status_code(self): + return self._get_attr_value("$.status_code") + + @property + def is_show_msg(self): + return self._get_attr_value("$.data[0].common.is_show_msg") + + @property + def msg_id(self): + return self._get_attr_value("$.data[0].common.msg_id") + + @property + def room_id(self): + return self._get_attr_value("$.data[0].common.room_id") + + @property + def internal_ext(self): + return self._get_attr_value("$.internal_ext") + + @property + def cursor(self): + return self._get_attr_value("$.extra.cursor") + + @property + def now(self): + return timestamp_2_str(str(self._get_attr_value("$.extra.now"))) + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + +class QueryUserFilter(JSONModel): + @property + def status_code(self): + return self._get_attr_value("$.status_code") + + @property + def status_msg(self): + return self._get_attr_value("$.status_msg") + + @property + def browser_name(self): + return self._get_attr_value("$.browser_name") + + @property + def create_time(self): + return timestamp_2_str(str(self._get_attr_value("$.create_time"))) + + @property + def firebase_instance_id(self): + return self._get_attr_value("$.firebase_instance_id") + + @property + def user_unique_id(self): + return self._get_attr_value("$.id") + + @property + def last_time(self): + return timestamp_2_str(str(self._get_attr_value("$.last_time"))) + + @property + def user_agent(self): + return self._get_attr_value("$.user_agent") + + @property + def user_uid(self): + return self._get_attr_value("$.user_uid") + + @property + def user_uid_type(self): + return self._get_attr_value("$.user_uid_type") + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + +class FollowingUserLiveFilter(JSONModel): + @property + def status_code(self): + return self._get_attr_value("$.status_code") + + @property + def status_msg(self): + return self._get_attr_value("$.data.message") + + @property + def cover_type(self): + return self._get_list_attr_value("$.data.data.[*].cover_type") + + @property + def is_recommend(self): + return self._get_list_attr_value("$.data.data.[*].is_recommend") + + @property + def tag_name(self): + return self._get_list_attr_value("$.data.data.[*].tag_name") + + @property + def title_type(self): + return self._get_list_attr_value("$.data.data.[*].title_type") + + @property + def uniq_id(self): + return self._get_list_attr_value("$.data.data.[*].uniq_id") + + @property + def web_rid(self): + return self._get_list_attr_value("$.data.data.[*].web_rid") + + @property + def cover(self): + return self._get_list_attr_value("$.data.data.[*].room.cover.url_list[0]") + + @property + def has_commerce_goods(self): + return self._get_list_attr_value("$.data.data.[*].room.has_commerce_goods") + + @property + def room_id(self): + return self._get_list_attr_value("$.data.data.[*].room.id_str") + + @property + def live_title(self): + return replaceT(self._get_list_attr_value("$.data.data.[*].room.title")) + + @property + def live_title_raw(self): + return self._get_list_attr_value("$.data.data.[*].room.title") + + @property + def live_room_mode(self): + return self._get_list_attr_value("$.data.data.[*].room.live_room_mode") + + @property + def mosaic_status(self): + return self._get_list_attr_value("$.data.data.[*].room.mosaic_status") + + @property + def user_count(self): + return self._get_list_attr_value("$.data.data.[*].room.stats.user_count_str") + + @property + def like_count(self): + return self._get_list_attr_value("$.data.data.[*].room.stats.like_count") + + @property + def total_count(self): + return self._get_list_attr_value("$.data.data.[*].room.stats.total_user_str") + + # user + @property + def avatar_thumb(self): + return self._get_list_attr_value( + "$.data.data.[*].room.owner.avatar_thumb.url_list[0]" + ) + + @property + def user_id(self): + return self._get_list_attr_value("$.data.data.[*].room.owner.id_str") + + @property + def user_sec_uid(self): + return self._get_list_attr_value("$.data.data.[*].room.owner.sec_uid") + + @property + def nickname(self): + return replaceT( + self._get_list_attr_value("$.data.data.[*].room.owner.nickname") + ) + + @property + def nickname_raw(self): + return self._get_list_attr_value("$.data.data.[*].room.owner.nickname") + + # stream_url + @property + def flv_pull_url(self): + return self._get_list_attr_value("$.data.data.[*].room.stream_url.flv_pull_url") + + @property + def hls_pull_url(self): + return self._get_list_attr_value( + "$.data.data.[*].room.stream_url.hls_pull_url_map" + ) + + @property + def stream_orientation(self): + return self._get_list_attr_value( + "$.data.data.[*].room.stream_url.stream_orientation" + ) + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + def _to_list(self): + exclude_list = [ + "status_code", + "status_msg", + ] + + keys = [ + prop_name + for prop_name in dir(self) + if not prop_name.startswith("__") + and not prop_name.startswith("_") + and prop_name not in exclude_list + ] + + friend_feed_entries = self._get_attr_value("$.data.data") or [] + + list_dicts = [] + for entry in friend_feed_entries: + d = {} + for key in keys: + attr_values = getattr(self, key) + index = friend_feed_entries.index(entry) + d[key] = attr_values[index] if index < len(attr_values) else None + list_dicts.append(d) + return list_dicts diff --git a/f2/apps/douyin/handler.py b/f2/apps/douyin/handler.py index 90231e11..5b4efb15 100644 --- a/f2/apps/douyin/handler.py +++ b/f2/apps/douyin/handler.py @@ -6,10 +6,11 @@ from f2.log.logger import logger from f2.i18n.translator import _ -from f2.utils.mode_handler import mode_handler, mode_function_map -from f2.utils.utils import split_set_cookie +from f2.utils.decorators import mode_handler, mode_function_map + +# from f2.utils.utils import split_set_cookie from f2.apps.douyin.db import AsyncUserDB, AsyncVideoDB -from f2.apps.douyin.crawler import DouyinCrawler +from f2.apps.douyin.crawler import DouyinCrawler, DouyinWebSocketCrawler from f2.apps.douyin.dl import DouyinDownloader from f2.apps.douyin.model import ( UserPost, @@ -23,10 +24,16 @@ PostDetail, UserLive, UserLive2, - LoginGetQr, - LoginCheckQr, + # LoginGetQr, + # LoginCheckQr, UserFollowing, UserFollower, + PostRelated, + FriendFeed, + LiveWebcast, + LiveImFetch, + QueryUser, + FollowingUserLive, ) from f2.apps.douyin.filter import ( UserPostFilter, @@ -38,19 +45,26 @@ PostDetailFilter, UserLiveFilter, UserLive2Filter, - GetQrcodeFilter, - CheckQrcodeFilter, + # GetQrcodeFilter, + # CheckQrcodeFilter, UserFollowingFilter, UserFollowerFilter, + PostRelatedFilter, + FriendFeedFilter, + LiveImFetchFilter, + QueryUserFilter, + FollowingUserLiveFilter, ) +from f2.apps.douyin.algorithm.webcast_signature import DouyinWebcastSignature from f2.apps.douyin.utils import ( SecUserIdFetcher, AwemeIdFetcher, MixIdFetcher, WebCastIdFetcher, - VerifyFpManager, + ClientConfManager, + # VerifyFpManager, create_or_rename_user_folder, - show_qrcode, + # show_qrcode, ) from f2.cli.cli_console import RichConsoleManager from f2.exceptions.api_exceptions import APIResponseError @@ -68,7 +82,7 @@ def __init__(self, kwargs: dict = ...) -> None: self.kwargs = kwargs self.downloader = DouyinDownloader(kwargs) - async def handler_user_profile( + async def fetch_user_profile( self, sec_user_id: str, ) -> UserProfileFilter: @@ -88,32 +102,11 @@ async def handler_user_profile( response = await crawler.fetch_user_profile(params) user = UserProfileFilter(response) if user.nickname is None: - raise APIResponseError(_("API内容请求失败,请更换新cookie后再试")) + raise APIResponseError( + _("`fetch_user_profile`请求失败,请更换cookie或稍后再试") + ) return UserProfileFilter(response) - async def get_user_nickname( - self, - sec_user_id: str, - db: AsyncUserDB, - ) -> str: - """ - 获取指定用户的昵称,如果不存在,则从服务器获取并存储到数据库中 - (Used to get personal info of specified users) - - Args: - sec_user_id (str): 用户ID (User ID) - db (AsyncUserDB): 用户数据库 (User database) - - Returns: - user_nickname: (str): 用户昵称 (User nickname) - """ - - user_dict = await db.get_user_info(sec_user_id) - if not user_dict: - user_dict = await self.handler_user_profile(sec_user_id) - await db.add_user_info(**user_dict._to_dict()) - return user_dict.get("nickname") - async def get_or_add_user_data( self, kwargs: dict, @@ -137,10 +130,10 @@ async def get_or_add_user_data( local_user_data = await db.get_user_info(sec_user_id) # 从服务器获取当前用户最新数据 - current_user_data = await self.handler_user_profile(sec_user_id) + current_user_data = await self.fetch_user_profile(sec_user_id) # 获取当前用户最新昵称 - current_nickname = current_user_data._to_dict().get("nickname") + current_nickname = current_user_data.nickname # 设置用户目录 user_path = create_or_rename_user_folder( @@ -221,7 +214,7 @@ async def fetch_one_video( aweme_id: str: 作品ID Return: - video: PostDetailFilter: 单个作品数据过滤器 + video: PostDetailFilter: 单个作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 """ logger.info(_("开始爬取作品:{0}").format(aweme_id)) @@ -286,7 +279,7 @@ async def fetch_user_post_videos( max_counts: int: 最大作品数 Return: - video: AsyncGenerator[UserPostFilter, Any]: 作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 + video: AsyncGenerator[UserPostFilter, Any]: 发布作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 """ max_counts = max_counts or float("inf") @@ -393,7 +386,7 @@ async def fetch_user_like_videos( max_counts: int: 最大作品数 Return: - video: AsyncGenerator[UserPostFilter, Any]: 作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 + video: AsyncGenerator[UserPostFilter, Any]: 喜欢作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 """ max_counts = max_counts or float("inf") @@ -587,7 +580,7 @@ async def fetch_user_collection_videos( max_counts: int: 最大作品数 (Maximum number of videos) Return: - collection: AsyncGenerator[UserCollectionFilter, Any]: 作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 + collection: AsyncGenerator[UserCollectionFilter, Any]: 收藏作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 Note: 该接口需要用POST且只靠cookie来获取数据。 @@ -751,7 +744,7 @@ async def fetch_user_collects( max_counts: int: 最大收藏夹数 (Max counts) Return: - collects: AsyncGenerator[UserCollectsFilter, Any]: 收藏夹数据过滤器,包含收藏夹数据的_to_raw、_to_dict、_to_list方法) + collects: AsyncGenerator[UserCollectsFilter, Any]: 收藏夹数据过滤器,包含收藏夹数据的_to_raw、_to_dict方法) """ max_counts = max_counts or float("inf") @@ -812,7 +805,7 @@ async def fetch_user_collects_videos( max_counts: int: 最大作品数 (Maximum number of videos) Return: - video: AsyncGenerator[UserCollectionFilter, Any]: 作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 + video: AsyncGenerator[UserCollectionFilter, Any]: 收藏夹作品数据过滤器,包含作品数据的_to_raw、_to_dict、_to_list方法 """ max_counts = max_counts or float("inf") @@ -1199,6 +1192,214 @@ async def fetch_user_feed_videos( logger.info(_("爬取结束,共爬取 {0} 个首页推荐作品").format(videos_collected)) + @mode_handler("related") + async def handle_related(self): + """ + 用于处理相关作品 (Used to process related videos) + + Args: + kwargs: dict: 参数字典 (Parameter dictionary) + """ + + page_counts = self.kwargs.get("page_counts", 20) + max_counts = self.kwargs.get("max_counts") + + aweme_id = await AwemeIdFetcher.get_aweme_id(self.kwargs.get("url")) + aweme_data = await self.fetch_one_video(aweme_id) + + async with AsyncUserDB("douyin_users.db") as udb: + user_path = ( + await self.get_or_add_user_data( + self.kwargs, aweme_data.sec_user_id, udb + ) + / aweme_id + ) + + async for aweme_data_list in self.fetch_related_videos( + aweme_id, "", page_counts, max_counts + ): + # 创建下载任务 + await self.downloader.create_download_tasks( + self.kwargs, aweme_data_list._to_list(), user_path + ) + + async def fetch_related_videos( + self, + aweme_id: str, + filterGids: str = "", + page_counts: int = 20, + max_counts: int = None, + ) -> AsyncGenerator[PostRelatedFilter, Any]: + """ + 用于获取指定作品的相关推荐作品列表。 + + Args: + aweme_id: str: 作品ID + page_counts: int: 每页作品数 + max_counts: int: 最大作品数 + + Return: + related: AsyncGenerator[PostRelatedFilter, Any]: 相关推荐作品数据过滤器 + ,包含相关作品数据的_to_raw、_to_dict、_to_list方法 + """ + from urllib.parse import quote + + max_counts = max_counts or float("inf") + videos_collected = 0 + # aweme_id,awme_id,aweme_id... + filterGids = filterGids or f"{aweme_id}," + + logger.info(_("开始爬取作品: {0} 的相关推荐").format(aweme_id)) + + while videos_collected < max_counts: + current_request_size = min(page_counts, max_counts - videos_collected) + + logger.debug("===================================") + logger.debug( + _("最大数量: {0} 每次请求数量: {1}").format( + max_counts, current_request_size + ) + ) + logger.info(_("开始爬取前 {0} 个相关推荐").format(current_request_size)) + + async with DouyinCrawler(self.kwargs) as crawler: + params = PostRelated( + count=current_request_size, + aweme_id=aweme_id, + filterGids=quote(filterGids), + ) + response = await crawler.fetch_post_related(params) + related = PostRelatedFilter(response) + yield related + + if not related.has_more: + logger.info(_("作品: {0} 的所有相关推荐采集完毕").format(aweme_id)) + break + + logger.debug(_("当前请求的相关推荐数量: {0}").format(len(related.aweme_id))) + logger.debug( + _("作品ID: {0} 作品文案: {1} 作者: {2}").format( + related.aweme_id, related.desc, related.nickname + ) + ) + logger.debug("===================================") + + # 更新已经处理的作品数量 (Update the number of videos processed) + videos_collected += len(related.aweme_id) + + # 更新过滤的作品ID (Update the filtered video ID) + filterGids = ",".join([str(aweme_id) for aweme_id in related.aweme_id]) + + # 避免请求过于频繁 + logger.info(_("等待 {0} 秒后继续").format(self.kwargs.get("timeout", 5))) + await asyncio.sleep(self.kwargs.get("timeout", 5)) + + logger.info(_("爬取结束,共爬取 {0} 个相关推荐").format(videos_collected)) + + @mode_handler("friend") + async def handle_friend_feed(self): + """ + 用于处理用户好友作品 (Used to process user friend videos) + + Args: + kwargs: dict: 参数字典 (Parameter dictionary) + """ + + max_counts = self.kwargs.get("max_counts") + sec_user_id = await SecUserIdFetcher.get_sec_user_id(self.kwargs.get("url")) + + async with AsyncUserDB("douyin_users.db") as db: + user_path = await self.get_or_add_user_data(self.kwargs, sec_user_id, db) + + async for aweme_data_list in self.fetch_friend_feed_videos( + max_counts=max_counts + ): + # 创建下载任务 + await self.downloader.create_download_tasks( + self.kwargs, aweme_data_list._to_list(), user_path + ) + + async def fetch_friend_feed_videos( + self, + cursor: int = 0, + level: int = 1, + pull_type: int = 0, + max_counts: int = None, + ) -> AsyncGenerator[FriendFeedFilter, Any]: + """ + 用于获取指定用户好友作品列表。 + + Args: + cursor: int: 起始页 + level: int: 作品等级 + pull_type: int: 拉取类型 + max_counts: int: 最大作品数 + + Return: + friend: AsyncGenerator[UserFriendFilter, Any]: 好友作品数据过滤器,包含好友作品数据的_to_raw、_to_dict、_to_list方法 + """ + + max_counts = max_counts or float("inf") + videos_collected = 0 + + logger.info(_("开始爬取好友作品")) + + while videos_collected < max_counts: + + logger.debug("===================================") + logger.debug(_("最大数量:{0} 个").format(max_counts)) + logger.info(_("开始爬取第:{0} 页").format(cursor)) + + async with DouyinCrawler(self.kwargs) as crawler: + params = FriendFeed( + cursor=cursor, + level=level, + pull_type=pull_type, + ) + response = await crawler.fetch_friend_feed(params) + friend = FriendFeedFilter(response) + + if not friend.has_more: + logger.info(_("所有好友作品采集完毕")) + break + + if friend.status_code != 0: + logger.warning( + _("请求失败,错误码:{0} 错误信息:{1}").format( + friend.status_code, friend.status_msg + ) + ) + break + else: + # 因为没有好友作品第一页也会返回has_more为False,所以需要访问下一页判断是否有作品 + if not friend.has_aweme: + logger.info(_("第 {0} 页没有找到作品").format(cursor)) + continue + + logger.debug(_("当前请求的cursor: {0}").format(cursor)) + logger.debug( + _("作品ID: {0} 作品文案: {1} 作者: {2}").format( + friend.aweme_id, friend.desc, friend.nickname + ) + ) + logger.debug("===================================") + + yield friend + + # 更新已经处理的作品数量 (Update the number of videos processed) + videos_collected += len(friend.aweme_id) + # 更新下一页的cursor (Update the cursor of the next page) + cursor = friend.cursor + # 更新其他参数 (Update other parameters) + level = friend.level + pull_type = friend.level + + # 避免请求过于频繁 + logger.info(_("等待 {0} 秒后继续").format(self.kwargs.get("timeout", 5))) + await asyncio.sleep(self.kwargs.get("timeout", 5)) + + logger.info(_("爬取结束,共爬取 {0} 个好友作品").format(videos_collected)) + async def fetch_user_following( self, user_id: str = "", @@ -1365,129 +1566,281 @@ async def fetch_user_follower( logger.info(_("爬取结束,共爬取 {0} 个用户").format(users_collected)) + async def fetch_query_user(self) -> QueryUserFilter: + """ + 用于查询用户信息,仅返回用户的基本信息,若需要获取更多信息请使用`fetch_user_profile`。 + + Return: + user: QueryUserFilter: 查询用户数据过滤器,包含用户数据的_to_raw、_to_dict方法 + """ -async def handle_sso_login(): - """ - 用于处理用户登录 (Used to process user login) - """ + logger.info(_("开始查询用户信息")) + logger.debug("===================================") + async with DouyinCrawler(self.kwargs) as crawler: + params = QueryUser() + response = await crawler.fetch_query_user(params) + user = QueryUserFilter(response) - kwargs = { - "proxies": {"http": None, "https": None}, - "cookie": "", - "headers": { - "Referer": "https://www.douyin.com/", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/104.0.0.0 Safari/537.36", - }, - } + if user.status_code is None: + logger.debug( + _("用户UniqueID:{0} 用户ID:{1} 用户创建时间:{2}").format( + user.user_unique_id, user.user_uid, user.create_time + ) + ) + logger.debug("===================================") + logger.info(_("用户信息查询结束")) + else: + logger.warning(_("请提供正确的ttwid")), - async def get_qrcode() -> str: - params = LoginGetQr(verifyFp=verify_fp, fp=verify_fp) - async with DouyinCrawler(kwargs) as crawler: - response = await crawler.fetch_login_qrcode(params) - sso = GetQrcodeFilter(response) - show_qrcode(sso.qrcode_index_url) - return await check_qrcode(sso.token, crawler) + return user - async def check_qrcode(token: str, crawler) -> bool: + async def fetch_live_im(self, room_id: str, unique_id: str) -> LiveImFetchFilter: """ - 检查二维码状态 + 用于获取直播间信息。 Args: - token (str): 二维码token + room_id: str: 直播间ID + unique_id: str: 用户ID - Returns: - bool: 是否成功登录 - """ - logger.debug(f"check_qrcode token:{token}") - - status_mapping = { - "1": {"message": _("[ 登录 ]:等待二维码扫描!"), "log": logger.info}, - "2": {"message": _("[ 登录 ]:扫描二维码成功!"), "log": logger.info}, - "3": {"message": _("[ 登录 ]:确认二维码登录!"), "log": logger.info}, - "4": { - "message": _("[ 登录 ]:访问频繁,请检查参数!"), - "log": logger.warning, - }, - "5": { - "message": _("[ 登录 ]:二维码过期,重新获取!"), - "log": logger.warning, - }, - "2046": { - "messages": _("[ 登录 ]:扫码环境异常,请前往app验证!"), - "log": logger.warning, - }, - } + Return: + live_im: LiveImFetchFilter: 直播间信息数据过滤器,包含直播间信息的_to_raw、_to_dict、_to_list方法 + """ - while True: - params = LoginCheckQr(token=token, verifyFp=verify_fp, fp=verify_fp) - check_response = await crawler.fetch_check_qrcode(params) - check = CheckQrcodeFilter(check_response.json()) - check_status = check.status - check_status = "2046" if check_status is None else check_status - - status_info = status_mapping.get(check_status, {}) - message = status_info.get("message", "") - log_func = status_info.get("log", logger.info) - logger.info(message) - log_func(message) - - if check_status == "3": - login_cookies = split_set_cookie( - check_response.headers.get("set-cookie", "") - ) - is_login, login_cookie = await login_redirect( - check.redirect_url, login_cookies, crawler + logger.info(_("开始查询直播间信息")) + logger.debug("===================================") + + # user = await self.fetch_query_user() + + async with DouyinCrawler(self.kwargs) as crawler: + params = LiveImFetch(room_id=room_id, user_unique_id=unique_id) + response = await crawler.fetch_live_im_fetch(params) + live_im = LiveImFetchFilter(response) + + if live_im.status_code == 0: + logger.debug( + _("直播间Room_ID:{0} 弹幕cursor:{1}").format( + live_im.room_id, live_im.cursor ) - return is_login, login_cookie - elif check_status == "5": - get_qrcode() - break - elif check_status is None: - break + ) + logger.debug("===================================") + logger.info(_("直播间信息查询结束")) + else: + logger.warning(_("请提供正确的Room_ID")) - await asyncio.sleep(5) + return live_im - async def login_redirect(redirect_url: str, login_cookies: str, crawler): + async def fetch_live_danmaku( + self, room_id: str, user_unique_id: str, internal_ext: str, cursor: str + ): """ - 登录重定向,获取登录后Cookie + 通过WebSocket连接获取直播间弹幕,再通过回调函数处理弹幕数据。 Args: - redirect_url (str): 重定向url - login_cookies (str): 登录cookie + room_id: str: 直播间ID + user_unique_id: str: 用户ID + internal_ext: str: 内部扩展参数 + cursor: str: 弹幕cursor - Returns: - is_login (bool): 是否成功登录 - login_cookie (str): 登录cookie + Return: + self.websocket: DouyinWebSocketCrawler: WebSocket连接对象 """ - crawler.headers["Cookie"] = login_cookies - redirect_response = await crawler.get_fetch_data(redirect_url) - - if redirect_response.history and len(redirect_response.history) > 1: - logger.debug(f"login_redirect headers:{redirect_response.headers}") - logger.debug(f"login_redirect history:{redirect_response.history}") - logger.debug( - f"login_redirect history[0] headers:{redirect_response.history[0].headers}" + wss_callbacks = { + "WebcastRoomMessage": DouyinWebSocketCrawler.WebcastRoomMessage, + "WebcastLikeMessage": DouyinWebSocketCrawler.WebcastLikeMessage, + "WebcastMemberMessage": DouyinWebSocketCrawler.WebcastMemberMessage, + "WebcastChatMessage": DouyinWebSocketCrawler.WebcastChatMessage, + "WebcastGiftMessage": DouyinWebSocketCrawler.WebcastGiftMessage, + "WebcastSocialMessage": DouyinWebSocketCrawler.WebcastSocialMessage, + "WebcastRoomUserSeqMessage": DouyinWebSocketCrawler.WebcastRoomUserSeqMessage, + "WebcastUpdateFanTicketMessage": DouyinWebSocketCrawler.WebcastUpdateFanTicketMessage, + "WebcastCommonTextMessage": DouyinWebSocketCrawler.WebcastCommonTextMessage, + "WebcastMatchAgainstScoreMessage": DouyinWebSocketCrawler.WebcastMatchAgainstScoreMessage, + "WebcastFansclubMessage": DouyinWebSocketCrawler.WebcastFansclubMessage, + # TODO: WebcastRanklistHourEntranceMessage + # TODO: WebcastRoomStatsMessage + # TODO: WebcastLiveShoppingMessage + # TODO: WebcastLiveEcomGeneralMessage + # TODO: WebcastProductChangeMessage + # TODO: WebcastRoomStreamAdaptationMessage + } + async with DouyinWebSocketCrawler(self.kwargs, callbacks=wss_callbacks) as wss: + signature = DouyinWebcastSignature( + ClientConfManager.user_agent() + ).get_signature(room_id, user_unique_id) + + params = LiveWebcast( + room_id=room_id, + user_unique_id=user_unique_id, + internal_ext=internal_ext, + cursor=cursor, + signature=signature, ) + + result = await wss.fetch_live_danmaku(params) + + if result == "closed": + logger.info(_("直播间:{0} 已结束直播").format(room_id)) + elif result == "error": + logger.error(_("直播间:{0} 弹幕连接异常").format(room_id)) + + return + + async def fetch_user_following_lives(self) -> FollowingUserLiveFilter: + """ + 用于获取关注用户的直播间信息。 + + Return: + follow_live: FollowingUserLiveFilter: 关注用户直播间信息数据过滤器,包含关注用户直播间信息的_to_raw、_to_dict、_to_list方法 + """ + + logger.info(_("开始查询关注用户直播间信息")) + logger.debug("===================================") + + async with DouyinCrawler(self.kwargs) as crawler: + params = FollowingUserLive() + response = await crawler.fetch_following_live(params) + follow_live = FollowingUserLiveFilter(response) + + if follow_live.status_code == 0: logger.debug( - f"login_redirect history[1] headers:{redirect_response.history[1].headers}" - ) - # 获取重最后一个重定向里的Cookie - login_cookie = split_set_cookie( - redirect_response.history[1].headers.get("set-cookie", "") + _("直播间Room_ID:{0} 直播间标题:{1} 直播间人数:{2}").format( + follow_live.room_id, + follow_live.live_title_raw, + follow_live.user_count, + ) ) - logger.debug(f"login_cookie:{login_cookie}") - return True, login_cookie + logger.debug("===================================") + logger.info(_("关注用户直播间信息查询结束")) else: - logger.warning("[ 登录 ]:自动重定向登录失败") - if redirect_response: - error_message = f"网络异常: 自动重定向登录失败。 状态码: {redirect_response.status_code}, 响应体: {redirect_response.text}" - else: - error_message = f"网络异常: 自动重定向登录失败。 无法连接到服务器。" - logger.warning(error_message) - return False, "" + logger.warning( + _("获取关注用户直播间信息失败:{0}").format(follow_live.status_msg) + ) - verify_fp = VerifyFpManager.gen_verify_fp() - return await get_qrcode() + return follow_live + + +# async def handle_sso_login(): +# """ +# 用于处理用户登录 (Used to process user login) +# """ + +# kwargs = { +# "proxies": {"http://": None, "https://": None}, +# "cookie": "", +# "headers": { +# "Referer": "https://www.douyin.com/", +# "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/104.0.0.0 Safari/537.36", +# }, +# } + +# async def get_qrcode() -> str: +# params = LoginGetQr(verifyFp=verify_fp, fp=verify_fp) +# async with DouyinCrawler(kwargs) as crawler: +# response = await crawler.fetch_login_qrcode(params) +# sso = GetQrcodeFilter(response) +# show_qrcode(sso.qrcode_index_url) +# return await check_qrcode(sso.token, crawler) + +# async def check_qrcode(token: str, crawler) -> bool: +# """ +# 检查二维码状态 + +# Args: +# token (str): 二维码token + +# Returns: +# bool: 是否成功登录 +# """ +# logger.debug(f"check_qrcode token:{token}") + +# status_mapping = { +# "1": {"message": _("[ 登录 ]:等待二维码扫描!"), "log": logger.info}, +# "2": {"message": _("[ 登录 ]:扫描二维码成功!"), "log": logger.info}, +# "3": {"message": _("[ 登录 ]:确认二维码登录!"), "log": logger.info}, +# "4": { +# "message": _("[ 登录 ]:访问频繁,请检查参数!"), +# "log": logger.warning, +# }, +# "5": { +# "message": _("[ 登录 ]:二维码过期,重新获取!"), +# "log": logger.warning, +# }, +# "2046": { +# "messages": _("[ 登录 ]:扫码环境异常,请前往app验证!"), +# "log": logger.warning, +# }, +# } + +# while True: +# params = LoginCheckQr(token=token, verifyFp=verify_fp, fp=verify_fp) +# check_response = await crawler.fetch_check_qrcode(params) +# check = CheckQrcodeFilter(check_response.json()) +# check_status = check.status +# check_status = "2046" if check_status is None else check_status + +# status_info = status_mapping.get(check_status, {}) +# message = status_info.get("message", "") +# log_func = status_info.get("log", logger.info) +# logger.info(message) +# log_func(message) + +# if check_status == "3": +# login_cookies = split_set_cookie( +# check_response.headers.get("set-cookie", "") +# ) +# is_login, login_cookie = await login_redirect( +# check.redirect_url, login_cookies, crawler +# ) +# return is_login, login_cookie +# elif check_status == "5": +# get_qrcode() +# break +# elif check_status is None: +# break + +# await asyncio.sleep(5) + +# async def login_redirect(redirect_url: str, login_cookies: str, crawler): +# """ +# 登录重定向,获取登录后Cookie + +# Args: +# redirect_url (str): 重定向url +# login_cookies (str): 登录cookie + +# Returns: +# is_login (bool): 是否成功登录 +# login_cookie (str): 登录cookie +# """ +# crawler.headers["Cookie"] = login_cookies +# redirect_response = await crawler.get_fetch_data(redirect_url) + +# if redirect_response.history and len(redirect_response.history) > 1: +# logger.debug(f"login_redirect headers:{redirect_response.headers}") +# logger.debug(f"login_redirect history:{redirect_response.history}") +# logger.debug( +# f"login_redirect history[0] headers:{redirect_response.history[0].headers}" +# ) +# logger.debug( +# f"login_redirect history[1] headers:{redirect_response.history[1].headers}" +# ) +# # 获取重最后一个重定向里的Cookie +# login_cookie = split_set_cookie( +# redirect_response.history[1].headers.get("set-cookie", "") +# ) +# logger.debug(f"login_cookie:{login_cookie}") +# return True, login_cookie +# else: +# logger.warning("[ 登录 ]:自动重定向登录失败") +# if redirect_response: +# error_message = f"网络异常: 自动重定向登录失败。 状态码: {redirect_response.status_code}, 响应体: {redirect_response.text}" +# else: +# error_message = f"网络异常: 自动重定向登录失败。 无法连接到服务器。" +# logger.warning(error_message) +# return False, "" + +# verify_fp = VerifyFpManager.gen_verify_fp() +# return await get_qrcode() async def main(kwargs): diff --git a/f2/apps/douyin/help.py b/f2/apps/douyin/help.py index 89298328..16b162b8 100644 --- a/f2/apps/douyin/help.py +++ b/f2/apps/douyin/help.py @@ -21,7 +21,7 @@ def help() -> None: "-u --url", "[dark_cyan]str", _( - "根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合辑与直播同上" + "根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合集与直播同上" ), ), ("-m --music", "[dark_cyan]Bool", _("是否保存视频原声")), @@ -37,7 +37,7 @@ def help() -> None: "-M --mode", "[dark_cyan]Choice", _( - "下载模式:单个作品(one),主页作品(post),点赞作品(like),收藏作品(collection),收藏夹作品(collects),合辑(mix),直播(live)" + "下载模式:单个作品(one),主页作品(post),点赞作品(like),收藏作品(collection),收藏夹作品(collects),合集(mix),直播(live)" ), ), ( @@ -67,7 +67,7 @@ def help() -> None: ( "-s --page-counts", "[dark_cyan]int", - _("从接口每页可获取作品数,不建议超过20"), + _("从接口每页可获取作品数,不建议超过 20"), ), ( "-l --languages", @@ -78,7 +78,7 @@ def help() -> None: "-P --proxies", "[dark_cyan]str", _( - "代理服务器,最多 2 个参数,http与https。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" + "代理服务器,最多 2 个参数,http://与https://。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" ), ), ("-L --lyric", "[dark_cyan]Bool", _("是否保存视频歌词")), @@ -102,7 +102,9 @@ def help() -> None: ( "--sso-login", "[dark_cyan]Flag", - _("使用SSO扫码登录获取[yellow]cookie[/yellow],保存低频主配置文件"), + _( + "使用SSO扫码登录获取[yellow]cookie[/yellow],保存低频主配置文件[red](暂时弃用)[/red]" + ), ), ("--help", "[dark_cyan]Flag", _("显示经典帮助信息")), ( diff --git a/f2/apps/douyin/model.py b/f2/apps/douyin/model.py index 8714a99e..16d97770 100644 --- a/f2/apps/douyin/model.py +++ b/f2/apps/douyin/model.py @@ -2,8 +2,9 @@ from typing import Any from pydantic import BaseModel +from urllib.parse import quote, unquote -from f2.apps.douyin.utils import TokenManager, VerifyFpManager +from f2.apps.douyin.utils import TokenManager, VerifyFpManager, ClientConfManager # Base Model @@ -12,27 +13,31 @@ class BaseRequestModel(BaseModel): aid: str = "6383" channel: str = "channel_pc_web" pc_client_type: int = 1 - version_code: str = "190500" - version_name: str = "19.5.0" + version_code: str = ClientConfManager.brm_version().get("code", "190500") + version_name: str = ClientConfManager.brm_version().get("name", "19.5.0") cookie_enabled: str = "true" screen_width: int = 1920 screen_height: int = 1080 - browser_language: str = "zh-CN" - browser_platform: str = "Win32" - browser_name: str = "Edge" - browser_version: str = "122.0.0.0" + browser_language: str = ClientConfManager.brm_browser().get("language", "zh-CN") + browser_platform: str = ClientConfManager.brm_browser().get("platform", "Win32") + browser_name: str = ClientConfManager.brm_browser().get("name", "Edge") + browser_version: str = ClientConfManager.brm_browser().get("version", "122.0.0.0") browser_online: str = "true" - engine_name: str = "Blink" - engine_version: str = "122.0.0.0" - os_name: str = "Windows" - os_version: str = "10" + engine_name: str = ClientConfManager.brm_engine().get("name", "Blink") + engine_version: str = ClientConfManager.brm_engine().get("version", "122.0.0.0") + os_name: str = ClientConfManager.brm_os().get("name", "Windows") + os_version: str = ClientConfManager.brm_os().get("version", "10") cpu_core_num: int = 12 device_memory: int = 8 platform: str = "PC" downlink: int = 10 effective_type: str = "4g" round_trip_time: int = 100 - msToken: str = TokenManager.gen_real_msToken() + try: + msToken: str = TokenManager.gen_real_msToken() + except: + # 返回虚假的msToken (Return a fake msToken) + msToken: str = TokenManager.gen_false_msToken() class BaseLiveModel(BaseModel): @@ -40,14 +45,14 @@ class BaseLiveModel(BaseModel): app_name: str = "douyin_web" live_id: int = 1 device_platform: str = "web" - language: str = "zh-CN" + language: str = ClientConfManager.blm_language() cookie_enabled: str = "true" screen_width: int = 1920 screen_height: int = 1080 - browser_language: str = "zh-CN" - browser_platform: str = "Win32" - browser_name: str = "Edge" - browser_version: str = "119.0.0.0" + browser_language: str = ClientConfManager.blm_browser().get("language", "zh-CN") + browser_platform: str = ClientConfManager.blm_browser().get("platform", "Win32") + browser_name: str = ClientConfManager.blm_browser().get("name", "Edge") + browser_version: str = ClientConfManager.blm_browser().get("version", "119.0.0.0") enter_source: Any = "" is_need_double_stream: str = "false" # msToken: str = TokenManager.gen_real_msToken() @@ -75,6 +80,34 @@ class BaseLoginModel(BaseModel): language: str = "zh" +class BaseWebCastModel(BaseModel): + app_name: str = "douyin_web" + version_code: str = "180800" + device_platform: str = "web" + cookie_enabled: str = "true" + screen_width: int = 1920 + screen_height: int = 1080 + browser_language: str = "zh-CN" + browser_platform: str = "Win32" + browser_name: str = "Mozilla" + browser_version: str = quote( + "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + safe="", + ) + browser_online: str = "true" + tz_name: str = "Asia/Hong_Kong" + host: str = "https://live.douyin.com" + aid: int = 6383 + live_id: int = 1 + did_rule: int = 3 + endpoint: str = "live_pc" + support_wrds: int = 1 + identity: str = "audience" + need_persist_msg_count: int = 15 + insert_task_id: Any = "" + live_reason: Any = "" + + # Model class UserProfile(BaseRequestModel): sec_user_id: str @@ -161,7 +194,7 @@ class PostRelated(BaseRequestModel): aweme_id: str count: int = 20 filterGids: str # id,id,id - awemePcRecRawData: dict = {} # {"is_client":false} + awemePcRecRawData: str = quote('{"is_client":false}', safe="") sub_channel_id: int = 3 # Seo-Flag: int = 0 @@ -200,7 +233,7 @@ class UserLive2(BaseLiveModel2): room_id: str -class FollowUserLive(BaseRequestModel): +class FollowingUserLive(BaseRequestModel): scene: str = "aweme_pc_follow_top" @@ -215,15 +248,17 @@ class SuggestWord(BaseRequestModel): class PostSearch(BaseRequestModel): search_channel: str = "aweme_general" - sort_type: int = 0 # 0 综合 - publish_time: int = 0 + filter_selected: str keyword: str - search_source: str = "hot_search_board" + search_source: str = "normal_search" + # search_sug # tab_search # normal_search # guide_search # hot_search_board + search_id: str = "" query_correct_type: int = 1 is_filter_search: int = 0 from_group_id: str = "" offset: int = 0 count: int = 15 + need_filter_settings: int = 1 class LoginGetQr(BaseLoginModel): @@ -265,3 +300,34 @@ class UserFollower(BaseRequestModel): gps_access: int = 0 address_book_access: int = 0 is_top: int = 1 + + +class LiveWebcast(BaseWebCastModel): + webcast_sdk_version: str = "1.0.12" # 当前为1.0.14-beta.0 + update_version_code: str = "1.0.12" + compress: str = "gzip" + im_path: str = "/webcast/im/fetch/" + heartbeatDuration: int = 0 + room_id: str + user_unique_id: str + cursor: str + internal_ext: str + signature: str # 暂时调用execjs,纯算还在扣 + + +class LiveImFetch(BaseWebCastModel): + # resp_content_type: str = "protobuf" + resp_content_type: str = "json" + fetch_rule: int = 1 + last_rtt: int = 0 + cursor: str = "" + internal_ext: str = "" + room_id: str + user_unique_id: str + + +class QueryUser(BaseRequestModel): + publish_video_strategy_type: int = 2 + update_version_code: str = "170400" + version_code: str = "170400" + version_name: str = "17.4.0" diff --git a/f2/apps/douyin/proto/douyin_webcast.proto b/f2/apps/douyin/proto/douyin_webcast.proto new file mode 100644 index 00000000..a0765919 --- /dev/null +++ b/f2/apps/douyin/proto/douyin_webcast.proto @@ -0,0 +1,998 @@ +syntax = "proto3"; + +package douyin; + + +message Response { + repeated Message messagesList = 1; + string cursor = 2; + uint64 fetchInterval = 3; + uint64 now = 4; + string internalExt = 5; + uint32 fetchType = 6; + map routeParams = 7; + uint64 heartbeatDuration = 8; + bool needAck = 9; + string pushServer = 10; + string liveCursor = 11; + bool historyNoMore = 12; +} + +message Message{ + string method = 1; + bytes payload = 2; + int64 msgId = 3; + int32 msgType = 4; + int64 offset = 5; + bool needWrdsStore = 6; + int64 wrdsVersion = 7; + string wrdsSubKey = 8; +} + +message ChatMessage { + Common common = 1; + User user = 2; + string content = 3; + bool visibleToSender = 4; + Image backgroundImage = 5; + string fullScreenTextColor = 6; + Image backgroundImageV2 = 7; + PublicAreaCommon publicAreaCommon = 8; + Image giftImage = 9; + uint64 agreeMsgId = 11; + uint32 priorityLevel = 12; + LandscapeAreaCommon landscapeAreaCommon = 13; + uint64 eventTime = 15; + bool sendReview = 16; + bool fromIntercom = 17; + bool intercomHideUserCard = 18; + // repeated chatTagsList = 19; + string chatBy = 20; + uint32 individualChatPriority = 21; + Text rtfContent = 22; +} + + +message LandscapeAreaCommon { + bool showHead = 1; + bool showNickname = 2; + bool showFontColor = 3; + repeated string colorValueList = 4; + repeated CommentTypeTag commentTypeTagsList = 5; +} + +message RoomUserSeqMessage { + Common common = 1; + repeated RoomUserSeqMessageContributor ranksList = 2; + int64 total = 3; + string popStr = 4; + repeated RoomUserSeqMessageContributor seatsList = 5; + int64 popularity = 6; + int64 totalUser = 7; + string totalUserStr = 8; + string totalStr = 9; + string onlineUserForAnchor = 10; + string totalPvForAnchor = 11; + string upRightStatsStr = 12; + string upRightStatsStrComplete = 13; +} + +message RoomMessage{ + Common common = 1; + string content = 2; + bool support_landscape = 3; +} + +message CommonTextMessage { + Common common = 1; + User user = 2; + string scene = 3; +} + +// 粉丝团更新 +message UpdateFanTicketMessage { + Common common = 1; + string roomFanTicketCountText = 2; + uint64 roomFanTicketCount = 3; + bool forceUpdate = 4; +} + + +message RoomUserSeqMessageContributor { + uint64 score = 1; + User user = 2; + uint64 rank = 3; + uint64 delta = 4; + bool isHidden = 5; + string scoreDescription = 6; + string exactlyScore = 7; +} + +// 礼物消息 +message GiftMessage { + Common common = 1; + uint64 giftId = 2; + uint64 fanTicketCount = 3; + uint64 groupCount = 4; + uint64 repeatCount = 5; + uint64 comboCount = 6; + User user = 7; + User toUser = 8; + uint32 repeatEnd = 9; + TextEffect textEffect = 10; + uint64 groupId = 11; + uint64 incomeTaskgifts = 12; + uint64 roomFanTicketCount = 13; + GiftIMPriority priority = 14; + GiftStruct gift = 15; + string logId = 16; + uint64 sendType = 17; + PublicAreaCommon publicAreaCommon = 18; + Text trayDisplayText = 19; + uint64 bannedDisplayEffects = 20; + GiftTrayInfo trayInfo = 21; + AssetEffectMixInfo assetEffectMixInfo = 22; + bool displayForSelf = 25; + string interactGiftInfo = 26; + string diyItemInfo = 27; + repeated uint64 minAssetSetList = 28; + uint64 totalCount = 29; + uint32 clientGiftSource = 30; + // AnchorGiftData anchorGift = 31; + repeated uint64 toUserIdsList = 32; + uint64 sendTime = 33; + uint64 forceDisplayEffects = 34; + string traceId = 35; + uint64 effectDisplayTs = 36; +} + +message AssetEffectMixInfo{ + +} + +message FansClubMessage { + Common commonInfo = 1; + int32 type = 2; + string content = 3; + User user = 4; +} + +message GiftTrayInfo{ + Text trayDisplayText = 1; + Image trayBaseImg = 2; + Image trayHeadImg = 3; + Image trayRightImg = 4; + int64 trayLevel = 5; + Image trayDynamicImg = 6; +} + +message GiftStruct { + Image image = 1; + string describe = 2; + bool notify = 3; + int64 duration = 4; + int64 id = 5; + GiftStructFansClubInfo fansclubInfo = 6; + bool forLinkmic = 7; + bool doodle = 8; + bool forFansclub = 9; + bool combo = 10; + int32 type = 11; + int32 diamondCount = 12; + int32 isDisplayedOnPanel = 13; + int64 primaryEffectId = 14; + Image giftLabelIcon = 15; + string name = 16; + string region = 17; + string manual = 18; + bool forCustom = 19; + map specialEffects = 20; + Image icon = 21; + int32 actionType = 22; + int32 watermelonSeeds = 23; + string goldEffect = 24; + repeated LuckyMoneyGiftMeta subs = 25; + int64 goldenBeans = 26; + int64 honorLevel = 27; + int32 itemType = 28; + string schemeUrl = 29; + GiftPanelOperation giftOperation = 30; + string eventName = 31; + int64 nobleLevel = 32; + string guideUrl = 33; + bool punishMedicine = 34; + bool forPortal = 35; + string businessText = 36; + bool cnyGift = 37; + int64 appId = 38; + int64 vipLevel = 39; + bool isGray = 40; + string graySchemeUrl = 41; + int64 giftScene = 42; + GiftBanner giftBanner = 43; + repeated string triggerWords = 44; + repeated GiftBuffInfo giftBuffInfos = 45; + bool forFirstRecharge = 46; + Image dynamicImgForSelected = 47; + int32 afterSendAction = 48; + int64 giftOfflineTime = 49; + string topBarText = 50; + Image topRightAvatar = 51; + string bannerSchemeUrl = 52; + bool isLocked = 53; + int64 reqExtraType = 54; + repeated int64 assetIds = 55; + GiftPreviewInfo giftPreviewInfo = 56; + GiftTip giftTip = 57; + int32 needSweepLightCount = 58; + repeated GiftGroupInfo groupInfo = 59; + + message GiftStructFansClubInfo { + int32 minLevel = 1; + int32 insertPos = 2; + } +} + +message LuckyMoneyGiftMeta { + +} + +message GiftPanelOperation { + +} + +message GiftBanner { + +} + +message GiftBuffInfo{ + +} + +message GiftPreviewInfo{ + +} + +message GiftTip { + +} + +message GiftGroupInfo { + +} + +message GiftIMPriority { + repeated uint64 queueSizesList = 1; + uint64 selfQueuePriority = 2; + uint64 priority = 3; +} + +message TextEffect { + TextEffectDetail portrait = 1; + TextEffectDetail landscape = 2; +} + +message TextEffectDetail { + Text text = 1; + uint32 textFontSize = 2; + Image background = 3; + uint32 start = 4; + uint32 duration = 5; + uint32 x = 6; + uint32 y = 7; + uint32 width = 8; + uint32 height = 9; + uint32 shadowDx = 10; + uint32 shadowDy = 11; + uint32 shadowRadius = 12; + string shadowColor = 13; + string strokeColor = 14; + uint32 strokeWidth = 15; +} + +// 成员消息 +message MemberMessage { + Common common = 1; + User user = 2; + uint64 memberCount = 3; + User operator = 4; + bool isSetToAdmin = 5; + bool isTopUser = 6; + uint64 rankScore = 7; + uint64 topUserNo = 8; + uint64 enterType = 9; + uint64 action = 10; + string actionDescription = 11; + uint64 userId = 12; + EffectConfig effectConfig = 13; + string popStr = 14; + EffectConfig enterEffectConfig = 15; + Image backgroundImage = 16; + Image backgroundImageV2 = 17; + Text anchorDisplayText = 18; + PublicAreaCommon publicAreaCommon = 19; + uint64 userEnterTipType = 20; + uint64 anchorEnterTipType = 21; + map buriedPointMap = 22; +} + + +message PublicAreaCommon { + Image userLabel = 1; + uint64 userConsumeInRoom = 2; + uint64 userSendGiftCntInRoom = 3; +} + +message EffectConfig { + uint64 type = 1; + Image icon = 2; + uint64 avatarPos = 3; + Text text = 4; + Image textIcon = 5; + uint32 stayTime = 6; + uint64 animAssetId = 7; + Image badge = 8; + repeated uint64 flexSettingArrayList = 9; + Image textIconOverlay = 10; + Image animatedBadge = 11; + bool hasSweepLight = 12; + repeated uint64 textFlexSettingArrayList = 13; + uint64 centerAnimAssetId = 14; + Image dynamicImage = 15; + map extraMap = 16; + uint64 mp4AnimAssetId = 17; + uint64 priority = 18; + uint64 maxWaitTime = 19; + string dressId = 20; + uint64 alignment = 21; + uint64 alignmentOffset = 22; + string effectScene = 23; + map pieceValuesMap = 24; +} + +message Text { + string key = 1; + string defaultPatter = 2; + TextFormat defaultFormat = 3; + repeated TextPiece piecesList = 4; +} + +message TextPiece { + bool type = 1; + TextFormat format = 2; + string stringValue = 3; + TextPieceUser userValue = 4; + TextPieceGift giftValue = 5; + TextPieceHeart heartValue = 6; + TextPiecePatternRef patternRefValue = 7; + TextPieceImage imageValue = 8; +} + + +message TextPieceImage { + Image image = 1; + float scalingRate = 2; +} + +message TextPiecePatternRef { + string key = 1; + string defaultPattern = 2; +} + +message TextPieceHeart { + string color = 1; +} + +message TextPieceGift { + uint64 giftId = 1; + PatternRef nameRef = 2; +} + +message PatternRef { + string key = 1; + string defaultPattern = 2; +} + +message TextPieceUser { + User user = 1; + bool withColon = 2; +} + +message TextFormat { + string color = 1; + bool bold = 2; + bool italic = 3; + uint32 weight = 4; + uint32 italicAngle = 5; + uint32 fontSize = 6; + bool useHeighLightColor = 7; + bool useRemoteClor = 8; +} + +// 点赞 +message LikeMessage { + Common common = 1; + uint64 count = 2; + uint64 total = 3; + uint64 color = 4; + User user = 5; + string icon = 6; + DoubleLikeDetail doubleLikeDetail = 7; + DisplayControlInfo displayControlInfo = 8; + uint64 linkmicGuestUid = 9; + string scene = 10; + PicoDisplayInfo picoDisplayInfo = 11; +} + +message SocialMessage { + Common common = 1; + User user = 2; + uint64 shareType = 3; + uint64 action = 4; + string shareTarget = 5; + uint64 followCount = 6; + PublicAreaCommon publicAreaCommon = 7; +} + +message PicoDisplayInfo { + uint64 comboSumCount = 1; + string emoji = 2; + Image emojiIcon = 3; + string emojiText = 4; +} + +message DoubleLikeDetail { + bool doubleFlag = 1; + uint32 seqId = 2; + uint32 renewalsNum = 3; + uint32 triggersNum = 4; +} + +message DisplayControlInfo { + bool showText = 1; + bool showIcons = 2; +} + +message EpisodeChatMessage { + Message common = 1; + User user = 2; + string content = 3; + bool visibleToSende = 4; +// BackgroundImage backgroundImage = 5; +// PublicAreaCommon publicAreaCommon = 6; + Image giftImage = 7; + uint64 agreeMsgId = 8; + repeated string colorValueList = 9; +} + + +message MatchAgainstScoreMessage { + Common common = 1; + Against against = 2; + uint32 matchStatus = 3; + uint32 displayStatus = 4; +} + +message Against { + string leftName = 1; + Image leftLogo = 2; + string leftGoal = 3; +// LeftPlayersList leftPlayersList = 4; +// LeftGoalStageDetail leftGoalStageDetail = 5; + string rightName = 6; + Image rightLogo = 7; + string rightGoal = 8; +// RightPlayersList rightPlayersList = 9; +// RightGoalStageDetail rightGoalStageDetail = 10; + uint64 timestamp = 11; + uint64 version = 12; + uint64 leftTeamId = 13; + uint64 rightTeamId = 14; + uint64 diffSei2absSecond = 15; + uint32 finalGoalStage = 16; + uint32 currentGoalStage =17; + uint32 leftScoreAddition =18; + uint32 rightScoreAddition =19; + uint64 leftGoalInt = 20; + uint64 rightGoalInt = 21; +} + +message Common { + string method = 1; + uint64 msgId = 2; + uint64 roomId = 3; + uint64 createTime = 4; + uint32 monitor = 5; + bool isShowMsg = 6; + string describe = 7; + Text displayText = 8; + uint64 foldType = 9; + uint64 anchorFoldType = 10; + uint64 priorityScore = 11; + string logId = 12; + string msgProcessFilterK = 13; + string msgProcessFilterV = 14; + User user = 15; + Room room = 16; + uint64 anchorFoldTypeV2 = 17; + uint64 processAtSeiTimeMs = 18; + uint64 randomDispatchMs = 19; + bool isDispatch = 20; + uint64 channelId = 21; + uint64 diffSei2absSecond = 22; + uint64 anchorFoldDuration = 23; +} + +message Room { + int64 id = 1; + string idStr = 2; + int64 status = 3; + int64 ownerUserId = 4; + string title = 5; + int64 userCount = 6; + int64 createTime = 7; + int64 linkmicLayout = 8; + int64 finishTime = 9; + RoomExtra extra = 10; + string dynamicCoverUri = 11; + map dynamicCoverDict = 12; + int64 lastPingTime = 13; + int64 liveId = 14; + int64 streamProvider = 15; + int64 osType = 16; + int64 clientVersion = 17; + bool withLinkmic = 18; + bool enableRoomPerspective = 19; + Image cover = 20; + Image dynamicCover = 21; + Image dynamicCoverLow = 22; + string shareUrl = 23; + string anchorShareText = 24; + string userShareText = 25; + int64 streamId = 26; + string streamIdStr = 27; + StreamUrl streamUrl = 28; + int64 mosaicStatus = 29; + string mosaicTip = 30; + int64 cellStyle = 31; + LinkMic linkMic = 32; + int64 luckymoneyNum = 33; + repeated Decoration decoList = 34; + repeated TopFan topFans = 35; + RoomStats stats = 36; + string sunDailyIconContent = 37; + string distance = 38; + string distanceCity = 39; + string location = 40; + string realDistance = 41; + Image feedRoomLabel = 42; + string commonLabelList = 43; + RoomUserAttr livingRoomAttrs = 44; + repeated int64 adminUserIds = 45; + User owner = 46; + string privateInfo = 47; +} + +message RoomExtra{ + +} + +message RoomStats{ + +} + +message RoomUserAttr{ + +} + +message StreamUrl{ + +} + +message LinkMic { + +} + +message Decoration{ + +} + +message TopFan { + +} + +message GradeBuffInfo { + int64 buffLevel = 1; + int32 status = 2; + int64 endTime = 3; + map statsInfoMap = 4; + Image buffBadge = 5; +} + +message UserVIPInfo{ + +} + +message IndustryCertification{ + +} + +// User +message User { + int64 id = 1; + int64 shortId = 2; + string nickname = 3; + int32 gender = 4; + string signature = 5; + int32 level = 6; + int64 birthday = 7; + string telephone = 8; + Image avatarThumb = 9; + Image avatarMedium = 10; + Image avatarLarge = 11; + bool verified = 12; + int32 experience = 13; + string city = 14; + int32 status = 15; + int64 createTime = 16; + int64 modifyTime = 17; + int32 secret = 18; + string shareQrcodeUri = 19; + int32 incomeSharePercent = 20; + Image badgeImageListList = 21; + FollowInfo followInfo = 22; + PayGrade payGrade = 23; + FansClub fansClub = 24; + Border border = 25; + string specialId = 26; + Image avatarBorder = 27; + Image medal = 28; + repeated Image realTimeIconsList = 29; + repeated Image newRealTimeIconsList = 30; + int64 topVipNo = 31; + UserAttr userAttr = 32; + OwnRoom ownRoom = 33; + int64 payScore = 34; + int64 ticketCount = 35; + AnchorInfo anchorInfo = 36; + int32 linkMicStats = 37; + string displayId = 38; + bool withCommercePermission = 39; + bool withFusionShopEntry = 40; + int64 totalRechargeDiamondCount = 41; + AnchorLevel webcastAnchorLevel = 42; + string verifiedContent = 43; + AuthorStats authorStats = 44; + repeated User topFansList = 45; + string secUid = 46; + int32 userRole = 47; + XiguaParams xiguaInfo = 48; + ActivityInfo activityReward = 49; + NobleLevelInfo nobleInfo = 50; + BrotherhoodInfo brotherhoodInfo = 51; + Image personalCard = 52; + AuthenticationInfo authenticationInfo = 53; + int32 authorizationInfo = 54; + int32 adversaryAuthorizationInfo = 55; + PoiInfo poiInfo = 56; + Image mediaBadgeImageListList = 57; + int32 adversaryUserStatus = 58; + UserVIPInfo userVipInfo = 59; + repeated int64 commerceWebcastConfigIdsList = 60; + Image badgeImageListV2List = 61; + IndustryCertification industryCertification = 62; + string locationCity = 63; + FansGroupInfo fansGroupInfo = 64; + string remarkName = 65; + int32 mysteryMan = 66; + string webRid = 67; + string desensitizedNickname = 68; + JAccreditInfo jAccreditInfo = 69; + Subscribe subscribe = 70; + bool isAnonymous = 71; + int32 consumeDiamondLevel = 72; + string webcastUid = 73; + ProfileStyleParams profileStyleParams = 74; + UserDressInfo userDressInfo = 75; + bool allowBeLocated = 1001; + bool allowFindByContacts = 1002; + bool allowOthersDownloadVideo = 1003; + bool allowOthersDownloadWhenSharingVideo = 1004; + bool allowShareShowProfile = 1005; + bool allowShowInGossip = 1006; + bool allowShowMyAction = 1007; + bool allowStrangeComment = 1008; + bool allowUnfollowerComment = 1009; + bool allowUseLinkmic = 1010; + AnchorLevel anchorLevel = 1011; + Image avatarJpg = 1012; + string bgImgUrl = 1013; + string birthdayDescription = 1014; + bool birthdayValid = 1015; + int32 blockStatus = 1016; + int32 commentRestrict = 1017; + string constellation = 1018; + int32 disableIchat = 1019; + int64 enableIchatImg = 1020; + int32 exp = 1021; + int64 fanTicketCount = 1022; + bool foldStrangerChat = 1023; + int64 followStatus = 1024; + bool hotsoonVerified = 1025; + string hotsoonVerifiedReason = 1026; + int32 ichatRestrictType = 1027; + string idStr = 1028; + bool isFollower = 1029; + bool isFollowing = 1030; + bool needProfileGuide = 1031; + int64 payScores = 1032; + bool pushCommentStatus = 1033; + bool pushDigg = 1034; + bool pushFollow = 1035; + bool pushFriendAction = 1036; + bool pushIchat = 1037; + bool pushStatus = 1038; + bool pushVideoPost = 1039; + bool pushVideoRecommend = 1040; + UserStats stats = 1041; + bool verifiedMobile = 1042; + string verifiedReason = 1043; + bool withCarManagementPermission = 1044; + int32 ageRange = 1045; + int64 watchDurationMonth = 1046; + + message ActivityInfo{ + + } + + message AnchorInfo { + + } + + message AnchorLevel{ + + } + + message AuthenticationInfo{ + + } + + message AuthorStats{ + + } + + message Border{ + + } + + message BrotherhoodInfo{ + + } + + message FansClub { + FansClubData data = 1; + map preferData = 2; + + message FansClubData { + string clubName = 1; + int32 level = 2; + int32 userFansClubStatus = 3; + UserBadge badge = 4; + repeated int64 availableGiftIds = 5; + int64 anchorId = 6; + + message UserBadge { + map icons = 1; + string title = 2; + } + + } + } + + message FansGroupInfo{ + + } + + message FollowInfo { + int64 followingCount = 1; + int64 followerCount = 2; + int64 followStatus = 3; + int64 pushStatus = 4; + string remarkName = 5; + } + + message JAccreditInfo{ + + } + + message NobleLevelInfo{ + + } + + message OwnRoom { + + } + + message PayGrade { + int64 totalDiamondCount = 1; + Image diamondIcon = 2; + string name = 3; + Image icon = 4; + string nextName = 5; + int64 level = 6; + Image nextIcon = 7; + int64 nextDiamond = 8; + int64 nowDiamond = 9; + int64 thisGradeMinDiamond = 10; + int64 thisGradeMaxDiamond = 11; + int64 payDiamondBak = 12; + string gradeDescribe = 13; + repeated GradeIcon gradeIconList = 14; + int64 screenChatType = 15; + Image imIcon = 16; + Image imIconWithLevel = 17; + Image liveIcon = 18; + Image newImIconWithLevel = 19; + Image newLiveIcon = 20; + int64 upgradeNeedConsume = 21; + string nextPrivileges = 22; + Image background = 23; + Image backgroundBack = 24; + int64 score = 25; + GradeBuffInfo buffInfo = 26; + string gradeBanner = 1001; + Image profileDialogBg = 1002; + Image profileDialogBgBack = 1003; + + message GradeIcon{ + Image icon = 1; + int64 iconDiamond = 2; + int64 level = 3; + string levelStr = 4; + } + + } + + message PoiInfo{ + + } + + message ProfileStyleParams{ + + } + + message Subscribe{ + + } + + message UserAttr{ + + } + + message UserDressInfo{ + + } + + message UserStats{ + + } + + message XiguaParams{ + + } +} + +message FollowInfo { + uint64 followingCount = 1; + uint64 followerCount = 2; + uint64 followStatus = 3; + uint64 pushStatus = 4; + string remarkName = 5; + string followerCountStr = 6; + string followingCountStr = 7; + +} + +message Image { + repeated string urlListList = 1; + string uri = 2; + uint64 height = 3; + uint64 width = 4; + string avgColor = 5; + uint32 imageType = 6; + string openWebUrl = 7; + ImageContent content = 8; + bool isAnimated = 9; + NinePatchSetting FlexSettingList = 10; + NinePatchSetting TextSettingList = 11; +} + +message NinePatchSetting { + repeated string settingListList = 1; +} + +message ImageContent { + string name = 1; + string fontColor = 2; + uint64 level = 3; + string alternativeText = 4; +} + +message PushFrame { + uint64 seqId = 1; + uint64 logId = 2; + uint64 service = 3; + uint64 method = 4; + repeated HeadersList headersList = 5; + string payloadEncoding = 6; + string payloadType = 7; + bytes payload = 8; + +} + +message kk { + uint32 k=14; +} + +message SendMessageBody { + string conversationId = 1; + uint32 conversationType = 2; + uint64 conversationShortId = 3; + string content = 4; + repeated ExtList ext = 5; + uint32 messageType = 6; + string ticket = 7; + string clientMessageId = 8; + +} + +message ExtList { + string key = 1; + string value = 2; +} + +message Rsp{ + int32 a = 1; + int32 b = 2; + int32 c = 3; + string d = 4; + int32 e = 5; + message F { + uint64 q1 = 1; + uint64 q3 = 3; + string q4 = 4; + uint64 q5 = 5; + } + F f = 6; + string g = 7; + uint64 h = 10; + uint64 i = 11; + uint64 j = 13; +} + +message PreMessage { + uint32 cmd = 1; + uint32 sequenceId = 2; + string sdkVersion = 3; + string token = 4; + uint32 refer = 5; + uint32 inboxType = 6; + string buildNumber = 7; + SendMessageBody sendMessageBody = 8; + string aa = 9; + string devicePlatform = 11; + repeated HeadersList headers = 15; + uint32 authType = 18; + string biz = 21; + string access = 22; +} + +message HeadersList { + string key = 1; + string value = 2; +} + +enum CommentTypeTag { + COMMENTTYPETAGUNKNOWN = 0; + COMMENTTYPETAGSTAR = 1; +} \ No newline at end of file diff --git a/f2/apps/douyin/proto/douyin_webcast_pb2.py b/f2/apps/douyin/proto/douyin_webcast_pb2.py new file mode 100644 index 00000000..8e8520fd --- /dev/null +++ b/f2/apps/douyin/proto/douyin_webcast_pb2.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: douyin_webcast.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x14\x64ouyin_webcast.proto\x12\x06\x64ouyin"\xe4\x02\n\x08Response\x12%\n\x0cmessagesList\x18\x01 \x03(\x0b\x32\x0f.douyin.Message\x12\x0e\n\x06\x63ursor\x18\x02 \x01(\t\x12\x15\n\rfetchInterval\x18\x03 \x01(\x04\x12\x0b\n\x03now\x18\x04 \x01(\x04\x12\x13\n\x0binternalExt\x18\x05 \x01(\t\x12\x11\n\tfetchType\x18\x06 \x01(\r\x12\x36\n\x0brouteParams\x18\x07 \x03(\x0b\x32!.douyin.Response.RouteParamsEntry\x12\x19\n\x11heartbeatDuration\x18\x08 \x01(\x04\x12\x0f\n\x07needAck\x18\t \x01(\x08\x12\x12\n\npushServer\x18\n \x01(\t\x12\x12\n\nliveCursor\x18\x0b \x01(\t\x12\x15\n\rhistoryNoMore\x18\x0c \x01(\x08\x1a\x32\n\x10RouteParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x9a\x01\n\x07Message\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\r\n\x05msgId\x18\x03 \x01(\x03\x12\x0f\n\x07msgType\x18\x04 \x01(\x05\x12\x0e\n\x06offset\x18\x05 \x01(\x03\x12\x15\n\rneedWrdsStore\x18\x06 \x01(\x08\x12\x13\n\x0bwrdsVersion\x18\x07 \x01(\x03\x12\x12\n\nwrdsSubKey\x18\x08 \x01(\t"\xca\x04\n\x0b\x43hatMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\x1a\n\x04user\x18\x02 \x01(\x0b\x32\x0c.douyin.User\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\x12\x17\n\x0fvisibleToSender\x18\x04 \x01(\x08\x12&\n\x0f\x62\x61\x63kgroundImage\x18\x05 \x01(\x0b\x32\r.douyin.Image\x12\x1b\n\x13\x66ullScreenTextColor\x18\x06 \x01(\t\x12(\n\x11\x62\x61\x63kgroundImageV2\x18\x07 \x01(\x0b\x32\r.douyin.Image\x12\x32\n\x10publicAreaCommon\x18\x08 \x01(\x0b\x32\x18.douyin.PublicAreaCommon\x12 \n\tgiftImage\x18\t \x01(\x0b\x32\r.douyin.Image\x12\x12\n\nagreeMsgId\x18\x0b \x01(\x04\x12\x15\n\rpriorityLevel\x18\x0c \x01(\r\x12\x38\n\x13landscapeAreaCommon\x18\r \x01(\x0b\x32\x1b.douyin.LandscapeAreaCommon\x12\x11\n\teventTime\x18\x0f \x01(\x04\x12\x12\n\nsendReview\x18\x10 \x01(\x08\x12\x14\n\x0c\x66romIntercom\x18\x11 \x01(\x08\x12\x1c\n\x14intercomHideUserCard\x18\x12 \x01(\x08\x12\x0e\n\x06\x63hatBy\x18\x14 \x01(\t\x12\x1e\n\x16individualChatPriority\x18\x15 \x01(\r\x12 \n\nrtfContent\x18\x16 \x01(\x0b\x32\x0c.douyin.Text"\xa1\x01\n\x13LandscapeAreaCommon\x12\x10\n\x08showHead\x18\x01 \x01(\x08\x12\x14\n\x0cshowNickname\x18\x02 \x01(\x08\x12\x15\n\rshowFontColor\x18\x03 \x01(\x08\x12\x16\n\x0e\x63olorValueList\x18\x04 \x03(\t\x12\x33\n\x13\x63ommentTypeTagsList\x18\x05 \x03(\x0e\x32\x16.douyin.CommentTypeTag"\x87\x03\n\x12RoomUserSeqMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\x38\n\tranksList\x18\x02 \x03(\x0b\x32%.douyin.RoomUserSeqMessageContributor\x12\r\n\x05total\x18\x03 \x01(\x03\x12\x0e\n\x06popStr\x18\x04 \x01(\t\x12\x38\n\tseatsList\x18\x05 \x03(\x0b\x32%.douyin.RoomUserSeqMessageContributor\x12\x12\n\npopularity\x18\x06 \x01(\x03\x12\x11\n\ttotalUser\x18\x07 \x01(\x03\x12\x14\n\x0ctotalUserStr\x18\x08 \x01(\t\x12\x10\n\x08totalStr\x18\t \x01(\t\x12\x1b\n\x13onlineUserForAnchor\x18\n \x01(\t\x12\x18\n\x10totalPvForAnchor\x18\x0b \x01(\t\x12\x17\n\x0fupRightStatsStr\x18\x0c \x01(\t\x12\x1f\n\x17upRightStatsStrComplete\x18\r \x01(\t"Y\n\x0bRoomMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x19\n\x11support_landscape\x18\x03 \x01(\x08"^\n\x11\x43ommonTextMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\x1a\n\x04user\x18\x02 \x01(\x0b\x32\x0c.douyin.User\x12\r\n\x05scene\x18\x03 \x01(\t"\x89\x01\n\x16UpdateFanTicketMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\x1e\n\x16roomFanTicketCountText\x18\x02 \x01(\t\x12\x1a\n\x12roomFanTicketCount\x18\x03 \x01(\x04\x12\x13\n\x0b\x66orceUpdate\x18\x04 \x01(\x08"\xa9\x01\n\x1dRoomUserSeqMessageContributor\x12\r\n\x05score\x18\x01 \x01(\x04\x12\x1a\n\x04user\x18\x02 \x01(\x0b\x32\x0c.douyin.User\x12\x0c\n\x04rank\x18\x03 \x01(\x04\x12\r\n\x05\x64\x65lta\x18\x04 \x01(\x04\x12\x10\n\x08isHidden\x18\x05 \x01(\x08\x12\x18\n\x10scoreDescription\x18\x06 \x01(\t\x12\x14\n\x0c\x65xactlyScore\x18\x07 \x01(\t"\x91\x07\n\x0bGiftMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\x0e\n\x06giftId\x18\x02 \x01(\x04\x12\x16\n\x0e\x66\x61nTicketCount\x18\x03 \x01(\x04\x12\x12\n\ngroupCount\x18\x04 \x01(\x04\x12\x13\n\x0brepeatCount\x18\x05 \x01(\x04\x12\x12\n\ncomboCount\x18\x06 \x01(\x04\x12\x1a\n\x04user\x18\x07 \x01(\x0b\x32\x0c.douyin.User\x12\x1c\n\x06toUser\x18\x08 \x01(\x0b\x32\x0c.douyin.User\x12\x11\n\trepeatEnd\x18\t \x01(\r\x12&\n\ntextEffect\x18\n \x01(\x0b\x32\x12.douyin.TextEffect\x12\x0f\n\x07groupId\x18\x0b \x01(\x04\x12\x17\n\x0fincomeTaskgifts\x18\x0c \x01(\x04\x12\x1a\n\x12roomFanTicketCount\x18\r \x01(\x04\x12(\n\x08priority\x18\x0e \x01(\x0b\x32\x16.douyin.GiftIMPriority\x12 \n\x04gift\x18\x0f \x01(\x0b\x32\x12.douyin.GiftStruct\x12\r\n\x05logId\x18\x10 \x01(\t\x12\x10\n\x08sendType\x18\x11 \x01(\x04\x12\x32\n\x10publicAreaCommon\x18\x12 \x01(\x0b\x32\x18.douyin.PublicAreaCommon\x12%\n\x0ftrayDisplayText\x18\x13 \x01(\x0b\x32\x0c.douyin.Text\x12\x1c\n\x14\x62\x61nnedDisplayEffects\x18\x14 \x01(\x04\x12&\n\x08trayInfo\x18\x15 \x01(\x0b\x32\x14.douyin.GiftTrayInfo\x12\x36\n\x12\x61ssetEffectMixInfo\x18\x16 \x01(\x0b\x32\x1a.douyin.AssetEffectMixInfo\x12\x16\n\x0e\x64isplayForSelf\x18\x19 \x01(\x08\x12\x18\n\x10interactGiftInfo\x18\x1a \x01(\t\x12\x13\n\x0b\x64iyItemInfo\x18\x1b \x01(\t\x12\x17\n\x0fminAssetSetList\x18\x1c \x03(\x04\x12\x12\n\ntotalCount\x18\x1d \x01(\x04\x12\x18\n\x10\x63lientGiftSource\x18\x1e \x01(\r\x12\x15\n\rtoUserIdsList\x18 \x03(\x04\x12\x10\n\x08sendTime\x18! \x01(\x04\x12\x1b\n\x13\x66orceDisplayEffects\x18" \x01(\x04\x12\x0f\n\x07traceId\x18# \x01(\t\x12\x17\n\x0f\x65\x66\x66\x65\x63tDisplayTs\x18$ \x01(\x04"\x14\n\x12\x41ssetEffectMixInfo"p\n\x0f\x46\x61nsClubMessage\x12"\n\ncommonInfo\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\x0c\n\x04type\x18\x02 \x01(\x05\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\x12\x1a\n\x04user\x18\x04 \x01(\x0b\x32\x0c.douyin.User"\xdc\x01\n\x0cGiftTrayInfo\x12%\n\x0ftrayDisplayText\x18\x01 \x01(\x0b\x32\x0c.douyin.Text\x12"\n\x0btrayBaseImg\x18\x02 \x01(\x0b\x32\r.douyin.Image\x12"\n\x0btrayHeadImg\x18\x03 \x01(\x0b\x32\r.douyin.Image\x12#\n\x0ctrayRightImg\x18\x04 \x01(\x0b\x32\r.douyin.Image\x12\x11\n\ttrayLevel\x18\x05 \x01(\x03\x12%\n\x0etrayDynamicImg\x18\x06 \x01(\x0b\x32\r.douyin.Image"\xe6\x0c\n\nGiftStruct\x12\x1c\n\x05image\x18\x01 \x01(\x0b\x32\r.douyin.Image\x12\x10\n\x08\x64\x65scribe\x18\x02 \x01(\t\x12\x0e\n\x06notify\x18\x03 \x01(\x08\x12\x10\n\x08\x64uration\x18\x04 \x01(\x03\x12\n\n\x02id\x18\x05 \x01(\x03\x12?\n\x0c\x66\x61nsclubInfo\x18\x06 \x01(\x0b\x32).douyin.GiftStruct.GiftStructFansClubInfo\x12\x12\n\nforLinkmic\x18\x07 \x01(\x08\x12\x0e\n\x06\x64oodle\x18\x08 \x01(\x08\x12\x13\n\x0b\x66orFansclub\x18\t \x01(\x08\x12\r\n\x05\x63ombo\x18\n \x01(\x08\x12\x0c\n\x04type\x18\x0b \x01(\x05\x12\x14\n\x0c\x64iamondCount\x18\x0c \x01(\x05\x12\x1a\n\x12isDisplayedOnPanel\x18\r \x01(\x05\x12\x17\n\x0fprimaryEffectId\x18\x0e \x01(\x03\x12$\n\rgiftLabelIcon\x18\x0f \x01(\x0b\x32\r.douyin.Image\x12\x0c\n\x04name\x18\x10 \x01(\t\x12\x0e\n\x06region\x18\x11 \x01(\t\x12\x0e\n\x06manual\x18\x12 \x01(\t\x12\x11\n\tforCustom\x18\x13 \x01(\x08\x12>\n\x0especialEffects\x18\x14 \x03(\x0b\x32&.douyin.GiftStruct.SpecialEffectsEntry\x12\x1b\n\x04icon\x18\x15 \x01(\x0b\x32\r.douyin.Image\x12\x12\n\nactionType\x18\x16 \x01(\x05\x12\x17\n\x0fwatermelonSeeds\x18\x17 \x01(\x05\x12\x12\n\ngoldEffect\x18\x18 \x01(\t\x12(\n\x04subs\x18\x19 \x03(\x0b\x32\x1a.douyin.LuckyMoneyGiftMeta\x12\x13\n\x0bgoldenBeans\x18\x1a \x01(\x03\x12\x12\n\nhonorLevel\x18\x1b \x01(\x03\x12\x10\n\x08itemType\x18\x1c \x01(\x05\x12\x11\n\tschemeUrl\x18\x1d \x01(\t\x12\x31\n\rgiftOperation\x18\x1e \x01(\x0b\x32\x1a.douyin.GiftPanelOperation\x12\x11\n\teventName\x18\x1f \x01(\t\x12\x12\n\nnobleLevel\x18 \x01(\x03\x12\x10\n\x08guideUrl\x18! \x01(\t\x12\x16\n\x0epunishMedicine\x18" \x01(\x08\x12\x11\n\tforPortal\x18# \x01(\x08\x12\x14\n\x0c\x62usinessText\x18$ \x01(\t\x12\x0f\n\x07\x63nyGift\x18% \x01(\x08\x12\r\n\x05\x61ppId\x18& \x01(\x03\x12\x10\n\x08vipLevel\x18\' \x01(\x03\x12\x0e\n\x06isGray\x18( \x01(\x08\x12\x15\n\rgraySchemeUrl\x18) \x01(\t\x12\x11\n\tgiftScene\x18* \x01(\x03\x12&\n\ngiftBanner\x18+ \x01(\x0b\x32\x12.douyin.GiftBanner\x12\x14\n\x0ctriggerWords\x18, \x03(\t\x12+\n\rgiftBuffInfos\x18- \x03(\x0b\x32\x14.douyin.GiftBuffInfo\x12\x18\n\x10\x66orFirstRecharge\x18. \x01(\x08\x12,\n\x15\x64ynamicImgForSelected\x18/ \x01(\x0b\x32\r.douyin.Image\x12\x17\n\x0f\x61\x66terSendAction\x18\x30 \x01(\x05\x12\x17\n\x0fgiftOfflineTime\x18\x31 \x01(\x03\x12\x12\n\ntopBarText\x18\x32 \x01(\t\x12%\n\x0etopRightAvatar\x18\x33 \x01(\x0b\x32\r.douyin.Image\x12\x17\n\x0f\x62\x61nnerSchemeUrl\x18\x34 \x01(\t\x12\x10\n\x08isLocked\x18\x35 \x01(\x08\x12\x14\n\x0creqExtraType\x18\x36 \x01(\x03\x12\x10\n\x08\x61ssetIds\x18\x37 \x03(\x03\x12\x30\n\x0fgiftPreviewInfo\x18\x38 \x01(\x0b\x32\x17.douyin.GiftPreviewInfo\x12 \n\x07giftTip\x18\x39 \x01(\x0b\x32\x0f.douyin.GiftTip\x12\x1b\n\x13needSweepLightCount\x18: \x01(\x05\x12(\n\tgroupInfo\x18; \x03(\x0b\x32\x15.douyin.GiftGroupInfo\x1a\x35\n\x13SpecialEffectsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03:\x02\x38\x01\x1a=\n\x16GiftStructFansClubInfo\x12\x10\n\x08minLevel\x18\x01 \x01(\x05\x12\x11\n\tinsertPos\x18\x02 \x01(\x05"\x14\n\x12LuckyMoneyGiftMeta"\x14\n\x12GiftPanelOperation"\x0c\n\nGiftBanner"\x0e\n\x0cGiftBuffInfo"\x11\n\x0fGiftPreviewInfo"\t\n\x07GiftTip"\x0f\n\rGiftGroupInfo"U\n\x0eGiftIMPriority\x12\x16\n\x0equeueSizesList\x18\x01 \x03(\x04\x12\x19\n\x11selfQueuePriority\x18\x02 \x01(\x04\x12\x10\n\x08priority\x18\x03 \x01(\x04"e\n\nTextEffect\x12*\n\x08portrait\x18\x01 \x01(\x0b\x32\x18.douyin.TextEffectDetail\x12+\n\tlandscape\x18\x02 \x01(\x0b\x32\x18.douyin.TextEffectDetail"\xb6\x02\n\x10TextEffectDetail\x12\x1a\n\x04text\x18\x01 \x01(\x0b\x32\x0c.douyin.Text\x12\x14\n\x0ctextFontSize\x18\x02 \x01(\r\x12!\n\nbackground\x18\x03 \x01(\x0b\x32\r.douyin.Image\x12\r\n\x05start\x18\x04 \x01(\r\x12\x10\n\x08\x64uration\x18\x05 \x01(\r\x12\t\n\x01x\x18\x06 \x01(\r\x12\t\n\x01y\x18\x07 \x01(\r\x12\r\n\x05width\x18\x08 \x01(\r\x12\x0e\n\x06height\x18\t \x01(\r\x12\x10\n\x08shadowDx\x18\n \x01(\r\x12\x10\n\x08shadowDy\x18\x0b \x01(\r\x12\x14\n\x0cshadowRadius\x18\x0c \x01(\r\x12\x13\n\x0bshadowColor\x18\r \x01(\t\x12\x13\n\x0bstrokeColor\x18\x0e \x01(\t\x12\x13\n\x0bstrokeWidth\x18\x0f \x01(\r"\xe9\x05\n\rMemberMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\x1a\n\x04user\x18\x02 \x01(\x0b\x32\x0c.douyin.User\x12\x13\n\x0bmemberCount\x18\x03 \x01(\x04\x12\x1e\n\x08operator\x18\x04 \x01(\x0b\x32\x0c.douyin.User\x12\x14\n\x0cisSetToAdmin\x18\x05 \x01(\x08\x12\x11\n\tisTopUser\x18\x06 \x01(\x08\x12\x11\n\trankScore\x18\x07 \x01(\x04\x12\x11\n\ttopUserNo\x18\x08 \x01(\x04\x12\x11\n\tenterType\x18\t \x01(\x04\x12\x0e\n\x06\x61\x63tion\x18\n \x01(\x04\x12\x19\n\x11\x61\x63tionDescription\x18\x0b \x01(\t\x12\x0e\n\x06userId\x18\x0c \x01(\x04\x12*\n\x0c\x65\x66\x66\x65\x63tConfig\x18\r \x01(\x0b\x32\x14.douyin.EffectConfig\x12\x0e\n\x06popStr\x18\x0e \x01(\t\x12/\n\x11\x65nterEffectConfig\x18\x0f \x01(\x0b\x32\x14.douyin.EffectConfig\x12&\n\x0f\x62\x61\x63kgroundImage\x18\x10 \x01(\x0b\x32\r.douyin.Image\x12(\n\x11\x62\x61\x63kgroundImageV2\x18\x11 \x01(\x0b\x32\r.douyin.Image\x12\'\n\x11\x61nchorDisplayText\x18\x12 \x01(\x0b\x32\x0c.douyin.Text\x12\x32\n\x10publicAreaCommon\x18\x13 \x01(\x0b\x32\x18.douyin.PublicAreaCommon\x12\x18\n\x10userEnterTipType\x18\x14 \x01(\x04\x12\x1a\n\x12\x61nchorEnterTipType\x18\x15 \x01(\x04\x12\x41\n\x0e\x62uriedPointMap\x18\x16 \x03(\x0b\x32).douyin.MemberMessage.BuriedPointMapEntry\x1a\x35\n\x13\x42uriedPointMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"n\n\x10PublicAreaCommon\x12 \n\tuserLabel\x18\x01 \x01(\x0b\x32\r.douyin.Image\x12\x19\n\x11userConsumeInRoom\x18\x02 \x01(\x04\x12\x1d\n\x15userSendGiftCntInRoom\x18\x03 \x01(\x04"\xb7\x06\n\x0c\x45\x66\x66\x65\x63tConfig\x12\x0c\n\x04type\x18\x01 \x01(\x04\x12\x1b\n\x04icon\x18\x02 \x01(\x0b\x32\r.douyin.Image\x12\x11\n\tavatarPos\x18\x03 \x01(\x04\x12\x1a\n\x04text\x18\x04 \x01(\x0b\x32\x0c.douyin.Text\x12\x1f\n\x08textIcon\x18\x05 \x01(\x0b\x32\r.douyin.Image\x12\x10\n\x08stayTime\x18\x06 \x01(\r\x12\x13\n\x0b\x61nimAssetId\x18\x07 \x01(\x04\x12\x1c\n\x05\x62\x61\x64ge\x18\x08 \x01(\x0b\x32\r.douyin.Image\x12\x1c\n\x14\x66lexSettingArrayList\x18\t \x03(\x04\x12&\n\x0ftextIconOverlay\x18\n \x01(\x0b\x32\r.douyin.Image\x12$\n\ranimatedBadge\x18\x0b \x01(\x0b\x32\r.douyin.Image\x12\x15\n\rhasSweepLight\x18\x0c \x01(\x08\x12 \n\x18textFlexSettingArrayList\x18\r \x03(\x04\x12\x19\n\x11\x63\x65nterAnimAssetId\x18\x0e \x01(\x04\x12#\n\x0c\x64ynamicImage\x18\x0f \x01(\x0b\x32\r.douyin.Image\x12\x34\n\x08\x65xtraMap\x18\x10 \x03(\x0b\x32".douyin.EffectConfig.ExtraMapEntry\x12\x16\n\x0emp4AnimAssetId\x18\x11 \x01(\x04\x12\x10\n\x08priority\x18\x12 \x01(\x04\x12\x13\n\x0bmaxWaitTime\x18\x13 \x01(\x04\x12\x0f\n\x07\x64ressId\x18\x14 \x01(\t\x12\x11\n\talignment\x18\x15 \x01(\x04\x12\x17\n\x0f\x61lignmentOffset\x18\x16 \x01(\x04\x12\x13\n\x0b\x65\x66\x66\x65\x63tScene\x18\x17 \x01(\t\x12@\n\x0epieceValuesMap\x18\x18 \x03(\x0b\x32(.douyin.EffectConfig.PieceValuesMapEntry\x1a/\n\rExtraMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aH\n\x13PieceValuesMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.douyin.TextPiece:\x02\x38\x01"|\n\x04Text\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x15\n\rdefaultPatter\x18\x02 \x01(\t\x12)\n\rdefaultFormat\x18\x03 \x01(\x0b\x32\x12.douyin.TextFormat\x12%\n\npiecesList\x18\x04 \x03(\x0b\x32\x11.douyin.TextPiece"\xb4\x02\n\tTextPiece\x12\x0c\n\x04type\x18\x01 \x01(\x08\x12"\n\x06\x66ormat\x18\x02 \x01(\x0b\x32\x12.douyin.TextFormat\x12\x13\n\x0bstringValue\x18\x03 \x01(\t\x12(\n\tuserValue\x18\x04 \x01(\x0b\x32\x15.douyin.TextPieceUser\x12(\n\tgiftValue\x18\x05 \x01(\x0b\x32\x15.douyin.TextPieceGift\x12*\n\nheartValue\x18\x06 \x01(\x0b\x32\x16.douyin.TextPieceHeart\x12\x34\n\x0fpatternRefValue\x18\x07 \x01(\x0b\x32\x1b.douyin.TextPiecePatternRef\x12*\n\nimageValue\x18\x08 \x01(\x0b\x32\x16.douyin.TextPieceImage"C\n\x0eTextPieceImage\x12\x1c\n\x05image\x18\x01 \x01(\x0b\x32\r.douyin.Image\x12\x13\n\x0bscalingRate\x18\x02 \x01(\x02":\n\x13TextPiecePatternRef\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x16\n\x0e\x64\x65\x66\x61ultPattern\x18\x02 \x01(\t"\x1f\n\x0eTextPieceHeart\x12\r\n\x05\x63olor\x18\x01 \x01(\t"D\n\rTextPieceGift\x12\x0e\n\x06giftId\x18\x01 \x01(\x04\x12#\n\x07nameRef\x18\x02 \x01(\x0b\x32\x12.douyin.PatternRef"1\n\nPatternRef\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x16\n\x0e\x64\x65\x66\x61ultPattern\x18\x02 \x01(\t">\n\rTextPieceUser\x12\x1a\n\x04user\x18\x01 \x01(\x0b\x32\x0c.douyin.User\x12\x11\n\twithColon\x18\x02 \x01(\x08"\xa3\x01\n\nTextFormat\x12\r\n\x05\x63olor\x18\x01 \x01(\t\x12\x0c\n\x04\x62old\x18\x02 \x01(\x08\x12\x0e\n\x06italic\x18\x03 \x01(\x08\x12\x0e\n\x06weight\x18\x04 \x01(\r\x12\x13\n\x0bitalicAngle\x18\x05 \x01(\r\x12\x10\n\x08\x66ontSize\x18\x06 \x01(\r\x12\x1a\n\x12useHeighLightColor\x18\x07 \x01(\x08\x12\x15\n\ruseRemoteClor\x18\x08 \x01(\x08"\xca\x02\n\x0bLikeMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\x12\r\n\x05total\x18\x03 \x01(\x04\x12\r\n\x05\x63olor\x18\x04 \x01(\x04\x12\x1a\n\x04user\x18\x05 \x01(\x0b\x32\x0c.douyin.User\x12\x0c\n\x04icon\x18\x06 \x01(\t\x12\x32\n\x10\x64oubleLikeDetail\x18\x07 \x01(\x0b\x32\x18.douyin.DoubleLikeDetail\x12\x36\n\x12\x64isplayControlInfo\x18\x08 \x01(\x0b\x32\x1a.douyin.DisplayControlInfo\x12\x17\n\x0flinkmicGuestUid\x18\t \x01(\x04\x12\r\n\x05scene\x18\n \x01(\t\x12\x30\n\x0fpicoDisplayInfo\x18\x0b \x01(\x0b\x32\x17.douyin.PicoDisplayInfo"\xcc\x01\n\rSocialMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12\x1a\n\x04user\x18\x02 \x01(\x0b\x32\x0c.douyin.User\x12\x11\n\tshareType\x18\x03 \x01(\x04\x12\x0e\n\x06\x61\x63tion\x18\x04 \x01(\x04\x12\x13\n\x0bshareTarget\x18\x05 \x01(\t\x12\x13\n\x0b\x66ollowCount\x18\x06 \x01(\x04\x12\x32\n\x10publicAreaCommon\x18\x07 \x01(\x0b\x32\x18.douyin.PublicAreaCommon"l\n\x0fPicoDisplayInfo\x12\x15\n\rcomboSumCount\x18\x01 \x01(\x04\x12\r\n\x05\x65moji\x18\x02 \x01(\t\x12 \n\temojiIcon\x18\x03 \x01(\x0b\x32\r.douyin.Image\x12\x11\n\temojiText\x18\x04 \x01(\t"_\n\x10\x44oubleLikeDetail\x12\x12\n\ndoubleFlag\x18\x01 \x01(\x08\x12\r\n\x05seqId\x18\x02 \x01(\r\x12\x13\n\x0brenewalsNum\x18\x03 \x01(\r\x12\x13\n\x0btriggersNum\x18\x04 \x01(\r"9\n\x12\x44isplayControlInfo\x12\x10\n\x08showText\x18\x01 \x01(\x08\x12\x11\n\tshowIcons\x18\x02 \x01(\x08"\xc8\x01\n\x12\x45pisodeChatMessage\x12\x1f\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0f.douyin.Message\x12\x1a\n\x04user\x18\x02 \x01(\x0b\x32\x0c.douyin.User\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\x12\x16\n\x0evisibleToSende\x18\x04 \x01(\x08\x12 \n\tgiftImage\x18\x07 \x01(\x0b\x32\r.douyin.Image\x12\x12\n\nagreeMsgId\x18\x08 \x01(\x04\x12\x16\n\x0e\x63olorValueList\x18\t \x03(\t"\x88\x01\n\x18MatchAgainstScoreMessage\x12\x1e\n\x06\x63ommon\x18\x01 \x01(\x0b\x32\x0e.douyin.Common\x12 \n\x07\x61gainst\x18\x02 \x01(\x0b\x32\x0f.douyin.Against\x12\x13\n\x0bmatchStatus\x18\x03 \x01(\r\x12\x15\n\rdisplayStatus\x18\x04 \x01(\r"\x92\x03\n\x07\x41gainst\x12\x10\n\x08leftName\x18\x01 \x01(\t\x12\x1f\n\x08leftLogo\x18\x02 \x01(\x0b\x32\r.douyin.Image\x12\x10\n\x08leftGoal\x18\x03 \x01(\t\x12\x11\n\trightName\x18\x06 \x01(\t\x12 \n\trightLogo\x18\x07 \x01(\x0b\x32\r.douyin.Image\x12\x11\n\trightGoal\x18\x08 \x01(\t\x12\x11\n\ttimestamp\x18\x0b \x01(\x04\x12\x0f\n\x07version\x18\x0c \x01(\x04\x12\x12\n\nleftTeamId\x18\r \x01(\x04\x12\x13\n\x0brightTeamId\x18\x0e \x01(\x04\x12\x19\n\x11\x64iffSei2absSecond\x18\x0f \x01(\x04\x12\x16\n\x0e\x66inalGoalStage\x18\x10 \x01(\r\x12\x18\n\x10\x63urrentGoalStage\x18\x11 \x01(\r\x12\x19\n\x11leftScoreAddition\x18\x12 \x01(\r\x12\x1a\n\x12rightScoreAddition\x18\x13 \x01(\r\x12\x13\n\x0bleftGoalInt\x18\x14 \x01(\x04\x12\x14\n\x0crightGoalInt\x18\x15 \x01(\x04"\x90\x04\n\x06\x43ommon\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\r\n\x05msgId\x18\x02 \x01(\x04\x12\x0e\n\x06roomId\x18\x03 \x01(\x04\x12\x12\n\ncreateTime\x18\x04 \x01(\x04\x12\x0f\n\x07monitor\x18\x05 \x01(\r\x12\x11\n\tisShowMsg\x18\x06 \x01(\x08\x12\x10\n\x08\x64\x65scribe\x18\x07 \x01(\t\x12!\n\x0b\x64isplayText\x18\x08 \x01(\x0b\x32\x0c.douyin.Text\x12\x10\n\x08\x66oldType\x18\t \x01(\x04\x12\x16\n\x0e\x61nchorFoldType\x18\n \x01(\x04\x12\x15\n\rpriorityScore\x18\x0b \x01(\x04\x12\r\n\x05logId\x18\x0c \x01(\t\x12\x19\n\x11msgProcessFilterK\x18\r \x01(\t\x12\x19\n\x11msgProcessFilterV\x18\x0e \x01(\t\x12\x1a\n\x04user\x18\x0f \x01(\x0b\x32\x0c.douyin.User\x12\x1a\n\x04room\x18\x10 \x01(\x0b\x32\x0c.douyin.Room\x12\x18\n\x10\x61nchorFoldTypeV2\x18\x11 \x01(\x04\x12\x1a\n\x12processAtSeiTimeMs\x18\x12 \x01(\x04\x12\x18\n\x10randomDispatchMs\x18\x13 \x01(\x04\x12\x12\n\nisDispatch\x18\x14 \x01(\x08\x12\x11\n\tchannelId\x18\x15 \x01(\x04\x12\x19\n\x11\x64iffSei2absSecond\x18\x16 \x01(\x04\x12\x1a\n\x12\x61nchorFoldDuration\x18\x17 \x01(\x04"\xed\t\n\x04Room\x12\n\n\x02id\x18\x01 \x01(\x03\x12\r\n\x05idStr\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x03\x12\x13\n\x0bownerUserId\x18\x04 \x01(\x03\x12\r\n\x05title\x18\x05 \x01(\t\x12\x11\n\tuserCount\x18\x06 \x01(\x03\x12\x12\n\ncreateTime\x18\x07 \x01(\x03\x12\x15\n\rlinkmicLayout\x18\x08 \x01(\x03\x12\x12\n\nfinishTime\x18\t \x01(\x03\x12 \n\x05\x65xtra\x18\n \x01(\x0b\x32\x11.douyin.RoomExtra\x12\x17\n\x0f\x64ynamicCoverUri\x18\x0b \x01(\t\x12<\n\x10\x64ynamicCoverDict\x18\x0c \x03(\x0b\x32".douyin.Room.DynamicCoverDictEntry\x12\x14\n\x0clastPingTime\x18\r \x01(\x03\x12\x0e\n\x06liveId\x18\x0e \x01(\x03\x12\x16\n\x0estreamProvider\x18\x0f \x01(\x03\x12\x0e\n\x06osType\x18\x10 \x01(\x03\x12\x15\n\rclientVersion\x18\x11 \x01(\x03\x12\x13\n\x0bwithLinkmic\x18\x12 \x01(\x08\x12\x1d\n\x15\x65nableRoomPerspective\x18\x13 \x01(\x08\x12\x1c\n\x05\x63over\x18\x14 \x01(\x0b\x32\r.douyin.Image\x12#\n\x0c\x64ynamicCover\x18\x15 \x01(\x0b\x32\r.douyin.Image\x12&\n\x0f\x64ynamicCoverLow\x18\x16 \x01(\x0b\x32\r.douyin.Image\x12\x10\n\x08shareUrl\x18\x17 \x01(\t\x12\x17\n\x0f\x61nchorShareText\x18\x18 \x01(\t\x12\x15\n\ruserShareText\x18\x19 \x01(\t\x12\x10\n\x08streamId\x18\x1a \x01(\x03\x12\x13\n\x0bstreamIdStr\x18\x1b \x01(\t\x12$\n\tstreamUrl\x18\x1c \x01(\x0b\x32\x11.douyin.StreamUrl\x12\x14\n\x0cmosaicStatus\x18\x1d \x01(\x03\x12\x11\n\tmosaicTip\x18\x1e \x01(\t\x12\x11\n\tcellStyle\x18\x1f \x01(\x03\x12 \n\x07linkMic\x18 \x01(\x0b\x32\x0f.douyin.LinkMic\x12\x15\n\rluckymoneyNum\x18! \x01(\x03\x12$\n\x08\x64\x65\x63oList\x18" \x03(\x0b\x32\x12.douyin.Decoration\x12\x1f\n\x07topFans\x18# \x03(\x0b\x32\x0e.douyin.TopFan\x12 \n\x05stats\x18$ \x01(\x0b\x32\x11.douyin.RoomStats\x12\x1b\n\x13sunDailyIconContent\x18% \x01(\t\x12\x10\n\x08\x64istance\x18& \x01(\t\x12\x14\n\x0c\x64istanceCity\x18\' \x01(\t\x12\x10\n\x08location\x18( \x01(\t\x12\x14\n\x0crealDistance\x18) \x01(\t\x12$\n\rfeedRoomLabel\x18* \x01(\x0b\x32\r.douyin.Image\x12\x17\n\x0f\x63ommonLabelList\x18+ \x01(\t\x12-\n\x0flivingRoomAttrs\x18, \x01(\x0b\x32\x14.douyin.RoomUserAttr\x12\x14\n\x0c\x61\x64minUserIds\x18- \x03(\x03\x12\x1b\n\x05owner\x18. \x01(\x0b\x32\x0c.douyin.User\x12\x13\n\x0bprivateInfo\x18/ \x01(\t\x1a\x37\n\x15\x44ynamicCoverDictEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"\x0b\n\tRoomExtra"\x0b\n\tRoomStats"\x0e\n\x0cRoomUserAttr"\x0b\n\tStreamUrl"\t\n\x07LinkMic"\x0c\n\nDecoration"\x08\n\x06TopFan"\xd9\x01\n\rGradeBuffInfo\x12\x11\n\tbuffLevel\x18\x01 \x01(\x03\x12\x0e\n\x06status\x18\x02 \x01(\x05\x12\x0f\n\x07\x65ndTime\x18\x03 \x01(\x03\x12=\n\x0cstatsInfoMap\x18\x04 \x03(\x0b\x32\'.douyin.GradeBuffInfo.StatsInfoMapEntry\x12 \n\tbuffBadge\x18\x05 \x01(\x0b\x32\r.douyin.Image\x1a\x33\n\x11StatsInfoMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x03:\x02\x38\x01"\r\n\x0bUserVIPInfo"\x17\n\x15IndustryCertification"\xd1+\n\x04User\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x0f\n\x07shortId\x18\x02 \x01(\x03\x12\x10\n\x08nickname\x18\x03 \x01(\t\x12\x0e\n\x06gender\x18\x04 \x01(\x05\x12\x11\n\tsignature\x18\x05 \x01(\t\x12\r\n\x05level\x18\x06 \x01(\x05\x12\x10\n\x08\x62irthday\x18\x07 \x01(\x03\x12\x11\n\ttelephone\x18\x08 \x01(\t\x12"\n\x0b\x61vatarThumb\x18\t \x01(\x0b\x32\r.douyin.Image\x12#\n\x0c\x61vatarMedium\x18\n \x01(\x0b\x32\r.douyin.Image\x12"\n\x0b\x61vatarLarge\x18\x0b \x01(\x0b\x32\r.douyin.Image\x12\x10\n\x08verified\x18\x0c \x01(\x08\x12\x12\n\nexperience\x18\r \x01(\x05\x12\x0c\n\x04\x63ity\x18\x0e \x01(\t\x12\x0e\n\x06status\x18\x0f \x01(\x05\x12\x12\n\ncreateTime\x18\x10 \x01(\x03\x12\x12\n\nmodifyTime\x18\x11 \x01(\x03\x12\x0e\n\x06secret\x18\x12 \x01(\x05\x12\x16\n\x0eshareQrcodeUri\x18\x13 \x01(\t\x12\x1a\n\x12incomeSharePercent\x18\x14 \x01(\x05\x12)\n\x12\x62\x61\x64geImageListList\x18\x15 \x01(\x0b\x32\r.douyin.Image\x12+\n\nfollowInfo\x18\x16 \x01(\x0b\x32\x17.douyin.User.FollowInfo\x12\'\n\x08payGrade\x18\x17 \x01(\x0b\x32\x15.douyin.User.PayGrade\x12\'\n\x08\x66\x61nsClub\x18\x18 \x01(\x0b\x32\x15.douyin.User.FansClub\x12#\n\x06\x62order\x18\x19 \x01(\x0b\x32\x13.douyin.User.Border\x12\x11\n\tspecialId\x18\x1a \x01(\t\x12#\n\x0c\x61vatarBorder\x18\x1b \x01(\x0b\x32\r.douyin.Image\x12\x1c\n\x05medal\x18\x1c \x01(\x0b\x32\r.douyin.Image\x12(\n\x11realTimeIconsList\x18\x1d \x03(\x0b\x32\r.douyin.Image\x12+\n\x14newRealTimeIconsList\x18\x1e \x03(\x0b\x32\r.douyin.Image\x12\x10\n\x08topVipNo\x18\x1f \x01(\x03\x12\'\n\x08userAttr\x18 \x01(\x0b\x32\x15.douyin.User.UserAttr\x12%\n\x07ownRoom\x18! \x01(\x0b\x32\x14.douyin.User.OwnRoom\x12\x10\n\x08payScore\x18" \x01(\x03\x12\x13\n\x0bticketCount\x18# \x01(\x03\x12+\n\nanchorInfo\x18$ \x01(\x0b\x32\x17.douyin.User.AnchorInfo\x12\x14\n\x0clinkMicStats\x18% \x01(\x05\x12\x11\n\tdisplayId\x18& \x01(\t\x12\x1e\n\x16withCommercePermission\x18\' \x01(\x08\x12\x1b\n\x13withFusionShopEntry\x18( \x01(\x08\x12!\n\x19totalRechargeDiamondCount\x18) \x01(\x03\x12\x34\n\x12webcastAnchorLevel\x18* \x01(\x0b\x32\x18.douyin.User.AnchorLevel\x12\x17\n\x0fverifiedContent\x18+ \x01(\t\x12-\n\x0b\x61uthorStats\x18, \x01(\x0b\x32\x18.douyin.User.AuthorStats\x12!\n\x0btopFansList\x18- \x03(\x0b\x32\x0c.douyin.User\x12\x0e\n\x06secUid\x18. \x01(\t\x12\x10\n\x08userRole\x18/ \x01(\x05\x12+\n\txiguaInfo\x18\x30 \x01(\x0b\x32\x18.douyin.User.XiguaParams\x12\x31\n\x0e\x61\x63tivityReward\x18\x31 \x01(\x0b\x32\x19.douyin.User.ActivityInfo\x12.\n\tnobleInfo\x18\x32 \x01(\x0b\x32\x1b.douyin.User.NobleLevelInfo\x12\x35\n\x0f\x62rotherhoodInfo\x18\x33 \x01(\x0b\x32\x1c.douyin.User.BrotherhoodInfo\x12#\n\x0cpersonalCard\x18\x34 \x01(\x0b\x32\r.douyin.Image\x12;\n\x12\x61uthenticationInfo\x18\x35 \x01(\x0b\x32\x1f.douyin.User.AuthenticationInfo\x12\x19\n\x11\x61uthorizationInfo\x18\x36 \x01(\x05\x12"\n\x1a\x61\x64versaryAuthorizationInfo\x18\x37 \x01(\x05\x12%\n\x07poiInfo\x18\x38 \x01(\x0b\x32\x14.douyin.User.PoiInfo\x12.\n\x17mediaBadgeImageListList\x18\x39 \x01(\x0b\x32\r.douyin.Image\x12\x1b\n\x13\x61\x64versaryUserStatus\x18: \x01(\x05\x12(\n\x0buserVipInfo\x18; \x01(\x0b\x32\x13.douyin.UserVIPInfo\x12$\n\x1c\x63ommerceWebcastConfigIdsList\x18< \x03(\x03\x12+\n\x14\x62\x61\x64geImageListV2List\x18= \x01(\x0b\x32\r.douyin.Image\x12<\n\x15industryCertification\x18> \x01(\x0b\x32\x1d.douyin.IndustryCertification\x12\x14\n\x0clocationCity\x18? \x01(\t\x12\x31\n\rfansGroupInfo\x18@ \x01(\x0b\x32\x1a.douyin.User.FansGroupInfo\x12\x12\n\nremarkName\x18\x41 \x01(\t\x12\x12\n\nmysteryMan\x18\x42 \x01(\x05\x12\x0e\n\x06webRid\x18\x43 \x01(\t\x12\x1c\n\x14\x64\x65sensitizedNickname\x18\x44 \x01(\t\x12\x31\n\rjAccreditInfo\x18\x45 \x01(\x0b\x32\x1a.douyin.User.JAccreditInfo\x12)\n\tsubscribe\x18\x46 \x01(\x0b\x32\x16.douyin.User.Subscribe\x12\x13\n\x0bisAnonymous\x18G \x01(\x08\x12\x1b\n\x13\x63onsumeDiamondLevel\x18H \x01(\x05\x12\x12\n\nwebcastUid\x18I \x01(\t\x12;\n\x12profileStyleParams\x18J \x01(\x0b\x32\x1f.douyin.User.ProfileStyleParams\x12\x31\n\ruserDressInfo\x18K \x01(\x0b\x32\x1a.douyin.User.UserDressInfo\x12\x17\n\x0e\x61llowBeLocated\x18\xe9\x07 \x01(\x08\x12\x1c\n\x13\x61llowFindByContacts\x18\xea\x07 \x01(\x08\x12!\n\x18\x61llowOthersDownloadVideo\x18\xeb\x07 \x01(\x08\x12,\n#allowOthersDownloadWhenSharingVideo\x18\xec\x07 \x01(\x08\x12\x1e\n\x15\x61llowShareShowProfile\x18\xed\x07 \x01(\x08\x12\x1a\n\x11\x61llowShowInGossip\x18\xee\x07 \x01(\x08\x12\x1a\n\x11\x61llowShowMyAction\x18\xef\x07 \x01(\x08\x12\x1c\n\x13\x61llowStrangeComment\x18\xf0\x07 \x01(\x08\x12\x1f\n\x16\x61llowUnfollowerComment\x18\xf1\x07 \x01(\x08\x12\x18\n\x0f\x61llowUseLinkmic\x18\xf2\x07 \x01(\x08\x12.\n\x0b\x61nchorLevel\x18\xf3\x07 \x01(\x0b\x32\x18.douyin.User.AnchorLevel\x12!\n\tavatarJpg\x18\xf4\x07 \x01(\x0b\x32\r.douyin.Image\x12\x11\n\x08\x62gImgUrl\x18\xf5\x07 \x01(\t\x12\x1c\n\x13\x62irthdayDescription\x18\xf6\x07 \x01(\t\x12\x16\n\rbirthdayValid\x18\xf7\x07 \x01(\x08\x12\x14\n\x0b\x62lockStatus\x18\xf8\x07 \x01(\x05\x12\x18\n\x0f\x63ommentRestrict\x18\xf9\x07 \x01(\x05\x12\x16\n\rconstellation\x18\xfa\x07 \x01(\t\x12\x15\n\x0c\x64isableIchat\x18\xfb\x07 \x01(\x05\x12\x17\n\x0e\x65nableIchatImg\x18\xfc\x07 \x01(\x03\x12\x0c\n\x03\x65xp\x18\xfd\x07 \x01(\x05\x12\x17\n\x0e\x66\x61nTicketCount\x18\xfe\x07 \x01(\x03\x12\x19\n\x10\x66oldStrangerChat\x18\xff\x07 \x01(\x08\x12\x15\n\x0c\x66ollowStatus\x18\x80\x08 \x01(\x03\x12\x18\n\x0fhotsoonVerified\x18\x81\x08 \x01(\x08\x12\x1e\n\x15hotsoonVerifiedReason\x18\x82\x08 \x01(\t\x12\x1a\n\x11ichatRestrictType\x18\x83\x08 \x01(\x05\x12\x0e\n\x05idStr\x18\x84\x08 \x01(\t\x12\x13\n\nisFollower\x18\x85\x08 \x01(\x08\x12\x14\n\x0bisFollowing\x18\x86\x08 \x01(\x08\x12\x19\n\x10needProfileGuide\x18\x87\x08 \x01(\x08\x12\x12\n\tpayScores\x18\x88\x08 \x01(\x03\x12\x1a\n\x11pushCommentStatus\x18\x89\x08 \x01(\x08\x12\x11\n\x08pushDigg\x18\x8a\x08 \x01(\x08\x12\x13\n\npushFollow\x18\x8b\x08 \x01(\x08\x12\x19\n\x10pushFriendAction\x18\x8c\x08 \x01(\x08\x12\x12\n\tpushIchat\x18\x8d\x08 \x01(\x08\x12\x13\n\npushStatus\x18\x8e\x08 \x01(\x08\x12\x16\n\rpushVideoPost\x18\x8f\x08 \x01(\x08\x12\x1b\n\x12pushVideoRecommend\x18\x90\x08 \x01(\x08\x12&\n\x05stats\x18\x91\x08 \x01(\x0b\x32\x16.douyin.User.UserStats\x12\x17\n\x0everifiedMobile\x18\x92\x08 \x01(\x08\x12\x17\n\x0everifiedReason\x18\x93\x08 \x01(\t\x12$\n\x1bwithCarManagementPermission\x18\x94\x08 \x01(\x08\x12\x11\n\x08\x61geRange\x18\x95\x08 \x01(\x05\x12\x1b\n\x12watchDurationMonth\x18\x96\x08 \x01(\x03\x1a\x0e\n\x0c\x41\x63tivityInfo\x1a\x0c\n\nAnchorInfo\x1a\r\n\x0b\x41nchorLevel\x1a\x14\n\x12\x41uthenticationInfo\x1a\r\n\x0b\x41uthorStats\x1a\x08\n\x06\x42order\x1a\x11\n\x0f\x42rotherhoodInfo\x1a\xa7\x04\n\x08\x46\x61nsClub\x12\x30\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32".douyin.User.FansClub.FansClubData\x12\x39\n\npreferData\x18\x02 \x03(\x0b\x32%.douyin.User.FansClub.PreferDataEntry\x1aU\n\x0fPreferDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\x31\n\x05value\x18\x02 \x01(\x0b\x32".douyin.User.FansClub.FansClubData:\x02\x38\x01\x1a\xd6\x02\n\x0c\x46\x61nsClubData\x12\x10\n\x08\x63lubName\x18\x01 \x01(\t\x12\r\n\x05level\x18\x02 \x01(\x05\x12\x1a\n\x12userFansClubStatus\x18\x03 \x01(\x05\x12;\n\x05\x62\x61\x64ge\x18\x04 \x01(\x0b\x32,.douyin.User.FansClub.FansClubData.UserBadge\x12\x18\n\x10\x61vailableGiftIds\x18\x05 \x03(\x03\x12\x10\n\x08\x61nchorId\x18\x06 \x01(\x03\x1a\x9f\x01\n\tUserBadge\x12\x46\n\x05icons\x18\x01 \x03(\x0b\x32\x37.douyin.User.FansClub.FansClubData.UserBadge.IconsEntry\x12\r\n\x05title\x18\x02 \x01(\t\x1a;\n\nIconsEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\x1c\n\x05value\x18\x02 \x01(\x0b\x32\r.douyin.Image:\x02\x38\x01\x1a\x0f\n\rFansGroupInfo\x1ay\n\nFollowInfo\x12\x16\n\x0e\x66ollowingCount\x18\x01 \x01(\x03\x12\x15\n\rfollowerCount\x18\x02 \x01(\x03\x12\x14\n\x0c\x66ollowStatus\x18\x03 \x01(\x03\x12\x12\n\npushStatus\x18\x04 \x01(\x03\x12\x12\n\nremarkName\x18\x05 \x01(\t\x1a\x0f\n\rJAccreditInfo\x1a\x10\n\x0eNobleLevelInfo\x1a\t\n\x07OwnRoom\x1a\xd0\x07\n\x08PayGrade\x12\x19\n\x11totalDiamondCount\x18\x01 \x01(\x03\x12"\n\x0b\x64iamondIcon\x18\x02 \x01(\x0b\x32\r.douyin.Image\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x1b\n\x04icon\x18\x04 \x01(\x0b\x32\r.douyin.Image\x12\x10\n\x08nextName\x18\x05 \x01(\t\x12\r\n\x05level\x18\x06 \x01(\x03\x12\x1f\n\x08nextIcon\x18\x07 \x01(\x0b\x32\r.douyin.Image\x12\x13\n\x0bnextDiamond\x18\x08 \x01(\x03\x12\x12\n\nnowDiamond\x18\t \x01(\x03\x12\x1b\n\x13thisGradeMinDiamond\x18\n \x01(\x03\x12\x1b\n\x13thisGradeMaxDiamond\x18\x0b \x01(\x03\x12\x15\n\rpayDiamondBak\x18\x0c \x01(\x03\x12\x15\n\rgradeDescribe\x18\r \x01(\t\x12\x36\n\rgradeIconList\x18\x0e \x03(\x0b\x32\x1f.douyin.User.PayGrade.GradeIcon\x12\x16\n\x0escreenChatType\x18\x0f \x01(\x03\x12\x1d\n\x06imIcon\x18\x10 \x01(\x0b\x32\r.douyin.Image\x12&\n\x0fimIconWithLevel\x18\x11 \x01(\x0b\x32\r.douyin.Image\x12\x1f\n\x08liveIcon\x18\x12 \x01(\x0b\x32\r.douyin.Image\x12)\n\x12newImIconWithLevel\x18\x13 \x01(\x0b\x32\r.douyin.Image\x12"\n\x0bnewLiveIcon\x18\x14 \x01(\x0b\x32\r.douyin.Image\x12\x1a\n\x12upgradeNeedConsume\x18\x15 \x01(\x03\x12\x16\n\x0enextPrivileges\x18\x16 \x01(\t\x12!\n\nbackground\x18\x17 \x01(\x0b\x32\r.douyin.Image\x12%\n\x0e\x62\x61\x63kgroundBack\x18\x18 \x01(\x0b\x32\r.douyin.Image\x12\r\n\x05score\x18\x19 \x01(\x03\x12\'\n\x08\x62uffInfo\x18\x1a \x01(\x0b\x32\x15.douyin.GradeBuffInfo\x12\x14\n\x0bgradeBanner\x18\xe9\x07 \x01(\t\x12\'\n\x0fprofileDialogBg\x18\xea\x07 \x01(\x0b\x32\r.douyin.Image\x12+\n\x13profileDialogBgBack\x18\xeb\x07 \x01(\x0b\x32\r.douyin.Image\x1a^\n\tGradeIcon\x12\x1b\n\x04icon\x18\x01 \x01(\x0b\x32\r.douyin.Image\x12\x13\n\x0biconDiamond\x18\x02 \x01(\x03\x12\r\n\x05level\x18\x03 \x01(\x03\x12\x10\n\x08levelStr\x18\x04 \x01(\t\x1a\t\n\x07PoiInfo\x1a\x14\n\x12ProfileStyleParams\x1a\x0b\n\tSubscribe\x1a\n\n\x08UserAttr\x1a\x0f\n\rUserDressInfo\x1a\x0b\n\tUserStats\x1a\r\n\x0bXiguaParams"\xae\x01\n\nFollowInfo\x12\x16\n\x0e\x66ollowingCount\x18\x01 \x01(\x04\x12\x15\n\rfollowerCount\x18\x02 \x01(\x04\x12\x14\n\x0c\x66ollowStatus\x18\x03 \x01(\x04\x12\x12\n\npushStatus\x18\x04 \x01(\x04\x12\x12\n\nremarkName\x18\x05 \x01(\t\x12\x18\n\x10\x66ollowerCountStr\x18\x06 \x01(\t\x12\x19\n\x11\x66ollowingCountStr\x18\x07 \x01(\t"\xa2\x02\n\x05Image\x12\x13\n\x0burlListList\x18\x01 \x03(\t\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x0e\n\x06height\x18\x03 \x01(\x04\x12\r\n\x05width\x18\x04 \x01(\x04\x12\x10\n\x08\x61vgColor\x18\x05 \x01(\t\x12\x11\n\timageType\x18\x06 \x01(\r\x12\x12\n\nopenWebUrl\x18\x07 \x01(\t\x12%\n\x07\x63ontent\x18\x08 \x01(\x0b\x32\x14.douyin.ImageContent\x12\x12\n\nisAnimated\x18\t \x01(\x08\x12\x31\n\x0f\x46lexSettingList\x18\n \x01(\x0b\x32\x18.douyin.NinePatchSetting\x12\x31\n\x0fTextSettingList\x18\x0b \x01(\x0b\x32\x18.douyin.NinePatchSetting"+\n\x10NinePatchSetting\x12\x17\n\x0fsettingListList\x18\x01 \x03(\t"W\n\x0cImageContent\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\tfontColor\x18\x02 \x01(\t\x12\r\n\x05level\x18\x03 \x01(\x04\x12\x17\n\x0f\x61lternativeText\x18\x04 \x01(\t"\xb3\x01\n\tPushFrame\x12\r\n\x05seqId\x18\x01 \x01(\x04\x12\r\n\x05logId\x18\x02 \x01(\x04\x12\x0f\n\x07service\x18\x03 \x01(\x04\x12\x0e\n\x06method\x18\x04 \x01(\x04\x12(\n\x0bheadersList\x18\x05 \x03(\x0b\x32\x13.douyin.HeadersList\x12\x17\n\x0fpayloadEncoding\x18\x06 \x01(\t\x12\x13\n\x0bpayloadType\x18\x07 \x01(\t\x12\x0f\n\x07payload\x18\x08 \x01(\x0c"\x0f\n\x02kk\x12\t\n\x01k\x18\x0e \x01(\r"\xcd\x01\n\x0fSendMessageBody\x12\x16\n\x0e\x63onversationId\x18\x01 \x01(\t\x12\x18\n\x10\x63onversationType\x18\x02 \x01(\r\x12\x1b\n\x13\x63onversationShortId\x18\x03 \x01(\x04\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\t\x12\x1c\n\x03\x65xt\x18\x05 \x03(\x0b\x32\x0f.douyin.ExtList\x12\x13\n\x0bmessageType\x18\x06 \x01(\r\x12\x0e\n\x06ticket\x18\x07 \x01(\t\x12\x17\n\x0f\x63lientMessageId\x18\x08 \x01(\t"%\n\x07\x45xtList\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t"\xb7\x01\n\x03Rsp\x12\t\n\x01\x61\x18\x01 \x01(\x05\x12\t\n\x01\x62\x18\x02 \x01(\x05\x12\t\n\x01\x63\x18\x03 \x01(\x05\x12\t\n\x01\x64\x18\x04 \x01(\t\x12\t\n\x01\x65\x18\x05 \x01(\x05\x12\x18\n\x01\x66\x18\x06 \x01(\x0b\x32\r.douyin.Rsp.F\x12\t\n\x01g\x18\x07 \x01(\t\x12\t\n\x01h\x18\n \x01(\x04\x12\t\n\x01i\x18\x0b \x01(\x04\x12\t\n\x01j\x18\r \x01(\x04\x1a\x33\n\x01\x46\x12\n\n\x02q1\x18\x01 \x01(\x04\x12\n\n\x02q3\x18\x03 \x01(\x04\x12\n\n\x02q4\x18\x04 \x01(\t\x12\n\n\x02q5\x18\x05 \x01(\x04"\xb2\x02\n\nPreMessage\x12\x0b\n\x03\x63md\x18\x01 \x01(\r\x12\x12\n\nsequenceId\x18\x02 \x01(\r\x12\x12\n\nsdkVersion\x18\x03 \x01(\t\x12\r\n\x05token\x18\x04 \x01(\t\x12\r\n\x05refer\x18\x05 \x01(\r\x12\x11\n\tinboxType\x18\x06 \x01(\r\x12\x13\n\x0b\x62uildNumber\x18\x07 \x01(\t\x12\x30\n\x0fsendMessageBody\x18\x08 \x01(\x0b\x32\x17.douyin.SendMessageBody\x12\n\n\x02\x61\x61\x18\t \x01(\t\x12\x16\n\x0e\x64\x65vicePlatform\x18\x0b \x01(\t\x12$\n\x07headers\x18\x0f \x03(\x0b\x32\x13.douyin.HeadersList\x12\x10\n\x08\x61uthType\x18\x12 \x01(\r\x12\x0b\n\x03\x62iz\x18\x15 \x01(\t\x12\x0e\n\x06\x61\x63\x63\x65ss\x18\x16 \x01(\t")\n\x0bHeadersList\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t*C\n\x0e\x43ommentTypeTag\x12\x19\n\x15\x43OMMENTTYPETAGUNKNOWN\x10\x00\x12\x16\n\x12\x43OMMENTTYPETAGSTAR\x10\x01\x62\x06proto3' +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "douyin_webcast_pb2", _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals["_RESPONSE_ROUTEPARAMSENTRY"]._options = None + _globals["_RESPONSE_ROUTEPARAMSENTRY"]._serialized_options = b"8\001" + _globals["_GIFTSTRUCT_SPECIALEFFECTSENTRY"]._options = None + _globals["_GIFTSTRUCT_SPECIALEFFECTSENTRY"]._serialized_options = b"8\001" + _globals["_MEMBERMESSAGE_BURIEDPOINTMAPENTRY"]._options = None + _globals["_MEMBERMESSAGE_BURIEDPOINTMAPENTRY"]._serialized_options = b"8\001" + _globals["_EFFECTCONFIG_EXTRAMAPENTRY"]._options = None + _globals["_EFFECTCONFIG_EXTRAMAPENTRY"]._serialized_options = b"8\001" + _globals["_EFFECTCONFIG_PIECEVALUESMAPENTRY"]._options = None + _globals["_EFFECTCONFIG_PIECEVALUESMAPENTRY"]._serialized_options = b"8\001" + _globals["_ROOM_DYNAMICCOVERDICTENTRY"]._options = None + _globals["_ROOM_DYNAMICCOVERDICTENTRY"]._serialized_options = b"8\001" + _globals["_GRADEBUFFINFO_STATSINFOMAPENTRY"]._options = None + _globals["_GRADEBUFFINFO_STATSINFOMAPENTRY"]._serialized_options = b"8\001" + _globals["_USER_FANSCLUB_PREFERDATAENTRY"]._options = None + _globals["_USER_FANSCLUB_PREFERDATAENTRY"]._serialized_options = b"8\001" + _globals["_USER_FANSCLUB_FANSCLUBDATA_USERBADGE_ICONSENTRY"]._options = None + _globals["_USER_FANSCLUB_FANSCLUBDATA_USERBADGE_ICONSENTRY"]._serialized_options = ( + b"8\001" + ) + _globals["_COMMENTTYPETAG"]._serialized_start = 19244 + _globals["_COMMENTTYPETAG"]._serialized_end = 19311 + _globals["_RESPONSE"]._serialized_start = 33 + _globals["_RESPONSE"]._serialized_end = 389 + _globals["_RESPONSE_ROUTEPARAMSENTRY"]._serialized_start = 339 + _globals["_RESPONSE_ROUTEPARAMSENTRY"]._serialized_end = 389 + _globals["_MESSAGE"]._serialized_start = 392 + _globals["_MESSAGE"]._serialized_end = 546 + _globals["_CHATMESSAGE"]._serialized_start = 549 + _globals["_CHATMESSAGE"]._serialized_end = 1135 + _globals["_LANDSCAPEAREACOMMON"]._serialized_start = 1138 + _globals["_LANDSCAPEAREACOMMON"]._serialized_end = 1299 + _globals["_ROOMUSERSEQMESSAGE"]._serialized_start = 1302 + _globals["_ROOMUSERSEQMESSAGE"]._serialized_end = 1693 + _globals["_ROOMMESSAGE"]._serialized_start = 1695 + _globals["_ROOMMESSAGE"]._serialized_end = 1784 + _globals["_COMMONTEXTMESSAGE"]._serialized_start = 1786 + _globals["_COMMONTEXTMESSAGE"]._serialized_end = 1880 + _globals["_UPDATEFANTICKETMESSAGE"]._serialized_start = 1883 + _globals["_UPDATEFANTICKETMESSAGE"]._serialized_end = 2020 + _globals["_ROOMUSERSEQMESSAGECONTRIBUTOR"]._serialized_start = 2023 + _globals["_ROOMUSERSEQMESSAGECONTRIBUTOR"]._serialized_end = 2192 + _globals["_GIFTMESSAGE"]._serialized_start = 2195 + _globals["_GIFTMESSAGE"]._serialized_end = 3108 + _globals["_ASSETEFFECTMIXINFO"]._serialized_start = 3110 + _globals["_ASSETEFFECTMIXINFO"]._serialized_end = 3130 + _globals["_FANSCLUBMESSAGE"]._serialized_start = 3132 + _globals["_FANSCLUBMESSAGE"]._serialized_end = 3244 + _globals["_GIFTTRAYINFO"]._serialized_start = 3247 + _globals["_GIFTTRAYINFO"]._serialized_end = 3467 + _globals["_GIFTSTRUCT"]._serialized_start = 3470 + _globals["_GIFTSTRUCT"]._serialized_end = 5108 + _globals["_GIFTSTRUCT_SPECIALEFFECTSENTRY"]._serialized_start = 4992 + _globals["_GIFTSTRUCT_SPECIALEFFECTSENTRY"]._serialized_end = 5045 + _globals["_GIFTSTRUCT_GIFTSTRUCTFANSCLUBINFO"]._serialized_start = 5047 + _globals["_GIFTSTRUCT_GIFTSTRUCTFANSCLUBINFO"]._serialized_end = 5108 + _globals["_LUCKYMONEYGIFTMETA"]._serialized_start = 5110 + _globals["_LUCKYMONEYGIFTMETA"]._serialized_end = 5130 + _globals["_GIFTPANELOPERATION"]._serialized_start = 5132 + _globals["_GIFTPANELOPERATION"]._serialized_end = 5152 + _globals["_GIFTBANNER"]._serialized_start = 5154 + _globals["_GIFTBANNER"]._serialized_end = 5166 + _globals["_GIFTBUFFINFO"]._serialized_start = 5168 + _globals["_GIFTBUFFINFO"]._serialized_end = 5182 + _globals["_GIFTPREVIEWINFO"]._serialized_start = 5184 + _globals["_GIFTPREVIEWINFO"]._serialized_end = 5201 + _globals["_GIFTTIP"]._serialized_start = 5203 + _globals["_GIFTTIP"]._serialized_end = 5212 + _globals["_GIFTGROUPINFO"]._serialized_start = 5214 + _globals["_GIFTGROUPINFO"]._serialized_end = 5229 + _globals["_GIFTIMPRIORITY"]._serialized_start = 5231 + _globals["_GIFTIMPRIORITY"]._serialized_end = 5316 + _globals["_TEXTEFFECT"]._serialized_start = 5318 + _globals["_TEXTEFFECT"]._serialized_end = 5419 + _globals["_TEXTEFFECTDETAIL"]._serialized_start = 5422 + _globals["_TEXTEFFECTDETAIL"]._serialized_end = 5732 + _globals["_MEMBERMESSAGE"]._serialized_start = 5735 + _globals["_MEMBERMESSAGE"]._serialized_end = 6480 + _globals["_MEMBERMESSAGE_BURIEDPOINTMAPENTRY"]._serialized_start = 6427 + _globals["_MEMBERMESSAGE_BURIEDPOINTMAPENTRY"]._serialized_end = 6480 + _globals["_PUBLICAREACOMMON"]._serialized_start = 6482 + _globals["_PUBLICAREACOMMON"]._serialized_end = 6592 + _globals["_EFFECTCONFIG"]._serialized_start = 6595 + _globals["_EFFECTCONFIG"]._serialized_end = 7418 + _globals["_EFFECTCONFIG_EXTRAMAPENTRY"]._serialized_start = 7297 + _globals["_EFFECTCONFIG_EXTRAMAPENTRY"]._serialized_end = 7344 + _globals["_EFFECTCONFIG_PIECEVALUESMAPENTRY"]._serialized_start = 7346 + _globals["_EFFECTCONFIG_PIECEVALUESMAPENTRY"]._serialized_end = 7418 + _globals["_TEXT"]._serialized_start = 7420 + _globals["_TEXT"]._serialized_end = 7544 + _globals["_TEXTPIECE"]._serialized_start = 7547 + _globals["_TEXTPIECE"]._serialized_end = 7855 + _globals["_TEXTPIECEIMAGE"]._serialized_start = 7857 + _globals["_TEXTPIECEIMAGE"]._serialized_end = 7924 + _globals["_TEXTPIECEPATTERNREF"]._serialized_start = 7926 + _globals["_TEXTPIECEPATTERNREF"]._serialized_end = 7984 + _globals["_TEXTPIECEHEART"]._serialized_start = 7986 + _globals["_TEXTPIECEHEART"]._serialized_end = 8017 + _globals["_TEXTPIECEGIFT"]._serialized_start = 8019 + _globals["_TEXTPIECEGIFT"]._serialized_end = 8087 + _globals["_PATTERNREF"]._serialized_start = 8089 + _globals["_PATTERNREF"]._serialized_end = 8138 + _globals["_TEXTPIECEUSER"]._serialized_start = 8140 + _globals["_TEXTPIECEUSER"]._serialized_end = 8202 + _globals["_TEXTFORMAT"]._serialized_start = 8205 + _globals["_TEXTFORMAT"]._serialized_end = 8368 + _globals["_LIKEMESSAGE"]._serialized_start = 8371 + _globals["_LIKEMESSAGE"]._serialized_end = 8701 + _globals["_SOCIALMESSAGE"]._serialized_start = 8704 + _globals["_SOCIALMESSAGE"]._serialized_end = 8908 + _globals["_PICODISPLAYINFO"]._serialized_start = 8910 + _globals["_PICODISPLAYINFO"]._serialized_end = 9018 + _globals["_DOUBLELIKEDETAIL"]._serialized_start = 9020 + _globals["_DOUBLELIKEDETAIL"]._serialized_end = 9115 + _globals["_DISPLAYCONTROLINFO"]._serialized_start = 9117 + _globals["_DISPLAYCONTROLINFO"]._serialized_end = 9174 + _globals["_EPISODECHATMESSAGE"]._serialized_start = 9177 + _globals["_EPISODECHATMESSAGE"]._serialized_end = 9377 + _globals["_MATCHAGAINSTSCOREMESSAGE"]._serialized_start = 9380 + _globals["_MATCHAGAINSTSCOREMESSAGE"]._serialized_end = 9516 + _globals["_AGAINST"]._serialized_start = 9519 + _globals["_AGAINST"]._serialized_end = 9921 + _globals["_COMMON"]._serialized_start = 9924 + _globals["_COMMON"]._serialized_end = 10452 + _globals["_ROOM"]._serialized_start = 10455 + _globals["_ROOM"]._serialized_end = 11716 + _globals["_ROOM_DYNAMICCOVERDICTENTRY"]._serialized_start = 11661 + _globals["_ROOM_DYNAMICCOVERDICTENTRY"]._serialized_end = 11716 + _globals["_ROOMEXTRA"]._serialized_start = 11718 + _globals["_ROOMEXTRA"]._serialized_end = 11729 + _globals["_ROOMSTATS"]._serialized_start = 11731 + _globals["_ROOMSTATS"]._serialized_end = 11742 + _globals["_ROOMUSERATTR"]._serialized_start = 11744 + _globals["_ROOMUSERATTR"]._serialized_end = 11758 + _globals["_STREAMURL"]._serialized_start = 11760 + _globals["_STREAMURL"]._serialized_end = 11771 + _globals["_LINKMIC"]._serialized_start = 11773 + _globals["_LINKMIC"]._serialized_end = 11782 + _globals["_DECORATION"]._serialized_start = 11784 + _globals["_DECORATION"]._serialized_end = 11796 + _globals["_TOPFAN"]._serialized_start = 11798 + _globals["_TOPFAN"]._serialized_end = 11806 + _globals["_GRADEBUFFINFO"]._serialized_start = 11809 + _globals["_GRADEBUFFINFO"]._serialized_end = 12026 + _globals["_GRADEBUFFINFO_STATSINFOMAPENTRY"]._serialized_start = 11975 + _globals["_GRADEBUFFINFO_STATSINFOMAPENTRY"]._serialized_end = 12026 + _globals["_USERVIPINFO"]._serialized_start = 12028 + _globals["_USERVIPINFO"]._serialized_end = 12041 + _globals["_INDUSTRYCERTIFICATION"]._serialized_start = 12043 + _globals["_INDUSTRYCERTIFICATION"]._serialized_end = 12066 + _globals["_USER"]._serialized_start = 12069 + _globals["_USER"]._serialized_end = 17654 + _globals["_USER_ACTIVITYINFO"]._serialized_start = 15723 + _globals["_USER_ACTIVITYINFO"]._serialized_end = 15737 + _globals["_USER_ANCHORINFO"]._serialized_start = 15739 + _globals["_USER_ANCHORINFO"]._serialized_end = 15751 + _globals["_USER_ANCHORLEVEL"]._serialized_start = 15753 + _globals["_USER_ANCHORLEVEL"]._serialized_end = 15766 + _globals["_USER_AUTHENTICATIONINFO"]._serialized_start = 15768 + _globals["_USER_AUTHENTICATIONINFO"]._serialized_end = 15788 + _globals["_USER_AUTHORSTATS"]._serialized_start = 15790 + _globals["_USER_AUTHORSTATS"]._serialized_end = 15803 + _globals["_USER_BORDER"]._serialized_start = 15805 + _globals["_USER_BORDER"]._serialized_end = 15813 + _globals["_USER_BROTHERHOODINFO"]._serialized_start = 15815 + _globals["_USER_BROTHERHOODINFO"]._serialized_end = 15832 + _globals["_USER_FANSCLUB"]._serialized_start = 15835 + _globals["_USER_FANSCLUB"]._serialized_end = 16386 + _globals["_USER_FANSCLUB_PREFERDATAENTRY"]._serialized_start = 15956 + _globals["_USER_FANSCLUB_PREFERDATAENTRY"]._serialized_end = 16041 + _globals["_USER_FANSCLUB_FANSCLUBDATA"]._serialized_start = 16044 + _globals["_USER_FANSCLUB_FANSCLUBDATA"]._serialized_end = 16386 + _globals["_USER_FANSCLUB_FANSCLUBDATA_USERBADGE"]._serialized_start = 16227 + _globals["_USER_FANSCLUB_FANSCLUBDATA_USERBADGE"]._serialized_end = 16386 + _globals["_USER_FANSCLUB_FANSCLUBDATA_USERBADGE_ICONSENTRY"]._serialized_start = ( + 16327 + ) + _globals["_USER_FANSCLUB_FANSCLUBDATA_USERBADGE_ICONSENTRY"]._serialized_end = 16386 + _globals["_USER_FANSGROUPINFO"]._serialized_start = 16388 + _globals["_USER_FANSGROUPINFO"]._serialized_end = 16403 + _globals["_USER_FOLLOWINFO"]._serialized_start = 16405 + _globals["_USER_FOLLOWINFO"]._serialized_end = 16526 + _globals["_USER_JACCREDITINFO"]._serialized_start = 16528 + _globals["_USER_JACCREDITINFO"]._serialized_end = 16543 + _globals["_USER_NOBLELEVELINFO"]._serialized_start = 16545 + _globals["_USER_NOBLELEVELINFO"]._serialized_end = 16561 + _globals["_USER_OWNROOM"]._serialized_start = 16563 + _globals["_USER_OWNROOM"]._serialized_end = 16572 + _globals["_USER_PAYGRADE"]._serialized_start = 16575 + _globals["_USER_PAYGRADE"]._serialized_end = 17551 + _globals["_USER_PAYGRADE_GRADEICON"]._serialized_start = 17457 + _globals["_USER_PAYGRADE_GRADEICON"]._serialized_end = 17551 + _globals["_USER_POIINFO"]._serialized_start = 17553 + _globals["_USER_POIINFO"]._serialized_end = 17562 + _globals["_USER_PROFILESTYLEPARAMS"]._serialized_start = 17564 + _globals["_USER_PROFILESTYLEPARAMS"]._serialized_end = 17584 + _globals["_USER_SUBSCRIBE"]._serialized_start = 17586 + _globals["_USER_SUBSCRIBE"]._serialized_end = 17597 + _globals["_USER_USERATTR"]._serialized_start = 17599 + _globals["_USER_USERATTR"]._serialized_end = 17609 + _globals["_USER_USERDRESSINFO"]._serialized_start = 17611 + _globals["_USER_USERDRESSINFO"]._serialized_end = 17626 + _globals["_USER_USERSTATS"]._serialized_start = 17628 + _globals["_USER_USERSTATS"]._serialized_end = 17639 + _globals["_USER_XIGUAPARAMS"]._serialized_start = 17641 + _globals["_USER_XIGUAPARAMS"]._serialized_end = 17654 + _globals["_FOLLOWINFO"]._serialized_start = 17657 + _globals["_FOLLOWINFO"]._serialized_end = 17831 + _globals["_IMAGE"]._serialized_start = 17834 + _globals["_IMAGE"]._serialized_end = 18124 + _globals["_NINEPATCHSETTING"]._serialized_start = 18126 + _globals["_NINEPATCHSETTING"]._serialized_end = 18169 + _globals["_IMAGECONTENT"]._serialized_start = 18171 + _globals["_IMAGECONTENT"]._serialized_end = 18258 + _globals["_PUSHFRAME"]._serialized_start = 18261 + _globals["_PUSHFRAME"]._serialized_end = 18440 + _globals["_KK"]._serialized_start = 18442 + _globals["_KK"]._serialized_end = 18457 + _globals["_SENDMESSAGEBODY"]._serialized_start = 18460 + _globals["_SENDMESSAGEBODY"]._serialized_end = 18665 + _globals["_EXTLIST"]._serialized_start = 18667 + _globals["_EXTLIST"]._serialized_end = 18704 + _globals["_RSP"]._serialized_start = 18707 + _globals["_RSP"]._serialized_end = 18890 + _globals["_RSP_F"]._serialized_start = 18839 + _globals["_RSP_F"]._serialized_end = 18890 + _globals["_PREMESSAGE"]._serialized_start = 18893 + _globals["_PREMESSAGE"]._serialized_end = 19199 + _globals["_HEADERSLIST"]._serialized_start = 19201 + _globals["_HEADERSLIST"]._serialized_end = 19242 +# @@protoc_insertion_point(module_scope) diff --git a/f2/apps/douyin/test/test_apps_model.py b/f2/apps/douyin/test/test_apps_model.py deleted file mode 100644 index ff759476..00000000 --- a/f2/apps/douyin/test/test_apps_model.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -from f2.apps.douyin.model import UserPost -from f2.apps.douyin.utils import XBogusManager -from f2.apps.douyin.api import DouyinAPIEndpoints as dyendpoint - - -def test_xbogus_manager(): - params = UserPost( - max_cursor=0, - count=20, - sec_user_id="MS4wLjABAAAA5OCaznf4ihGfC65u0imbLzmBOuWDpUMo58CdnVTcX_R8bD9HZQknOJ4ZX9FdZnIq", - ) - - final_endpoint = XBogusManager.model_2_endpoint( - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", - base_endpoint=dyendpoint.USER_DETAIL, - params=params.dict(), - ) - - assert final_endpoint, "Failed to get a final endpoint." diff --git a/f2/apps/douyin/test/test_douyin_apps_model.py b/f2/apps/douyin/test/test_douyin_apps_model.py new file mode 100644 index 00000000..11f939ea --- /dev/null +++ b/f2/apps/douyin/test/test_douyin_apps_model.py @@ -0,0 +1,36 @@ +import pytest +from f2.apps.douyin.model import UserPost +from f2.apps.douyin.utils import XBogusManager, ABogusManager +from f2.apps.douyin.api import DouyinAPIEndpoints as dyendpoint + + +def test_xbogus_manager(): + params = UserPost( + max_cursor=0, + count=20, + sec_user_id="MS4wLjABAAAA5OCaznf4ihGfC65u0imbLzmBOuWDpUMo58CdnVTcX_R8bD9HZQknOJ4ZX9FdZnIq", + ) + + final_endpoint = XBogusManager.model_2_endpoint( + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", + base_endpoint=dyendpoint.USER_DETAIL, + params=params.model_dump(), + ) + + assert final_endpoint, "Failed to get a final endpoint." + + +def test_abogus_manager(): + params = UserPost( + max_cursor=0, + count=20, + sec_user_id="MS4wLjABAAAA5OCaznf4ihGfC65u0imbLzmBOuWDpUMo58CdnVTcX_R8bD9HZQknOJ4ZX9FdZnIq", + ) + + final_endpoint = ABogusManager.model_2_endpoint( + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0", + base_endpoint=dyendpoint.USER_DETAIL, + params=params.model_dump(), + ) + + assert final_endpoint, "Failed to get a final endpoint." diff --git a/f2/apps/douyin/test/test_aweme_id.py b/f2/apps/douyin/test/test_douyin_aweme_id.py similarity index 100% rename from f2/apps/douyin/test/test_aweme_id.py rename to f2/apps/douyin/test/test_douyin_aweme_id.py diff --git a/f2/apps/douyin/test/test_crawler.py b/f2/apps/douyin/test/test_douyin_crawler.py similarity index 100% rename from f2/apps/douyin/test/test_crawler.py rename to f2/apps/douyin/test/test_douyin_crawler.py diff --git a/f2/apps/douyin/test/test_handler.py b/f2/apps/douyin/test/test_douyin_handler.py similarity index 100% rename from f2/apps/douyin/test/test_handler.py rename to f2/apps/douyin/test/test_douyin_handler.py diff --git a/f2/apps/douyin/test/test_lrc.py b/f2/apps/douyin/test/test_douyin_lrc.py similarity index 100% rename from f2/apps/douyin/test/test_lrc.py rename to f2/apps/douyin/test/test_douyin_lrc.py diff --git a/f2/apps/douyin/test/test_room_id.py b/f2/apps/douyin/test/test_douyin_room_id.py similarity index 100% rename from f2/apps/douyin/test/test_room_id.py rename to f2/apps/douyin/test/test_douyin_room_id.py diff --git a/f2/apps/douyin/test/test_sec_user_id.py b/f2/apps/douyin/test/test_douyin_sec_user_id.py similarity index 100% rename from f2/apps/douyin/test/test_sec_user_id.py rename to f2/apps/douyin/test/test_douyin_sec_user_id.py diff --git a/f2/apps/douyin/test/test_douyin_token.py b/f2/apps/douyin/test/test_douyin_token.py new file mode 100644 index 00000000..d1a7b6c8 --- /dev/null +++ b/f2/apps/douyin/test/test_douyin_token.py @@ -0,0 +1,26 @@ +import pytest +from f2.apps.douyin.utils import TokenManager + + +def test_gen_real_msToken(): + token = TokenManager.gen_real_msToken() + assert token is not None, "gen_real_msToken() should return a valid token" + assert isinstance(token, str), "gen_real_msToken() should return a string" + + +def test_gen_false_msToken(): + token = TokenManager.gen_false_msToken() + assert token is not None, "gen_false_msToken() should return a valid token" + assert isinstance(token, str), "gen_false_msToken() should return a string" + + +def test_gen_ttwid(): + ttwid = TokenManager.gen_ttwid() + assert ttwid is not None, "gen_ttwid() should return a valid ttwid" + assert isinstance(ttwid, str), "gen_ttwid() should return a string" + + +def test_gen_webid(): + webid = TokenManager.gen_webid() + assert webid is not None, "gen_webid() should return a valid webid" + assert isinstance(webid, str), "gen_webid() should return a string" diff --git a/f2/apps/douyin/test/test_webcast_id.py b/f2/apps/douyin/test/test_douyin_webcast_id.py similarity index 100% rename from f2/apps/douyin/test/test_webcast_id.py rename to f2/apps/douyin/test/test_douyin_webcast_id.py diff --git a/f2/apps/douyin/test/test_douyin_webcast_signature.py b/f2/apps/douyin/test/test_douyin_webcast_signature.py new file mode 100644 index 00000000..f5c060af --- /dev/null +++ b/f2/apps/douyin/test/test_douyin_webcast_signature.py @@ -0,0 +1,13 @@ +import pytest +from f2.apps.douyin.algorithm.webcast_signature import DouyinWebcastSignature +from f2.apps.douyin.utils import ClientConfManager + + +def test_DouyinWebcastSignature(): + room_id = "7383573503129258802" + user_unique_id = "7383588170770138661" + signature = DouyinWebcastSignature(ClientConfManager.user_agent()).get_signature( + room_id, user_unique_id + ) + assert signature is not None + assert len(signature) == 16 diff --git a/f2/apps/douyin/utils.py b/f2/apps/douyin/utils.py index 2924a0fa..7927118f 100644 --- a/f2/apps/douyin/utils.py +++ b/f2/apps/douyin/utils.py @@ -8,13 +8,16 @@ import qrcode import random import asyncio +import traceback from typing import Union from pathlib import Path +from f2.apps.douyin.algorithm import webcast_signature from f2.i18n.translator import _ from f2.log.logger import logger from f2.utils.xbogus import XBogus as XB +from f2.utils.abogus import ABogus as AB, BrowserFingerprintGenerator as BrowserFpGen from f2.utils.conf_manager import ConfigManager from f2.utils.utils import ( gen_random_str, @@ -22,141 +25,486 @@ extract_valid_urls, split_filename, ) +from f2.crawlers.base_crawler import BaseCrawler from f2.exceptions.api_exceptions import ( - APIError, APIConnectionError, APIResponseError, APIUnavailableError, APIUnauthorizedError, APINotFoundError, + APITimeoutError, ) -class TokenManager: - f2_manager = ConfigManager(f2.F2_CONFIG_FILE_PATH).get_config("f2").get("douyin") - token_conf = f2_manager.get("msToken", None) - ttwid_conf = f2_manager.get("ttwid", None) - proxies_conf = f2_manager.get("proxies", None) - proxies = { - "http://": proxies_conf.get("http", None), - "https://": proxies_conf.get("https", None), +class ClientConfManager: + """ + 用于管理客户端配置 (Used to manage client configuration) + """ + + client_conf = ConfigManager(f2.F2_CONFIG_FILE_PATH).get_config("f2") + douyin_conf = client_conf.get("douyin", {}) + + @classmethod + def client(cls) -> dict: + return cls.douyin_conf + + @classmethod + def conf_version(cls) -> str: + return cls.client_conf.get("version", "unknown") + + @classmethod + def encryption(cls) -> dict: + return cls.client().get("encryption", "ab") + + @classmethod + def base_request_model(cls) -> dict: + return cls.client().get("BaseRequestModel", {}) + + @classmethod + def base_live_model(cls) -> dict: + return cls.client().get("BaseLiveModel", {}) + + @classmethod + def brm_version(cls) -> dict: + return cls.base_request_model().get("version", {}) + + @classmethod + def brm_browser(cls) -> dict: + return cls.base_request_model().get("browser", {}) + + @classmethod + def brm_engine(cls) -> dict: + return cls.base_request_model().get("engine", {}) + + @classmethod + def brm_os(cls) -> str: + return cls.base_request_model().get("os", "") + + @classmethod + def blm_language(cls) -> str: + return cls.base_live_model().get("language", "") + + @classmethod + def blm_browser(cls) -> dict: + return cls.base_live_model().get("browser", {}) + + @classmethod + def proxies(cls) -> dict: + return cls.client().get("proxies", {}) + + @classmethod + def headers(cls) -> dict: + return cls.client().get("headers", {}) + + @classmethod + def user_agent(cls) -> str: + return cls.headers().get("User-Agent", "") + + @classmethod + def referer(cls) -> str: + return cls.headers().get("Referer", "") + + @classmethod + def msToken(cls) -> dict: + return cls.client().get("msToken", {}) + + @classmethod + def ttwid(cls) -> dict: + return cls.client().get("ttwid", {}) + + @classmethod + def webid(cls) -> dict: + return cls.client().get("webid", {}) + + +class TokenManager(BaseCrawler): + """ + TokenManager 类用于生成和管理与抖音相关的 token,包括 msToken 和 ttwid。 + + 该类继承自 BaseCrawler,利用其中的 HTTP 客户端来发送请求。主要包含以下方法: + - gen_real_msToken: 生成真实的 msToken。 + - gen_false_msToken: 生成虚假的 msToken。 + - gen_ttwid: 生成请求必带的 ttwid。 + + 类属性: + - token_conf: 配置 msToken 的相关信息。 + - ttwid_conf: 配置 ttwid 的相关信息。 + - proxies: 代理配置。 + - user_agent: 用户代理字符串。 + - mstoken_headers: 生成 msToken 的请求头。 + - ttwid_headers: 生成 ttwid 的请求头。 + + 方法: + - __init__: 初始化 TokenManager 实例,并调用父类的初始化方法。 + - gen_real_msToken: 类方法,生成真实的 msToken,当出现错误时返回虚假的值。 + - gen_false_msToken: 类方法,生成随机 msToken。 + - gen_ttwid: 类方法,生成请求必带的 ttwid。 + + 异常处理: + - 在 HTTP 请求过程中,处理可能出现的 TimeoutException、NetworkError、ProtocolError、ProxyError 和 HTTPStatusError 异常,并记录相应的错误信息。 + + 使用示例: + # 生成真实的 msToken + msToken = TokenManager.gen_real_msToken() + + # 生成虚假的 msToken + false_msToken = TokenManager.gen_false_msToken() + + # 生成 ttwid + ttwid = TokenManager.gen_ttwid() + + # 生成 webid + webid = TokenManager.gen_webid() + """ + + token_conf = ClientConfManager.msToken() + ttwid_conf = ClientConfManager.ttwid() + webid_conf = ClientConfManager.webid() + proxies = ClientConfManager.proxies() + user_agent = ClientConfManager.user_agent() + mstoken_headers = { + "Content-Type": "application/json; charset=utf-8", + "User-Agent": user_agent, } + ttwid_headers = { + "User-Agent": user_agent, + "Content-Type": "application/json; charset=utf-8", + } + webid_headers = { + "User-Agent": user_agent, + "Content-Type": "application/json; charset=UTF-8", + "Referer": "https://www.douyin.com/", + } + + def __init__(self): + super().__init__(proxies=self.proxies) @classmethod def gen_real_msToken(cls) -> str: """ - 生成真实的msToken,当出现错误时返回虚假的值 - (Generate a real msToken and return a false value when an error occurs) + 生成真实的 msToken。 + + Returns: + str: 生成的 msToken。 + + Raises: + APITimeoutError: 请求超时错误。 + APIConnectionError: 网络连接错误。 + APIUnauthorizedError: 请求协议错误。 + APIResponseError: 状态码错误或响应内容不符合要求。 """ - payload = json.dumps( - { - "magic": cls.token_conf["magic"], - "version": cls.token_conf["version"], - "dataType": cls.token_conf["dataType"], - "strData": cls.token_conf["strData"], - "tspFromClient": get_timestamp(), - } - ) - headers = { - "User-Agent": cls.token_conf["User-Agent"], - "Content-Type": "application/json", - } - - transport = httpx.HTTPTransport(retries=5) - with httpx.Client(transport=transport, proxies=cls.proxies) as client: - try: - response = client.post( - cls.token_conf["url"], content=payload, headers=headers - ) - response.raise_for_status() + instance = cls() + + try: + payload = json.dumps( + { + "magic": instance.token_conf["magic"], + "version": instance.token_conf["version"], + "dataType": instance.token_conf["dataType"], + "strData": instance.token_conf["strData"], + "tspFromClient": get_timestamp(), + } + ) + response = instance.client.post( + instance.token_conf["url"], + content=payload, + headers=instance.mstoken_headers, + ) + response.raise_for_status() - msToken = str(httpx.Cookies(response.cookies).get("msToken")) - if len(msToken) not in [120, 128]: - raise APIResponseError(_("{0} 内容不符合要求").format("msToken")) + msToken = str(httpx.Cookies(response.cookies).get("msToken")) + if len(msToken) not in [120, 128]: + raise APIResponseError(_("{0} 内容不符合要求").format("msToken")) - return msToken + logger.debug(_("生成真实的msToken")) + return msToken - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) - raise APIConnectionError( - _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(cls.token_conf["url"], cls.proxies, cls.__name__, exc) + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求端点超时"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, ) + ) - except httpx.HTTPStatusError as e: - # 捕获 httpx 的状态代码错误 (captures specific status code errors from httpx) - if e.response.status_code == 401: - raise APIUnauthorizedError( - _( - "参数验证失败,请更新 F2 配置文件中的 {0},以匹配 {1} 新规则" - ).format("msToken", "douyin") - ) + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("网络连接失败,请检查当前网络环境"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) - elif e.response.status_code == 404: - raise APINotFoundError(_("{0} 无法找到API端点").format("msToken")) - else: - raise APIResponseError( - _("链接:{0},状态码 {1}:{2} ").format( - e.response.url, e.response.status_code, e.response.text - ) - ) + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求协议错误"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) - except APIError as e: - # 返回虚假的msToken (Return a fake msToken) - logger.error(_("msToken API错误:{0}").format(e)) - logger.info(_("生成虚假的msToken")) - return cls.gen_false_msToken() + except httpx.ProxyError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求代理错误"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) @classmethod def gen_false_msToken(cls) -> str: - """生成随机msToken (Generate random msToken)""" - return gen_random_str(126) + "==" + """生成随机 msToken (Generate random msToken)""" + false_msToken = gen_random_str(126) + "==" + logger.debug(_("生成虚假的 msToken:{0}").format(false_msToken)) + return false_msToken @classmethod def gen_ttwid(cls) -> str: """ - 生成请求必带的ttwid - (Generate the essential ttwid for requests) + 生成请求必带的 ttwid。 + + Returns: + str: 生成的 ttwid。 + + Raises: + APITimeoutError: 请求超时错误。 + APIConnectionError: 网络连接错误。 + APIUnauthorizedError: 请求协议错误。 + APIResponseError: 状态码错误或响应内容不符合要求。 """ - transport = httpx.HTTPTransport(retries=5) - with httpx.Client(transport=transport) as client: - try: - response = client.post( - cls.ttwid_conf["url"], content=cls.ttwid_conf["data"] + instance = cls() + + try: + response = instance.client.post( + instance.ttwid_conf["url"], + content=instance.ttwid_conf["data"], + headers=instance.ttwid_headers, + ) + response.raise_for_status() + + ttwid = httpx.Cookies(response.cookies).get("ttwid") + + if ttwid is None: + raise APIResponseError( + _("ttwid: 检查没有通过, 请更新配置文件中的 ttwid") ) - response.raise_for_status() - ttwid = str(httpx.Cookies(response.cookies).get("ttwid")) - return ttwid + return ttwid - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) - raise APIConnectionError( - _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(cls.ttwid_conf["url"], cls.proxies, cls.__name__, exc) + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求端点超时"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, ) + ) - except httpx.HTTPStatusError as e: - # 捕获 httpx 的状态代码错误 (captures specific status code errors from httpx) - if e.response.status_code == 401: - raise APIUnauthorizedError( - _( - "参数验证失败,请更新 F2 配置文件中的 {0},以匹配 {1} 新规则" - ).format("ttwid", "douyin") - ) + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("网络连接失败,请检查当前网络环境"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) - elif e.response.status_code == 404: - raise APINotFoundError(_("ttwid无法找到API端点")) - else: - raise APIResponseError( - _("链接:{0},状态码 {1}:{2} ").format( - e.response.url, e.response.status_code, e.response.text - ) - ) + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求协议错误"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProxyError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求代理错误"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + @classmethod + def gen_webid(cls) -> str: + """ + 生成个性化追踪webid (Generate personalized tracking webid) + + Returns: + str: 生成的webid (Generated webid) + + Raises: + APITimeoutError: 请求超时错误 (Request timeout error) + APIConnectionError: 网络连接错误 (Network connection error) + APIUnauthorizedError: 请求协议错误 (Request protocol error) + APIResponseError: 状态码错误或响应内容不符合要求 (Status code error or response content does not meet the requirements) + """ + + instance = cls() + + body = json.dumps( + { + "app_id": instance.webid_conf["body"]["app_id"], + "referer": instance.webid_conf["body"]["referer"], + "url": instance.webid_conf["body"]["url"], + "user_agent": instance.webid_conf["body"]["user_agent"], + "user_unique_id": "", + } + ) + + try: + response = instance.client.post( + instance.webid_conf["url"], + content=body, + headers=instance.webid_headers, + ) + response.raise_for_status() + + webid = response.json().get("web_id") + if not webid: + raise APIResponseError(_("{0} 内容不符合要求").format("webid")) + + return webid + + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求端点超时"), + instance.webid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("网络连接失败,请检查当前网络环境"), + instance.webid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求协议错误"), + instance.webid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProxyError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求代理错误"), + instance.webid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), + instance.webid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) class VerifyFpManager: @@ -195,6 +543,30 @@ def gen_s_v_web_id(cls) -> str: return cls.gen_verify_fp() +class WebcastSignatureManager: + @classmethod + def model_2_endpoint( + cls, + user_agent: str, + base_endpoint: str, + params: dict, + ) -> str: + if not isinstance(params, dict): + raise TypeError(_("参数必须是字典类型")) + + param_str = ",".join([f"{k}={v}" for k, v in params.items()]) + + try: + signature = webcast_signature(user_agent).get_signature(param_str) + except Exception as e: + logger.error(traceback.format_exc()) + raise RuntimeError(_("生成signature失败: {0})").format(e)) + + separator = "&" if "?" in base_endpoint else "?" + + return f"{base_endpoint}{separator}{param_str}&signature={signature}" + + class XBogusManager: @classmethod def str_2_endpoint( @@ -205,6 +577,7 @@ def str_2_endpoint( try: final_endpoint = XB(user_agent).getXBogus(endpoint) except Exception as e: + logger.error(traceback.format_exc()) raise RuntimeError(_("生成X-Bogus失败: {0})").format(e)) return final_endpoint[0] @@ -224,6 +597,7 @@ def model_2_endpoint( try: xb_value = XB(user_agent).getXBogus(param_str) except Exception as e: + logger.error(traceback.format_exc()) raise RuntimeError(_("生成X-Bogus失败: {0})").format(e)) # 检查base_endpoint是否已有查询参数 (Check if base_endpoint already has query parameters) @@ -234,10 +608,92 @@ def model_2_endpoint( return final_endpoint -class SecUserIdFetcher: - # 预编译正则表达式 +class ABogusManager: + @classmethod + def str_2_endpoint( + cls, + user_agent: str, + params: str, + request_type: str = "", + ) -> str: + try: + browser_fp = BrowserFpGen.generate_fingerprint("Edge") + final_endpoint = AB(fp=browser_fp, user_agent=user_agent).generate_abogus( + params, request_type + ) + except Exception as e: + logger.error(traceback.format_exc()) + raise RuntimeError(_("生成A-Bogus失败: {0})").format(e)) + + return final_endpoint[0] + + @classmethod + def model_2_endpoint( + cls, + user_agent: str, + base_endpoint: str, + params: dict, + request_type: str = "", + ) -> str: + if not isinstance(params, dict): + raise TypeError(_("参数必须是字典类型")) + + param_str = "&".join([f"{k}={v}" for k, v in params.items()]) + + try: + browser_fp = BrowserFpGen.generate_fingerprint("Edge") + ab_value = AB(fp=browser_fp, user_agent=user_agent).generate_abogus( + param_str, request_type + ) + except Exception as e: + logger.error(traceback.format_exc()) + raise RuntimeError(_("生成A-Bogus失败: {0})").format(e)) + + # 检查base_endpoint是否已有查询参数 (Check if base_endpoint already has query parameters) + separator = "&" if "?" in base_endpoint else "?" + + final_endpoint = f"{base_endpoint}{separator}{param_str}&a_bogus={ab_value[1]}" + + return final_endpoint + + +class SecUserIdFetcher(BaseCrawler): + """ + SecUserIdFetcher 用于从给定的 URL 中获取 sec_user_id。 + + 该类继承自 BaseCrawler,并利用其 HTTP 客户端功能来发送请求。 + + 类属性: + - _DOUYIN_URL_PATTERN (re.Pattern): 抖音用户主页 URL 的正则表达式模式。 + - _REDIRECT_URL_PATTERN (re.Pattern): 重定向 URL 中 sec_uid 的正则表达式模式。 + - proxies (dict): 代理配置。 + + 方法: + - get_sec_user_id: 从单个 URL 中获取 sec_user_id。 + - get_all_sec_user_id: 从 URL 列表中获取所有 sec_user_id。 + + 异常处理: + - 在 HTTP 请求过程中,处理可能出现的 TimeoutException、NetworkError、ProtocolError、ProxyError 和 HTTPStatusError 异常,并记录相应的错误信息。 + + 使用示例: + # 获取单个用户的 sec_user_id + url = "https://www.douyin.com/user/MS4wLjABAAAAEc8EKgWVEpEQoDRRp8_M53psgc-BoKqre-jICyt6aBA" + sec_user_id = await SecUserIdFetcher.get_sec_user_id(url) + + # 获取多个用户的 sec_user_id + urls = [ + "https://www.douyin.com/user/MS4wLjABAAAAEc8EKgWVEpEQoDRRp8_M53psgc-BoKqre-jICyt6aBA", + "https://v.douyin.com/i2wyU53P/", + ] + sec_user_ids = await SecUserIdFetcher.get_all_sec_user_id(urls) + """ + _DOUYIN_URL_PATTERN = re.compile(r"user/([^/?]*)") _REDIRECT_URL_PATTERN = re.compile(r"sec_uid=([^&]*)") + proxies = ClientConfManager.proxies() + + def __init__(self): + super().__init__(proxies=self.proxies) @classmethod async def get_sec_user_id(cls, url: str) -> str: @@ -249,6 +705,14 @@ async def get_sec_user_id(cls, url: str) -> str: Returns: str: 匹配到的sec_user_id (Matched sec_user_id)。 + + Raises: + TypeError: 参数不是字符串类型。 + APINotFoundError: 输入的URL不合法。 + APITimeoutError: 请求超时。 + APIConnectionError: 网络连接失败。 + APIUnauthorizedError: 请求协议错误。 + APIResponseError: 未找到sec_user_id或状态码错误。 """ if not isinstance(url, str): @@ -258,9 +722,10 @@ async def get_sec_user_id(cls, url: str) -> str: url = extract_valid_urls(url) if url is None: - raise ( - APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - ) + raise APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) + + # 创建一个实例以访问 aclient + instance = cls() pattern = ( cls._REDIRECT_URL_PATTERN @@ -269,47 +734,58 @@ async def get_sec_user_id(cls, url: str) -> str: ) try: - transport = httpx.AsyncHTTPTransport(retries=5) - async with httpx.AsyncClient( - transport=transport, proxies=TokenManager.proxies, timeout=10 - ) as client: - response = await client.get(url, follow_redirects=True) - # 444一般为Nginx拦截,不返回状态 (444 is generally intercepted by Nginx and does not return status) - if response.status_code in {200, 444}: - match = pattern.search(str(response.url)) - if match: - return match.group(1) - else: - raise APIResponseError( - _( - "未在响应的地址中找到sec_user_id,检查链接是否为用户主页类名:{0}" - ).format(cls.__name__) - ) - - elif response.status_code == 401: - raise APIUnauthorizedError( - _("未授权的请求。类名:{0}").format(cls.__name__) - ) - elif response.status_code == 404: - raise APINotFoundError( - _("未找到API端点。类名:{0}").format(cls.__name__) - ) - elif response.status_code == 503: - raise APIUnavailableError( - _("API服务不可用。类名:{0}").format(cls.__name__) - ) + response = await instance.aclient.get(url, follow_redirects=True) + if response.status_code in {200, 444}: + match = pattern.search(str(response.url)) + if match: + return match.group(1) else: raise APIResponseError( - _("链接:{0},状态码 {1}:{2} ").format( - response.url, response.status_code, response.text - ) + _( + "未在响应的地址中找到sec_user_id,检查链接是否为用户主页类名:{0}" + ).format(cls.__name__) ) + response.raise_for_status() + + except httpx.TimeoutException as exc: + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求端点超时"), url, cls.proxies, cls.__name__, exc) + ) + + except httpx.NetworkError as exc: + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("网络连接失败,请检查当前网络环境"), + url, + cls.proxies, + cls.__name__, + exc, + ) + ) - except httpx.RequestError as exc: + except httpx.ProtocolError as exc: + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求协议错误"), url, cls.proxies, cls.__name__, exc) + ) + + except httpx.ProxyError as exc: raise APIConnectionError( _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(url, TokenManager.proxies, cls.__name__, exc) + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求代理错误"), url, cls.proxies, cls.__name__, exc) + ) + + except httpx.HTTPStatusError as exc: + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), url, cls.proxies, cls.__name__, exc + ) ) @classmethod @@ -318,10 +794,14 @@ async def get_all_sec_user_id(cls, urls: list) -> list: 获取列表sec_user_id列表 (Get list sec_user_id list) Args: - urls: list: 用户url列表 (User url list) + urls (list): 用户url列表 (User url list) - Return: - sec_user_ids: list: 用户sec_user_id列表 (User sec_user_id list) + Returns: + list: 用户sec_user_id列表 (User sec_user_id list) + + Raises: + TypeError: 参数不是列表类型。 + APINotFoundError: 输入的URL List不合法。 """ if not isinstance(urls, list): @@ -331,20 +811,52 @@ async def get_all_sec_user_id(cls, urls: list) -> list: urls = extract_valid_urls(urls) if urls == []: - raise ( - APINotFoundError( - _("输入的URL List不合法。类名:{0}").format(cls.__name__) - ) + raise APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) ) sec_user_ids = [cls.get_sec_user_id(url) for url in urls] return await asyncio.gather(*sec_user_ids) -class AwemeIdFetcher: - # 预编译正则表达式 +class AwemeIdFetcher(BaseCrawler): + """ + AwemeIdFetcher 用于从给定的 URL 中获取 aweme_id。 + + 该类继承自 BaseCrawler,并利用其 HTTP 客户端功能来发送请求。 + + 类属性: + - _DOUYIN_VIDEO_URL_PATTERN (re.Pattern): 抖音视频 URL 的正则表达式模式。 + - _DOUYIN_NOTE_URL_PATTERN (re.Pattern): 抖音笔记 URL 的正则表达式模式。 + - proxies (dict): 代理配置。 + + 方法: + - get_aweme_id: 从单个 URL 中获取 aweme_id。 + - get_all_aweme_id: 从 URL 列表中获取所有 aweme_id。 + + 异常处理: + - 在 HTTP 请求过程中,处理可能出现的 TimeoutException、NetworkError、ProtocolError、ProxyError 和 HTTPStatusError 异常,并记录相应的错误信息。 + + 使用示例: + # 获取单个作品的 aweme_id + url = "https://www.douyin.com/video/6969696969696969696" + aweme_id = await AwemeIdFetcher.get_aweme_id(url) + + # 获取多个作品的 aweme_id + urls = [ + "https://www.douyin.com/video/6969696969696969696", + "https://www.douyin.com/note/6969696969696969696", + "https://v.douyin.com/tttttttt/", + ] + aweme_ids = await AwemeIdFetcher.get_all_aweme_id(urls) + """ + _DOUYIN_VIDEO_URL_PATTERN = re.compile(r"video/([^/?]*)") _DOUYIN_NOTE_URL_PATTERN = re.compile(r"note/([^/?]*)") + proxies = ClientConfManager.proxies() + + def __init__(self): + super().__init__(proxies=self.proxies) @classmethod async def get_aweme_id(cls, url: str) -> str: @@ -356,6 +868,14 @@ async def get_aweme_id(cls, url: str) -> str: Returns: str: 匹配到的aweme_id (Matched aweme_id)。 + + Raises: + TypeError: 参数不是字符串类型。 + APINotFoundError: 输入的URL不合法。 + APITimeoutError: 请求超时。 + APIConnectionError: 网络连接失败。 + APIUnauthorizedError: 请求协议错误。 + APIResponseError: 未找到aweme_id或状态码错误。 """ if not isinstance(url, str): @@ -365,49 +885,71 @@ async def get_aweme_id(cls, url: str) -> str: url = extract_valid_urls(url) if url is None: - raise ( - APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - ) + raise APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - # 重定向到完整链接 - transport = httpx.AsyncHTTPTransport(retries=5) - async with httpx.AsyncClient( - transport=transport, proxies=TokenManager.proxies, timeout=10 - ) as client: - try: - response = await client.get(url, follow_redirects=True) - response.raise_for_status() + # 创建一个实例以访问 aclient + instance = cls() + + try: + response = await instance.aclient.get(url, follow_redirects=True) + response.raise_for_status() - video_pattern = cls._DOUYIN_VIDEO_URL_PATTERN - note_pattern = cls._DOUYIN_NOTE_URL_PATTERN + video_pattern = cls._DOUYIN_VIDEO_URL_PATTERN + note_pattern = cls._DOUYIN_NOTE_URL_PATTERN - match = video_pattern.search(str(response.url)) + match = video_pattern.search(str(response.url)) + if match: + aweme_id = match.group(1) + else: + match = note_pattern.search(str(response.url)) if match: aweme_id = match.group(1) else: - match = note_pattern.search(str(response.url)) - if match: - aweme_id = match.group(1) - else: - raise APIResponseError( - _("未在响应的地址中找到aweme_id,检查链接是否为作品页") - ) - return aweme_id - - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) - raise APIConnectionError( - _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(url, TokenManager.proxies, cls.__name__, exc) + raise APIResponseError( + _("未在响应的地址中找到aweme_id,检查链接是否为作品页") + ) + return aweme_id + + except httpx.TimeoutException as exc: + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求端点超时"), url, cls.proxies, cls.__name__, exc) + ) + + except httpx.NetworkError as exc: + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("网络连接失败,请检查当前网络环境"), + url, + cls.proxies, + cls.__name__, + exc, ) + ) - except httpx.HTTPStatusError as e: - raise APIResponseError( - _("链接:{0},状态码 {1}:{2} ").format( - e.response.url, e.response.status_code, e.response.text - ) + except httpx.ProtocolError as exc: + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求协议错误"), url, cls.proxies, cls.__name__, exc) + ) + + except httpx.ProxyError as exc: + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求代理错误"), url, cls.proxies, cls.__name__, exc) + ) + + except httpx.HTTPStatusError as exc: + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), url, cls.proxies, cls.__name__, exc ) + ) @classmethod async def get_all_aweme_id(cls, urls: list) -> list: @@ -415,10 +957,14 @@ async def get_all_aweme_id(cls, urls: list) -> list: 获取视频aweme_id,传入列表url都可以解析出aweme_id (Get video aweme_id, pass in the list url can parse out aweme_id) Args: - urls: list: 列表url (list url) + urls (list): 列表url (list url) + + Returns: + list: 视频的唯一标识,返回列表 (The unique identifier of the video, return list) - Return: - aweme_ids: list: 视频的唯一标识,返回列表 (The unique identifier of the video, return list) + Raises: + TypeError: 参数不是列表类型。 + APINotFoundError: 输入的URL List不合法。 """ if not isinstance(urls, list): @@ -428,19 +974,49 @@ async def get_all_aweme_id(cls, urls: list) -> list: urls = extract_valid_urls(urls) if urls == []: - raise ( - APINotFoundError( - _("输入的URL List不合法。类名:{0}").format(cls.__name__) - ) + raise APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) ) aweme_ids = [cls.get_aweme_id(url) for url in urls] return await asyncio.gather(*aweme_ids) -class MixIdFetcher: - # 预编译正则表达式 +class MixIdFetcher(BaseCrawler): + """ + MixIdFetcher 用于从给定的 URL 中获取 mix_id。 + + 该类继承自 BaseCrawler,并利用其 HTTP 客户端功能来发送请求。 + + 类属性: + - _DOUYIN_MIX_URL_PATTERN (re.Pattern): 抖音合集 URL 的正则表达式模式。 + - proxies (dict): 代理配置。 + + 方法: + - get_mix_id: 从单个 URL 中获取 mix_id。 + - get_all_mix_id: 从 URL 列表中获取所有 mix_id。 + + 异常处理: + - 在 HTTP 请求过程中,处理可能出现的 TimeoutException、NetworkError、ProtocolError、ProxyError 和 HTTPStatusError 异常,并记录相应的错误信息。 + + 使用示例: + # 获取单个合集的 mix_id + url = "https://www.douyin.com/collection/7360898383181809676" + mix_id = await MixIdFetcher.get_mix_id(url) + + # 获取多个合集的 mix_id + urls = [ + "https://www.douyin.com/collection/6812345678901234567", + "https://www.douyin.com/collection/6812345678901234568", + ] + mix_ids = await MixIdFetcher.get_all_mix_id(urls) + """ + _DOUYIN_MIX_URL_PATTERN = re.compile(r"collection/([^/?]*)") + proxies = ClientConfManager.proxies() + + def __init__(self): + super().__init__(proxies=self.proxies) @classmethod async def get_mix_id(cls, url: str) -> str: @@ -452,6 +1028,14 @@ async def get_mix_id(cls, url: str) -> str: Returns: str: 匹配到的mix_id (Matched mix_id)。 + + Raises: + TypeError: 参数不是字符串类型。 + APINotFoundError: 输入的URL不合法。 + APITimeoutError: 请求超时。 + APIConnectionError: 网络连接失败。 + APIUnauthorizedError: 请求协议错误。 + APIResponseError: 未找到mix_id或状态码错误。 """ if not isinstance(url, str): @@ -461,54 +1045,80 @@ async def get_mix_id(cls, url: str) -> str: url = extract_valid_urls(url) if url is None: - raise ( - APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - ) + raise APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - # 重定向到完整链接 - transport = httpx.AsyncHTTPTransport(retries=5) - async with httpx.AsyncClient( - transport=transport, proxies=TokenManager.proxies, timeout=10 - ) as client: - try: - response = await client.get(url, follow_redirects=True) - response.raise_for_status() + instance = cls() - mix_pattern = cls._DOUYIN_MIX_URL_PATTERN + try: + response = await instance.aclient.get(url, follow_redirects=True) + response.raise_for_status() - match = mix_pattern.search(str(response.url)) - if match: - mix_id = match.group(1) - else: - raise APIResponseError( - _("未在响应的地址中找到mix_id,检查链接是否为合集页"), 404 - ) - return mix_id + mix_pattern = cls._DOUYIN_MIX_URL_PATTERN - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) - raise APIConnectionError( - _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(url, TokenManager.proxies, cls.__name__, exc) + match = mix_pattern.search(str(response.url)) + if match: + mix_id = match.group(1) + else: + raise APIResponseError( + _("未在响应的地址中找到mix_id,检查链接是否为合集页") ) + return mix_id - except httpx.HTTPStatusError as e: - raise APIResponseError( - _("链接:{0},状态码 {1}:{2} ").format( - e.response.url, e.response.status_code, e.response.text - ) + except httpx.TimeoutException as exc: + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求端点超时"), url, cls.proxies, cls.__name__, exc) + ) + + except httpx.NetworkError as exc: + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("网络连接失败,请检查当前网络环境"), + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProtocolError as exc: + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求协议错误"), url, cls.proxies, cls.__name__, exc) + ) + + except httpx.ProxyError as exc: + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求代理错误"), url, cls.proxies, cls.__name__, exc) + ) + + except httpx.HTTPStatusError as exc: + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), url, cls.proxies, cls.__name__, exc ) + ) - async def get_all_mix_id(cls, urls: list) -> str: + @classmethod + async def get_all_mix_id(cls, urls: list) -> list: """ 获取合集mix_id,传入列表url都可以解析出mix_id (Get video mix_id, pass in the list url can parse out aweme_id) Args: - urls: list: 列表url (list url) + urls (list): 列表url (list url) + + Returns: + list: 视频的唯一标识,返回列表 (The unique identifier of the video, return list) - Return: - mix_ids: list: 视频的唯一标识,返回列表 (The unique identifier of the video, return list) + Raises: + TypeError: 参数不是列表类型。 + APINotFoundError: 输入的URL List不合法。 """ if not isinstance(urls, list): raise TypeError(_("参数必须是列表类型")) @@ -517,24 +1127,53 @@ async def get_all_mix_id(cls, urls: list) -> str: urls = extract_valid_urls(urls) if urls == []: - raise ( - APINotFoundError( - _("输入的URL List不合法。类名:{0}").format(cls.__name__) - ) + raise APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) ) mix_ids = [cls.get_mix_id(url) for url in urls] return await asyncio.gather(*mix_ids) -class WebCastIdFetcher: - # 预编译正则表达式 +class WebCastIdFetcher(BaseCrawler): + """ + WebCastIdFetcher 用于从给定的 URL 中获取 webcast_id。 + + 该类继承自 BaseCrawler,并利用其 HTTP 客户端功能来发送请求。 + + 类属性: + - _DOUYIN_LIVE_URL_PATTERN (re.Pattern): 抖音直播 URL 的正则表达式模式。 + - _DOUYIN_LIVE_URL_PATTERN2 (re.Pattern): 抖音直播 URL 的第二种正则表达式模式。 + - _DOUYIN_LIVE_URL_PATTERN3 (re.Pattern): 抖音直播 URL 的第三种正则表达式模式。 + - proxies (dict): 代理配置。 + + 方法: + - get_webcast_id: 从单个 URL 中获取 webcast_id。 + - get_all_webcast_id: 从 URL 列表中获取所有 webcast_id。 + + 异常处理: + - 在 HTTP 请求过程中,处理可能出现的 TimeoutException、NetworkError、ProtocolError、ProxyError 和 HTTPStatusError 异常,并记录相应的错误信息。 + + 使用示例: + # 获取单个直播的 webcast_id + url = "https://live.douyin.com/36299127202" + webcast_id = await WebCastIdFetcher.get_webcast_id(url) + + # 获取多个直播的 webcast_id + urls = [ + "https://live.douyin.com/36299127202", + "https://live.douyin.com/36299127202?enter_from_merge=link_share&enter_method=copy_link_share&action_type=click&from=web_code_link", + ] + webcast_ids = await WebCastIdFetcher.get_all_webcast_id(urls) + """ + _DOUYIN_LIVE_URL_PATTERN = re.compile(r"live/([^/?]*)") - # https://live.douyin.com/766545142636?cover_type=0&enter_from_merge=web_live&enter_method=web_card&game_name=&is_recommend=1&live_type=game&more_detail=&request_id=20231110224012D47CD00C18B4AE4BFF9B&room_id=7299828646049827596&stream_type=vertical&title_type=1&web_live_page=hot_live&web_live_tab=all - # https://live.douyin.com/766545142636 _DOUYIN_LIVE_URL_PATTERN2 = re.compile(r"http[s]?://live.douyin.com/(\d+)") - # https://webcast.amemv.com/douyin/webcast/reflow/7318296342189919011?u_code=l1j9bkbd&did=MS4wLjABAAAAEs86TBQPNwAo-RGrcxWyCdwKhI66AK3Pqf3ieo6HaxI&iid=MS4wLjABAAAA0ptpM-zzoliLEeyvWOCUt-_dQza4uSjlIvbtIazXnCY&with_sec_did=1&use_link_command=1&ecom_share_track_params=&extra_params={"from_request_id":"20231230162057EC005772A8EAA0199906","im_channel_invite_id":"0"}&user_id=3644207898042206&liveId=7318296342189919011&from=share&style=share&enter_method=click_share&roomId=7318296342189919011&activity_info={} _DOUYIN_LIVE_URL_PATTERN3 = re.compile(r"reflow/([^/?]*)") + proxies = ClientConfManager.proxies() + + def __init__(self): + super().__init__(proxies=self.proxies) @classmethod async def get_webcast_id(cls, url: str) -> str: @@ -546,6 +1185,14 @@ async def get_webcast_id(cls, url: str) -> str: Returns: str: 匹配到的webcast_id (Matched webcast_id)。 + + Raises: + TypeError: 参数不是字符串类型。 + APINotFoundError: 输入的URL不合法。 + APITimeoutError: 请求超时。 + APIConnectionError: 网络连接失败。 + APIUnauthorizedError: 请求协议错误。 + APIResponseError: 未找到webcast_id或状态码错误。 """ if not isinstance(url, str): @@ -555,53 +1202,72 @@ async def get_webcast_id(cls, url: str) -> str: url = extract_valid_urls(url) if url is None: - raise ( - APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - ) + raise APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) + + instance = cls() + try: - # 重定向到完整链接 - transport = httpx.AsyncHTTPTransport(retries=5) - async with httpx.AsyncClient( - transport=transport, proxies=TokenManager.proxies, timeout=10 - ) as client: - response = await client.get(url, follow_redirects=True) - response.raise_for_status() - url = str(response.url) - - live_pattern = cls._DOUYIN_LIVE_URL_PATTERN - live_pattern2 = cls._DOUYIN_LIVE_URL_PATTERN2 - live_pattern3 = cls._DOUYIN_LIVE_URL_PATTERN3 - - if live_pattern.search(url): - match = live_pattern.search(url) - elif live_pattern2.search(url): - match = live_pattern2.search(url) - elif live_pattern3.search(url): - match = live_pattern3.search(url) - logger.warning( - _( - "该链接返回的是room_id,请使用`fetch_user_live_videos_by_room_id`接口" - ) - ) - else: - raise APIResponseError( - _("未在响应的地址中找到webcast_id,检查链接是否为直播页") + response = await instance.aclient.get(url, follow_redirects=True) + response.raise_for_status() + url = str(response.url) + + live_pattern = cls._DOUYIN_LIVE_URL_PATTERN + live_pattern2 = cls._DOUYIN_LIVE_URL_PATTERN2 + live_pattern3 = cls._DOUYIN_LIVE_URL_PATTERN3 + + if live_pattern.search(url): + match = live_pattern.search(url) + elif live_pattern2.search(url): + match = live_pattern2.search(url) + elif live_pattern3.search(url): + match = live_pattern3.search(url) + logger.warning( + _( + "该链接返回的是room_id,请使用`fetch_user_live_videos_by_room_id`接口" ) + ) + else: + raise APIResponseError( + _("未在响应的地址中找到webcast_id,检查链接是否为直播页") + ) - return match.group(1) + return match.group(1) + except httpx.TimeoutException as exc: + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求端点超时"), url, cls.proxies, cls.__name__, exc) + ) + except httpx.NetworkError as exc: + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("网络连接失败,请检查当前网络环境"), + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + except httpx.ProtocolError as exc: + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求协议错误"), url, cls.proxies, cls.__name__, exc) + ) - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) + except httpx.ProxyError as exc: raise APIConnectionError( _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(url, TokenManager.proxies, cls.__name__, exc) + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format(_("请求代理错误"), url, cls.proxies, cls.__name__, exc) ) - except httpx.HTTPStatusError as e: + except httpx.HTTPStatusError as exc: raise APIResponseError( - _("链接:{0},状态码 {1}:{2} ").format( - e.response.url, e.response.status_code, e.response.text + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), url, cls.proxies, cls.__name__, exc ) ) @@ -611,10 +1277,14 @@ async def get_all_webcast_id(cls, urls: list) -> list: 获取直播webcast_id,传入列表url都可以解析出webcast_id (Get live webcast_id, pass in the list url can parse out webcast_id) Args: - urls: list: 列表url (list url) + urls (list): 列表url (list url) - Return: - webcast_ids: list: 直播的唯一标识,返回列表 (The unique identifier of the live, return list) + Returns: + list: 直播的唯一标识,返回列表 (The unique identifier of the live, return list) + + Raises: + TypeError: 参数不是列表类型。 + APINotFoundError: 输入的URL List不合法。 """ if not isinstance(urls, list): @@ -624,10 +1294,8 @@ async def get_all_webcast_id(cls, urls: list) -> list: urls = extract_valid_urls(urls) if urls == []: - raise ( - APINotFoundError( - _("输入的URL List不合法。类名:{0}").format(cls.__name__) - ) + raise APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) ) webcast_ids = [cls.get_webcast_id(url) for url in urls] diff --git a/f2/apps/tiktok/api.py b/f2/apps/tiktok/api.py index f390b45e..e9bace8b 100644 --- a/f2/apps/tiktok/api.py +++ b/f2/apps/tiktok/api.py @@ -33,7 +33,7 @@ class TiktokAPIEndpoints: # 用户播放列表 (User Play List) USER_PLAY_LIST = f"{TIKTOK_DOMAIN}/api/user/playlist/" - # 用户合辑 (User Mix) + # 用户合集 (User Mix) USER_MIX = f"{TIKTOK_DOMAIN}/api/mix/item_list/" # 猜你喜欢 (Guess You Like) @@ -50,3 +50,12 @@ class TiktokAPIEndpoints: # 作品评论 (Post Comment) POST_COMMENT = f"{TIKTOK_DOMAIN}/api/comment/list/" + + # 作品搜索 (Post Search) + POST_SEARCH = f"{TIKTOK_DOMAIN}/api/search/item/full/" + + # 用户直播 (User Live) + USER_LIVE = f"{TIKTOK_DOMAIN}/api-live/user/room/" + + # 检查开播状态 (Check Live Status) + CHECK_LIVE_ALIVE = f"{WEBCAST_DOMAIN}/webcast/room/check_alive/" diff --git a/f2/apps/tiktok/cli.py b/f2/apps/tiktok/cli.py index 35b1ee39..062678db 100644 --- a/f2/apps/tiktok/cli.py +++ b/f2/apps/tiktok/cli.py @@ -18,6 +18,7 @@ ) from f2.utils.conf_manager import ConfigManager from f2.i18n.translator import TranslationManager, _ +from f2.apps.tiktok.utils import ClientConfManager def handler_help( @@ -77,6 +78,8 @@ def handler_auto_cookie( except Exception as e: logger.error(_("自动获取Cookie失败:{0}").format(str(e))) ctx.abort() + finally: + ctx.exit(0) def handler_language( @@ -123,7 +126,14 @@ def handler_naming( return # 允许的模式和分隔符 - ALLOWED_PATTERNS = ["{nickname}", "{create}", "{aweme_id}", "{desc}", "{uid}"] + ALLOWED_PATTERNS = [ + "{nickname}", + "{uniqueId}", + "{create}", + "{aweme_id}", + "{desc}", + "{uid}", + ] ALLOWED_SEPARATORS = ["-", "_"] # 检查命名是否符合命名规范 @@ -152,7 +162,7 @@ def handler_naming( type=str, # default="", help=_( - "根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合辑与直播同上" + "根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合集与直播同上" ), ) @click.option( @@ -197,7 +207,7 @@ def handler_naming( # default="post", # required=True, help=_( - "下载模式:单个作品(one),主页作品(post), 点赞作品(like), 收藏作品(collect), 合辑播放列表(mix)" + "下载模式:单个作品(one),主页作品(post), 点赞作品(like), 收藏作品(collect), 合集播放列表(mix)" ), ) @click.option( @@ -225,6 +235,13 @@ def handler_naming( # default="all", help=_("下载日期区间发布的作品,格式:2022-01-01|2023-01-01,'all' 为下载所有作品"), ) +@click.option( + "--keyword", + "-w", + type=str, + # default="", + help=_("搜索关键字,用于搜索作品"), +) @click.option( "--timeout", "-e", @@ -265,7 +282,7 @@ def handler_naming( "-s", type=int, # default=20, - help=_("从接口每页可获取作品数,不建议超过20。"), + help=_("从接口每页可获取作品数,不建议超过 20"), ) @click.option( "--languages", @@ -281,7 +298,7 @@ def handler_naming( type=str, nargs=2, help=_( - "代理服务器,最多 2 个参数,http与https。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" + "代理服务器,最多 2 个参数,http://与https://。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" ), ) @click.option( @@ -334,27 +351,18 @@ def tiktok( main_conf_path = get_resource_path(f2.APP_CONFIG_FILE_PATH) main_conf = main_manager.get_config("tiktok") - # 读取f2低频配置文件 - f2_manager = ConfigManager(f2.F2_CONFIG_FILE_PATH) - - f2_conf = f2_manager.get_config("f2").get("tiktok") - f2_proxies = f2_conf.get("proxies") - # 更新主配置文件中的代理参数 - main_conf["proxies"] = { - "http": f2_proxies.get("http"), - "https": f2_proxies.get("https"), - } + main_conf["proxies"] = ClientConfManager.proxies() # 更新主配置文件中的headers参数 kwargs.setdefault("headers", {}) - kwargs["headers"]["User-Agent"] = f2_conf["headers"].get("User-Agent", "") - kwargs["headers"]["Referer"] = f2_conf["headers"].get("Referer", "") + kwargs["headers"]["User-Agent"] = ClientConfManager.user_agent() + kwargs["headers"]["Referer"] = ClientConfManager.referer() # 如果初始化配置文件,则与更新配置文件互斥 if init_config and not update_config: main_manager.generate_config("tiktok", init_config) - # return + return elif init_config: raise click.UsageError(_("不能同时初始化和更新配置文件")) # 如果没有初始化配置文件,但是更新配置文件,则需要提供配置文件路径 @@ -375,17 +383,19 @@ def tiktok( if update_config: # 如果指定了 update_config,更新配置文件 update_manger = ConfigManager(config) update_manger.update_config_with_args("tiktok", **kwargs) + return # 将kwargs["proxies"]中的tuple转换为dict if kwargs.get("proxies"): kwargs["proxies"] = { - "http": kwargs["proxies"][0], - "https": kwargs["proxies"][1], + "http://": kwargs["proxies"][0], + "https://": kwargs["proxies"][1], } # 从低频配置开始到高频配置再到cli参数,逐级覆盖,如果键值不存在使用父级的键值 kwargs = merge_config(main_conf, custom_conf, **kwargs) + logger.info(_("模式:{0}").format(kwargs.get("mode"))) logger.info(_("主配置路径:{0}").format(main_conf_path)) logger.info(_("自定义配置路径:{0}").format(Path.cwd() / config)) logger.debug(_("主配置参数:{0}").format(main_conf)) @@ -394,7 +404,7 @@ def tiktok( # 尝试从命令行参数或kwargs中获取URL if not kwargs.get("url"): - logger.error("缺乏URL参数,详情看命令帮助") + logger.error(_("缺乏URL参数,详情看命令帮助")) handler_help(ctx, None, True) # 添加app_name到kwargs diff --git a/f2/apps/tiktok/crawler.py b/f2/apps/tiktok/crawler.py index cb65c292..1287ad0b 100644 --- a/f2/apps/tiktok/crawler.py +++ b/f2/apps/tiktok/crawler.py @@ -1,10 +1,7 @@ # path: f2/apps/tiktok/crawler.py -import f2 - from f2.log.logger import logger from f2.i18n.translator import _ -from f2.utils.conf_manager import ConfigManager from f2.crawlers.base_crawler import BaseCrawler from f2.apps.tiktok.api import TiktokAPIEndpoints as tkendpoint from f2.apps.tiktok.model import ( @@ -16,6 +13,9 @@ PostDetail, UserPlayList, PostComment, + PostSearch, + UserLive, + CheckLiveAlive, ) from f2.apps.tiktok.utils import XBogusManager @@ -25,27 +25,16 @@ def __init__( self, kwargs: dict = ..., ): - f2_manager = ConfigManager(f2.F2_CONFIG_FILE_PATH) - f2_conf = f2_manager.get_config("f2").get("tiktok") - proxies_conf = kwargs.get("proxies", {"http": None, "https": None}) - proxies = { - "http://": proxies_conf.get("http", None), - "https://": proxies_conf.get("https", None), - } - - self.headers = { - "User-Agent": f2_conf["headers"]["User-Agent"], - "Referer": f2_conf["headers"]["Referer"], - "Cookie": kwargs["cookie"], - } - + # 需要与cli同步 + proxies = kwargs.get("proxies", {"http://": None, "https://": None}) + self.headers = kwargs.get("headers") | {"Cookie": kwargs["cookie"]} super().__init__(proxies=proxies, crawler_headers=self.headers) async def fetch_user_profile(self, params: UserProfile): endpoint = XBogusManager.model_2_endpoint( self.headers.get("User-Agent"), tkendpoint.USER_DETAIL, - params.dict(), + params.model_dump(), ) logger.debug(_("用户信息接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) @@ -54,7 +43,7 @@ async def fetch_user_post(self, params: UserPost): endpoint = XBogusManager.model_2_endpoint( self.headers.get("User-Agent"), tkendpoint.USER_POST, - params.dict(), + params.model_dump(), ) logger.debug(_("主页作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) @@ -63,7 +52,7 @@ async def fetch_user_like(self, params: UserLike): endpoint = XBogusManager.model_2_endpoint( self.headers.get("User-Agent"), tkendpoint.USER_LIKE, - params.dict(), + params.model_dump(), ) logger.debug(_("喜欢作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) @@ -72,7 +61,7 @@ async def fetch_user_collect(self, params: UserCollect): endpoint = XBogusManager.model_2_endpoint( self.headers.get("User-Agent"), tkendpoint.USER_COLLECT, - params.dict(), + params.model_dump(), ) logger.debug(_("收藏作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) @@ -81,25 +70,25 @@ async def fetch_user_play_list(self, params: UserPlayList): endpoint = XBogusManager.model_2_endpoint( self.headers.get("User-Agent"), tkendpoint.USER_PLAY_LIST, - params.dict(), + params.model_dump(), ) - logger.debug(_("合辑列表接口地址:{0}").format(endpoint)) + logger.debug(_("合集列表接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_user_mix(self, params: UserMix): endpoint = XBogusManager.model_2_endpoint( self.headers.get("User-Agent"), tkendpoint.USER_MIX, - params.dict(), + params.model_dump(), ) - logger.debug(_("合辑作品接口地址:{0}").format(endpoint)) + logger.debug(_("合集作品接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) async def fetch_post_detail(self, params: PostDetail): endpoint = XBogusManager.model_2_endpoint( self.headers.get("User-Agent"), tkendpoint.AWEME_DETAIL, - params.dict(), + params.model_dump(), ) logger.debug(_("作品详情接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) @@ -108,7 +97,7 @@ async def fetch_post_comment(self, params: PostComment): endpoint = XBogusManager.model_2_endpoint( self.headers.get("User-Agent"), tkendpoint.POST_COMMENT, - params.dict(), + params.model_dump(), ) logger.debug(_("作品评论接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) @@ -117,11 +106,38 @@ async def fetch_post_recommend(self, params: PostDetail): endpoint = XBogusManager.model_2_endpoint( self.headers.get("User-Agent"), tkendpoint.HOME_RECOMMEND, - params.dict(), + params.model_dump(), ) logger.debug(_("首页推荐接口地址:{0}").format(endpoint)) return await self._fetch_get_json(endpoint) + async def fetch_post_search(self, params: PostSearch): + endpoint = XBogusManager.model_2_endpoint( + self.headers.get("User-Agent"), + tkendpoint.POST_SEARCH, + params.model_dump(), + ) + logger.debug(_("搜索作品接口地址:{0}").format(endpoint)) + return await self._fetch_get_json(endpoint) + + async def fetch_user_live(self, params: UserLive): + endpoint = XBogusManager.model_2_endpoint( + self.headers.get("User-Agent"), + tkendpoint.USER_LIVE, + params.model_dump(), + ) + logger.debug(_("用户直播接口地址:{0}").format(endpoint)) + return await self._fetch_get_json(endpoint) + + async def fetch_check_live_alive(self, params: CheckLiveAlive): + endpoint = XBogusManager.model_2_endpoint( + self.headers.get("User-Agent"), + tkendpoint.CHECK_LIVE_ALIVE, + params.model_dump(), + ) + logger.debug(_("检查开播状态接口地址:{0}").format(endpoint)) + return await self._fetch_get_json(endpoint) + async def __aenter__(self): return self diff --git a/f2/apps/tiktok/db.py b/f2/apps/tiktok/db.py index 89690b3d..108e979d 100644 --- a/f2/apps/tiktok/db.py +++ b/f2/apps/tiktok/db.py @@ -89,19 +89,25 @@ async def update_user_info(self, secUid, **kwargs) -> None: ) await self.commit() - async def get_user_info(self, secUid: str) -> dict: + async def get_user_info(self, secUid: str = "", uniqueId: str = "") -> dict: """ 获取用户信息 Args: secUid (str): 用户唯一标识 + uniqueId (str): 用户名 Returns: dict: 对应的用户信息,如果不存在则返回 None """ - cursor = await self.execute( - f"SELECT * FROM {self.TABLE_NAME} WHERE secUid=?", (secUid,) - ) + if secUid: + cursor = await self.execute( + f"SELECT * FROM {self.TABLE_NAME} WHERE secUid=?", (secUid,) + ) + elif uniqueId: + cursor = await self.execute( + f"SELECT * FROM {self.TABLE_NAME} WHERE uniqueId=?", (uniqueId,) + ) result = await cursor.fetchone() if not result: return {} diff --git a/f2/apps/tiktok/dl.py b/f2/apps/tiktok/dl.py index 85559671..5e3febca 100644 --- a/f2/apps/tiktok/dl.py +++ b/f2/apps/tiktok/dl.py @@ -288,7 +288,8 @@ async def handler_stream( custom_fields = { "create": timestamp_2_str(timestamp=get_timestamp(unit="sec")), "nickname": webcast_data_dict.get("nickname", ""), - "aweme_id": webcast_data_dict.get("room_id", ""), + "uniqueId": webcast_data_dict.get("uniqueId", ""), + "aweme_id": webcast_data_dict.get("live_room_id", ""), "desc": webcast_data_dict.get("live_title", ""), "uid": webcast_data_dict.get("user_id", ""), } @@ -303,8 +304,8 @@ async def handler_stream( ) webcast_name = f"{format_file_name(kwargs.get('naming', '{create}_{desc}'), custom_fields=custom_fields)}_live" - webcast_url = webcast_data_dict.get("m3u8_pull_url", None).get("FULL_HD1") + webcast_url = webcast_data_dict.get("live_hls_url", None) await self.initiate_m3u8_download( - _("直播"), webcast_url, base_path, webcast_name, ".mp4" + _("直播"), webcast_url, base_path, webcast_name, ".flv" ) diff --git a/f2/apps/tiktok/filter.py b/f2/apps/tiktok/filter.py index 1f68492b..f13fe0fe 100644 --- a/f2/apps/tiktok/filter.py +++ b/f2/apps/tiktok/filter.py @@ -1,7 +1,12 @@ # path: f2/apps/tiktok/filter.py from f2.utils.json_filter import JSONModel -from f2.utils.utils import _get_first_item_from_list, timestamp_2_str, replaceT +from f2.utils.utils import ( + _get_first_item_from_list, + timestamp_2_str, + replaceT, + unescape_json, +) class UserProfileFilter(JSONModel): @@ -56,28 +61,28 @@ def uniqueId(self): return self._get_attr_value("$.userInfo.user.uniqueId") @property - def commentSetting(self) -> bool: - return bool(self._get_attr_value("$.userInfo.user.commentSetting")) + def commentSetting(self): + return self._get_attr_value("$.userInfo.user.commentSetting") @property - def followingVisibility(self) -> bool: - return bool(self._get_attr_value("$.userInfo.user.followingVisibility")) + def followingVisibility(self): + return self._get_attr_value("$.userInfo.user.followingVisibility") @property - def openFavorite(self) -> bool: - return bool(self._get_attr_value("$.userInfo.user.openFavorite")) + def openFavorite(self): + return self._get_attr_value("$.userInfo.user.openFavorite") @property - def privateAccount(self) -> bool: - return bool(self._get_attr_value("$.userInfo.user.privateAccount")) + def privateAccount(self): + return self._get_attr_value("$.userInfo.user.privateAccount") @property - def showPlayListTab(self) -> bool: - return bool(self._get_attr_value("$.userInfo.user.profileTab.showPlayListTab")) + def showPlayListTab(self): + return self._get_attr_value("$.userInfo.user.profileTab.showPlayListTab") @property - def relation(self) -> bool: # follow 1, no follow 0 - return bool(self._get_attr_value("$.userInfo.user.relation")) + def relation(self): # follow 1, no follow 0 + return self._get_attr_value("$.userInfo.user.relation") @property def signature(self): @@ -88,12 +93,12 @@ def signature_raw(self): return self._get_attr_value("$.userInfo.user.signature") @property - def ttSeller(self) -> bool: - return bool(self._get_attr_value("$.userInfo.user.ttSeller")) + def ttSeller(self): + return self._get_attr_value("$.userInfo.user.ttSeller") @property - def verified(self) -> bool: - return bool(self._get_attr_value("$.userInfo.user.verified")) + def verified(self): + return self._get_attr_value("$.userInfo.user.verified") def _to_raw(self) -> dict: return self._data @@ -520,32 +525,32 @@ def aweme_id(self): # aweme stats @property - def collected(self) -> bool: - return bool(self._get_attr_value("$.itemInfo.itemStruct.collected")) + def collected(self): + return self._get_attr_value("$.itemInfo.itemStruct.collected") @property - def digged(self) -> bool: - return bool(self._get_attr_value("$.itemInfo.itemStruct.digged")) + def digged(self): + return self._get_attr_value("$.itemInfo.itemStruct.digged") @property - def forFriend(self) -> bool: - return bool(self._get_attr_value("$.itemInfo.itemStruct.forFriend")) + def forFriend(self): + return self._get_attr_value("$.itemInfo.itemStruct.forFriend") @property - def itemCommentStatus(self) -> bool: - return bool(self._get_attr_value("$.itemInfo.itemStruct.itemCommentStatus")) + def itemCommentStatus(self): + return self._get_attr_value("$.itemInfo.itemStruct.itemCommentStatus") @property - def privateItem(self) -> bool: - return bool(self._get_attr_value("$.itemInfo.itemStruct.privateItem")) + def privateItem(self): + return self._get_attr_value("$.itemInfo.itemStruct.privateItem") @property - def secret(self) -> bool: - return bool(self._get_attr_value("$.itemInfo.itemStruct.secret")) + def secret(self): + return self._get_attr_value("$.itemInfo.itemStruct.secret") @property - def shareEnabled(self) -> bool: - return bool(self._get_attr_value("$.itemInfo.itemStruct.shareEnabled")) + def shareEnabled(self): + return self._get_attr_value("$.itemInfo.itemStruct.shareEnabled") # stats @property @@ -686,3 +691,403 @@ def _to_list(self): d[key] = attr_values[index] if index < len(attr_values) else None list_dicts.append(d) return list_dicts + + +class PostSearchFilter(JSONModel): + @property + def api_status_code(self): + return self._get_attr_value("$.status_code") + + @property + def has_aweme(self) -> bool: + return bool(self._get_attr_value("$.item_list")) + + @property + def has_more(self) -> bool: + return bool(self._get_attr_value("$.has_more")) + + @property + def cursor(self): + return self._get_attr_value("$.cursor") + + @property + def backtrace(self): + return self._get_attr_value("$.backtrace") + + @property + def search_id(self): + return self._get_attr_value("$.extra.logid") + + @property + def aweme_id(self): + ids = self._get_list_attr_value("$.item_list[*].id") + return ids if isinstance(ids, list) else [ids] + + @property + def createTime(self): + create_times = self._get_list_attr_value("$.item_list[*].createTime") + return ( + [timestamp_2_str(ct) for ct in create_times] + if isinstance(create_times, list) + else timestamp_2_str(create_times) + ) + + @property + def desc(self): + return replaceT(self._get_list_attr_value("$.item_list[*].desc")) + + @property + def desc_raw(self): + return self._get_list_attr_value("$.item_list[*].desc") + + @property + def textExtra(self): + return self._get_list_attr_value("$.item_list[*].textExtra") + + # music + @property + def music_album(self): + return self._get_list_attr_value("$.item_list[*].music.album") + + @property + def music_authorName(self): + return replaceT(self._get_list_attr_value("$.item_list[*].music.authorName")) + + @property + def music_authorName_raw(self): + return self._get_list_attr_value("$.item_list[*].music.authorName") + + @property + def music_coverLarge(self): + return self._get_list_attr_value("$.item_list[*].music.coverLarge") + + @property + def music_duration(self): + return self._get_list_attr_value("$.item_list[*].music.duration") + + @property + def music_id(self): + return self._get_list_attr_value("$.item_list[*].music.id") + + @property + def music_original(self): + return self._get_list_attr_value("$.item_list[*].music.original") + + @property + def music_playUrl(self): + return self._get_list_attr_value("$.item_list[*].music.playUrl") + + @property + def music_title(self): + return replaceT(self._get_list_attr_value("$.item_list[*].music.title")) + + @property + def music_title_raw(self): + return self._get_list_attr_value("$.item_list[*].music.title") + + # video + @property + def video_bitrate(self): + return self._get_list_attr_value("$.item_list[*].video.bitrate") + + @property + def video_codecType(self): + return self._get_list_attr_value("$.item_list[*].video.codecType") + + @property + def video_cover(self): + return self._get_list_attr_value("$.item_list[*].video.cover") + + @property + def video_dynamicCover(self): + return self._get_list_attr_value("$.item_list[*].video.dynamicCover") + + @property + def video_playAddr(self): + return self._get_list_attr_value("$.item_list[*].video.playAddr") + + @property + def video_duration(self): + return self._get_list_attr_value("$.item_list[*].video.duration") + + @property + def video_height(self): + return self._get_list_attr_value("$.item_list[*].video.height") + + @property + def video_width(self): + return self._get_list_attr_value("$.item_list[*].video.width") + + # author + @property + def nickname(self): + return replaceT(self._get_list_attr_value("$.item_list[*].author.nickname")) + + @property + def nickname_raw(self): + return self._get_list_attr_value("$.item_list[*].author.nickname") + + @property + def uid(self): + return self._get_list_attr_value("$.item_list[*].author.id") + + @property + def secUid(self): + return self._get_list_attr_value("$.item_list[*].author.secUid") + + # your stats + @property + def collected(self): + return self._get_list_attr_value("$.item_list[*].collected") + + @property + def digged(self): + return self._get_list_attr_value("$.item_list[*].digged") + + @property + def duetEnabled(self): + return self._get_list_attr_value("$.item_list[*].duetEnabled") + + @property + def forFriend(self): + return self._get_list_attr_value("$.item_list[*].forFriend") + + @property + def itemCommentStatus(self): + return self._get_list_attr_value("$.item_list[*].itemCommentStatus") + + @property + def privateItem(self): + return self._get_list_attr_value("$.item_list[*].privateItem") + + @property + def secret(self): + return self._get_list_attr_value("$.item_list[*].secret") + + @property + def shareEnabled(self): + return self._get_list_attr_value("$.item_list[*].shareEnabled") + + # aweme stats + @property + def collectCount(self): + return self._get_list_attr_value("$.item_list[*].stats.collectCount") + + @property + def commentCount(self): + return self._get_list_attr_value("$.item_list[*].stats.commentCount") + + @property + def diggCount(self): + return self._get_list_attr_value("$.item_list[*].stats.diggCount") + + @property + def playCount(self): + return self._get_list_attr_value("$.item_list[*].stats.playCount") + + @property + def shareCount(self): + return self._get_list_attr_value("$.item_list[*].stats.shareCount") + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + def _to_list(self): + exclude_list = ["has_more", "cursor", "has_aweme", "api_status_code"] + keys = [ + prop_name + for prop_name in dir(self) + if not prop_name.startswith("__") + and not prop_name.startswith("_") + and prop_name not in exclude_list + ] + aweme_entries = self._get_attr_value("$.item_list") or [] + list_dicts = [] + for entry in aweme_entries: + d = {"has_more": self.has_more, "cursor": self.cursor} + for key in keys: + attr_values = getattr(self, key) + index = aweme_entries.index(entry) + d[key] = attr_values[index] if index < len(attr_values) else None + list_dicts.append(d) + return list_dicts + + +class UserLiveFilter(JSONModel): + @property + def api_status_code(self): + return self._get_attr_value("$.statusCode") + + @property + def has_live(self) -> bool: + if len(self._get_attr_value("$.data")) == 4: + return False + return True + + # user + @property + def user_avatar_larger(self): + return self._get_attr_value("$.data.user.avatarLarger") + + @property + def user_id(self): + return self._get_attr_value("$.data.user.id") + + @property + def nickname(self): + return replaceT(self._get_attr_value("$.data.user.nickname")) + + @property + def nickname_raw(self): + return self._get_attr_value("$.data.user.nickname") + + @property + def user_secUid(self): + return self._get_attr_value("$.data.user.secUid") + + @property + def user_uniqueId(self): + return self._get_attr_value("$.data.user.uniqueId") + + @property + def user_secret(self): + return self._get_attr_value("$.data.user.secret") + + @property + def user_verified(self): + return self._get_attr_value("$.data.user.verified") + + @property + def user_signature(self): + return replaceT(self._get_attr_value("$.data.user.signature")) + + # stats + @property + def live_following_count(self): + return self._get_attr_value("$.data.stats.followingCount") + + @property + def live_follower_count(self): + return self._get_attr_value("$.data.stats.followerCount") + + @property + def live_user_count(self): + return self._get_attr_value("$.data.liveRoom.liveRoomStats.userCount") + + # live + @property + def live_title(self): + return replaceT(self._get_attr_value("$.data.liveRoom.title")) + + @property + def live_title_raw(self): + return self._get_attr_value("$.data.liveRoom.title") + + @property + def live_startTime(self): + return timestamp_2_str(self._get_attr_value("$.data.liveRoom.startTime")) + + @property + def live_status(self): + return self._get_attr_value("$.data.liveRoom.status") # 2开播 + + @property + def live_coverUrl(self): + return self._get_attr_value("$.data.liveRoom.coverUrl") + + @property + def live_room_mode(self): + return self._get_attr_value("$.data.liveRoom.mode") # 0直播 1聊天室 + + @property + def live_room_id(self): + return self._get_attr_value("$.data.user.roomId") + + @property + def live_stream_id(self): + return self._get_attr_value("$.data.liveRoom.streamId") + + @property + def live_qualities(self): + return self._get_list_attr_value( + "$.data.liveRoom.streamData.pull_data.options.qualities[*].sdk_key" + ) + + @property + def live_stream_data(self): + return unescape_json( + self._get_attr_value("$.data.liveRoom.streamData.pull_data.stream_data") + ) + + @property + def live_flv_url(self): + return JSONModel(self.live_stream_data)._get_attr_value( + "$.data.origin.main.flv" + ) + + @property + def live_hls_url(self): + return JSONModel(self.live_stream_data)._get_attr_value( + "$.data.origin.main.hls" + ) + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + +class CheckLiveAliveFilter(JSONModel): + @property + def api_status_code(self): + return self._get_attr_value("$.status_code") + + @property + def is_alive(self): + return self._get_list_attr_value("$.data[*].alive") + + @property + def room_id(self): + return self._get_list_attr_value("$.data[*].room_id") + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + def _to_list(self): + exclude_list = ["api_status_code"] + keys = [ + prop_name + for prop_name in dir(self) + if not prop_name.startswith("__") + and not prop_name.startswith("_") + and prop_name not in exclude_list + ] + aweme_entries = self._get_attr_value("$.data") or [] + list_dicts = [] + for entry in aweme_entries: + d = {} + for key in keys: + attr_values = getattr(self, key) + index = aweme_entries.index(entry) + d[key] = attr_values[index] if index < len(attr_values) else None + list_dicts.append(d) + return list_dicts diff --git a/f2/apps/tiktok/handler.py b/f2/apps/tiktok/handler.py index d6d8a149..b261bc6a 100644 --- a/f2/apps/tiktok/handler.py +++ b/f2/apps/tiktok/handler.py @@ -1,13 +1,12 @@ # path: f2/apps/tiktok/handler.py -import sys - from pathlib import Path +from urllib.parse import quote, unquote from typing import AsyncGenerator, Union, List, Any from f2.i18n.translator import _ from f2.log.logger import logger -from f2.utils.mode_handler import mode_handler, mode_function_map +from f2.utils.decorators import mode_handler, mode_function_map from f2.apps.tiktok.db import AsyncUserDB, AsyncVideoDB from f2.apps.tiktok.crawler import TiktokCrawler from f2.apps.tiktok.dl import TiktokDownloader @@ -19,6 +18,9 @@ UserMix, UserPlayList, PostDetail, + PostSearch, + UserLive, + CheckLiveAlive, ) from f2.apps.tiktok.filter import ( UserProfileFilter, @@ -26,6 +28,9 @@ PostDetailFilter, UserMixFilter, UserPlayListFilter, + PostSearchFilter, + UserLiveFilter, + CheckLiveAliveFilter, ) from f2.apps.tiktok.utils import ( SecUserIdFetcher, @@ -48,7 +53,7 @@ def __init__(self, kwargs: dict = ...) -> None: self.kwargs = kwargs self.downloader = TiktokDownloader(kwargs) - async def handler_user_profile( + async def fetch_user_profile( self, secUid: str = "", uniqueId: str = "", @@ -72,36 +77,16 @@ async def handler_user_profile( params = UserProfile(secUid=secUid, uniqueId=uniqueId) response = await crawler.fetch_user_profile(params) user = UserProfileFilter(response) - if user.nickname is None: - raise APIResponseError(_("API内容请求失败,请更换新cookie后再试")) + if user.uniqueId is None: + raise APIResponseError( + _("`fetch_user_profile`请求失败,请更换cookie或稍后再试") + ) return UserProfileFilter(response) - async def get_user_nickname( - self, - secUid: str, - db: AsyncUserDB, - ) -> str: - """ - 用于获取指定用户的昵称 - (Used to get nickname of specified users) - - Args: - secUid: str: 用户ID (User ID) - db: AsyncUserDB: 用户数据库 (User database) - - Return: - nick_name: str: 用户昵称 (User nickname) - """ - - user_dict = await db.get_user_info(secUid) - if not user_dict: - user_dict = await self.handler_user_profile(secUid) - await db.add_user_info(**user_dict._to_dict()) - return user_dict.get("nickname", "") - async def get_or_add_user_data( self, secUid: str, + uniqueId: str, db: AsyncUserDB, ) -> Path: """ @@ -111,6 +96,7 @@ async def get_or_add_user_data( Args: kwargs (dict): 配置参数 (Conf parameters) secUid (str): 用户ID (User ID) + uniqueId (str): 用户名 (Username) db (AsyncUserDB): 用户数据库 (User database) Returns: @@ -118,17 +104,19 @@ async def get_or_add_user_data( """ # 尝试从数据库中获取用户数据 - local_user_data = await db.get_user_info(secUid) + local_user_data = await db.get_user_info(secUid=secUid, uniqueId=uniqueId) # 从服务器获取当前用户最新数据 - current_user_data = await self.handler_user_profile(secUid) + current_user_data = await self.fetch_user_profile( + secUid=secUid, uniqueId=uniqueId + ) # 获取当前用户最新昵称 - current_nickname = current_user_data._to_dict().get("nickname") + current_uniqueId = current_user_data.uniqueId # 设置用户目录 user_path = create_or_rename_user_folder( - self.kwargs, local_user_data, current_nickname + self.kwargs, local_user_data, current_uniqueId ) # 如果用户不在数据库中,将其添加到数据库 @@ -163,91 +151,8 @@ async def get_or_add_video_data( # current_video_data = await fetch_one_video(aweme_data.get("aweme_id")) await db.add_video_info(ignore_fields=ignore_fields, **aweme_data) - async def fetch_play_list( - self, - secUid: str, - cursor: int, - page_counts: int, - ) -> UserPlayListFilter: - """ - 用于获取指定用户的作品合集列表 - (Used to get video mix list of specified user) - - Args: - secUid: str: 用户ID (User ID) - cursor: int: 分页游标 (Page cursor) - page_counts: int: 分页数量 (Page counts) - - Return: - playlist: UserPlayListFilter: 作品合集列表 (Video mix list) - """ - - logger.debug(_("开始爬取用户:{0} 的作品合集列表").format(secUid)) - - async with TiktokCrawler(self.kwargs) as crawler: - params = UserPlayList(secUid=secUid, cursor=cursor, count=page_counts) - response = await crawler.fetch_user_play_list(params) - playlist = UserPlayListFilter(response) - - if not playlist.hasPlayList: - logger.info(_("用户:{0} 没有作品合集").format(secUid)) - return {} - - logger.debug(_("当前请求的cursor:{0}").format(cursor)) - logger.debug( - _("作品合集ID:{0} 作品合集标题:{1}").format( - playlist.mixId, playlist.mixName - ) - ) - logger.debug("===================================") - return playlist - - async def select_playlist( - self, playlists: Union[dict, UserPlayListFilter] - ) -> Union[str, List[str]]: - """ - 用于选择要下载的作品合辑 - (Used to select the video mix to download) - - Args: - playlists: Union[dict, UserPlayListFilter]: 作品合辑列表 (Video mix list) - - Return: - selected_index: Union[str, List[str]]: 选择的作品合辑序号 (Selected video mix index) - """ - - if playlists == {}: - sys.exit(_("用户没有作品合辑")) - - rich_console.print("[bold]请选择要下载的合辑:[/bold]") - rich_console.print("0: [bold]全部下载[/bold]") - - for i in range(len(playlists.mixId)): - rich_console.print( - _("{0}: {1} (包含 {2} 个作品,收藏夹ID {3})").format( - i + 1, - playlists.mixName[i], - playlists.videoCount[i], - playlists.mixId[i], - ) - ) - - # rich_prompt 会有字符刷新问题,暂时使用rich_print - rich_console.print(_("[bold yellow]请输入希望下载的合辑序号:[/bold yellow]")) - selected_index = int( - rich_prompt.ask( - # _("[bold yellow]请输入希望下载的合辑序号:[/bold yellow]"), - choices=[str(i) for i in range(len(playlists.mixId) + 1)], - ) - ) - - if selected_index == 0: - return playlists.mixId - else: - return playlists.mixId[selected_index - 1] - @mode_handler("one") - async def handler_one_video(self): + async def handle_one_video(self): """ 用于获取指定作品的信息 (Used to get video info of specified video) @@ -261,7 +166,9 @@ async def handler_one_video(self): aweme_data = await self.fetch_one_video(aweme_id) async with AsyncUserDB("tiktok_users.db") as udb: - user_path = await self.get_or_add_user_data(aweme_data.secUid, udb) + user_path = await self.get_or_add_user_data( + secUid="", uniqueId=aweme_data.uniqueId, db=udb + ) async with AsyncVideoDB("tiktok_videos.db") as vdb: await self.get_or_add_video_data( @@ -275,7 +182,10 @@ async def handler_one_video(self): self.kwargs, aweme_data._to_dict(), user_path ) - async def fetch_one_video(self, itemId: str) -> PostDetailFilter: + async def fetch_one_video( + self, + itemId: str, + ) -> PostDetailFilter: """ 用于获取指定作品的详细信息 (Used to get detailed information of specified video) @@ -302,7 +212,7 @@ async def fetch_one_video(self, itemId: str) -> PostDetailFilter: return video @mode_handler("post") - async def handler_user_post(self): + async def handle_user_post(self): """ 用于获取指定用户的作品信息 (Used to get video info of specified user) @@ -318,7 +228,9 @@ async def handler_user_post(self): secUid = await SecUserIdFetcher.get_secuid(self.kwargs.get("url")) async with AsyncUserDB("tiktok_users.db") as udb: - user_path = await self.get_or_add_user_data(secUid, udb) + user_path = await self.get_or_add_user_data( + secUid=secUid, uniqueId="", db=udb + ) async for aweme_data_list in self.fetch_user_post_videos( secUid, cursor, page_counts, max_counts @@ -329,7 +241,11 @@ async def handler_user_post(self): ) async def fetch_user_post_videos( - self, secUid: str, cursor: int, page_counts: int, max_counts: float + self, + secUid: str, + cursor: int, + page_counts: int, + max_counts: float, ) -> AsyncGenerator[UserPostFilter, Any]: """ 用于获取指定用户发布的作品列表 @@ -362,7 +278,11 @@ async def fetch_user_post_videos( logger.debug(_("开始爬取第 {0} 页").format(cursor)) async with TiktokCrawler(self.kwargs) as crawler: - params = UserPost(secUid=secUid, cursor=cursor, count=page_counts) + params = UserPost( + secUid=secUid, + cursor=cursor, + count=page_counts, + ) response = await crawler.fetch_user_post(params) video = UserPostFilter(response) @@ -392,7 +312,7 @@ async def fetch_user_post_videos( logger.debug(_("爬取结束,共爬取 {0} 个作品").format(videos_collected)) @mode_handler("like") - async def handler_user_like(self): + async def handle_user_like(self): """ 用于获取指定用户的点赞作品信息 (Used to get liked video info of specified user) @@ -408,7 +328,9 @@ async def handler_user_like(self): secUid = await SecUserIdFetcher.get_secuid(self.kwargs.get("url")) async with AsyncUserDB("tiktok_users.db") as udb: - user_path = await self.get_or_add_user_data(secUid, udb) + user_path = await self.get_or_add_user_data( + secUid=secUid, uniqueId="", db=udb + ) async for aweme_data_list in self.fetch_user_like_videos( secUid, cursor, page_counts, max_counts @@ -419,7 +341,11 @@ async def handler_user_like(self): ) async def fetch_user_like_videos( - self, secUid: str, cursor: int, page_counts: int, max_counts: float + self, + secUid: str, + cursor: int, + page_counts: int, + max_counts: float, ) -> AsyncGenerator[UserPostFilter, Any]: """ 用于获取指定用户点赞的作品列表 @@ -488,7 +414,7 @@ async def fetch_user_like_videos( logger.debug(_("爬取结束,共爬取 {0} 个作品").format(videos_collected)) @mode_handler("collect") - async def handler_user_collect(self): + async def handle_user_collect(self): """ 用于获取指定用户的收藏作品信息 (Used to get collected video info of specified user) @@ -504,7 +430,9 @@ async def handler_user_collect(self): secUid = await SecUserIdFetcher.get_secuid(self.kwargs.get("url")) async with AsyncUserDB("tiktok_users.db") as udb: - user_path = await self.get_or_add_user_data(secUid, udb) + user_path = await self.get_or_add_user_data( + secUid=secUid, uniqueId="", db=udb + ) async for aweme_data_list in self.fetch_user_collect_videos( secUid, cursor, page_counts, max_counts @@ -515,7 +443,11 @@ async def handler_user_collect(self): ) async def fetch_user_collect_videos( - self, secUid: str, cursor: int, page_counts: int, max_counts: float + self, + secUid: str, + cursor: int, + page_counts: int, + max_counts: float, ) -> AsyncGenerator[UserPostFilter, Any]: """ 用于获取指定用户收藏的作品列表 @@ -584,7 +516,7 @@ async def fetch_user_collect_videos( logger.debug(_("爬取结束,共爬取 {0} 个作品").format(videos_collected)) @mode_handler("mix") - async def handler_user_mix(self): + async def handle_user_mix(self): """ 用于获取指定用户的合集作品信息 (Used to get mix video info of specified user) @@ -602,7 +534,9 @@ async def handler_user_mix(self): mixId = await self.select_playlist(playlist) async with AsyncUserDB("tiktok_users.db") as audb: - user_path = await self.get_or_add_user_data(secUid, audb) + user_path = await self.get_or_add_user_data( + secUid=secUid, uniqueId="", db=audb + ) if isinstance(mixId, str): mixId = [mixId] @@ -616,8 +550,97 @@ async def handler_user_mix(self): self.kwargs, aweme_data_list._to_list(), user_path ) + async def fetch_play_list( + self, + secUid: str, + cursor: int, + page_counts: int, + ) -> UserPlayListFilter: + """ + 用于获取指定用户的作品合集列表 + (Used to get video mix list of specified user) + + Args: + secUid: str: 用户ID (User ID) + cursor: int: 分页游标 (Page cursor) + page_counts: int: 分页数量 (Page counts) + + Return: + playlist: UserPlayListFilter: 作品合集列表 (Video mix list) + """ + + logger.debug(_("开始爬取用户:{0} 的作品合集列表").format(secUid)) + + async with TiktokCrawler(self.kwargs) as crawler: + params = UserPlayList(secUid=secUid, cursor=cursor, count=page_counts) + response = await crawler.fetch_user_play_list(params) + playlist = UserPlayListFilter(response) + + if not playlist.hasPlayList: + logger.info(_("用户:{0} 没有作品合集").format(secUid)) + return {} + + logger.debug(_("当前请求的cursor:{0}").format(cursor)) + logger.debug( + _("作品合集ID:{0} 作品合集标题:{1}").format( + playlist.mixId, playlist.mixName + ) + ) + logger.debug("===================================") + return playlist + + async def select_playlist( + self, + playlists: Union[dict, UserPlayListFilter], + ) -> Union[str, List[str]]: + """ + 用于选择要下载的作品合集 + (Used to select the video mix to download) + + Args: + playlists: Union[dict, UserPlayListFilter]: 作品合集列表 (Video mix list) + + Return: + selected_index: Union[str, List[str]]: 选择的作品合集序号 (Selected video mix index) + """ + + if playlists == {}: + logger.warning(_("用户没有作品合集")) + return + + rich_console.print("[bold]请选择要下载的合集:[/bold]") + rich_console.print("0: [bold]全部下载[/bold]") + + for i in range(len(playlists.mixId)): + rich_console.print( + _("{0}: {1} (包含 {2} 个作品,收藏夹ID {3})").format( + i + 1, + playlists.mixName[i], + playlists.videoCount[i], + playlists.mixId[i], + ) + ) + + # rich_prompt 会有字符刷新问题,暂时使用rich_print + rich_console.print(_("[bold yellow]请输入希望下载的合集序号:[/bold yellow]")) + selected_index = int( + rich_prompt.ask( + # _("[bold yellow]请输入希望下载的合集序号:[/bold yellow]"), + choices=[str(i) for i in range(len(playlists.mixId) + 1)], + ) + ) + + if selected_index == 0: + return playlists.mixId + else: + return playlists.mixId[selected_index - 1] + async def fetch_user_mix_videos( - self, mixId: str, cursor: int, page_counts: int, max_counts: float + self, + mixId: str, + cursor: int, + page_counts: int, + max_counts: float, ) -> AsyncGenerator[UserMixFilter, Any]: """ 用于获取指定用户合集的作品列表 @@ -669,14 +692,14 @@ async def fetch_user_mix_videos( videos_collected += len(mix.aweme_id) if not mix.hasMore and str(mix.api_status_code) == "0": - logger.debug(_("合辑: {0} 所有作品采集完毕").format(mixId)) + logger.debug(_("合集: {0} 所有作品采集完毕").format(mixId)) break else: logger.debug(_("第 {0} 页没有找到作品").format(cursor)) if not mix.hasMore and str(mix.api_status_code) == "0": - logger.debug(_("合辑: {0} 所有作品采集完毕").format(mixId)) + logger.debug(_("合集: {0} 所有作品采集完毕").format(mixId)) break # 更新已经处理的作品数量 (Update the number of videos processed) @@ -685,6 +708,223 @@ async def fetch_user_mix_videos( logger.debug(_("爬取结束,共爬取 {0} 个作品").format(videos_collected)) + @mode_handler("search") + async def handle_search(self): + """ + 用于搜索指定关键词的作品信息 + (Used to search video info of specified keyword) + + Args: + kwargs: dict: 参数字典 (Parameter dictionary) + """ + + cursor = self.kwargs.get("cursor", 0) + page_counts = self.kwargs.get("page_counts", 30) + max_counts = self.kwargs.get("max_counts") + keyword = self.kwargs.get("keyword") + + secUid = await SecUserIdFetcher.get_secuid(self.kwargs.get("url")) + + async with AsyncUserDB("tiktok_users.db") as udb: + user_path = await self.get_or_add_user_data( + secUid=secUid, uniqueId="", db=udb + ) + + async for aweme_data_list in self.fetch_search_videos( + keyword, cursor, page_counts, max_counts + ): + # 创建下载任务 + await self.downloader.create_download_tasks( + self.kwargs, aweme_data_list._to_list(), user_path + ) + + async def fetch_search_videos( + self, + keyword: str, + offset: int, + page_counts: int, + max_counts: float, + ) -> AsyncGenerator[PostSearchFilter, Any]: + """ + 用于搜索指定关键词的作品列表 + (Used to search video list of specified keyword) + + Args: + keyword: str: 搜索关键词 (Search keyword) + offset: int: 分页游标 (Page offset) + page_counts: int: 分页数量 (Page counts) + max_counts: float: 最大数量 (Max counts) + + Return: + search: AsyncGenerator[PostSearchFilter, Any]: 搜索作品信息过滤器 (Search video info filter) + """ + + max_counts = max_counts or float("inf") + videos_collected = 0 + search_id = "" + + logger.info( + _("开始搜索关键词:{0} 的作品,最大作品数量 {1} ").format( + keyword, max_counts + ) + ) + + while videos_collected < max_counts: + current_request_size = min(page_counts, max_counts - videos_collected) + + logger.debug("===================================") + logger.info( + _("开始搜索第 {0} 个作品,每次请求数量:{1}").format( + offset + 1, current_request_size + ) + ) + + async with TiktokCrawler(self.kwargs) as crawler: + params = PostSearch( + keyword=quote(keyword, safe=""), + offset=offset, + count=page_counts, + search_id=search_id, + ) + response = await crawler.fetch_post_search(params) + search = PostSearchFilter(response) + + if not search.has_aweme: + logger.info(_("第 {0} 个offset没有找到作品").format(offset)) + if not search.has_more and str(search.api_status_code) == "0": + logger.info(_("关键词:{0} 所有作品采集完毕").format(keyword)) + break + else: + offset = search.cursor + continue + + logger.debug(_("当前请求的offset:{0}").format(offset)) + logger.debug( + _("作品ID:{0} 作品文案:{1} 作者:{2}").format( + search.aweme_id, search.desc, search.nickname + ) + ) + logger.debug("===================================") + + if videos_collected >= max_counts: + logger.info( + _("关键词:{0} 已达到最大下载数量 {1} 个").format( + keyword, max_counts + ) + ) + break + + yield search + + # 更新已经处理的作品数量 (Update the number of videos processed) + videos_collected += len(search.aweme_id) + offset = search.cursor + search_id = search.search_id + + logger.info(_("搜索结束,共搜索到 {0} 个作品").format(videos_collected)) + + @mode_handler("live") + async def handle_user_live(self): + """ + 用于获取指定用户的直播信息 + (Used to get live info of specified user) + + Args: + kwargs: dict: 参数字典 (Parameter dictionary) + """ + + uniqueId = await SecUserIdFetcher.get_uniqueid(self.kwargs.get("url")) + + webcast_data = await self.fetch_user_live_videos(uniqueId) + + # 判断是否有直播间 + if not webcast_data.has_live: + logger.info(_("用户:{0} 没有直播间").format(uniqueId)) + return + + # 是否正在直播 + if webcast_data.live_status != 2: + logger.info(_("当前 {0} 直播已结束").format(webcast_data.live_title_raw)) + return + + async with AsyncUserDB("tiktok_users.db") as udb: + user_path = await self.get_or_add_user_data( + secUid="", uniqueId=uniqueId, db=udb + ) + + # 创建下载任务 + await self.downloader.create_stream_tasks( + self.kwargs, webcast_data._to_dict(), user_path + ) + + async def fetch_user_live_videos( + self, + uniqueId: str, + ) -> UserLiveFilter: + """ + 用于获取指定用户直播的作品列表 + (Used to get live video list of specified user) + + Args: + secUid: str: 用户ID (User ID) + cursor: int: 分页游标 (Page cursor) + page_counts: int: 分页数量 (Page counts) + max_counts: float: 最大数量 (Max counts) + + Return: + live: [UserLiveFilter: 直播作品信息过滤器 (Live video info filter) + """ + + logger.info(_("开始爬取用户:{0} 的直播").format(uniqueId)) + logger.debug("===================================") + + async with TiktokCrawler(self.kwargs) as crawler: + params = UserLive(uniqueId=uniqueId) + response = await crawler.fetch_user_live(params) + live = UserLiveFilter(response) + + logger.debug( + _("直播间ID:{0} 直播间标题:{1} 直播状态: {2} 观看人数: {3}").format( + live.live_room_id, + live.live_title_raw, + live.live_status, + live.live_user_count, + ) + ) + logger.debug("===================================") + logger.info(_("直播信息爬取结束")) + + return live + + async def fetch_check_live_alive(self, room_ids: str) -> CheckLiveAliveFilter: + """ + 用于检查直播间是否在线 + (Used to check if the live room is online) + + Args: + room_ids: str: 直播间ID (Live room ID) + + Return: + check: CheckLiveAliveFilter: 检查直播间在线状态过滤器 (Check live status filter) + + Note: + 房间号参数为字符串形式,多个房间号用逗号分隔,如:7381444193462078214,7381457815116466949, + """ + logger.info(_("开始检查直播间在线状态")) + logger.debug("===================================") + async with TiktokCrawler(self.kwargs) as crawler: + response = await crawler.fetch_check_live_alive( + CheckLiveAlive(room_ids=room_ids) + ) + check = CheckLiveAliveFilter(response) + + logger.info( + _("直播间:{0},在线状态:{1}").format(check.room_id, check.is_alive) + ) + logger.debug("===================================") + logger.info(_("直播间在线状态检查结束")) + return check + async def main(kwargs): mode = kwargs.get("mode") @@ -692,4 +932,3 @@ async def main(kwargs): await mode_function_map[mode](TiktokHandler(kwargs)) else: logger.error(_("不存在该模式: {0}").format(mode)) - rich_console.print(_("不存在该模式: {0}").format(mode)) diff --git a/f2/apps/tiktok/help.py b/f2/apps/tiktok/help.py index c8628941..b9c55f39 100644 --- a/f2/apps/tiktok/help.py +++ b/f2/apps/tiktok/help.py @@ -21,7 +21,7 @@ def help() -> None: "-u --url", "[dark_cyan]str", _( - "根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合辑与直播同上" + "根据模式提供相应的链接。例如:主页、点赞、收藏作品填入主页链接,单作品填入作品链接,合集与直播同上" ), ), ("-m --music", "[dark_cyan]Choice", _("是否保存视频原声。可选:'yes'、'no'")), @@ -37,7 +37,7 @@ def help() -> None: "-M --mode", "[dark_cyan]Choice", _( - "下载模式:单个作品(one),主页作品(post), 点赞作品(like), 收藏作品(collect), 合辑播放列表(mix)" + "下载模式:单个作品(one),主页作品(post), 点赞作品(like), 收藏作品(collect), 合集播放列表(mix)" ), ), ( @@ -59,6 +59,7 @@ def help() -> None: "下载日期区间发布的作品,格式:2022-01-01|2023-01-01,'all' 为下载所有作品" ), ), + ("-w --keyword", "[dark_cyan]str", _("搜索关键词,用于搜索作品。")), ("-e --timeout", "[dark_cyan]int", _("网络请求超时时间。")), ("-r --max-retries", "[dark_cyan]int", _("网络请求超时重试数。")), ("-x --max-connections", "[dark_cyan]int", _("网络请求并发连接数。")), @@ -71,7 +72,7 @@ def help() -> None: ( "-s --page-counts", "[dark_cyan]int", - _("从接口每页可获取作品数,不建议超过20。"), + _("从接口每页可获取作品数,不建议超过 20"), ), ( "-l --languages", @@ -82,7 +83,7 @@ def help() -> None: "-P --proxies", "[dark_cyan]str", _( - "代理服务器,最多 2 个参数,http与https。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" + "代理服务器,最多 2 个参数,http://与https://。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" ), ), ( diff --git a/f2/apps/tiktok/model.py b/f2/apps/tiktok/model.py index 2f3002c5..8d14d11c 100644 --- a/f2/apps/tiktok/model.py +++ b/f2/apps/tiktok/model.py @@ -4,7 +4,7 @@ from pydantic import BaseModel from urllib.parse import quote, unquote -from f2.apps.tiktok.utils import TokenManager +from f2.apps.tiktok.utils import TokenManager, ClientConfManager from f2.utils.utils import get_timestamp @@ -14,34 +14,52 @@ class BaseRequestModel(BaseModel): aid: str = "1988" app_language: str = "zh-Hans" app_name: str = "tiktok_web" - browser_language: str = "zh-CN" - browser_name: str = "Mozilla" + browser_language: str = ClientConfManager.brm_browser().get("language", "zh-CN") + browser_name: str = ClientConfManager.brm_browser().get("name", "Mozilla") browser_online: str = "true" - browser_platform: str = "Win32" + browser_platform: str = ClientConfManager.brm_browser().get("platform", "Win32") browser_version: str = quote( - "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", + ClientConfManager.brm_browser().get( + "version", + "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", + ), safe="", ) channel: str = "tiktok_web" cookie_enabled: str = "true" - device_id: str = "7306060721837852167" - device_platform: str = "web_pc" + device_id: str = ClientConfManager.brm_device().get( + "id", "7379572768071239176" + ) # 风控参数 + device_platform: str = ClientConfManager.brm_device().get("platform", "web_pc") focus_state: str = "true" from_page: str = "user" history_len: int = 4 is_fullscreen: str = "false" is_page_visible: str = "true" language: str = "zh-Hans" - os: str = "windows" - priority_region: str = "" + os: str = ClientConfManager.base_request_model().get("os", "windows") + priority_region: str = ClientConfManager.base_request_model().get( + "priority_region", "US" + ) referer: str = "" - region: str = "SG" # SG JP KR... - root_referer: str = quote("https://www.tiktok.com/", safe="") + region: str = ClientConfManager.base_request_model().get( + "region", "SG" + ) # SG JP KR... + # root_referer: str = quote("https://www.tiktok.com/", safe="") screen_height: int = 1080 screen_width: int = 1920 - webcast_language: str = "zh-Hans" - tz_name: str = quote("Asia/Hong_Kong", safe="") - msToken: str = TokenManager.gen_real_msToken() + webcast_language: str = ClientConfManager.base_request_model().get( + "webcast_language", "zh-Hans" + ) + tz_name: str = quote( + ClientConfManager.base_request_model().get("tz_name", "Asia/Hong_Kong"), safe="" + ) + try: + msToken: str = TokenManager.gen_real_msToken() + except Exception as e: + print(f"Error generating msToken: {e}") + # 发生异常时,重新生成msToken,不生成虚假msToken + msToken: str = TokenManager.gen_real_msToken() # router model @@ -92,3 +110,37 @@ class PostComment(BaseRequestModel): count: int = 20 cursor: int = 0 current_region: str = "" + + +class PostSearch(BaseRequestModel): + count: int = 20 + keyword: str + offset: int = 0 + from_page: str = "search" + search_id: str = "" + web_search_code: str = quote( + str( + { + "tiktok": { + "client_params_x": { + "search_engine": { + "ies_mt_user_live_video_card_use_libra": 1, + "mt_search_general_user_live_card": 1, + } + }, + "search_server": {}, + } + } + ), + safe="", + ) + + +class UserLive(BaseRequestModel): + uniqueId: str + sourceType: int = 54 + + +class CheckLiveAlive(BaseRequestModel): + from_page: str = "live" + room_ids: str diff --git a/f2/apps/tiktok/test/test_tiktok_crawler.py b/f2/apps/tiktok/test/test_tiktok_crawler.py new file mode 100644 index 00000000..77e5660e --- /dev/null +++ b/f2/apps/tiktok/test/test_tiktok_crawler.py @@ -0,0 +1,26 @@ +import pytest +from f2.apps.tiktok.model import UserPost +from f2.apps.tiktok.filter import UserPostFilter +from f2.apps.tiktok.crawler import TiktokCrawler +from f2.utils.conf_manager import TestConfigManager + + +@pytest.fixture +def cookie_fixture(): + return TestConfigManager.get_test_config("tiktok") + + +@pytest.mark.asyncio +async def test_crawler_by_secUid(cookie_fixture): + async with TiktokCrawler(cookie_fixture) as crawler: + params = UserPost( + cursor=0, + count=5, + secUid="MS4wLjABAAAAREbjjYuEFoUJN86G9f2byGC_LSOTz4N7BGdreT_8Cro-NkzZYf_nxpDpLp9R6ElJ", + ) + response = await crawler.fetch_user_post(params) + assert response, "Failed to fetch user post" + + video = UserPostFilter(response) + video_id = video.aweme_id + assert video_id, "Failed to extract video ID" diff --git a/f2/apps/tiktok/test/test_tiktok_device_id.py b/f2/apps/tiktok/test/test_tiktok_device_id.py new file mode 100644 index 00000000..acfd7e55 --- /dev/null +++ b/f2/apps/tiktok/test/test_tiktok_device_id.py @@ -0,0 +1,68 @@ +import pytest +from f2.apps.tiktok.utils import DeviceIdManager + + +@pytest.mark.asyncio +async def test_gen_device_id(): + device = await DeviceIdManager.gen_device_id() + + deviceId = device["deviceId"] + assert deviceId is not None + assert len(deviceId) == 19 + + tt_chain_token = device["cookie"] + assert tt_chain_token is not None + + +@pytest.mark.asyncio +async def test_gen_device_id_with_full_cookie(): + device = await DeviceIdManager.gen_device_id(full_cookie=True) + + deviceId = device["deviceId"] + assert deviceId is not None + assert len(deviceId) == 19 + + cookie = device["cookie"] + assert cookie is not None + + +@pytest.mark.asyncio +async def test_gen_device_ids(): + devices = await DeviceIdManager.gen_device_ids(3) + + assert "deviceId" in devices + assert "cookie" in devices + + device_ids = devices["deviceId"] + tt_chain_tokens = devices["cookie"] + + assert len(device_ids) == 3 + assert len(tt_chain_tokens) == 3 + + for deviceId in device_ids: + assert deviceId is not None + assert len(deviceId) == 19 + + for tt_chain_token in tt_chain_tokens: + assert tt_chain_token is not None + + +@pytest.mark.asyncio +async def test_gen_device_ids_with_full_cookie(): + devices = await DeviceIdManager.gen_device_ids(3, full_cookie=True) + + assert "deviceId" in devices + assert "cookie" in devices + + device_ids = devices["deviceId"] + cookies = devices["cookie"] + + assert len(device_ids) == 3 + assert len(cookies) == 3 + + for deviceId in device_ids: + assert deviceId is not None + assert len(deviceId) == 19 + + for tt_chain_token in cookies: + assert tt_chain_token is not None diff --git a/f2/apps/tiktok/test/test_tiktok_token.py b/f2/apps/tiktok/test/test_tiktok_token.py new file mode 100644 index 00000000..f2272816 --- /dev/null +++ b/f2/apps/tiktok/test/test_tiktok_token.py @@ -0,0 +1,33 @@ +import os +import pytest +from f2.apps.tiktok.utils import TokenManager + +# 检查环境变量是否设置为跳过测试 +skip_in_ci = os.getenv("SKIP_IN_CI", "false").lower() == "true" + + +@pytest.mark.skipif(skip_in_ci, reason="Skipping test in CI environment") +def test_gen_real_msToken(): + token = TokenManager.gen_real_msToken() + assert token is not None, "gen_real_msToken() should return a valid token" + assert isinstance(token, str), "gen_real_msToken() should return a string" + + +def test_gen_false_msToken(): + token = TokenManager.gen_false_msToken() + assert token is not None, "gen_false_msToken() should return a valid token" + assert isinstance(token, str), "gen_false_msToken() should return a string" + + +@pytest.mark.skipif(skip_in_ci, reason="Skipping test in CI environment") +def test_gen_ttwid(): + ttwid = TokenManager.gen_ttwid() + assert ttwid is not None, "gen_ttwid() should return a valid ttwid" + assert isinstance(ttwid, str), "gen_ttwid() should return a string" + + +@pytest.mark.skipif(skip_in_ci, reason="Skipping test in CI environment") +def test_gen_odin_tt(): + csrf_token = TokenManager.gen_odin_tt() + assert csrf_token is not None, "gen_odin_tt() should return a valid csrf token" + assert isinstance(csrf_token, str), "gen_odin_tt() should return a string" diff --git a/f2/apps/tiktok/utils.py b/f2/apps/tiktok/utils.py index 2688313f..5f3cca4a 100644 --- a/f2/apps/tiktok/utils.py +++ b/f2/apps/tiktok/utils.py @@ -5,6 +5,7 @@ import json import httpx import asyncio +import traceback from typing import Union from pathlib import Path @@ -18,197 +19,415 @@ get_timestamp, extract_valid_urls, split_filename, + split_set_cookie, ) +from f2.crawlers.base_crawler import BaseCrawler from f2.exceptions.api_exceptions import ( - APIError, APIConnectionError, APIResponseError, APIUnauthorizedError, APINotFoundError, + APITimeoutError, ) -class TokenManager: - f2_manager = ConfigManager(f2.F2_CONFIG_FILE_PATH).get_config("f2").get("tiktok") - token_conf = f2_manager.get("msToken", None) - ttwid_conf = f2_manager.get("ttwid", None) - odin_tt_conf = f2_manager.get("odin_tt", None) - proxies_conf = f2_manager.get("proxies", None) - proxies = { - "http://": proxies_conf.get("http", None), - "https://": proxies_conf.get("https", None), +class ClientConfManager: + """ + 用于管理客户端配置 (Used to manage client configuration) + """ + + client_conf = ConfigManager(f2.F2_CONFIG_FILE_PATH).get_config("f2") + tiktok_conf = client_conf.get("tiktok", {}) + + @classmethod + def client(cls) -> dict: + return cls.tiktok_conf + + @classmethod + def conf_version(cls) -> str: + return cls.client_conf.get("version", "unknown") + + @classmethod + def base_request_model(cls) -> dict: + return cls.client().get("model", {}) + + @classmethod + def brm_browser(cls) -> dict: + return cls.base_request_model().get("browser", {}) + + @classmethod + def brm_device(cls) -> dict: + return cls.base_request_model().get("device", {}) + + @classmethod + def proxies(cls) -> dict: + return cls.client().get("proxies", {}) + + @classmethod + def headers(cls) -> dict: + return cls.client().get("headers", {}) + + @classmethod + def user_agent(cls) -> str: + return cls.headers().get("User-Agent", "") + + @classmethod + def referer(cls) -> str: + return cls.headers().get("Referer", "") + + @classmethod + def msToken(cls) -> dict: + return cls.client().get("msToken", {}) + + @classmethod + def ttwid(cls) -> dict: + return cls.client().get("ttwid", {}) + + @classmethod + def odin_tt(cls) -> dict: + return cls.client().get("odin_tt", {}) + + +class TokenManager(BaseCrawler): + """ + TokenManager 类用于生成和管理 TikTok 请求所需的各种令牌。 + + 该类继承自 BaseCrawler,利用其中的 client 进行 HTTP 请求。主要包含以下方法: + - gen_real_msToken: 生成真实的 msToken。 + - gen_false_msToken: 生成虚假的 msToken。 + - gen_ttwid: 生成 ttwid。 + - gen_odin_tt: 生成 odin_tt。 + + 类属性: + - token_conf: 从 ClientConfManager 获取的 msToken 配置。 + - ttwid_conf: 从 ClientConfManager 获取的 ttwid 配置。 + - odin_tt_conf: 从 ClientConfManager 获取的 odin_tt 配置。 + - proxies: 从 ClientConfManager 获取的代理配置。 + - mstoken_headers: 生成 msToken 请求所需的 HTTP 头信息。 + - ttwid_headers: 生成 ttwid 请求所需的 HTTP 头信息。 + """ + + token_conf = ClientConfManager.msToken() + ttwid_conf = ClientConfManager.ttwid() + odin_tt_conf = ClientConfManager.odin_tt() + proxies = ClientConfManager.proxies() + user_agent = ClientConfManager.user_agent() + mstoken_headers = { + "Content-Type": "application/json", + "User-Agent": user_agent, + } + ttwid_headers = { + "Cookie": ttwid_conf.get("cookie"), + "Content-Type": "text/plain", + "User-Agent": user_agent, + } + odin_tt_headers = { + "Referer": ClientConfManager.referer(), + "User-Agent": user_agent, } + def __init__(self): + super().__init__(proxies=self.proxies) + @classmethod def gen_real_msToken(cls) -> str: """ - 生成真实的msToken,当出现错误时返回虚假的值 - (Generate a real msToken and return a false value when an error occurs) + 生成真实的 msToken。 + + Returns: + msToken: 生成的 msToken + + Raises: + APITimeoutError: 如果请求超时。 + APIConnectionError: 如果网络连接失败。 + APIUnauthorizedError: 如果请求协议错误。 + APIResponseError: 如果响应不符合要求。 """ - payload = json.dumps( - { - "magic": cls.token_conf["magic"], - "version": cls.token_conf["version"], - "dataType": cls.token_conf["dataType"], - "strData": cls.token_conf["strData"], - "tspFromClient": get_timestamp(), - } - ) - - headers = { - "User-Agent": cls.token_conf["User-Agent"], - "Content-Type": "application/json", - } - - transport = httpx.HTTPTransport(retries=5) - with httpx.Client(transport=transport, proxies=cls.proxies) as client: - try: - response = client.post( - cls.token_conf["url"], headers=headers, content=payload - ) - response.raise_for_status() - - msToken = str(httpx.Cookies(response.cookies).get("msToken")) - - if len(msToken) not in [148]: - raise APIResponseError(_("{0} 内容不符合要求").format("msToken")) - - return msToken - - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) - raise APIConnectionError( - _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(cls.token_conf["url"], cls.proxies, cls.__name__, exc) - ) - - except httpx.HTTPStatusError as e: - # 捕获 httpx 的状态代码错误 (captures specific status code errors from httpx) - if response.status_code == 401: - raise APIUnauthorizedError( - _( - "参数验证失败,请更新 F2 配置文件中的 {0},以匹配 {1} 新规则" - ).format("msToken", "tiktok") - ) + instance = cls() - elif response.status_code == 404: - raise APINotFoundError(_("{0} 无法找到API端点").format("msToken")) - else: - raise APIResponseError( - _("链接:{0},状态码 {1}:{2} ").format( - e.response.url, e.response.status_code, e.response.text - ) - ) + try: + payload = json.dumps( + { + "magic": instance.token_conf["magic"], + "version": instance.token_conf["version"], + "dataType": instance.token_conf["dataType"], + "strData": instance.token_conf["strData"], + "tspFromClient": get_timestamp(), + } + ) + response = instance.client.post( + instance.token_conf["url"], + content=payload, + headers=instance.mstoken_headers, + ) + response.raise_for_status() + + msToken = str(httpx.Cookies(response.cookies).get("msToken")) + + if len(msToken) != 148 or msToken is None: + raise APIResponseError(_("{0} 内容不符合要求").format("msToken")) + + logger.debug(_("生成真实的 msToken:{0}").format(msToken)) + return msToken + + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求端点超时"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("网络连接失败,请检查当前网络环境"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求协议错误"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) - except APIError as e: - # 返回虚假的msToken (Return a fake msToken) - logger.error(_("msToken API错误:{0}").format(e)) - logger.info(_("生成虚假的msToken")) - return cls.gen_false_msToken() + except httpx.ProxyError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求代理错误"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), + instance.token_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) @classmethod def gen_false_msToken(cls) -> str: - """生成随机msToken (Generate random msToken)""" - return gen_random_str(146) + "==" + """ + 生成随机的虚假 msToken。 + + Returns: + false_msToken: 生成的虚假 msToken + """ + false_msToken = gen_random_str(146) + "==" + logger.debug(_("生成虚假的 msToken:{0}").format(false_msToken)) + return false_msToken @classmethod def gen_ttwid(cls) -> str: """ - 生成请求必带的ttwid (Generate the essential ttwid for requests) + 生成请求必带的 ttwid。 + + Returns: + ttwid: 生成的 ttwid + + Raises: + APITimeoutError: 如果请求超时。 + APIConnectionError: 如果网络连接失败。 + APIUnauthorizedError: 如果请求协议错误。 + APIResponseError: 如果响应不符合要求。 """ - transport = httpx.HTTPTransport(retries=5) - with httpx.Client(transport=transport, proxies=cls.proxies) as client: - try: - response = client.post( - cls.ttwid_conf["url"], - content=cls.ttwid_conf["data"], - headers={ - "Cookie": cls.ttwid_conf.get("cookie"), - "Content-Type": "text/plain", - }, - ) - response.raise_for_status() - - ttwid = httpx.Cookies(response.cookies).get("ttwid") - - if ttwid is None: - raise APIResponseError( - _("ttwid: 检查没有通过, 请更新配置文件中的ttwid") - ) - return ttwid + instance = cls() + + try: + response = instance.client.post( + instance.ttwid_conf["url"], + content=instance.ttwid_conf["data"], + headers=instance.ttwid_headers, + ) + response.raise_for_status() + + ttwid = httpx.Cookies(response.cookies).get("ttwid") - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) - raise APIConnectionError( - _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(cls.ttwid_conf["url"], cls.proxies, cls.__name__, exc) + if ttwid is None: + raise APIResponseError( + _("ttwid: 检查没有通过, 请更新配置文件中的 ttwid") ) - except httpx.HTTPStatusError as e: - # 捕获 httpx 的状态代码错误 (captures specific status code errors from httpx) - if response.status_code == 401: - raise APIUnauthorizedError( - _( - "参数验证失败,请更新 F2 配置文件中的 {0},以匹配 {1} 新规则" - ).format("ttwid", "tiktok") - ) + logger.debug(_("生成 ttwid:{0}").format(str(ttwid))) + return str(ttwid) + + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求端点超时"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) - elif response.status_code == 404: - raise APINotFoundError(_("{0} 无法找到API端点").format("ttwid")) - else: - raise APIResponseError( - _("链接:{0},状态码 {1}:{2} ").format( - e.response.url, e.response.status_code, e.response.text - ) - ) + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("网络连接失败,请检查当前网络环境"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求协议错误"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProxyError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求代理错误"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), + instance.ttwid_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) @classmethod - def gen_odin_tt(cls): + def gen_odin_tt(cls) -> str: """ - 生成请求必带的odin_tt (Generate the essential odin_tt for requests) + 生成请求必带的 odin_tt。 + + Returns: + odin_tt: 生成的 odin_tt + + Raises: + APITimeoutError: 如果请求超时。 + APIConnectionError: 如果网络连接失败。 + APIUnauthorizedError: 如果请求协议错误。 + APIResponseError: 如果响应不符合要求。 """ - transport = httpx.HTTPTransport(retries=5) - with httpx.Client(transport=transport, proxies=cls.proxies) as client: - try: - response = client.get(cls.odin_tt_conf["url"]) - response.raise_for_status() - - odin_tt = httpx.Cookies(response.cookies).get("odin_tt") - - if odin_tt is None: - raise APIResponseError(_("{0} 内容不符合要求").format("odin_tt")) - - return odin_tt - - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) - raise APIConnectionError( - _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(cls.odin_tt_conf["url"], cls.proxies, cls.__name__, exc) - ) - - except httpx.HTTPStatusError as e: - # 捕获 httpx 的状态代码错误 (captures specific status code errors from httpx) - if response.status_code == 401: - raise APIUnauthorizedError( - _( - "参数验证失败,请更新 F2 配置文件中的 {0},以匹配 {1} 新规则" - ).format("odin_tt", "tiktok") - ) - elif response.status_code == 404: - raise APINotFoundError(_("{0} 无法找到API端点").format("odin_tt")) - else: - raise APIResponseError( - _("链接:{0},状态码 {1}:{2} ").format( - e.response.url, e.response.status_code, e.response.text - ) - ) + instance = cls() + + try: + response = instance.client.get( + instance.odin_tt_conf["url"], + headers=instance.odin_tt_headers, + ) + response.raise_for_status() + + odin_tt = httpx.Cookies(response.cookies).get("odin_tt") + + if odin_tt is None: + raise APIResponseError(_("{0} 内容不符合要求").format("odin_tt")) + + return odin_tt + + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求端点超时"), + instance.odin_tt_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("网络连接失败,请检查当前网络环境"), + instance.odin_tt_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求协议错误"), + instance.odin_tt_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProxyError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求代理错误"), + instance.odin_tt_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), + instance.odin_tt_conf["url"], + cls.proxies, + cls.__name__, + exc, + ) + ) class XBogusManager: @@ -251,106 +470,192 @@ def model_2_endpoint( return final_endpoint -class SecUserIdFetcher: - # 预编译正则表达式 +class SecUserIdFetcher(BaseCrawler): + """ + SecUserIdFetcher 类用于从 TikTok 用户主页链接中提取用户的 sec_uid 和 unique_id。 + + 该类继承自 BaseCrawler,利用其中的 aclient 进行 HTTP 请求。主要包含四个方法: + - get_secuid: 异步类方法,用于获取单个 TikTok 用户的 sec_uid。 + - get_all_secuid: 异步类方法,用于获取多个 TikTok 用户的 sec_uid 列表。 + - get_uniqueid: 异步类方法,用于获取单个 TikTok 用户的 unique_id。 + - get_all_uniqueid: 异步类方法,用于获取多个 TikTok 用户的 unique_id 列表。 + + 类属性: + - _TIKTOK_SECUID_PARREN: 编译后的正则表达式,用于匹配 sec_uid。 + - _TIKTOK_UNIQUEID_PARREN: 编译后的正则表达式,用于匹配 unique_id。 + - _TIKTOK_NOTFOUND_PARREN: 编译后的正则表达式,用于检查页面是否不存在。 + - proxies: 从 ClientConfManager 获取的代理配置。 + + 方法: + - __init__: 初始化 SecUserIdFetcher 实例,并调用父类的初始化方法。 + - get_secuid: 异步类方法,接受一个用户主页链接,返回对应用户的 sec_uid。 + - get_all_secuid: 异步类方法,接受一个用户主页链接列表,返回对应用户的 sec_uid 列表。 + - get_uniqueid: 异步类方法,接受一个用户主页链接,返回对应用户的 unique_id。 + - get_all_uniqueid: 异步类方法,接受一个用户主页链接列表,返回对应用户的 unique_id 列表。 + + 异常处理: + - 在 HTTP 请求过程中,处理可能出现的 TimeoutException、NetworkError、ProtocolError、ProxyError 和 HTTPStatusError 异常,并记录相应的错误信息。 + + 使用示例: + # 获取单个用户的 sec_uid + url = "https://www.tiktok.com/@someuser" + sec_uid = await SecUserIdFetcher.get_secuid(url) + + # 获取多个用户的 sec_uid 列表 + urls = [ + "https://www.tiktok.com/@user1", + "https://www.tiktok.com/@user2", + "https://www.tiktok.com/@user3" + ] + sec_uids = await SecUserIdFetcher.get_all_secuid(urls) + + # 获取单个用户的 unique_id + unique_id = await SecUserIdFetcher.get_uniqueid(url) + + # 获取多个用户的 unique_id 列表 + unique_ids = await SecUserIdFetcher.get_all_uniqueid(urls) + """ + _TIKTOK_SECUID_PARREN = re.compile( r"" ) _TIKTOK_UNIQUEID_PARREN = re.compile(r"/@([^/?]*)") _TIKTOK_NOTFOUND_PARREN = re.compile(r"notfound") + proxies = ClientConfManager.proxies() + + def __init__(self): + super().__init__(proxies=self.proxies) + @classmethod async def get_secuid(cls, url: str) -> str: """ - 获取TikTok用户sec_uid + 获取TikTok用户sec_uid。 + Args: url: 用户主页链接 - Return: + + Returns: sec_uid: 用户唯一标识 + + Raises: + TypeError: 如果输入的 URL 不是字符串。 + APINotFoundError: 如果输入的 URL 不合法或页面不可用。 + APIResponseError: 如果在响应中未找到 sec_uid。 + ConnectionError: 如果接口状态码异常。 + APITimeoutError: 如果请求端点超时。 + APIConnectionError: 如果网络连接失败。 + APIUnauthorizedError: 如果请求协议错误。 """ - # 进行参数检查 if not isinstance(url, str): raise TypeError("输入参数必须是字符串") - # 提取有效URL url = extract_valid_urls(url) if url is None: - raise ( - APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - ) - - transport = httpx.AsyncHTTPTransport(retries=5) - async with httpx.AsyncClient( - transport=transport, proxies=TokenManager.proxies, timeout=10 - ) as client: - try: - response = await client.get(url, follow_redirects=True) - # 444一般为Nginx拦截,不返回状态 (444 is generally intercepted by Nginx and does not return status) - if response.status_code in {200, 444}: - if cls._TIKTOK_NOTFOUND_PARREN.search(str(response.url)): - raise APINotFoundError( - _( - "页面不可用,可能是由于区域限制(代理)造成的。类名: {0}" - ).format(cls.__name__) - ) + raise APINotFoundError("输入的URL不合法。类名:{0}".format(cls.__name__)) + + # 创建一个实例以访问 aclient + instance = cls() - match = cls._TIKTOK_SECUID_PARREN.search(str(response.text)) - if not match: - raise APIResponseError( - _( - "未在响应中找到 {0},检查链接是否为用户主页。类名: {1}" - ).format("sec_uid", cls.__name__) + try: + response = await instance.aclient.get(url, follow_redirects=True) + if response.status_code in {200, 444}: + if cls._TIKTOK_NOTFOUND_PARREN.search(str(response.url)): + raise APINotFoundError( + "页面不可用,可能是由于区域限制(代理)造成的。类名:{0}" + ).format(cls.__name__) + + match = cls._TIKTOK_SECUID_PARREN.search(str(response.text)) + if not match: + raise APIResponseError( + _("未在响应中找到 {0},请检查链接。类名:{1}").format( + "sec_uid", cls.__name__ ) + ) - # 提取SIGI_STATE对象中的sec_uid - data = json.loads(match.group(1)) - default_scope = data.get("__DEFAULT_SCOPE__", {}) - user_detail = default_scope.get("webapp.user-detail", {}) - user_info = user_detail.get("userInfo", {}).get("user", {}) - sec_uid = user_info.get("secUid") + data = json.loads(match.group(1)) + default_scope = data.get("__DEFAULT_SCOPE__", {}) + user_detail = default_scope.get("webapp.user-detail", {}) + user_info = user_detail.get("userInfo", {}).get("user", {}) + sec_uid = user_info.get("secUid") - if sec_uid is None: - raise RuntimeError( - _("获取 {0} 失败,{1}").format(sec_uid, user_info) - ) + if sec_uid is None: + raise RuntimeError(_("获取 {0} 失败").format("sec_uid")) + + return sec_uid + else: + raise ConnectionError("接口状态码异常,请检查重试") + + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}".format( + "请求端点超时", url, cls.proxies, cls.__name__, exc + ) + ) - return sec_uid - else: - raise ConnectionError(_("接口状态码异常, 请检查重试")) + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}".format( + "网络连接失败,请检查当前网络环境", + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}".format( + "请求协议错误", url, cls.proxies, cls.__name__, exc + ) + ) + + except httpx.ProxyError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}".format( + "请求代理错误", url, cls.proxies, cls.__name__, exc + ) + ) - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) - raise APIConnectionError( - _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(url, TokenManager.proxies, cls.__name__, exc) + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + "{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}".format( + "状态码错误", url, cls.proxies, cls.__name__, exc ) + ) @classmethod async def get_all_secuid(cls, urls: list) -> list: """ - 获取列表secuid列表 (Get list sec_user_id list) + 获取多个TikTok用户的sec_uid。 Args: - urls: list: 用户url列表 (User url list) + urls: 用户主页链接列表 + + Returns: + secuids: 用户sec_uid列表 - Return: - secuids: list: 用户secuid列表 (User secuid list) + Raises: + TypeError: 如果输入的 URL 列表不是列表类型。 + APINotFoundError: 如果输入的 URL 列表不合法。 """ if not isinstance(urls, list): - raise TypeError(_("参数必须是列表类型")) + raise TypeError("参数必须是列表类型") - # 提取有效URL urls = extract_valid_urls(urls) if urls == []: - raise ( - APINotFoundError( - _("输入的URL List不合法。类名:{0}").format(cls.__name__) - ) + raise APINotFoundError( + "输入的URL List不合法。类名:{0}".format(cls.__name__) ) secuids = [cls.get_secuid(url) for url in urls] @@ -359,199 +664,564 @@ async def get_all_secuid(cls, urls: list) -> list: @classmethod async def get_uniqueid(cls, url: str) -> str: """ - 获取TikTok用户unique_id + 获取TikTok用户unique_id。 + Args: url: 用户主页链接 - Return: - unique_id: 用户唯一id + + Returns: + unique_id: 用户唯一标识 + + Raises: + TypeError: 如果输入的 URL 不是字符串。 + APINotFoundError: 如果输入的 URL 不合法或页面不可用。 + APIResponseError: 如果在响应中未找到 unique_id。 + ConnectionError: 如果接口状态码异常。 + APITimeoutError: 如果请求端点超时。 + APIConnectionError: 如果网络连接失败。 + APIUnauthorizedError: 如果请求协议错误。 """ - # 进行参数检查 if not isinstance(url, str): raise TypeError("输入参数必须是字符串") - # 提取有效URL url = extract_valid_urls(url) if url is None: - raise ( - APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - ) - - transport = httpx.AsyncHTTPTransport(retries=5) - async with httpx.AsyncClient( - transport=transport, proxies=TokenManager.proxies, timeout=10 - ) as client: - try: - response = await client.get(url, follow_redirects=True) - - if response.status_code in {200, 444}: - if cls._TIKTOK_NOTFOUND_PARREN.search(str(response.url)): - raise APINotFoundError( - _( - "页面不可用,可能是由于区域限制(代理)造成的。类名: {0}" - ).format(cls.__name__) - ) - - match = cls._TIKTOK_UNIQUEID_PARREN.search(str(response.url)) - if not match: - raise APIResponseError( - _("未在响应中找到 {0}").format("unique_id") - ) + raise APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - unique_id = match.group(1) + # 创建一个实例以访问 aclient + instance = cls() - if unique_id is None: - raise RuntimeError( - _("获取 {0} 失败,{1}").format("unique_id", response.url) + try: + response = await instance.aclient.get(url, follow_redirects=True) + if response.status_code in {200, 444}: + if cls._TIKTOK_NOTFOUND_PARREN.search(str(response.url)): + raise APINotFoundError( + "页面不可用,可能是由于区域限制(代理)造成的。类名:{0}" + ).format(cls.__name__) + + match = cls._TIKTOK_UNIQUEID_PARREN.search(str(response.url)) + if not match: + raise APIResponseError( + _("未在响应中找到 {0},请检查链接。类名:{1}").format( + "unique_id", cls.__name__ ) + ) - return unique_id - else: - raise ConnectionError( - _("接口状态码异常 {0}, 请检查重试").format(response.status_code) + unique_id = match.group(1) + + if unique_id is None: + raise RuntimeError( + _("获取 {0} 失败,{1}").format("unique_id", response.url) ) - except httpx.RequestError: - raise APIConnectionError( - _( - "连接端点失败,检查网络环境或代理:{0} 代理:{1} 类名:{2}" - ).format(url, TokenManager.proxies, cls.__name__), + return unique_id + + else: + raise ConnectionError("接口状态码异常,请检查重试") + + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + "请求端点超时", + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + "网络连接失败,请检查当前网络环境", + url, + cls.proxies, + cls.__name__, + exc, ) + ) + + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + "请求协议错误", + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProxyError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + "请求代理错误", + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + "状态码错误", + url, + cls.proxies, + cls.__name__, + exc, + ) + ) @classmethod async def get_all_uniqueid(cls, urls: list) -> list: """ - 获取列表unique_id列表 (Get list sec_user_id list) + 获取多个TikTok用户的unique_id。 Args: - urls: list: 用户url列表 (User url list) + urls: 用户主页链接列表 - Return: - unique_ids: list: 用户unique_id列表 (User unique_id list) + Returns: + unique_ids: 用户unique_id列表 + + Raises: + TypeError: 如果输入的 URL 列表不是列表类型。 + APINotFoundError: 如果输入的 URL 列表不合法。 """ if not isinstance(urls, list): raise TypeError(_("参数必须是列表类型")) - # 提取有效URL urls = extract_valid_urls(urls) if urls == []: - raise ( - APINotFoundError( - _("输入的URL List不合法。类名:{0}").format(cls.__name__) - ) + raise APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) ) unique_ids = [cls.get_uniqueid(url) for url in urls] return await asyncio.gather(*unique_ids) -class AwemeIdFetcher: - # https://www.tiktok.com/@scarlettjonesuk/video/7255716763118226715 - # https://www.tiktok.com/@scarlettjonesuk/video/7255716763118226715?is_from_webapp=1&sender_device=pc&web_id=7306060721837852167 +class AwemeIdFetcher(BaseCrawler): + """ + AwemeIdFetcher 类用于从 TikTok 视频链接中提取视频的 aweme_id。 + + 该类继承自 BaseCrawler,利用其中的 aclient 进行 HTTP 请求。主要包含两个方法: + - get_aweme_id: 异步类方法,用于获取单个 TikTok 视频的 aweme_id。 + - get_all_aweme_id: 异步类方法,用于获取多个 TikTok 视频的 aweme_id 列表。 + + 类属性: + - _TIKTOK_AWEMEID_PARREN: 编译后的正则表达式,用于匹配 aweme_id。 + - _TIKTOK_NOTFOUND_PARREN: 编译后的正则表达式,用于检查页面是否不存在。 + - proxies: 从 ClientConfManager 获取的代理配置。 + + 方法: + - __init__: 初始化 AwemeIdFetcher 实例,并调用父类的初始化方法。 + - get_aweme_id: 异步类方法,接受一个视频链接,返回对应视频的 aweme_id。 + - get_all_aweme_id: 异步类方法,接受一个视频链接列表,返回对应视频的 aweme_id 列表。 + + 异常处理: + - 在 HTTP 请求过程中,处理可能出现的 TimeoutException、NetworkError、ProtocolError、ProxyError 和 HTTPStatusError 异常,并记录相应的错误信息。 + + 使用示例: + # 获取单个视频的 aweme_id + url = "https://www.tiktok.com/@scarlettjonesuk/video/7255716763118226715" + aweme_id = await AwemeIdFetcher.get_aweme_id(url) + + # 获取多个视频的 aweme_id 列表 + urls = [ + "https://www.tiktok.com/@scarlettjonesuk/video/7255716763118226715", + "https://www.tiktok.com/@scarlettjonesuk/video/7255716763118226715?is_from_webapp=1&sender_device=pc&web_id=7306060721837852167" + ] + aweme_ids = await AwemeIdFetcher.get_all_aweme_id(urls) + """ - # 预编译正则表达式 _TIKTOK_AWEMEID_PARREN = re.compile(r"video/(\d*)") _TIKTOK_NOTFOUND_PARREN = re.compile(r"notfound") + proxies = ClientConfManager.proxies() + + def __init__(self): + super().__init__(proxies=self.proxies) + @classmethod async def get_aweme_id(cls, url: str) -> str: """ - 获取TikTok作品aweme_id + 获取TikTok作品aweme_id。 + Args: url: 作品链接 - Return: + + Returns: aweme_id: 作品唯一标识 + + Raises: + TypeError: 如果输入的 URL 不是字符串。 + APINotFoundError: 如果输入的 URL 不合法或页面不可用。 + APIResponseError: 如果在响应中未找到 aweme_id。 + ConnectionError: 如果接口状态码异常。 + APITimeoutError: 如果请求端点超时。 + APIConnectionError: 如果网络连接失败。 + APIUnauthorizedError: 如果请求协议错误。 """ - # 进行参数检查 if not isinstance(url, str): raise TypeError("输入参数必须是字符串") - # 提取有效URL url = extract_valid_urls(url) if url is None: - raise ( - APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - ) - - transport = httpx.AsyncHTTPTransport(retries=5) - async with httpx.AsyncClient( - transport=transport, proxies=TokenManager.proxies, timeout=10 - ) as client: - try: - response = await client.get(url, follow_redirects=True) - - if response.status_code in {200, 444}: - if cls._TIKTOK_NOTFOUND_PARREN.search(str(response.url)): - raise APINotFoundError( - _( - "页面不可用,可能是由于区域限制(代理)造成的。类名: {0}" - ).format(cls.__name__) - ) + raise APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) - match = cls._TIKTOK_AWEMEID_PARREN.search(str(response.url)) - if not match: - raise APIResponseError( - _("未在响应中找到 {0}").format("aweme_id") - ) + # 创建一个实例以访问 aclient + instance = cls() - aweme_id = match.group(1) + try: + response = await instance.aclient.get(url, follow_redirects=True) + if response.status_code in {200, 444}: + if cls._TIKTOK_NOTFOUND_PARREN.search(str(response.url)): + raise APINotFoundError( + "页面不可用,可能是由于区域限制(代理)造成的。类名:{0}".format( + cls.__name__ + ) + ) - if aweme_id is None: - raise RuntimeError( - _("获取 {0} 失败,{1}").format("aweme_id", response.url) + match = cls._TIKTOK_AWEMEID_PARREN.search(str(response.url)) + if not match: + raise APIResponseError( + _("未在响应中找到 {0},请检查链接。类名:{1}").format( + "aweme_id", cls.__name__ ) + ) + + aweme_id = match.group(1) - return aweme_id - else: - raise ConnectionError( - _("接口状态码异常 {0},请检查重试").format(response.status_code) + if aweme_id is None: + raise RuntimeError( + _("获取 {0} 失败,{1}").format("aweme_id", response.url) ) - except httpx.RequestError as exc: - # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) - raise APIConnectionError( - _( - "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" - ).format(url, TokenManager.proxies, cls.__name__, exc) + return aweme_id + else: + raise ConnectionError( + _("接口状态码异常 {0},请检查重试").format(response.status_code) ) + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求端点超时"), + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("网络连接失败,请检查当前网络环境"), + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求协议错误"), + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProxyError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求代理错误"), + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), + url, + cls.proxies, + cls.__name__, + exc, + ) + ) + @classmethod async def get_all_aweme_id(cls, urls: list) -> list: """ - 获取视频aweme_id,传入列表url都可以解析出aweme_id (Get video aweme_id, pass in the list url can parse out aweme_id) + 获取多个TikTok视频的aweme_id。 Args: - urls: list: 列表url (list url) + urls: 视频链接列表 + + Returns: + aweme_ids: 视频的唯一标识列表 - Return: - aweme_ids: list: 视频的唯一标识,返回列表 (The unique identifier of the video, return list) + Raises: + TypeError: 如果输入的 URL 列表不是列表类型。 + APINotFoundError: 如果输入的 URL 列表不合法。 """ if not isinstance(urls, list): raise TypeError(_("参数必须是列表类型")) - # 提取有效URL urls = extract_valid_urls(urls) if urls == []: - raise ( - APINotFoundError( - _("输入的URL List不合法。类名:{0}").format(cls.__name__) - ) + raise APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) ) aweme_ids = [cls.get_aweme_id(url) for url in urls] return await asyncio.gather(*aweme_ids) +class DeviceIdManager(BaseCrawler): + """ + DeviceIdManager 类用于生成设备 ID和 tt_chain_token。 + 设备 ID 和 tt_chain_token 是 TikTok API 请求的必要参数。 + + 该类继承自 BaseCrawler,利用其中的 aclient 进行 HTTP 请求。主要包含两个方法: + - gen_device_id: 异步类方法,用于生成设备 ID。 + - gen_device_ids: 异步类方法,用于生成多个设备 ID。 + + 类属性: + - _DEVICE_ID_PARTTERN: 编译后的正则表达式,用于匹配设备 ID。 + - _DEVICE_ID_URL: 设备 ID 生成器的 URL。 + - _DEVICE_ID_HEADERS: 设备 ID 生成器的请求头。 + - proxies: 从 ClientConfManager 获取的代理配置。 + + 方法: + - __init__: 初始化 DeviceIdManager 实例,并调用父类的初始化方法。 + - gen_device_id: 异步类方法,用于生成设备 ID。 + - gen_device_ids: 异步类方法,用于生成多个设备 ID。 + + 异常处理: + - 在 HTTP 请求过程中,处理可能出现的 TimeoutException、NetworkError、ProtocolError、ProxyError 和 HTTPStatusError 异常,并记录相应的错误信息。 + + 使用示例: + # 生成单个设备 ID + device = await DeviceIdManager.gen_device_id() + deviceId = device["deviceId"] + tt_chain_token = device["cookie"] + + # 生成单个设备 ID,返回完整的 cookie 信息 + device = await DeviceIdManager.gen_device_id(full_cookie=True) + deviceId = device["deviceId"] + cookie = device["cookie"] + + # 生成多个设备 ID + devices = await DeviceIdManager.gen_device_ids(3) + deviceIds = devices["deviceId"] + tt_chain_tokens = devices["cookies"] + + # 生成多个设备 ID,返回完整的 cookie 信息 + devices = await DeviceIdManager.gen_device_ids(3, full_cookie=True) + deviceIds = devices["deviceId"] + cookies = devices["cookie"] + """ + + # 预编译正则表达式 + _DEVICE_ID_PARTTERN = re.compile( + r'', + re.DOTALL, + ) + + _DEVICE_ID_URL = "https://www.tiktok.com/" + _DEVICE_ID_FULL_URL = "https://www.tiktok.com/explore" + + _DEVICE_ID_HEADERS = { + "User-Agent": ClientConfManager.user_agent(), + "Cookie": f"msToken={TokenManager.gen_real_msToken()}", + } + proxies = ClientConfManager.proxies() + + def __init__(self): + super().__init__(proxies=self.proxies) + + @classmethod + async def gen_device_id(cls, full_cookie: bool = False) -> dict: + """ + 生成设备 ID。 + + Args: + full_cookie(bool): 是否返回完整的 cookie 信息,默认为 False。 + + Returns: + dict: 生成的设备 ID 和 tt_chain_token + + Notes: + full_cookie为True时,返回完整的cookie信息,否则只返回tt_chain_token。默认即可。 + + Raises: + APITimeoutError: 如果请求超时。 + APIConnectionError: 如果网络连接失败。 + APIUnauthorizedError: 如果请求协议错误。 + APIResponseError: 如果响应不符合要求。 + """ + + instance = cls() + + try: + response = await instance.aclient.get( + ( + instance._DEVICE_ID_URL + if not full_cookie + else instance._DEVICE_ID_FULL_URL + ), + headers=instance._DEVICE_ID_HEADERS, + follow_redirects=True, + ) + response.raise_for_status() + data = instance._DEVICE_ID_PARTTERN.search(response.text).group(1).strip() + + cookie = split_set_cookie(response.headers["Set-Cookie"]) + deviceId = json.loads(data)["__DEFAULT_SCOPE__"]["webapp.app-context"][ + "wid" + ] + + if deviceId is None: + raise APIResponseError(_("{0}生成失败").format("deviceId")) + + if cookie is None: + raise APIResponseError(_("{0}生成失败").format("tt_chain_token")) + + return {"deviceId": deviceId, "cookie": cookie} + + except httpx.TimeoutException as exc: + logger.error(traceback.format_exc()) + raise APITimeoutError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求端点超时"), + instance._DEVICE_ID_URL, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.NetworkError as exc: + logger.error(traceback.format_exc()) + raise APIConnectionError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("网络连接失败,请检查当前网络环境"), + instance._DEVICE_ID_URL, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.ProtocolError as exc: + logger.error(traceback.format_exc()) + raise APIUnauthorizedError( + _("{0}。链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("请求协议错误"), + instance._DEVICE_ID_URL, + cls.proxies, + cls.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + logger.error(traceback.format_exc()) + raise APIResponseError( + _("{0}。链接:{1} 代理:{2},异常类名:{3},异常详细信息:{4}").format( + _("状态码错误"), + instance._DEVICE_ID_URL, + cls.proxies, + cls.__name__, + exc, + ) + ) + + @classmethod + async def gen_device_ids(cls, count: int, full_cookie: bool = False) -> dict: + """ + 生成多个设备 ID。 + + Args: + count: 生成的设备 ID 数量 + + Returns: + device_ids: 生成的设备 ID 字典 + + Raises: + TypeError: 如果输入的 count 不是整数。 + ValueError: 如果输入的 count 小于 1。 + """ + + if not isinstance(count, int): + raise TypeError(_("count 必须是整数")) + + if not isinstance(full_cookie, bool): + raise TypeError(_("full_cookie 必须是布尔值")) + + if count < 1: + raise ValueError(_("count 参数必须大于 0")) + + if count > 10: + logger.warning(_("生成设备 ID 数量过多,可能会导致请求失败。")) + + tasks = [cls.gen_device_id(full_cookie) for _ in range(count)] + results = await asyncio.gather(*tasks) + + device_ids = [result["deviceId"] for result in results] + cookies = [result["cookie"] for result in results] + + return {"deviceId": device_ids, "cookie": cookies} + + def format_file_name( naming_template: str, - aweme_data: dict = {}, + aweme_data: dict = ..., custom_fields: dict = {}, ) -> str: """ @@ -588,6 +1258,7 @@ def format_file_name( fields = { "create": aweme_data.get("createTime", ""), # 长度固定19 "nickname": aweme_data.get("nickname", ""), # 最长30 + "uniqueId": aweme_data.get("uniqueId", ""), "aweme_id": aweme_data.get("aweme_id", ""), # 长度固定19 "desc": split_filename(aweme_data.get("desc", ""), os_limit), "uid": aweme_data.get("uid", ""), # 固定11 @@ -603,14 +1274,14 @@ def format_file_name( raise KeyError(_("文件名模板字段 {0} 不存在,请检查").format(e)) -def create_user_folder(kwargs: dict, nickname: Union[str, int]) -> Path: +def create_user_folder(kwargs: dict, uniqueId: Union[str, int]) -> Path: """ - 根据提供的配置文件和昵称,创建对应的保存目录。 - (Create the corresponding save directory according to the provided conf file and nickname.) + 根据提供的配置文件和uniqueId,创建对应的保存目录。 + (Create the corresponding save directory according to the provided conf file and uniqueId.) Args: kwargs (dict): 配置文件,字典格式。(Conf file, dict format) - nickname (Union[str, int]): 用户的昵称,允许字符串或整数。 (User nickname, allow strings or integers) + uniqueId (Union[str, int]): 用户的uniqueId,允许字符串或整数。 (User uniqueId, allow strings or integers) Note: 如果未在配置文件中指定路径,则默认为 "Download"。 @@ -632,7 +1303,7 @@ def create_user_folder(kwargs: dict, nickname: Union[str, int]) -> Path: # 添加下载模式和用户名 user_path = ( - base_path / "tiktok" / kwargs.get("mode", "PLEASE_SETUP_MODE") / str(nickname) + base_path / "tiktok" / kwargs.get("mode", "PLEASE_SETUP_MODE") / str(uniqueId) ) # 获取绝对路径并确保它存在 @@ -644,13 +1315,13 @@ def create_user_folder(kwargs: dict, nickname: Union[str, int]) -> Path: return resolve_user_path -def rename_user_folder(old_path: Path, new_nickname: str) -> Path: +def rename_user_folder(old_path: Path, new_uniqueId: str) -> Path: """ 重命名用户目录 (Rename User Folder). Args: old_path (Path): 旧的用户目录路径 (Path of the old user folder) - new_nickname (str): 新的用户昵称 (New user nickname) + new_uniqueId (str): 新的用户uniqueId (New user uniqueId) Returns: Path: 重命名后的用户目录路径 (Path of the renamed user folder) @@ -659,13 +1330,13 @@ def rename_user_folder(old_path: Path, new_nickname: str) -> Path: parent_directory = old_path.parent # 构建新目录路径 (Construct the new directory path) - new_path = old_path.rename(parent_directory / new_nickname).resolve() + new_path = old_path.rename(parent_directory / new_uniqueId).resolve() return new_path def create_or_rename_user_folder( - kwargs: dict, local_user_data: dict, current_nickname: str + kwargs: dict, local_user_data: dict, current_uniqueId: str ) -> Path: """ 创建或重命名用户目录 (Create or rename user directory) @@ -673,18 +1344,18 @@ def create_or_rename_user_folder( Args: kwargs (dict): 配置参数 (Conf parameters) local_user_data (dict): 本地用户数据 (Local user data) - current_nickname (str): 当前用户昵称 (Current user nickname) + current_uniqueId (str): 当前用户uniqueId (Current user uniqueId) Returns: user_path (Path): 用户目录路径 (User directory path) """ - user_path = create_user_folder(kwargs, current_nickname) + user_path = create_user_folder(kwargs, current_uniqueId) if not local_user_data: return user_path - if local_user_data.get("nickname") != current_nickname: - # 昵称不一致,触发目录更新操作 - user_path = rename_user_folder(user_path, current_nickname) + if local_user_data.get("uniqueId") != current_uniqueId: + # uniqueId不一致,触发目录更新操作 + user_path = rename_user_folder(user_path, current_uniqueId) return user_path diff --git a/f2/apps/twitter/api.py b/f2/apps/twitter/api.py new file mode 100644 index 00000000..60d9dfe4 --- /dev/null +++ b/f2/apps/twitter/api.py @@ -0,0 +1,54 @@ +# path: f2/apps/twitter/api.py + + +class TwitterAPIEndpoints: + """ + API Endpoints for Twitter + """ + + # Twitter Domain + TWITTER_DOMAIN = "https://twitter.com" + + API_DOMAIN = "https://twitter.com/i/api/graphql" + + # Login + LOGIN_ENDPOINT = f"{TWITTER_DOMAIN}/login/" + + # Home Timeline + HOME_TIMELINE = f"{TWITTER_DOMAIN}/api/timeline/home.json" + + # User Timeline + USER_TIMELINE = f"{TWITTER_DOMAIN}/api/timeline/user_timeline.json" + + # User Detail + USER_PROFILE = f"{API_DOMAIN}/qW5u-DAuXpMEG0zA1F7UGQ/UserByScreenName" + + # User Post + USER_POST = f"{API_DOMAIN}/9zyyd1hebl7oNWIPdA8HRw/UserTweets" + + # User Like + USER_LIKE = f"{TWITTER_DOMAIN}/api/favorite/item_list.json" + + # User Collect + USER_COLLECT = f"{TWITTER_DOMAIN}/api/user/collect/item_list.json" + + # User Play List + USER_PLAY_LIST = f"{TWITTER_DOMAIN}/api/user/playlist.json" + + # User Mix + USER_MIX = f"{TWITTER_DOMAIN}/api/mix/item_list.json" + + # Guess You Like + GUESS_YOU_LIKE = f"{TWITTER_DOMAIN}/api/related/item_list.json" + + # User Follow + USER_FOLLOW = f"{TWITTER_DOMAIN}/api/relation/user/list.json" + + # User Fans + USER_FANS = f"{TWITTER_DOMAIN}/api/relation/follower/list.json" + + # Post Detail + POST_DETAIL = f"{API_DOMAIN}/F45teiuFI9MDxaS9UYKv-g/TweetDetail" + + # Post Comment + POST_COMMENT = f"{TWITTER_DOMAIN}/api/comment/list.json" diff --git a/f2/apps/twitter/cli.py b/f2/apps/twitter/cli.py new file mode 100644 index 00000000..044bfc8d --- /dev/null +++ b/f2/apps/twitter/cli.py @@ -0,0 +1,349 @@ +# path: f2/apps/twitter/cli.py + +import f2 +import click +import typing + +from pathlib import Path + +from f2 import helps +from f2.cli.cli_commands import set_cli_config +from f2.log.logger import logger +from f2.utils.utils import ( + split_dict_cookie, + get_resource_path, + get_cookie_from_browser, + check_invalid_naming, + merge_config, +) +from f2.utils.conf_manager import ConfigManager +from f2.i18n.translator import TranslationManager, _ +from f2.apps.twitter.utils import ClientConfManager + + +def handler_help( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + """ + 处理帮助信息 (Handle help messages) + + 根据提供的值显示帮助信息或退出上下文 + (Display help messages based on the provided value or exit the context) + + Args: + ctx: click的上下文对象 (Click's context object). + param: 提供的参数或选项 (The provided parameter or option). + value: 参数或选项的值 (The value of the parameter or option). + """ + + if not value or ctx.resilient_parsing: + return + + helps.get_help("twitter") + ctx.exit() + + +def handler_auto_cookie( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + """ + 用于自动从浏览器获取cookie (Used to automatically get cookies from the browser) + + Args: + ctx: click的上下文对象 (Click's context object) + param: 提供的参数或选项 (The provided parameter or option) + value: 参数或选项的值 (The value of the parameter or option) + """ + # 如果用户没有提供值或者设置了 resilient_parsing 或者设置了 --cookie,那么跳过自动获取过程 + if not value or ctx.resilient_parsing or ctx.params.get("cookie"): + return + + # 根据浏览器选择获取cookie + try: + cookie_value = split_dict_cookie(get_cookie_from_browser(value, "twitter.com")) + + if not cookie_value: + raise ValueError(_("无法从 {0} 浏览器中获取cookie").format(value)) + + # 如果没有提供配置文件,那么使用高频配置文件 + manager = ConfigManager( + ctx.params.get("config", get_resource_path(f2.APP_CONFIG_FILE_PATH)) + ) + manager.update_config_with_args("twitter", cookie=cookie_value) + except PermissionError: + logger.error(_("请关闭所有已打开的浏览器重试,并且你有适当的权限访问浏览器!")) + ctx.abort() + except Exception as e: + logger.error(_("自动获取Cookie失败:{0}").format(str(e))) + ctx.abort() + finally: + ctx.exit(0) + + +def handler_language( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + """ + 处理语言选项 (Handle language options) + + Args: + ctx: click的上下文对象 (Click's context object) + param: 提供的参数或选项 (The provided parameter or option) + value: 参数或选项的值 (The value of the parameter or option) + """ + + if not value or ctx.resilient_parsing: + return + TranslationManager.get_instance().set_language(value) + global _ + _ = TranslationManager.get_instance().gettext + return value + + +def handler_naming( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + """ + 处理命名选项 (Handle naming options) + + Args: + ctx: click的上下文对象 (Click's context object) + param: 提供的参数或选项 (The provided parameter or option) + value: 参数或选项的值 (The value of the parameter or option) + """ + + # 避免和配置文件参数冲突 + if not value or ctx.resilient_parsing: + return + + # 允许的模式和分隔符 + ALLOWED_PATTERNS = ["{nickname}", "{create}", "{tweet_id}", "{desc}", "{uid}"] + ALLOWED_SEPARATORS = ["-", "_"] + + # 检查命名是否符合命名规范 + invalid_patterns = check_invalid_naming(value, ALLOWED_PATTERNS, ALLOWED_SEPARATORS) + + if invalid_patterns: + raise click.BadParameter( + _("`{0}` 中的 `{1}` 不符合命名模式").format( + value, "".join(invalid_patterns) + ) + ) + + return value + + +@click.command(name="twitter", help=_("推文下载器")) +@click.option( + "-c", + "--config", + type=click.Path(file_okay=True, dir_okay=False, readable=True), + help=_("配置文件的路径,最低优先"), +) +@click.option( + "--url", + "-u", + type=str, + help=_("根据模式提供相应的链接"), +) +@click.option( + "--path", + "-p", + type=str, + help=_("作品保存位置,支持绝对与相对路径"), +) +@click.option( + "--folderize", + "-f", + type=bool, + help=_("是否将作品保存到单独的文件夹"), +) +@click.option( + "--mode", + "-M", + type=click.Choice(f2.TWITTER_MODE_LIST), + help=_("下载模式:单个推文(one),主页推文(post)"), +) +@click.option( + "--naming", + "-n", + type=str, + help=_("全局推文文件命名方式,前往文档查看更多帮助"), + callback=handler_naming, +) +@click.option( + "--cookie", + "-k", + type=str, + help=_("登录后的[yellow]cookie[/yellow]"), +) +@click.option( + "--timeout", + "-e", + type=int, + help=_("网络请求超时时间"), +) +@click.option( + "--max_retries", + "-r", + type=int, + help=_("网络请求超时重试数"), +) +@click.option( + "--max-connections", + "-x", + type=int, + help=_("网络请求并发连接数"), +) +@click.option( + "--max-tasks", + "-t", + type=int, + help=_("异步的任务数"), +) +@click.option( + "--max-counts", + "-o", + type=int, + help=_("最大推文下载数。0 表示无限制"), +) +@click.option( + "--page-counts", + "-s", + type=int, + help=_("从接口每页可获取推文数,不建议超过 20"), +) +@click.option( + "--languages", + "-l", + type=click.Choice(["zh_CN", "en_US"]), + default="zh_CN", + help=_("显示语言。默认为 'zh_CN',可选:'zh_CN'、'en_US',不支持配置文件修改"), + callback=handler_language, +) +@click.option( + "--proxies", + "-P", + type=str, + nargs=2, + help=_( + "代理服务器,最多 2 个参数,http://与https://。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" + ), +) +@click.option( + "--update-config", + type=bool, + is_flag=True, + help=_("使用命令行选项更新配置文件。需要先使用'-c'选项提供一个配置文件路径"), +) +@click.option( + "--init-config", type=str, help=_("初始化配置文件。不能同时初始化和更新配置文件") +) +@click.option( + "--auto-cookie", + type=click.Choice(f2.BROWSER_LIST), + help=_("自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器"), + callback=handler_auto_cookie, +) +@click.option( + "-h", + is_flag=True, + is_eager=True, + expose_value=False, + help=_("显示富文本帮助"), + callback=handler_help, +) +@click.pass_context +def twitter( + ctx: click.Context, + config: str, + init_config: str, + update_config: bool, + **kwargs, +) -> None: + ################## + # f2 存在2个主配置文件,分别是app低频配置(app.yaml)和f2低频配置(conf.yaml) + # app低频配置存放app相关的参数 + # f2低频配置存放计算值所需的参数 + + # 其中cli参数具有最高优先,cli >= 自定义 >= 低频 + # 在f2低频配置中设置代理参数 + # 在app低频配置中设置好重试次数,超时时间,下载路径,下载线程,cookie等低频的参数 + # 在自定义配置中可以设置不同用户的高频参数,如用户主页,原声下载,封面下载,文案下载,下载模式等 + # cli参数为配置文件的热修改,可以随时修改每一个参数。 + ################## + + # 读取低频主配置文件 + main_manager = ConfigManager(f2.APP_CONFIG_FILE_PATH) + main_conf_path = get_resource_path(f2.APP_CONFIG_FILE_PATH) + main_conf = main_manager.get_config("twitter") + + # 更新主配置文件中的代理参数 + main_conf["proxies"] = ClientConfManager.proxies() + + # 更新主配置文件中的headers参数 + kwargs.setdefault("headers", {}) + kwargs["headers"]["User-Agent"] = ClientConfManager.user_agent() + kwargs["headers"]["Referer"] = ClientConfManager.referer() + kwargs["headers"]["Authorization"] = ClientConfManager.authorization() + kwargs["headers"]["X-Csrf-Token"] = ClientConfManager.x_csrf_token() + + # 如果初始化配置文件,则与更新配置文件互斥 + if init_config and not update_config: + main_manager.generate_config("twitter", init_config) + return + elif init_config: + raise click.UsageError(_("不能同时初始化和更新配置文件")) + # 如果没有初始化配置文件,但是更新配置文件,则需要提供配置文件路径 + elif update_config and not config: + raise click.UsageError( + _("要更新配置, 首先需要使用'-c'选项提供一个自定义配置文件路径") + ) + + # 读取自定义配置文件 + if config: + custom_manager = ConfigManager(config) + else: + custom_manager = main_manager + config = main_conf_path + + custom_conf = custom_manager.get_config("twitter") + + if update_config: # 如果指定了 update_config,更新配置文件 + update_manger = ConfigManager(config) + update_manger.update_config_with_args("twitter", **kwargs) + return + + # 将kwargs["proxies"]中的tuple转换为dict + if kwargs.get("proxies"): + kwargs["proxies"] = { + "http": kwargs["proxies"][0], + "https": kwargs["proxies"][1], + } + + # 从低频配置开始到高频配置再到cli参数,逐级覆盖,如果键值不存在使用父级的键值 + kwargs = merge_config(main_conf, custom_conf, **kwargs) + + logger.info(_("模式:{0}").format(kwargs.get("mode"))) + logger.info(_("主配置路径:{0}").format(main_conf_path)) + logger.info(_("自定义配置路径:{0}").format(Path.cwd() / config)) + logger.debug(_("主配置参数:{0}").format(main_conf)) + logger.debug(_("自定义配置参数:{0}").format(custom_conf)) + logger.debug(_("CLI参数:{0}").format(kwargs)) + + # 尝试从命令行参数或kwargs中获取URL + if not kwargs.get("url"): + logger.error(_("缺乏URL参数,详情看命令帮助")) + handler_help(ctx, None, True) + + # 添加app_name到kwargs + kwargs["app_name"] = "twitter" + ctx.invoke(set_cli_config, **kwargs) diff --git a/f2/apps/twitter/crawler.py b/f2/apps/twitter/crawler.py new file mode 100644 index 00000000..782da4ca --- /dev/null +++ b/f2/apps/twitter/crawler.py @@ -0,0 +1,64 @@ +# path: f2/apps/twitter/crawler.py + +from f2.log.logger import logger +from f2.i18n.translator import _ +from f2.crawlers.base_crawler import BaseCrawler +from f2.apps.twitter.api import TwitterAPIEndpoints as xendpoints +from f2.apps.twitter.model import ( + TweetDetail, + TweetDetailEncode, + UserProfile, + UserProfileEncode, + PostTweet, + PostTweetEncode, + encode_model, +) +from f2.apps.twitter.utils import ModelManager, ClientConfManager + + +class TwitterCrawler(BaseCrawler): + def __init__( + self, + kwargs: dict = ..., + ): + # 需要与cli同步 + proxies = kwargs.get("proxies", {"http://": None, "https://": None}) + + self.user_agent = ClientConfManager.user_agent() + self.referrer = ClientConfManager.referer() + self.authorization = ClientConfManager.authorization() + self.x_csrf_token = ClientConfManager.x_csrf_token() + + self.headers = { + "User-Agent": self.user_agent, + "Referer": self.referrer, + "Cookie": kwargs["cookie"], + "Authorization": self.authorization, + "X-Csrf-Token": self.x_csrf_token, + } + + super().__init__(proxies=proxies, crawler_headers=self.headers) + + async def fetch_tweet_detail(self, params: TweetDetailEncode): + endpoint = ModelManager.model_2_endpoint( + xendpoints.POST_DETAIL, + TweetDetail(variables=encode_model(params)).model_dump(), + ) + logger.debug(_("推文详情接口地址: {0}").format(endpoint)) + return await self._fetch_get_json(endpoint) + + async def fetch_user_profile(self, params: UserProfileEncode): + endpoint = ModelManager.model_2_endpoint( + xendpoints.USER_PROFILE, + UserProfile(variables=encode_model(params)).model_dump(), + ) + logger.debug(_("用户信息接口地址: {0}").format(endpoint)) + return await self._fetch_get_json(endpoint) + + async def fetch_post_tweet(self, params: PostTweetEncode): + endpoint = ModelManager.model_2_endpoint( + xendpoints.USER_POST, + PostTweet(variables=encode_model(params)).model_dump(), + ) + logger.debug(_("推文接口地址: {0}").format(endpoint)) + return await self._fetch_get_json(endpoint) diff --git a/f2/apps/twitter/db.py b/f2/apps/twitter/db.py new file mode 100644 index 00000000..6766d408 --- /dev/null +++ b/f2/apps/twitter/db.py @@ -0,0 +1,123 @@ +# path: f2/apps/twitter/db.py + +from f2.db.base_db import BaseDB + + +class AsyncUserDB(BaseDB): + TABLE_NAME = "user_info_web" + + async def _create_table(self) -> None: + """ + 在数据库中创建用户信息表 + """ + await super()._create_table() + + fields = [ + "user_id TEXT", + "user_unique_id TEXT PRIMARY KEY", + "user_rest_id TEXT", + "join_time TEXT", + "nickname TEXT", + "nickname_raw TEXT", + "user_description TEXT", + "user_description_raw TEXT", + "user_pined_tweet_id TEXT", + "user_profile_banner_url TEXT", + "followers_count INTEGER", + "friends_count INTEGER", + "statuses_count INTEGER", + "media_count INTEGER", + "favourites_count INTEGER", + "has_custom_timelines BOOLEAN", + "location TEXT", + "can_dm BOOLEAN", + "is_blue_verified BOOLEAN", + ] + + fields_sql = ", ".join(fields) + await self.execute( + f"""CREATE TABLE IF NOT EXISTS {self.TABLE_NAME} ({fields_sql})""" + ) + await self.commit() + + async def add_user_info(self, ignore_fields=None, **kwargs) -> None: + """ + 添加用户信息 + + Args: + ignore_fields: 要忽略的字段列表,例如 ["field1", "field2"] + **kwargs: 用户的其他字段键值对 + """ + + # 如果 ignore_fields 未提供或者为 None,将其设置为空列表 + ignore_fields = ignore_fields or [] + + # 从 kwargs 中删除要忽略的字段 + for field in ignore_fields: + if field in kwargs: + del kwargs[field] + + keys = ", ".join(kwargs.keys()) + placeholders = ", ".join(["?"] * len(kwargs)) + values = tuple(kwargs.values()) + + await self.execute( + f"INSERT OR REPLACE INTO {self.TABLE_NAME} ({keys}) VALUES ({placeholders})", + values, + ) + # VALUES (?, {placeholders})', (kwargs.get('user_id'), *values)) + await self.commit() + + async def update_user_info(self, user_id: str, **kwargs) -> None: + """ + 更新用户信息 + + Args: + user_id (str): 用户唯一标识 + **kwargs: 用户的其他字段键值对 + """ + + user_data = await self.get_user_info(user_id) + if user_data: + set_sql = ", ".join([f"{key} = ?" for key in kwargs.keys()]) + await self.execute( + f"UPDATE {self.TABLE_NAME} SET {set_sql} WHERE user_id=?", + (*kwargs.values(), user_id), + ) + await self.commit() + + async def get_user_info(self, user_id: str) -> dict: + """ + 获取用户信息 + + Args: + user_id (str): 用户唯一标识 + + Returns: + dict: 对应的用户信息,如果不存在则返回 None + """ + cursor = await self.execute( + f"SELECT * FROM {self.TABLE_NAME} WHERE user_id=?", (user_id,) + ) + result = await cursor.fetchone() + if not result: + return {} + columns = [description[0] for description in cursor.description] + return dict(zip(columns, result)) + + async def delete_user_info(self, user_id: str) -> None: + """ + 删除用户信息 + + Args: + user_id (str): 用户唯一标识 + """ + await self.execute(f"DELETE FROM {self.TABLE_NAME} WHERE user_id=?", (user_id,)) + await self.commit() + + async def __aenter__(self): + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() diff --git a/f2/apps/twitter/dl.py b/f2/apps/twitter/dl.py new file mode 100644 index 00000000..6ae79c04 --- /dev/null +++ b/f2/apps/twitter/dl.py @@ -0,0 +1,177 @@ +# path: f2/apps/twitter/dl.py + +import sys +from datetime import datetime +from typing import Any, Union + +from f2.i18n.translator import _ +from f2.log.logger import logger +from f2.dl.base_downloader import BaseDownloader +from f2.utils.utils import get_timestamp, timestamp_2_str +from f2.apps.twitter.db import AsyncUserDB +from f2.apps.twitter.utils import format_file_name + + +class TwitterDownloader(BaseDownloader): + def __init__(self, kwargs: dict = {}): + if kwargs["cookie"] is None: + raise ValueError( + _( + "cookie不能为空。请提供有效的 cookie 参数,或自动从浏览器获取。如 `--auto-cookie edge`" + ) + ) + + super().__init__(kwargs) + + async def create_download_tasks( + self, kwargs: dict, tweet_datas: Union[list, dict], user_path: Any + ) -> None: + """ + 创建下载任务 + + Args: + kwargs (dict): 命令行参数 + tweet_datas (list, dict): 推文数据列表或字典 + user_path (str): 用户目录路径 + """ + if ( + not kwargs + or not tweet_datas + or not isinstance(tweet_datas, (list, dict)) + or not user_path + ): + return + + # 统一处理,将 tweet_datas 转为列表 + tweet_datas_list = ( + [tweet_datas] if isinstance(tweet_datas, dict) else tweet_datas + ) + + # 筛选指定日期区间内的推文 + if kwargs.get("interval") != "all": + tweet_datas_list = await self.filter_tweet_datas_by_interval( + tweet_datas_list, kwargs.get("interval") + ) + + # 检查是否有符合条件的推文 + if not tweet_datas_list: + logger.warning(_("没有找到符合条件的推文")) + await self.close() + sys.exit(0) + + # 创建下载任务 + for tweet_data in tweet_datas_list: + await self.handler_download(kwargs, tweet_data, user_path) + + # 执行下载任务 + await self.execute_tasks() + + async def handler_download( + self, kwargs: dict, tweet_data_dict: dict, user_path: Any + ) -> None: + """ + 处理下载任务 + + Args: + kwargs (dict): 命令行参数 + tweet_data_dict (dict): 作品数据字典 + user_path (Any): 用户目录路径 + """ + + user_id = tweet_data_dict.get("user_id") + + if user_id is None: + return + + tweet_id = tweet_data_dict.get("tweet_id") + tweet_media_type = tweet_data_dict.get("tweet_media_type") + tweet_media_url = tweet_data_dict.get("tweet_media_url") + + # logger.info(f"========{tweet_id}========") + # logger.info(tweet_data_dict) + # logger.info("===================================") + + # 构建文件夹路径 + base_path = ( + user_path + / format_file_name(kwargs.get("naming", "{create}_{desc}"), tweet_data_dict) + if kwargs.get("folderize") + else user_path + ) + + # # 检查微博是否可见 + # if tweet_data_dict.get("is_visible"): + # logger.error(_("微博 {0} 无查看权限").format(tweet_id)) + # return + + # 检查推文是否有图片 + if tweet_media_type == "video": + + # 说明是视频推文 + # logger.info( + # _("推文视频时长:{0}s,码率列表:{1}").format( + # tweet_data_dict.get("tweet_video_duration") // 1000, + # tweet_data_dict.get("tweet_video_bitrate"), + # ) + # ) + # logger.info(tweet_data_dict.get("playback_list")[0]) + + video_name = ( + format_file_name( + kwargs.get("naming", "{create}_{desc}"), tweet_data_dict + ) + + "_video" + ) + + video_url = tweet_data_dict.get("tweet_video_url") + if isinstance(video_url, list): + video_url = video_url[-1] + + await self.initiate_download( + _("视频"), + video_url, + base_path, + video_name, + ".mp4", + ) + elif tweet_media_type == "photo": + # 处理图片下载任务 + logger.info( + _("推文图片列表:{0}").format(tweet_data_dict.get("tweet_media_url")) + ) + if not tweet_data_dict.get("tweet_media_url"): + logger.warning( + _("{0} : {1}该推文没有图片链接").format( + tweet_id, tweet_data_dict.get("tweet_desc") + ) + ) + else: + if isinstance(tweet_media_url, str): + tweet_media_url = [tweet_data_dict.get("tweet_media_url")] + for i, image_url in enumerate(tweet_media_url): + image_name = f"{format_file_name(kwargs.get('naming'), tweet_data_dict)}_image_{i + 1}" + if image_url != None: + await self.initiate_download( + _("图片"), + f"{image_url}?format=jpg&name=large", + base_path, + image_name, + ".jpg", + ) + else: + logger.warning( + _("{0} 该推文没有图片链接,无法下载").format(tweet_id) + ) + + # 处理文案下载任务 + if kwargs.get("desc"): + desc_name = ( + format_file_name( + kwargs.get("naming", "{create}_{desc}"), tweet_data_dict + ) + + "_desc" + ) + desc_content = tweet_data_dict.get("tweet_desc") + await self.initiate_static_download( + _("文案"), desc_content, base_path, desc_name, ".txt" + ) diff --git a/f2/apps/twitter/filter.py b/f2/apps/twitter/filter.py new file mode 100644 index 00000000..71266bba --- /dev/null +++ b/f2/apps/twitter/filter.py @@ -0,0 +1,1150 @@ +# path: f2/apps/twitter/filter.py + +from f2.utils.json_filter import JSONModel +from f2.utils.utils import _get_first_item_from_list, timestamp_2_str, replaceT + +# Filter + + +class TweetDetailFilter(JSONModel): + # tweet + @property + def tweet_id(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.id_str" + ) + + # tweet_id = property( + # lambda self: self._get_attr_value( + # "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.rest_id" + # ) + # ) + + @property + def tweet_type(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.itemType" + ) + + @property + def tweet_views_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.views.count" + ) + + # 收藏数 + @property + def tweet_bookmark_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.bookmark_count" + ) + + # 点赞数 + @property + def tweet_favorite_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.favorite_count" + ) + + # 评论数 + @property + def tweet_reply_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.reply_count" + ) + + # 转推数 + @property + def tweet_retweet_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.retweet_count" + ) + + # 发布时间 + @property + def tweet_created_time(self): + return timestamp_2_str( + self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.created_at" + ) + ) + + # 推文内容 + @property + def tweet_desc(self): + return replaceT( + self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.full_text" + ).split()[0] + ) + + @property + def tweet_desc_raw(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.full_text" + ) + + # 媒体状态 + @property + def tweet_media_status(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.entities.media[*].ext_media_availability.status" + ) + + # 媒体类型 + @property + def tweet_media_type(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.entities.media[*].type" + ) + + # 图片链接 + @property + def tweet_media_url(self): + media_urls = self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.entities.media[*].media_url_https" + ) + if not isinstance(media_urls, list): + media_urls = [media_urls] + return media_urls + + # 视频链接(清晰度依次提高) + @property + def tweet_video_url(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.extended_entities.media[*].video_info.variants[*].url" + ) + + # 视频时长 + @property + def tweet_video_duration(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.extended_entities.media[*].video_info.duration_millis" + ) + + # 视频码率 + @property + def tweet_video_bitrate(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.legacy.extended_entities.media[*].video_info.variants[*].bitrate" + ) + + # User + # 注册时间 + @property + def join_time(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.legacy.created_at" + ) + + # 蓝V认证 + @property + def is_blue_verified(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.is_blue_verified" + ) + + # 用户ID example: VXNlcjoxNDkzODI0MTA2Njk2OTAwNjEx + @property + def user_id(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.id" + ) + + # 用户唯一ID(推特ID) example: Asai_chan_ + @property + def user_unique_id(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.legacy.screen_name" + ) + + # 昵称 example: 核酸酱 + @property + def nickname(self): + return replaceT( + self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.legacy.name" + ) + ) + + @property + def nicename_raw(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.legacy.name" + ) + + @property + def user_description(self): + return replaceT( + self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.legacy.description" + ) + ) + + @property + def user_description_raw(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.legacy.description" + ) + + # 置顶推文ID + @property + def user_pined_tweet_id(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.legacy.pinned_tweet_ids_str[0]" + ) + + # 主页背景图片 + @property + def user_profile_banner_url(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.profile_banner_url" + ) + + # 关注者 + @property + def followers_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.followers_count" + ) + + # 正在关注 + @property + def friends_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.friends_count" + ) + + # 帖子数(推文数&回复 maybe?) + @property + def statuses_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.statuses_count" + ) + + # 媒体数(图片数&视频数) + @property + def media_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.media_count" + ) + + # 喜欢数 + @property + def favourites_count(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.favourites_count" + ) + + @property + def has_custom_timelines(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.has_custom_timelines" + ) + + @property + def location(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.location" + ) + + @property + def can_dm(self): + return self._get_attr_value( + "$.data.threaded_conversation_with_injections_v2.instructions[0].entries[0].content.itemContent.tweet_results.result.core.user_results.result.can_dm" + ) + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + +class UserProfileFilter(JSONModel): + # User + + # 蓝V认证 + @property + def is_blue_verified(self): + return self._get_attr_value("$.data.user.result.is_blue_verified") + + # 用户ID example: VXNlcjoxNDkzODI0MTA2Njk2OTAwNjEx + @property + def user_id(self): + return self._get_attr_value("$.data.user.result.id") + + # 获取主页需要这个rest_id + @property + def user_rest_id(self): + return self._get_attr_value("$.data.user.result.rest_id") + + # 用户唯一ID(推特ID) example: Asai_chan_ + @property + def user_unique_id(self): + return self._get_attr_value("$.data.user.result.legacy.screen_name") + + # 注册时间 + @property + def join_time(self): + return timestamp_2_str( + self._get_attr_value("$.data.user.result.legacy.created_at") + ) + + # 昵称 example: 核酸酱 + @property + def nickname(self): + return replaceT(self._get_attr_value("$.data.user.result.legacy.name")) + + @property + def nickname_raw(self): + return self._get_attr_value("$.data.user.result.legacy.name") + + @property + def user_description(self): + return replaceT(self._get_attr_value("$.data.user.result.legacy.description")) + + @property + def user_description_raw(self): + return self._get_attr_value("$.data.user.result.legacy.description") + + # 置顶推文ID + @property + def user_pined_tweet_id(self): + return self._get_attr_value("$.data.user.result.legacy.pinned_tweet_ids_str[0]") + + # 主页背景图片 + @property + def user_profile_banner_url(self): + return self._get_attr_value("$.data.user.result.legacy.profile_banner_url") + + # 关注者 + @property + def followers_count(self): + return self._get_attr_value("$.data.user.result.legacy.followers_count") + + # 正在关注 + @property + def friends_count(self): + return self._get_attr_value("$.data.user.result.legacy.friends_count") + + # 帖子数(推文数&回复 maybe?) + @property + def statuses_count(self): + return self._get_attr_value("$.data.user.result.legacy.statuses_count") + + # 媒体数(图片数&视频数) + @property + def media_count(self): + return self._get_attr_value("$.data.user.result.legacy.media_count") + + # 喜欢数 + @property + def favourites_count(self): + return self._get_attr_value("$.data.user.result.legacy.favourites_count") + + @property + def has_custom_timelines(self): + return self._get_attr_value("$.data.user.result.legacy.has_custom_timelines") + + @property + def location(self): + return self._get_attr_value("$.data.user.result.legacy.location") + + @property + def can_dm(self): + return self._get_attr_value("$.data.user.result.legacy.can_dm") + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + +class PostTweetFilter(JSONModel): + # 用户发布的推文__typename是TweetWithVisibilityResults + @property + def min_cursor(self): + return self._get_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[-2].content.value" + ) + + @property + def max_cursor(self): + return self._get_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[-1].content.value" + ) + + # tweet + @property + def tweet_id(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.conversation_id_str" + ) + + @property + def tweet_created_at(self): + create_times = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.created_at" + ) + return ( + [timestamp_2_str(str(ct)) for ct in create_times] + if isinstance(create_times, list) + else timestamp_2_str(str(create_times)) + ) + + @property + def tweet_favorite_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.favorite_count" + ) + + @property + def tweet_reply_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.reply_count" + ) + + @property + def tweet_retweet_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.retweet_count" + ) + + @property + def tweet_quote_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.quote_count" + ) + + @property + def tweet_views_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.views.count" + ) + + @property + def tweet_desc(self): + return replaceT( + self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.full_text" + ) + ) + + @property + def tweet_desc_raw(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.full_text" + ) + + @property + def tweet_media_status(self): + # root = self._get_list_attr_value( + # "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities" + # ) + # print(root, type(root)) + # if root[0].get("media", None) is None: + # return None + + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities.media[*].ext_media_availability.status" + ) + + @property + def tweet_media_type(self): + # root = self._get_list_attr_value( + # "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities" + # ) + # if root[0].get("media", None) is None: + # return None + + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities.media[0].type" + ) + + @property + def tweet_media_url(self): + + media_list = [] + # root = self._get_list_attr_value( + # "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities" + # ) + # if root[0].get("media", None) is None: + # media_list.append(None) + + entries = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities.media[*]" + ) + + if entries is not None: + for entry in entries: + media_list.append(entry.get("media_url_https", None)) + return media_list + + @property + def tweet_video_url(self): + + # [*].video_info.variants + # return [ + # ( + # [ + # video["url"] + # for video in video_url + # if "url" in video and video["url"] + # ] + # if video_url + # else None + # ) + # for video_url in video_url_list + # ] + + video_list = [] + # root = self._get_list_attr_value( + # "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities" + # ) + # if root[0].get("media", None) is None: + # video_list.append(None) + + video_url_list = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities.media" + ) + + if video_url_list == []: + return [] + + if isinstance(video_url_list[0], dict): + video_url_list = [video_url_list] + + for video_url in video_url_list: + urls = [] + # 如果没有video_info字段,说明不是视频则为None + # 例如 [[1,2,3],None,[1,2,3]],None要与index对应 + # 如果有video_info字段,说明是视频,返回url列表 + # 例如 [[1,2,3],[1,2,3],[1,2,3]] + for video in video_url: + video_info = video.get("video_info") + if video_info: + variants = video_info.get("variants") + for url in variants: + urls.append(url.get("url", None)) + video_list.append(urls if urls else None) + return video_list + + @property + def tweet_video_bitrate(self): + biterate_list = [] + # root = self._get_list_attr_value( + # "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities" + # ) + # if root[0].get("media", None) is None: + # biterate_list.append(None) + + biterate_url_list = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.legacy.entities.media[*].video_info.variants[*]" + ) + + if biterate_url_list == []: + return [] + + if isinstance(biterate_url_list[0], dict): + biterate_url_list = [biterate_url_list] + + for biterate_url in biterate_url_list: + urls = [] + for biterate in biterate_url: + biterate_info = biterate.get("video_info") + if biterate_info: + variants = biterate_info.get("variants") + for url in variants: + urls.append(url.get("biterate", None)) + biterate_list.append(urls if urls else None) + return biterate_list + + # user + @property + def user_id(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.id" + ) + + @property + def is_blue_verified(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.is_blue_verified" + ) + + @property + def user_created_at(self): + create_times = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.created_at" + ) + return ( + [timestamp_2_str(str(ct)) for ct in create_times] + if isinstance(create_times, list) + else timestamp_2_str(str(create_times)) + ) + + @property + def user_description(self): + return replaceT( + self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.description" + ) + ) + + @property + def user_description_raw(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.description" + ) + + @property + def user_location(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.location" + ) + + @property + def user_friends_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.friends_count" + ) + + @property + def user_followers_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.followers_count" + ) + + @property + def user_favourites_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.favourites_count" + ) + + @property + def user_media_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.media_count" + ) + + @property + def user_statuses_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.statuses_count" + ) + + @property + def nickname(self): + return replaceT( + self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.name" + ) + ) + + @property + def nickname_raw(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.name" + ) + + @property + def user_screen_name(self): + return replaceT( + self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.screen_name" + ) + ) + + @property + def user_screen_name_raw(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.screen_name" + ) + + @property + def user_profile_banner_url(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.tweet.core.user_results.result.legacy.profile_banner_url" + ) + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + def _to_list(self) -> list: + exclude_list = [ + "max_cursor", + "min_cursor", + ] + + keys = [ + prop_name + for prop_name in dir(self) + if not prop_name.startswith("__") + and not prop_name.startswith("_") + and prop_name not in exclude_list + ] + + tweet_entries = ( + self._get_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries" + ) + or [] + ) + + list_dicts = [] + for entry in tweet_entries: + d = { + "max_cursor": self.max_cursor, + "min_cursor": self.min_cursor, + } + for key in keys: + attr_values = getattr(self, key) + index = tweet_entries.index(entry) + d[key] = attr_values[index] if index < len(attr_values) else None + list_dicts.append(d) + return list_dicts + + # list_dicts = [] + # for index, (key, entry) in enumerate(tweet_entries.items()): + # d = { + # "max_cursor": self.max_cursor, + # "min_cursor": self.min_cursor, + # } + # for key in keys: + # attr_values = getattr(self, key) + # if attr_values is not None: + # d[key] = attr_values[index] if index < len(attr_values) else None + # else: + # d[key] = None + # list_dicts.append(d) + # return list_dicts + + +class PostRetweetFilter(JSONModel): + # 用户发布的推文__typename是TweetWithVisibilityResults + @property + def min_cursor(self): + return self._get_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[-2].content.value" + ) + + @property + def max_cursor(self): + return self._get_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[-1].content.value" + ) + + # tweet + @property + def tweet_id(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.conversation_id_str" + ) + + @property + def tweet_created_at(self): + create_times = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.created_at" + ) + return ( + [timestamp_2_str(str(ct)) for ct in create_times] + if isinstance(create_times, list) + else timestamp_2_str(str(create_times)) + ) + + @property + def tweet_favorite_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.favorite_count" + ) + + @property + def tweet_reply_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.reply_count" + ) + + @property + def tweet_retweet_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.retweet_count" + ) + + @property + def tweet_quote_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.quote_count" + ) + + @property + def tweet_views_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.views.count" + ) + + @property + def tweet_desc(self): + return replaceT( + self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.full_text" + ) + ) + + @property + def tweet_desc_raw(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.full_text" + ) + + @property + def tweet_media_status(self): + root = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities" + ) + if root[0].get("media", None) is None: + return [] + + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities.media[*].ext_media_availability.status" + ) + + @property + def tweet_media_type(self): + root = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities" + ) + if root[0].get("media", None) is None: + return [] + + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities.media[0].type" + ) + + @property + def tweet_media_url(self): + + media_list = [] + root = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities" + ) + + if isinstance(root, dict): + root = [root] + + entries = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities.media[*]" + ) + + if entries is not None: + for entry in entries: + if "media" not in root[entries.index(entry)]: + # print("media not in root") + media_list.append(None) + else: + # print("media in root") + media_list.append(entry.get("media_url_https", None)) + # print("media_list:", media_list) + + return media_list + + @property + def tweet_video_url(self): + + video_list = [] + root = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities" + ) + + if isinstance(root, dict): + root = [root] + + video_url_list = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities.media[*]" + ) + # print( + # "第一条推文==================================", + # video_url_list, + # type(video_url_list), + # ) + + # if video_url_list == []: + # return video_url_list + + # if isinstance(video_url_list[0], dict): + # video_url_list = [video_url_list] + + if video_url_list is not None: + for video_dict in video_url_list: + urls = [] + + # # 将有media的下标与root对应 + # video_index = video_url_list.index(video_dict) + # video_dict_list = root[video_index][ + # "media" + # ] # list,其中的media可能[{0},{1},{2}]或者{0} + + # 如果没有video_info字段,说明不是视频则为None + # 例如 [[1,2,3],None,[1,2,3]],None要与index对应 + # 如果有video_info字段,说明是视频,返回url列表 + # 例如 [[1,2,3],[1,2,3],[1,2,3]] + # print("----------------------------------") + # print(root[video_url_list.index(video_dict)]) + # print("++++++++++++++++++++++++++++++++++") + if "media" not in root[video_url_list.index(video_dict)]: + print( + "这个内容没有media", + root[video_url_list.index(video_dict)], + "下标", + video_url_list.index(video_dict), + ) + print("++++++++++++++++++++++++++++++++++") + video_list.append(None) + else: + + video_dict_list = root[video_url_list.index(video_dict)][ + "media" + ] # list类型,其中的media可能[{0},{1},{2}]或者{0} + + # print("有media内容类型", video_dict.get("type")) + # print( + # "有media内容", + # root[video_url_list.index(video_dict)], + # "下标", + # video_url_list.index(video_dict), + # ) + # print( + # video_dict.get("expanded_url"), + # root[video_url_list.index(video_dict)]["media"][0][ + # "expanded_url" + # ], + # ) + # 视频类型的数据结构[[11],[11],[11,22,33]] + if video_dict_list[0].get("type") == "video": + for video_dict in video_dict_list: + # video_dict 为每一个视频字典 + video_info = video_dict.get("video_info") + if video_info: + variants = video_info.get("variants") + + # 只拿最后一个链接 + # urls.append() + # for url in variants: + # urls.append(url.get("url", None)) + # print("视频链接", urls[-1]) + + video_list.append(urls if urls else None) + else: + print("有media内容类型是照片的", video_dict_list[0].get("type")) + print( + "这个media内容没有video", + video_dict_list, + ) + print("++++++++++++++++++++++++++++++++++") + video_list.append(None) + print("video_list==============", video_list) + return video_list + + @property + def tweet_video_bitrate(self): + biterate_list = [] + root = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities" + ) + # print("tweet_video_bitrate:", root[0].get("media", None)) + if root[0].get("media", None) is None: + print( + "============================tweet_video_bitrate:", + root[0].get("media", None), + "============================", + ) + return [] + + biterate_url_list = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.legacy.entities.media[*].video_info.variants[*]" + ) + + if biterate_url_list == []: + return [] + + if isinstance(biterate_url_list[0], dict): + biterate_url_list = [biterate_url_list] + + for biterate_url in biterate_url_list: + urls = [] + for biterate in biterate_url: + biterate_info = biterate.get("video_info") + if biterate_info: + variants = biterate_info.get("variants") + for url in variants: + urls.append(url.get("biterate", None)) + biterate_list.append(urls if urls else None) + return biterate_list + + # user + @property + def user_id(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.id" + ) + + @property + def is_blue_verified(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.is_blue_verified" + ) + + @property + def user_created_at(self): + create_times = self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.created_at" + ) + return ( + [timestamp_2_str(str(ct)) for ct in create_times] + if isinstance(create_times, list) + else timestamp_2_str(str(create_times)) + ) + + @property + def user_description(self): + return replaceT( + self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.description" + ) + ) + + @property + def user_description_raw(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.description" + ) + + @property + def user_location(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.location" + ) + + @property + def user_friends_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.friends_count" + ) + + @property + def user_followers_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.followers_count" + ) + + @property + def user_favourites_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.favourites_count" + ) + + @property + def user_media_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.media_count" + ) + + @property + def user_statuses_count(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.statuses_count" + ) + + @property + def nickname(self): + return replaceT( + self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.name" + ) + ) + + @property + def nickname_raw(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.name" + ) + + @property + def user_screen_name(self): + return replaceT( + self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.screen_name" + ) + ) + + @property + def user_screen_name_raw(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.screen_name" + ) + + @property + def user_profile_banner_url(self): + return self._get_list_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries[*].content.itemContent.tweet_results.result.core.user_results.result.legacy.profile_banner_url" + ) + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + def _to_list(self) -> list: + exclude_list = [ + "max_cursor", + "min_cursor", + ] + + keys = [ + prop_name + for prop_name in dir(self) + if not prop_name.startswith("__") + and not prop_name.startswith("_") + and prop_name not in exclude_list + ] + + tweet_entries = ( + self._get_attr_value( + "$.data.user.result.timeline_v2.timeline.instructions[-1].entries" + ) + or [] + ) + + list_dicts = [] + for entry in tweet_entries: + d = { + "max_cursor": self.max_cursor, + "min_cursor": self.min_cursor, + } + for key in keys: + attr_values = getattr(self, key) + index = tweet_entries.index(entry) + d[key] = attr_values[index] if index < len(attr_values) else None + list_dicts.append(d) + return list_dicts diff --git a/f2/apps/twitter/handler.py b/f2/apps/twitter/handler.py new file mode 100644 index 00000000..fa00b18a --- /dev/null +++ b/f2/apps/twitter/handler.py @@ -0,0 +1,388 @@ +# path: f2/apps/twitter/handler.py + +import asyncio +from pathlib import Path +from typing import AsyncGenerator, Union, Dict, Any, List + +from f2.log.logger import logger +from f2.i18n.translator import _ +from f2.utils.decorators import mode_handler, mode_function_map +from f2.utils.utils import split_set_cookie +from f2.apps.twitter.db import AsyncUserDB +from f2.apps.twitter.crawler import TwitterCrawler +from f2.apps.twitter.dl import TwitterDownloader +from f2.apps.twitter.model import ( + TweetDetailEncode, + UserProfileEncode, + PostTweetEncode, +) +from f2.apps.twitter.filter import ( + TweetDetailFilter, + UserProfileFilter, + PostTweetFilter, + PostRetweetFilter, +) +from f2.apps.twitter.utils import ( + UserIdFetcher, + TweetIdFetcher, + create_or_rename_user_folder, +) +from f2.cli.cli_console import RichConsoleManager +from f2.exceptions.api_exceptions import APIResponseError + +rich_console = RichConsoleManager().rich_console +rich_prompt = RichConsoleManager().rich_prompt + + +class TwitterHandler: + + def __init__(self, kwargs: dict = ...) -> None: + self.kwargs = kwargs + self.downloader = TwitterDownloader(kwargs) + + async def fetch_user_profile( + self, + uniqueId: str, + ) -> UserProfileFilter: + """ + 用于获取指定用户的个人信息 + (Used to get personal info of specified users) + + Args: + uniqueId: str: 用户ID (User ID) + + Return: + user: UserProfileFilter: 用户信息过滤器 (User info filter) + """ + + async with TwitterCrawler(self.kwargs) as crawler: + params = UserProfileEncode(screen_name=uniqueId) + response = await crawler.fetch_user_profile(params) + user = UserProfileFilter(response) + if user.nickname is None: + raise APIResponseError( + _("`fetch_user_profile`请求失败,请更换cookie或稍后再试") + ) + return UserProfileFilter(response) + + async def get_or_add_user_data( + self, + kwargs: dict, + uniqueId: str, + db: AsyncUserDB, + ) -> Path: + """ + 获取或创建用户数据同时创建用户目录 + (Get or create user data and create user directory) + + Args: + kwargs (dict): 配置参数 (Conf parameters) + uniqueId (str): 用户ID (User ID) + db (AsyncUserDB): 用户数据库 (User database) + + Returns: + user_path (Path): 用户目录路径 (User directory path) + """ + + # 尝试从数据库中获取用户数据 + local_user_data = await db.get_user_info(uniqueId) + + # 从服务器获取当前用户最新数据 + current_user_data = await self.fetch_user_profile(uniqueId) + + # 获取当前用户最新昵称 + current_nickname = current_user_data.nickname + + # 设置用户目录 + user_path = create_or_rename_user_folder( + kwargs, local_user_data, current_nickname + ) + + # 如果用户不在数据库中,将其添加到数据库 + if not local_user_data: + await db.add_user_info(**current_user_data._to_dict()) + + return user_path + + @mode_handler("one") + async def handle_one_tweet(self): + """ + 用于处理单个推文。 + (Used to process a single tweet.) + + Args: + kwargs: dict: 参数字典 (Parameter dictionary) + """ + + tweet_id = await TweetIdFetcher.get_tweet_id(self.kwargs.get("url")) + tweet_data = await self.fetch_one_tweet(tweet_id) + + async with AsyncUserDB("twitter_users.db") as db: + user_path = await self.get_or_add_user_data( + self.kwargs, tweet_data.user_unique_id, db + ) + + # async with AsynctweetDB("twitter_tweets.db") as db: + # await self.get_or_add_tweet_data( + # tweet_data._to_dict(), db, self.ignore_fields + # ) + + # logger.info(_("单个推文数据:{0}").format(tweet_data._to_dict())) + + # 创建下载任务 + await self.downloader.create_download_tasks( + self.kwargs, tweet_data._to_dict(), user_path + ) + + async def fetch_one_tweet( + self, + tweet_id: str, + ) -> TweetDetailFilter: + """ + 用于获取单个推文。 + + Args: + tweet_id: str: 推文ID + + Return: + tweet: TweetDetailFilter: 单个推文数据过滤器 + """ + + logger.info(_("开始爬取推文:{0}").format(tweet_id)) + async with TwitterCrawler(self.kwargs) as crawler: + params = TweetDetailEncode(focalTweetId=tweet_id) + response = await crawler.fetch_tweet_detail(params) + tweet = TweetDetailFilter(response) + + logger.info( + _("推文ID:{0} 推文文案:{1} 作者:{2}").format( + tweet.tweet_id, tweet.tweet_desc, tweet.nickname + ) + ) + + return tweet + + @mode_handler("post") + async def handle_post_tweet(self): + """ + 用于处理主页推文。 + (Used to process a post tweet.) + + Args: + kwargs: dict: 参数字典 (Parameter dictionary) + """ + + max_cursor = self.kwargs.get("max_cursor", "") + page_counts = self.kwargs.get("page_counts", 20) + max_counts = self.kwargs.get("max_counts") + + uniqueID = await UserIdFetcher.get_user_id(self.kwargs.get("url")) + user = await self.fetch_user_profile(uniqueID) + + async with AsyncUserDB("twitter_users.db") as udb: + user_path = await self.get_or_add_user_data(self.kwargs, uniqueID, udb) + + async for tweet_list in self.fetch_post_tweet( + user.user_rest_id, page_counts, max_cursor, max_counts + ): + # 创建下载任务 + await self.downloader.create_download_tasks( + self.kwargs, tweet_list._to_list(), user_path + ) + + async def fetch_post_tweet( + self, + userId: str, + page_counts: int = 20, + max_cursor: str = "", + max_counts: int = None, + ) -> AsyncGenerator[PostTweetFilter, Any]: + """ + 用于获取用户发布的推文。 + + Args: + userId: str: 用户ID + page_counts: int: 每次请求的推文数量 + max_cursor: str: 游标 + max_counts: int: 最大请求次数 + + Return: + tweet: PostTweetFilter: 用户发布的推文数据过滤器 + """ + + max_counts = max_counts or float("inf") + tweets_collected = 0 + + logger.info(_("开始爬取用户:{0} 发布的推文").format(userId)) + + while tweets_collected < max_counts: + current_request_size = min(page_counts, max_counts - tweets_collected) + + logger.debug("===================================") + logger.debug( + _("最大数量:{0} 每次请求数量:{1}").format( + max_counts, current_request_size + ) + ) + logger.info(_("开始爬取第 {0} 页").format(max_cursor)) + + async with TwitterCrawler(self.kwargs) as crawler: + params = PostTweetEncode( + userId=userId, count=current_request_size, cursor=max_cursor + ) + response = await crawler.fetch_post_tweet(params) + tweet = PostTweetFilter(response) + + logger.debug(_("当前请求的max_cursor:{0}").format(max_cursor)) + logger.info( + _("推文ID:{0} 推文文案:{1} 作者:{2}").format( + tweet.tweet_id, tweet.tweet_desc, tweet.nickname + ) + ) + logger.info(tweet._to_dict()) + if len(tweet.tweet_id) == 0: + # 只有tweet.tweet_id 和 tweet.tweet_desc都为None时,才认为已经爬取完毕 + # 且只有min_cursor与max_cursor 2个值时没有其他值时才认为到达底部 + if tweet.tweet_id is None and tweet.tweet_desc is None: + logger.info( + _("用户:{0} 所有发布的推文采集完毕").format(tweet.nickname) + ) + break + + logger.info(_("max_cursor:{0} 未找到发布的推文").format(max_cursor)) + max_cursor = tweet.max_cursor + await asyncio.sleep(self.kwargs.get("timeout", 5)) + continue + + yield tweet + + # 更新已经处理的作品数量 (Update the number of videos processed) + tweets_collected += len(tweet.tweet_id) + + max_cursor = tweet.max_cursor + logger.info(f"下一页{tweet.max_cursor}") + + # 避免请求过于频繁 + logger.info(_("等待 {0} 秒后继续").format(self.kwargs.get("timeout", 5))) + await asyncio.sleep(self.kwargs.get("timeout", 5)) + + logger.info(_("爬取结束,共爬取 {0} 个作品").format(tweets_collected)) + + @mode_handler("retweet") + async def handle_retweet(self): + """ + 用于处理转发推文。 + (Used to process retweet tweets.) + + Args: + kwargs: dict: 参数字典 (Parameter dictionary) + """ + + max_cursor = self.kwargs.get("max_cursor", "") + page_counts = self.kwargs.get("page_counts", 20) + max_counts = self.kwargs.get("max_counts") + + uniqueID = await UserIdFetcher.get_user_id(self.kwargs.get("url")) + user = await self.fetch_user_profile(uniqueID) + + async with AsyncUserDB("twitter_users.db") as udb: + user_path = await self.get_or_add_user_data(self.kwargs, uniqueID, udb) + + async for tweet_list in self.fetch_retweet( + user.user_rest_id, page_counts, max_cursor, max_counts + ): + # 创建下载任务 + await self.downloader.create_download_tasks( + self.kwargs, tweet_list._to_list(), user_path + ) + + async def fetch_retweet( + self, + userId: str, + page_counts: int = 20, + max_cursor: str = "", + max_counts: int = None, + ) -> AsyncGenerator[PostTweetFilter, Any]: + """ + 用于获取用户转发的推文。 + + Args: + userId: str: 用户ID + page_counts: int: 每次请求的推文数量 + max_cursor: str: 游标 + max_counts: int: 最大请求次数 + + Return: + tweet: PostTweetFilter: 用户转发的推文数据过滤器 + """ + + max_counts = max_counts or float("inf") + tweets_collected = 0 + + logger.info(_("开始爬取用户:{0} 转发的推文").format(userId)) + + while tweets_collected < max_counts: + current_request_size = min(page_counts, max_counts - tweets_collected) + + logger.debug("===================================") + logger.debug( + _("最大数量:{0} 每次请求数量:{1}").format( + max_counts, current_request_size + ) + ) + logger.info(_("开始爬取第 {0} 页").format(max_cursor)) + + async with TwitterCrawler(self.kwargs) as crawler: + params = PostTweetEncode( + userId=userId, count=current_request_size, cursor=max_cursor + ) + response = await crawler.fetch_post_tweet(params) + retweet = PostRetweetFilter(response) + + logger.debug(_("当前请求的max_cursor:{0}").format(max_cursor)) + # logger.info( + # _("推文ID:{0} 推文文案:{1} 作者:{2}").format( + # retweet.tweet_id, retweet.tweet_desc, retweet.nickname + # ) + # ) + logger.info( + _("推文文案:{0} 推文图片:{1} 推文视频:{2}").format( + retweet.tweet_desc, retweet.tweet_media_url, retweet.tweet_video_url + ) + ) + # logger.info(retweet._to_dict()) + if len(retweet.tweet_id) == 0: + # 只有tweet.tweet_id 和 tweet.tweet_desc都为None时,才认为已经爬取完毕 + # 且只有min_cursor与max_cursor 2个值时没有其他值时才认为到达底部 + if retweet.tweet_id is None and retweet.tweet_desc is None: + logger.info( + _("用户:{0} 所有转发的推文采集完毕").format(retweet.nickname) + ) + break + + logger.info(_("max_cursor:{0} 未找到转发的推文").format(max_cursor)) + max_cursor = retweet.max_cursor + await asyncio.sleep(self.kwargs.get("timeout", 5)) + continue + + yield retweet + + # 更新已经处理的作品数量 (Update the number of videos processed) + tweets_collected += len(retweet.tweet_id) + + max_cursor = retweet.max_cursor + logger.info(f"下一页{retweet.max_cursor}") + + # 避免请求过于频繁 + logger.info(_("等待 {0} 秒后继续").format(self.kwargs.get("timeout", 5))) + await asyncio.sleep(self.kwargs.get("timeout", 5)) + + logger.info(_("爬取结束,共爬取 {0} 个作品").format(tweets_collected)) + + +async def main(kwargs): + mode = kwargs.get("mode") + if mode in mode_function_map: + await mode_function_map[mode](TwitterHandler(kwargs)) + else: + logger.error(_("不存在该模式: {0}").format(mode)) diff --git a/f2/apps/twitter/help.py b/f2/apps/twitter/help.py new file mode 100644 index 00000000..72cc8388 --- /dev/null +++ b/f2/apps/twitter/help.py @@ -0,0 +1,66 @@ +# path: f2/apps/twitter/help.py + +from rich.console import Console +from rich.panel import Panel +from rich.table import Table + +from f2.i18n.translator import _ + + +def help() -> None: + # 真彩 + console = Console(color_system="truecolor") + table = Table(highlight=True, box=None, show_header=False) + table.add_column("OPTIONS", no_wrap=True, justify="left", style="bold") + table.add_column("Type", no_wrap=True, justify="left", style="bold") + table.add_column("Description", no_wrap=True) + + options = [ + ("-c --config", "[dark_cyan]Path", _("配置文件的路径,最低优先")), + ("-u --url", "[dark_cyan]str", _("除了单个推文外,其他URL都需要用户主页URL")), + ("-p --path", "[dark_cyan]str", _("推文保存位置,默认为 'Download'")), + ( + "-f --folderize", + "[dark_cyan]Choice", + _("是否将推文保存到单独的文件夹,默认为 'yes'"), + ), + ( + "-M --mode", + "[dark_cyan]Choice", + _( + "下载模式:单个推文[one]、用户推文[post]、用户转推[retweet]、喜欢推文[like]、用户收藏[bookmark]" + ), + ), + ("-n --naming", "[dark_cyan]str", _("全局推文文件命名方式")), + ( + "--auto-cookie", + "[dark_cyan]Choice", + _( + "自动从浏览器获取[yellow]cookie[/yellow],使用该命令前请确保关闭所选的浏览器" + ), + ), + ("-k --cookie", "[dark_cyan]str", _("登录后的cookie")), + ("-e --timeout", "[dark_cyan]int", _("网络请求超时时间,默认为 10")), + ("-r --max-retries", "[dark_cyan]int", _("网络请求超时重试数,默认为 5")), + ("-x --max-connections", "[dark_cyan]int", _("网络请求并发连接数,默认为 5")), + ("-t --max-tasks", "[dark_cyan]int", _("异步的任务数,默认为 10")), + ("-o --max-counts", "[dark_cyan]int", _("最大推文下载数 默认为 0,表示无限制")), + ("-s --page-counts", "[dark_cyan]int", _("每页推文数,默认为 20个推文/页")), + ("-l --languages", "[dark_cyan]Choice", _("语言设置,默认为 'zh_CN'")), + ( + "-P --proxies", + "[dark_cyan]str", + _( + "代理服务器,最多 2 个参数,http://与https://。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" + ), + ), + ("--update-config", "[dark_cyan]Flag", _("使用命令行选项更新配置文件")), + ("--init-config", "[dark_cyan]Flag", _("初始化配置文件")), + ] + + for option in options: + table.add_row(*option) + + console.print( + Panel(table, border_style="bold", title="[Twitter]", title_align="left") + ) diff --git a/f2/apps/twitter/model.py b/f2/apps/twitter/model.py new file mode 100644 index 00000000..d5d3ddb0 --- /dev/null +++ b/f2/apps/twitter/model.py @@ -0,0 +1,139 @@ +# path: f2/apps/twitter/models.py + +from typing import Any +from pydantic import BaseModel +import json +from urllib.parse import quote, unquote + + +def encode_model(model: BaseModel) -> str: + """ + 将 BaseModel 实例转换为 JSON 编码并进行 URL 编码后的字符串 + """ + return quote(model.model_dump_json()) + + +class BaseRequestModel(BaseModel): + variables: str + + +class TweetDetail(BaseRequestModel): + features: str = quote( + json.dumps( + { + "rweb_tipjar_consumption_enabled": False, + "responsive_web_graphql_exclude_directive_enabled": True, + "verified_phone_label_enabled": False, + "creator_subscriptions_tweet_preview_api_enabled": True, + "responsive_web_graphql_timeline_navigation_enabled": True, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False, + "communities_web_enable_tweet_community_results_fetch": True, + "c9s_tweet_anatomy_moderator_badge_enabled": True, + "tweetypie_unmention_optimization_enabled": True, + "responsive_web_edit_tweet_api_enabled": True, + "graphql_is_translatable_rweb_tweet_is_translatable_enabled": True, + "view_counts_everywhere_api_enabled": True, + "longform_notetweets_consumption_enabled": True, + "responsive_web_twitter_article_tweet_consumption_enabled": True, + "tweet_awards_web_tipping_enabled": False, + "creator_subscriptions_quote_tweet_preview_enabled": False, + "freedom_of_speech_not_reach_fetch_enabled": True, + "standardized_nudges_misinfo": True, + "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True, + "rweb_video_timestamps_enabled": True, + "longform_notetweets_rich_text_read_enabled": True, + "longform_notetweets_inline_media_enabled": True, + "responsive_web_enhance_cards_enabled": False, + } + ) + ) + fieldToggles: str = quote( + json.dumps({"withArticleRichContentState": True, "withArticlePlainText": False}) + ) + + +class TweetDetailEncode(BaseModel): + focalTweetId: str + cursor: str = "" + referrer: str = "tweet" + with_rux_injections: bool = True + includePromotedContent: bool = True + withCommunity: bool = True + withQuickPromoteEligibilityTweetFields: bool = True + withBirdwatchNotes: bool = True + withVoice: bool = True + withV2Timeline: bool = True + + +class UserProfile(BaseRequestModel): + features: str = quote( + json.dumps( + { + "hidden_profile_likes_enabled": True, + "hidden_profile_subscriptions_enabled": True, + "rweb_tipjar_consumption_enabled": True, + "responsive_web_graphql_exclude_directive_enabled": True, + "verified_phone_label_enabled": True, + "subscriptions_verification_info_is_identity_verified_enabled": True, + "subscriptions_verification_info_verified_since_enabled": True, + "highlights_tweets_tab_ui_enabled": True, + "responsive_web_twitter_article_notes_tab_enabled": True, + "creator_subscriptions_tweet_preview_api_enabled": True, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False, + "responsive_web_graphql_timeline_navigation_enabled": True, + } + ) + ) + fieldToggles: str = quote(json.dumps({"withAuxiliaryUserLabels": False})) + + +class UserProfileEncode(BaseModel): + # uniqueId: asai_chan_ + screen_name: str + withSafetyModeUserFields: bool = True + + +class PostTweet(BaseRequestModel): + features: str = quote( + json.dumps( + { + "rweb_tipjar_consumption_enabled": True, + "responsive_web_graphql_exclude_directive_enabled": True, + "verified_phone_label_enabled": False, + "creator_subscriptions_tweet_preview_api_enabled": True, + "responsive_web_graphql_timeline_navigation_enabled": True, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False, + "communities_web_enable_tweet_community_results_fetch": True, + "c9s_tweet_anatomy_moderator_badge_enabled": True, + "articles_preview_enabled": False, + "tweetypie_unmention_optimization_enabled": True, + "responsive_web_edit_tweet_api_enabled": True, + "graphql_is_translatable_rweb_tweet_is_translatable_enabled": True, + "view_counts_everywhere_api_enabled": True, + "longform_notetweets_consumption_enabled": True, + "responsive_web_twitter_article_tweet_consumption_enabled": True, + "tweet_awards_web_tipping_enabled": False, + "creator_subscriptions_quote_tweet_preview_enabled": False, + "freedom_of_speech_not_reach_fetch_enabled": True, + "standardized_nudges_misinfo": True, + "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True, + "tweet_with_visibility_results_prefer_gql_media_interstitial_enabled": True, + "rweb_video_timestamps_enabled": True, + "longform_notetweets_rich_text_read_enabled": True, + "longform_notetweets_inline_media_enabled": True, + "responsive_web_enhance_cards_enabled": False, + } + ) + ) + + fieldToggles: str = quote(json.dumps({"withArticlePlainText": False})) + + +class PostTweetEncode(BaseModel): + userId: str + count: int + cursor: str = "" + includePromotedContent: bool = True + withQuickPromoteEligibilityTweetFields: bool = True + withVoice: bool = True + withV2Timeline: bool = True diff --git a/f2/apps/twitter/test/test_model.py b/f2/apps/twitter/test/test_model.py new file mode 100644 index 00000000..5aab255a --- /dev/null +++ b/f2/apps/twitter/test/test_model.py @@ -0,0 +1,13 @@ +from f2.apps.twitter.model import encode_model, TweetDetail, TweetDetailEncode + +if __name__ == "__main__": + + # tweet_detail_encode = quote( + # TweetDetailEncode(focalTweetId="1777291676568166526").model_dump_json() + # ) + # tweet_detail = TweetDetail(variables=tweet_detail_encode) + # print(tweet_detail.model_dump()) + + encoded_data = encode_model(TweetDetailEncode(focalTweetId="1777291676568166526")) + tweet_detail = TweetDetail(variables=encoded_data) + print(tweet_detail.model_dump()) diff --git a/f2/apps/twitter/test/test_tweet_id.py b/f2/apps/twitter/test/test_tweet_id.py new file mode 100644 index 00000000..0ab406b9 --- /dev/null +++ b/f2/apps/twitter/test/test_tweet_id.py @@ -0,0 +1,63 @@ +import pytest +from f2.apps.twitter.utils import TweetIdFetcher +from f2.exceptions.api_exceptions import ( + APINotFoundError, +) + + +@pytest.mark.asyncio +class TestTweetIdFetcher: + async def test_get_tweet_id(self): + tweet_link = "https://twitter.com/realDonaldTrump/status/1265255835124539392" + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1265255835124539392" + + tweet_link = "https://twitter.com/realDonaldTrump/status/1265255835124539392/" + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1265255835124539392" + + tweet_link = ( + "https://twitter.com/realDonaldTrump/status/1265255835124539392/?test=123" + ) + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1265255835124539392" + + tweet_link = ( + "https://twitter.com/realDonaldTrump/status/1265255835124539392/%$#" + ) + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1265255835124539392" + + tweet_link = ( + "https://www.twitter.com/realDonaldTrump/status/1265255835124539392" + ) + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1265255835124539392" + + tweet_link = "https://www.twitter.com/realDonaldTrump/status/1265255835124539392?test=123" + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1265255835124539392" + + tweet_link = ( + "https://www.twitter.com/realDonaldTrump/status/1265255835124539392/" + ) + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1265255835124539392" + + tweet_link = "https://www.twitter.com/realDonaldTrump/status/1265255835124539392/?test=123" + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1265255835124539392" + + tweet_link = ( + "https://www.twitter.com/realDonaldTrump/status/1265255835124539392/%$#" + ) + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1265255835124539392" + + tweet_link = "twitter.com/realDonaldTrump/status/1265255835124539392" + with pytest.raises(APINotFoundError): + await TweetIdFetcher.get_tweet_id(tweet_link) + + tweet_link = "https://t.co/1dBHtrG72J" + tweet_id = await TweetIdFetcher.get_tweet_id(tweet_link) + assert tweet_id == "1777291676568166526" diff --git a/f2/apps/twitter/utils.py b/f2/apps/twitter/utils.py new file mode 100644 index 00000000..6e9811fc --- /dev/null +++ b/f2/apps/twitter/utils.py @@ -0,0 +1,418 @@ +# path: f2/apps/twitter/utils.py + +import f2 +import re +import httpx +import asyncio +from typing import Union +from pathlib import Path + +from f2.i18n.translator import _ +from f2.utils.conf_manager import ConfigManager +from f2.utils.utils import extract_valid_urls, split_filename +from f2.exceptions.api_exceptions import ( + APIError, + APIConnectionError, + APIResponseError, + APIUnavailableError, + APIUnauthorizedError, + APINotFoundError, +) + + +class ClientConfManager: + """ + 用于管理客户端配置 (Used to manage client configuration) + """ + + client_conf = ConfigManager(f2.F2_CONFIG_FILE_PATH).get_config("f2") + twitter_conf = client_conf.get("twitter", {}) + + @classmethod + def client(cls) -> dict: + return cls.twitter_conf + + @classmethod + def version(cls) -> str: + return cls.client_conf.get("version", "unknown") + + @classmethod + def proxies(cls) -> dict: + return cls.twitter_conf.get("proxies", {}) + + @classmethod + def headers(cls) -> dict: + return cls.twitter_conf.get("headers", {}) + + @classmethod + def user_agent(cls) -> str: + return cls.headers().get("User-Agent", "") + + @classmethod + def referer(cls) -> str: + return cls.headers().get("Referer", "") + + @classmethod + def authorization(cls) -> str: + return cls.headers().get("Authorization", "") + + @classmethod + def x_csrf_token(cls) -> str: + return cls.headers().get("X-Csrf-Token", "") + + +class ModelManager: + + @classmethod + def model_2_endpoint( + cls, + base_endpoint: str, + params: dict, + ) -> str: + if not isinstance(params, dict): + raise TypeError(_("参数必须是字典类型")) + + param_str = "&".join([f"{k}={v}" for k, v in params.items()]) + + # 检查base_endpoint是否已有查询参数 (Check if base_endpoint already has query parameters) + separator = "&" if "?" in base_endpoint else "?" + + final_endpoint = f"{base_endpoint}{separator}{param_str}" + + return final_endpoint + + +class UserIdFetcher: + # https://x.com/CaroylnG61544 + # https://x.com/CaroylnG61544/ + # https://x.com/CaroylnG61544/followers + # https://x.com/CaroylnG61544/status/1440000000000000000 + # https://twitter.com/CaroylnG61544/status/1440000000000000000/photo/1 + + # 预编译正则表达式 + _USER_ID_PATTERN = re.compile( + r"(?:https?://)?(?:www\.)?(twitter\.com|x\.com)/(?:@)?([a-zA-Z0-9_]+)" + ) + + @classmethod + async def get_user_id(cls, url: str) -> str: + """ + 从用户URL中提取用户ID + (Extract user ID from user URL) + + Args: + url (str): 用户URL (User URL) + + Returns: + str: 用户ID (User ID) + """ + + if not isinstance(url, str): + raise TypeError(_("参数必须是字符串类型")) + + # 提取有效URL + url = extract_valid_urls(url) + + match = cls._USER_ID_PATTERN.search(url) + + if match: + return match.group(2) + else: + raise APINotFoundError( + _( + "未在响应的地址中找到user_id,检查链接是否为用户链接。类名:{0}" + ).format(cls.__name__) + ) + + @classmethod + async def get_all_user_ids(cls, urls: list) -> list: + """ + 从用户URL列表中提取所有用户ID + (Extract all user IDs from the list of user URLs) + + Args: + urls (list): 用户URL列表 (List of user URLs) + + Returns: + list: 用户ID列表 (List of user IDs) + """ + + if not isinstance(urls, list): + raise TypeError(_("参数必须是列表类型")) + + # 提取有效URL + urls = extract_valid_urls(urls) + + # 获取所有用户ID + if urls == []: + raise ( + APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) + ) + ) + + user_ids = [cls.get_user_id(url) for url in urls] + return await asyncio.gather(*user_ids) + + +class TweetIdFetcher: + # 预编译正则表达式 + _TWEET_URL_PATTERN = re.compile( + r"(?:https?://)?(?:www\.)?(?:twitter|x)\.com/.*/status/(\d+)(?:/|\?|#.*$|$)" + ) + + @classmethod + async def get_tweet_id(cls, url: str) -> str: + """ + 从推文URL中提取推文ID + (Extract tweet ID from tweet URL) + + Args: + url (str): 推文URL (Tweet URL) + + Returns: + str: 推文ID (Tweet ID) + """ + + if not isinstance(url, str): + raise TypeError(_("参数必须是字符串类型")) + + # 提取有效URL + url = extract_valid_urls(url) + + if url is None: + raise ( + APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) + ) + + if "t.co" in url: + try: + transport = httpx.AsyncHTTPTransport(retries=5) + async with httpx.AsyncClient( + transport=transport, proxies=ClientConfManager.proxies(), timeout=10 + ) as client: + response = await client.get( + url, headers=ClientConfManager.headers(), follow_redirects=True + ) + url = response.text + response.raise_for_status() + except httpx.HTTPStatusError as e: + raise APINotFoundError( + _("未找到推文,请检查推文链接是否正确。类名:{0}").format( + cls.__name__ + ), + e.response.status_code, + ) + except httpx.RequestError as exc: + raise APIConnectionError( + _( + "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" + ).format(url, ClientConfManager.proxies(), cls.__name__, exc) + ) + + match = cls._TWEET_URL_PATTERN.search(url) + + if match: + return match.group(1) + else: + raise APINotFoundError( + _( + "未在响应的地址中找到tweet_id,检查链接是否为推文链接。类名:{0}" + ).format(cls.__name__) + ) + + @classmethod + async def get_all_tweet_ids(cls, urls: list) -> list: + """ + 从推文URL列表中提取所有推文ID + (Extract all tweet IDs from the list of tweet URLs) + + Args: + urls (list): 推文URL列表 (List of tweet URLs) + + Returns: + list: 推文ID列表 (List of tweet IDs) + """ + + if not isinstance(urls, list): + raise TypeError(_("参数必须是列表类型")) + + # 提取有效URL + urls = extract_valid_urls(urls) + + # 获取所有推文ID + if urls == []: + raise ( + APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) + ) + ) + + tweet_ids = [cls.get_tweet_id(url) for url in urls] + return await asyncio.gather(*tweet_ids) + + +def format_file_name( + naming_template: str, + tweet_data: dict = {}, + custom_fields: dict = {}, +) -> str: + """ + 根据配置文件的全局格式化文件名 + (Format file name according to the global conf file) + + Args: + tweet_data (dict): 微博数据的字典 (dict of douyin data) + naming_template (str): 文件的命名模板, 如 "{create}_{desc}" (Naming template for files, such as "{create}_{desc}") + custom_fields (dict): 用户自定义字段, 用于替代默认的字段值 (Custom fields for replacing default field values) + + Note: + windows 文件名长度限制为 255 个字符, 开启了长文件名支持后为 32,767 个字符 + (Windows file name length limit is 255 characters, 32,767 characters after long file name support is enabled) + Unix 文件名长度限制为 255 个字符 + (Unix file name length limit is 255 characters) + 取去除后的50个字符, 加上后缀, 一般不会超过255个字符 + (Take the removed 50 characters, add the suffix, and generally not exceed 255 characters) + 详细信息请参考: https://en.wikipedia.org/wiki/Filename#Length + (For more information, please refer to: https://en.wikipedia.org/wiki/Filename#Length) + + Returns: + str: 格式化的文件名 (Formatted file name) + """ + + # 为不同系统设置不同的文件名长度限制 + os_limit = { + "win32": 200, + "cygwin": 60, + "darwin": 60, + "linux": 60, + } + fields = { + "create": tweet_data.get("tweet_created_time", ""), # 长度固定19 + "nickname": tweet_data.get("nickname", ""), # 不固定 + "tweet_id": tweet_data.get("tweet_id", ""), # 长度固定19 + "desc": split_filename(tweet_data.get("tweet_desc", ""), os_limit), + "uid": tweet_data.get("user_unique_id", ""), # 不固定 + } + + if custom_fields: + # 更新自定义字段 + fields.update(custom_fields) + + try: + return naming_template.format(**fields) + except KeyError as e: + raise KeyError(_("文件名模板字段 {0} 不存在,请检查".format(e))) + + +def create_or_rename_user_folder( + kwargs: dict, local_user_data: dict, current_nickname: str +) -> Path: + """ + 创建或重命名用户目录 (Create or rename user directory) + + Args: + kwargs (dict): 配置参数 (Conf parameters) + local_user_data (dict): 本地用户数据 (Local user data) + current_nickname (str): 当前用户昵称 (Current user nickname) + + Returns: + user_path (Path): 用户目录路径 (User directory path) + """ + user_path = create_user_folder(kwargs, current_nickname) + + if not local_user_data: + return user_path + + if local_user_data.get("nickname") != current_nickname: + # 昵称不一致,触发目录更新操作 + user_path = rename_user_folder(user_path, current_nickname) + + return user_path + + +def create_user_folder(kwargs: dict, nickname: Union[str, int]) -> Path: + """ + 根据提供的配置文件和昵称,创建对应的保存目录。 + (Create the corresponding save directory according to the provided conf file and nickname.) + + Args: + kwargs (dict): 配置文件,字典格式。(Conf file, dict format) + nickname (Union[str, int]): 用户的昵称,允许字符串或整数。 (User nickname, allow strings or integers) + + Note: + 如果未在配置文件中指定路径,则默认为 "Download"。 + (If the path is not specified in the conf file, it defaults to "Download".) + 支持绝对与相对路径。 + (Support absolute and relative paths) + + Raises: + TypeError: 如果 kwargs 不是字典格式,将引发 TypeError。 + (If kwargs is not in dict format, TypeError will be raised.) + """ + + # 确定函数参数是否正确 + if not isinstance(kwargs, dict): + raise TypeError("kwargs 参数必须是字典") + + # 创建基础路径 + base_path = Path(kwargs.get("path", "Download")) + + # 添加下载模式和用户名 + user_path = ( + base_path / "twitter" / kwargs.get("mode", "PLEASE_SETUP_MODE") / str(nickname) + ) + + # 获取绝对路径并确保它存在 + resolve_user_path = user_path.resolve() + + # 创建目录 + resolve_user_path.mkdir(parents=True, exist_ok=True) + + return resolve_user_path + + +def rename_user_folder(old_path: Path, new_nickname: str) -> Path: + """ + 重命名用户目录 (Rename User Folder). + + Args: + old_path (Path): 旧的用户目录路径 (Path of the old user folder) + new_nickname (str): 新的用户昵称 (New user nickname) + + Returns: + Path: 重命名后的用户目录路径 (Path of the renamed user folder) + """ + # 获取目标目录的父目录 (Get the parent directory of the target folder) + parent_directory = old_path.parent + + # 构建新目录路径 (Construct the new directory path) + new_path = old_path.rename(parent_directory / new_nickname).resolve() + + return new_path + + +def create_or_rename_user_folder( + kwargs: dict, local_user_data: dict, current_nickname: str +) -> Path: + """ + 创建或重命名用户目录 (Create or rename user directory) + + Args: + kwargs (dict): 配置参数 (Conf parameters) + local_user_data (dict): 本地用户数据 (Local user data) + current_nickname (str): 当前用户昵称 (Current user nickname) + + Returns: + user_path (Path): 用户目录路径 (User directory path) + """ + user_path = create_user_folder(kwargs, current_nickname) + + if not local_user_data: + return user_path + + if local_user_data.get("nickname") != current_nickname: + # 昵称不一致,触发目录更新操作 + user_path = rename_user_folder(user_path, current_nickname) + + return user_path diff --git a/f2/apps/weibo/api.py b/f2/apps/weibo/api.py new file mode 100644 index 00000000..a9463ae5 --- /dev/null +++ b/f2/apps/weibo/api.py @@ -0,0 +1,74 @@ +# path: f2/apps/weibo/api.py + + +class WeiboAPIEndpoints: + """ + API Endpoints for Weibo + """ + + # Weibo Domain + WEIBO_DOMAIN = "https://weibo.com" + + # Weibo img domain + WEIBO_IMG_DOMAIN = "https://wx4.sinaimg.cn" + + # 未读好友时间轴 / 全部 + UNREAD_FRIENDS_TIMELINE = "/ajax/feed/unreadfriendstimeline" + + # 原创微博 + ORIGINAL_WEIBO = f"{WEIBO_DOMAIN}/ajax/feed/friendstimeline" + + # 视频微博 + VIDEO_WEIBO = f"{WEIBO_DOMAIN}/ajax/feed/friendstimeline" + + # 最新微博 + NEWEST_WEIBO = f"{WEIBO_DOMAIN}/ajax/feed/friendstimeline" + + # 特别关注 + SPECIAL_WEIBO = f"{WEIBO_DOMAIN}/ajax/feed/groupstimeline" + + # 好友圈 + FRIENDS_TIMELINE = f"{WEIBO_DOMAIN}/ajax/feed/groupstimeline" + + # 超话社区 + SUPER_TOPIC = f"{WEIBO_DOMAIN}/ajax/feed/groupstimeline" + + # 单条微博 + WeiboDetail = f"{WEIBO_DOMAIN}/ajax/statuses/show" + + # 微博评论 + WEIBO_COMMENTS = f"{WEIBO_DOMAIN}/ajax/statuses/buildComments" + + # 个人信息 + USER_INFO = f"{WEIBO_DOMAIN}/ajax/profile/info" + + # 个人详情 + USER_DETAIL = f"{WEIBO_DOMAIN}/ajax/profile/detail" + + # 个人微博 + USER_WEIBO = f"{WEIBO_DOMAIN}/ajax/statuses/mymblog" + + # 个人关注 + USER_FOLLOW = f"{WEIBO_DOMAIN}/ajax/friendships/friends" + + # 个人粉丝 + USER_FANS = f"{WEIBO_DOMAIN}/ajax/friendships/friends" + + # 个人收藏 + USER_FAVORITES = f"{WEIBO_DOMAIN}/ajax/favorites/all_fav" + + # IMG + THUMBNAIL = f"{WEIBO_IMG_DOMAIN}/wap180" + + BMIIDDLE = f"{WEIBO_IMG_DOMAIN}/wap360" + + LARGE = f"{WEIBO_IMG_DOMAIN}/orj960" + + ORIGINAL = f"{WEIBO_IMG_DOMAIN}/orj1080" + + # 原图 + LARGEST = f"{WEIBO_IMG_DOMAIN}/large" + + MW2000 = f"{WEIBO_IMG_DOMAIN}/mw2000" + + LARGE_COVER = f"{WEIBO_IMG_DOMAIN}/cmw960" diff --git a/f2/apps/weibo/cli.py b/f2/apps/weibo/cli.py new file mode 100644 index 00000000..71f4f87f --- /dev/null +++ b/f2/apps/weibo/cli.py @@ -0,0 +1,347 @@ +# path: f2/apps/weibo/cli.py + +import f2 +import click +import typing + +from pathlib import Path + +from f2 import helps +from f2.cli.cli_commands import set_cli_config +from f2.log.logger import logger +from f2.utils.utils import ( + split_dict_cookie, + get_resource_path, + get_cookie_from_browser, + check_invalid_naming, + merge_config, +) +from f2.utils.conf_manager import ConfigManager +from f2.i18n.translator import TranslationManager, _ +from f2.apps.weibo.utils import ClientConfManager + + +def handler_help( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + """ + 处理帮助信息 (Handle help messages) + + 根据提供的值显示帮助信息或退出上下文 + (Display help messages based on the provided value or exit the context) + + Args: + ctx: click的上下文对象 (Click's context object). + param: 提供的参数或选项 (The provided parameter or option). + value: 参数或选项的值 (The value of the parameter or option). + """ + + if not value or ctx.resilient_parsing: + return + + helps.get_help("weibo") + ctx.exit() + + +def handler_auto_cookie( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + """ + 用于自动从浏览器获取cookie (Used to automatically get cookies from the browser) + + Args: + ctx: click的上下文对象 (Click's context object) + param: 提供的参数或选项 (The provided parameter or option) + value: 参数或选项的值 (The value of the parameter or option) + """ + # 如果用户没有提供值或者设置了 resilient_parsing 或者设置了 --cookie,那么跳过自动获取过程 + if not value or ctx.resilient_parsing or ctx.params.get("cookie"): + return + + # 根据浏览器选择获取cookie + try: + cookie_value = split_dict_cookie(get_cookie_from_browser(value, "weibo.com")) + + if not cookie_value: + raise ValueError(_("无法从 {0} 浏览器中获取cookie").format(value)) + + # 如果没有提供配置文件,那么使用高频配置文件 + manager = ConfigManager( + ctx.params.get("config", get_resource_path(f2.APP_CONFIG_FILE_PATH)) + ) + manager.update_config_with_args("weibo", cookie=cookie_value) + except PermissionError: + logger.error(_("请关闭所有已打开的浏览器重试,并且你有适当的权限访问浏览器!")) + ctx.abort() + except Exception as e: + logger.error(_("自动获取Cookie失败:{0}").format(str(e))) + ctx.abort() + finally: + ctx.exit(0) + + +def handler_language( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + """ + 处理语言选项 (Handle language options) + + Args: + ctx: click的上下文对象 (Click's context object) + param: 提供的参数或选项 (The provided parameter or option) + value: 参数或选项的值 (The value of the parameter or option) + """ + + if not value or ctx.resilient_parsing: + return + TranslationManager.get_instance().set_language(value) + global _ + _ = TranslationManager.get_instance().gettext + return value + + +def handler_naming( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + """ + 处理命名选项 (Handle naming options) + + Args: + ctx: click的上下文对象 (Click's context object) + param: 提供的参数或选项 (The provided parameter or option) + value: 参数或选项的值 (The value of the parameter or option) + """ + + # 避免和配置文件参数冲突 + if not value or ctx.resilient_parsing: + return + + # 允许的模式和分隔符 + ALLOWED_PATTERNS = ["{nickname}", "{create}", "{weibo_id}", "{desc}", "{uid}"] + ALLOWED_SEPARATORS = ["-", "_"] + + # 检查命名是否符合命名规范 + invalid_patterns = check_invalid_naming(value, ALLOWED_PATTERNS, ALLOWED_SEPARATORS) + + if invalid_patterns: + raise click.BadParameter( + _("`{0}` 中的 `{1}` 不符合命名模式").format( + value, "".join(invalid_patterns) + ) + ) + + return value + + +@click.command(name="weibo", help=_("微博下载器")) +@click.option( + "-c", + "--config", + type=click.Path(file_okay=True, dir_okay=False, readable=True), + help=_("配置文件的路径,最低优先"), +) +@click.option( + "--url", + "-u", + type=str, + help=_("根据模式提供相应的链接"), +) +@click.option( + "--path", + "-p", + type=str, + help=_("作品保存位置,支持绝对与相对路径"), +) +@click.option( + "--folderize", + "-f", + type=bool, + help=_("是否将作品保存到单独的文件夹"), +) +@click.option( + "--mode", + "-M", + type=click.Choice(f2.WEIBO_MODE_LIST), + help=_("下载模式:单个微博(one),主页微博(post)"), +) +@click.option( + "--naming", + "-n", + type=str, + help=_("全局微博文件命名方式,前往文档查看更多帮助"), + callback=handler_naming, +) +@click.option( + "--cookie", + "-k", + type=str, + help=_("登录后的[yellow]cookie[/yellow]"), +) +@click.option( + "--timeout", + "-e", + type=int, + help=_("网络请求超时时间"), +) +@click.option( + "--max_retries", + "-r", + type=int, + help=_("网络请求超时重试数"), +) +@click.option( + "--max-connections", + "-x", + type=int, + help=_("网络请求并发连接数"), +) +@click.option( + "--max-tasks", + "-t", + type=int, + help=_("异步的任务数"), +) +@click.option( + "--max-counts", + "-o", + type=int, + help=_("最大微博下载数。0 表示无限制"), +) +@click.option( + "--page-counts", + "-s", + type=int, + help=_("从接口每页可获取微博数,不建议超过 20"), +) +@click.option( + "--languages", + "-l", + type=click.Choice(["zh_CN", "en_US"]), + default="zh_CN", + help=_("显示语言。默认为 'zh_CN',可选:'zh_CN'、'en_US',不支持配置文件修改"), + callback=handler_language, +) +@click.option( + "--proxies", + "-P", + type=str, + nargs=2, + help=_( + "代理服务器,最多 2 个参数,http://与https://。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" + ), +) +@click.option( + "--update-config", + type=bool, + is_flag=True, + help=_("使用命令行选项更新配置文件。需要先使用'-c'选项提供一个配置文件路径"), +) +@click.option( + "--init-config", type=str, help=_("初始化配置文件。不能同时初始化和更新配置文件") +) +@click.option( + "--auto-cookie", + type=click.Choice(f2.BROWSER_LIST), + help=_("自动从浏览器获取cookie,使用该命令前请确保关闭所选的浏览器"), + callback=handler_auto_cookie, +) +@click.option( + "-h", + is_flag=True, + is_eager=True, + expose_value=False, + help=_("显示富文本帮助"), + callback=handler_help, +) +@click.pass_context +def weibo( + ctx: click.Context, + config: str, + init_config: str, + update_config: bool, + **kwargs, +) -> None: + ################## + # f2 存在2个主配置文件,分别是app低频配置(app.yaml)和f2低频配置(conf.yaml) + # app低频配置存放app相关的参数 + # f2低频配置存放计算值所需的参数 + + # 其中cli参数具有最高优先,cli >= 自定义 >= 低频 + # 在f2低频配置中设置代理参数 + # 在app低频配置中设置好重试次数,超时时间,下载路径,下载线程,cookie等低频的参数 + # 在自定义配置中可以设置不同用户的高频参数,如用户主页,原声下载,封面下载,文案下载,下载模式等 + # cli参数为配置文件的热修改,可以随时修改每一个参数。 + ################## + + # 读取低频主配置文件 + main_manager = ConfigManager(f2.APP_CONFIG_FILE_PATH) + main_conf_path = get_resource_path(f2.APP_CONFIG_FILE_PATH) + main_conf = main_manager.get_config("weibo") + + # 更新主配置文件中的代理参数 + main_conf["proxies"] = ClientConfManager.proxies() + + # 更新主配置文件中的headers参数 + kwargs.setdefault("headers", {}) + kwargs["headers"]["User-Agent"] = ClientConfManager.user_agent() + kwargs["headers"]["Referer"] = ClientConfManager.referer() + + # 如果初始化配置文件,则与更新配置文件互斥 + if init_config and not update_config: + main_manager.generate_config("weibo", init_config) + return + elif init_config: + raise click.UsageError(_("不能同时初始化和更新配置文件")) + # 如果没有初始化配置文件,但是更新配置文件,则需要提供配置文件路径 + elif update_config and not config: + raise click.UsageError( + _("要更新配置, 首先需要使用'-c'选项提供一个自定义配置文件路径") + ) + + # 读取自定义配置文件 + if config: + custom_manager = ConfigManager(config) + else: + custom_manager = main_manager + config = main_conf_path + + custom_conf = custom_manager.get_config("weibo") + + if update_config: # 如果指定了 update_config,更新配置文件 + update_manger = ConfigManager(config) + update_manger.update_config_with_args("weibo", **kwargs) + return + + # 将kwargs["proxies"]中的tuple转换为dict + if kwargs.get("proxies"): + kwargs["proxies"] = { + "http://": kwargs["proxies"][0], + "https://": kwargs["proxies"][1], + } + + # 从低频配置开始到高频配置再到cli参数,逐级覆盖,如果键值不存在使用父级的键值 + kwargs = merge_config(main_conf, custom_conf, **kwargs) + + logger.info(_("模式:{0}").format(kwargs.get("mode"))) + logger.info(_("主配置路径:{0}").format(main_conf_path)) + logger.info(_("自定义配置路径:{0}").format(Path.cwd() / config)) + logger.debug(_("主配置参数:{0}").format(main_conf)) + logger.debug(_("自定义配置参数:{0}").format(custom_conf)) + logger.debug(_("CLI参数:{0}").format(kwargs)) + + # 尝试从命令行参数或kwargs中获取URL + if not kwargs.get("url"): + logger.error(_("缺乏URL参数,详情看命令帮助")) + handler_help(ctx, None, True) + + # 添加app_name到kwargs + kwargs["app_name"] = "weibo" + ctx.invoke(set_cli_config, **kwargs) diff --git a/f2/apps/weibo/crawler.py b/f2/apps/weibo/crawler.py new file mode 100644 index 00000000..7a70c853 --- /dev/null +++ b/f2/apps/weibo/crawler.py @@ -0,0 +1,52 @@ +# path: f2/apps/weibo/crawler.py + +from f2.log.logger import logger +from f2.i18n.translator import _ +from f2.crawlers.base_crawler import BaseCrawler +from f2.apps.weibo.api import WeiboAPIEndpoints as wbendpoint +from f2.apps.weibo.model import ( + UserInfo, + UserDetail, + UserWeibo, + WeiboDetail, +) +from f2.apps.weibo.utils import ModelManager, ClientConfManager + + +class WeiboCrawler(BaseCrawler): + def __init__( + self, + kwargs: dict = ..., + ): + # 需要与cli同步 + proxies = kwargs.get("proxies", {"http://": None, "https://": None}) + self.headers = kwargs.get("headers") | {"Cookie": kwargs["cookie"]} + super().__init__(proxies=proxies, crawler_headers=self.headers) + + async def fetch_user_info(self, params: UserInfo): + endpoint = ModelManager.model_2_endpoint( + wbendpoint.USER_INFO, params.model_dump() + ) + logger.debug(_("用户信息接口地址:" + endpoint)) + return await self._fetch_get_json(endpoint) + + async def fetch_user_detail(self, params: UserDetail): + endpoint = ModelManager.model_2_endpoint( + wbendpoint.USER_DETAIL, params.model_dump() + ) + logger.debug(_("用户详情接口地址:" + endpoint)) + return await self._fetch_get_json(endpoint) + + async def fetch_user_weibo(self, params: UserWeibo): + endpoint = ModelManager.model_2_endpoint( + wbendpoint.USER_WEIBO, params.model_dump() + ) + logger.debug(_("用户微博接口地址:" + endpoint)) + return await self._fetch_get_json(endpoint) + + async def fetch_weibo_detail(self, params: WeiboDetail): + endpoint = ModelManager.model_2_endpoint( + wbendpoint.WeiboDetail, params.model_dump() + ) + logger.debug(_("单条微博接口地址:" + endpoint)) + return await self._fetch_get_json(endpoint) diff --git a/f2/apps/weibo/db.py b/f2/apps/weibo/db.py new file mode 100644 index 00000000..a40bc35d --- /dev/null +++ b/f2/apps/weibo/db.py @@ -0,0 +1,123 @@ +# path: f2/apps/weibo/db.py + +from f2.db.base_db import BaseDB + + +class AsyncUserDB(BaseDB): + TABLE_NAME = "user_info_web" + + async def _create_table(self) -> None: + """在数据库中创建用户信息表""" + await super()._create_table() + + fields = [ + "uid TEXT PRIMARY KEY", + "nickname TEXT", + "blockText TEXT", + "avatar_hd TEXT", + "cover_image TEXT", + "description TEXT", + "follow_me BOOLEAN", + "following BOOLEAN", + "followers_count INTEGER", + "friends_count INTEGER", + "weibo_count INTEGER", + "gender TEXT", + "weihao TEXT", + "is_muteuser BOOLEAN", + "is_star TEXT", + "location TEXT", + "profile_url TEXT", + "user_type TEXT", + "verified BOOLEAN", + "vvip INTEGER", + ] + + fields_sql = ", ".join(fields) + await self.execute( + f"""CREATE TABLE IF NOT EXISTS {self.TABLE_NAME} ({fields_sql})""" + ) + await self.commit() + + async def add_user_info(self, ignore_fields: list = None, **kwargs) -> None: + """ + 添加用户信息 + + Args: + ignore_fields: 要忽略的字段列表,例如 ["field1", "field2"] + **kwargs: 用户的其他字段键值对 + """ + + # 如果 ignore_fields 未提供或者为 None,将其设置为空列表 + ignore_fields = ignore_fields or [] + + # 从 kwargs 中删除要忽略的字段 + for field in ignore_fields: + if field in kwargs: + del kwargs[field] + + keys = ", ".join(kwargs.keys()) + placeholders = ", ".join(["?"] * len(kwargs)) + values = tuple(kwargs.values()) + + await self.execute( + f"INSERT OR REPLACE INTO {self.TABLE_NAME} ({keys}) VALUES ({placeholders})", + values, + ) + await self.commit() + + async def updat_user_info(self, uid: str, **kwargs) -> None: + """ + 更新用户信息 + + Args: + uid: 用户 ID + **kwargs: 用户的其他字段键值对 + """ + + user_data = await self.get_user_info(uid) + if user_data: + set_sql = ", ".join([f"{key} = ?" for key in kwargs.keys()]) + await self.execute( + f"UPDATE {self.TABLE_NAME} SET {set_sql} WHERE uid=?", + (*kwargs.values(), uid), + ) + await self.commit() + + async def get_user_info(self, uid: str) -> dict: + """ + 获取用户信息 + + Args: + uid: 用户 ID + + Returns: + dict: 对应的用户信息,如果不存在则返回 None + """ + + cursor = await self.execute( + f"SELECT * FROM {self.TABLE_NAME} WHERE uid=?", (uid,) + ) + result = await cursor.fetchone() + if not result: + return {} + columns = [description[0] for description in cursor.description] + return dict(zip(columns, result)) + + async def delete_user_info(self, uid: str) -> None: + """ + 删除用户信息 + + Args: + uid: 用户 ID + """ + + await self.execute(f"DELETE FROM {self.TABLE_NAME} WHERE uid=?", (uid,)) + await self.commit() + + async def __aenter__(self): + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() diff --git a/f2/apps/weibo/dl.py b/f2/apps/weibo/dl.py new file mode 100644 index 00000000..6add9865 --- /dev/null +++ b/f2/apps/weibo/dl.py @@ -0,0 +1,153 @@ +# path: f2/apps/weibo/dl.py + +import sys +from datetime import datetime +from typing import Any, Union + +from f2.log.logger import logger +from f2.i18n.translator import _ +from f2.dl.base_downloader import BaseDownloader +from f2.utils.utils import get_timestamp, timestamp_2_str +from f2.apps.weibo.db import AsyncUserDB +from f2.apps.weibo.utils import format_file_name +from f2.apps.weibo.api import WeiboAPIEndpoints + + +class WeiboDownloader(BaseDownloader): + def __init__(self, kwargs: dict = ...) -> None: + if kwargs["cookie"] is None: + raise ValueError( + _( + "cookie不能为空。请提供有效的 cookie 参数,或自动从浏览器获取 `--auto-cookie edge`" + ) + ) + + super().__init__(kwargs) + + async def create_download_tasks( + self, + kwargs: dict = ..., + weibo_datas: Union[list, dict] = ..., + user_path: Any = ..., + ) -> None: + """ + 创建下载任务 + + Args: + kwargs (dict): 命令行参数 + weibo_datas (list, dict): 微博数据列表或字典 + user_path (str): 用户目录路径 + """ + + if ( + not kwargs + or not weibo_datas + or not isinstance(weibo_datas, (list, dict)) + or not user_path + ): + return + + # 统一处理,将 weibo_datas 转为列表 + weibo_datas_list = ( + [weibo_datas] if isinstance(weibo_datas, dict) else weibo_datas + ) + + # 创建下载任务 + for weibo_data in weibo_datas_list: + await self.handler_download(kwargs, weibo_data, user_path) + + # 执行下载任务 + await self.execute_tasks() + + async def handler_download( + self, kwargs: dict, weibo_data_dict: dict, user_path: Any + ) -> None: + """ + 处理下载任务 + + Args: + kwargs (dict): 命令行参数 + weibo_data_dict (dict): 作品数据字典 + user_path (Any): 用户目录路径 + """ + + # 构建文件夹路径 + base_path = ( + user_path + / format_file_name(kwargs.get("naming", "{create}_{desc}"), weibo_data_dict) + if kwargs.get("folderize") + else user_path + ) + + user_id = weibo_data_dict.get("user_id") + weibo_id = weibo_data_dict.get("weibo_id") + + logger.debug(f"========{weibo_id}========") + logger.debug(weibo_data_dict) + logger.debug("===================================") + + # 检查微博是否可见 + if weibo_data_dict.get("is_visible"): + logger.error(_("微博 {0} 无查看权限").format(weibo_id)) + return + + # 检查微博是否有图片 + if weibo_data_dict.get("pic_num") == 0: + + # 说明是视频微博 + # print(weibo_data_dict.get("playback_list")) + logger.info( + _("清晰度列表:{0},码率列表:{1}").format( + weibo_data_dict.get("quality_list"), + weibo_data_dict.get("bitrate_list"), + ) + ) + logger.info(weibo_data_dict.get("playback_list")[0]) + + video_name = ( + format_file_name( + kwargs.get("naming", "{create}_{desc}"), weibo_data_dict + ) + + "_video" + ) + video_url = weibo_data_dict.get("playback_list") + if video_url[0]: + await self.initiate_download( + _("视频"), + video_url[0], + base_path, + video_name, + ".mp4", + ) + else: + # 处理图片下载任务 + # logger.info( + # _("图片ID列表:{0},图片数量:{1}").format( + # weibo_data_dict.get("pic_infos"), + # weibo_data_dict.get("pic_num"), + # ) + # ) + for i, image_url in enumerate(weibo_data_dict.get("pic_infos")): + image_name = f"{format_file_name(kwargs.get('naming'), weibo_data_dict)}_image_{i + 1}" + image_url = WeiboAPIEndpoints.LARGEST + f"/{image_url}" + if image_url != None: + await self.initiate_download( + _("图片"), image_url, base_path, image_name, ".jpg" + ) + else: + logger.warning( + _("{0} 该微博没有图片链接,无法下载").format(weibo_id) + ) + + # 处理文案下载任务 + if kwargs.get("desc"): + desc_name = ( + format_file_name( + kwargs.get("naming", "{create}_{desc}"), weibo_data_dict + ) + + "_desc" + ) + desc_content = weibo_data_dict.get("desc") + await self.initiate_static_download( + _("文案"), desc_content, base_path, desc_name, ".txt" + ) diff --git a/f2/apps/weibo/filter.py b/f2/apps/weibo/filter.py new file mode 100644 index 00000000..ccad63a7 --- /dev/null +++ b/f2/apps/weibo/filter.py @@ -0,0 +1,429 @@ +# path: f2/apps/weibo/filter.py + +from f2.utils.json_filter import JSONModel +from f2.utils.utils import _get_first_item_from_list, timestamp_2_str, replaceT + +# Filter + + +class UserInfoFilter(JSONModel): + + @property + def status(self): + return self._get_attr_value("$.data.ok") + + @property + def blockText(self): + return self._get_attr_value("$.data.blockText") + + @property + def avatar_hd(self): + return self._get_attr_value("$.data.user.avatar_hd") + + @property + def cover_image(self): + return self._get_attr_value("$.data.user.cover_image_phone") + + @property + def description(self): + return replaceT(self._get_attr_value("$.data.user.description")) + + @property + def nickname(self): + return replaceT(self._get_attr_value("$.data.user.screen_name")) + + @property + def follow_me(self): + return self._get_attr_value("$.data.user.follow_me") + + @property + def following(self): + return self._get_attr_value("$.data.user.following") + + @property + def followers_count(self): + return self._get_attr_value("$.data.user.followers_count") + + @property + def friends_count(self): + return self._get_attr_value("$.data.user.friends_count") + + @property + def weibo_count(self): + return self._get_attr_value("$.data.user.statuses_count") + + @property + def gender(self): + return self._get_attr_value("$.data.user.gender") + + @property + def uid(self): + return self._get_attr_value("$.data.user.idstr") + + @property + def weihao(self): + return self._get_attr_value("$.data.user.weihao") + + @property + def is_muteuser(self): + return self._get_attr_value("$.data.user.is_muteuser") + + @property + def is_star(self): + return self._get_attr_value("$.data.user.is_star") + + @property + def location(self): + return self._get_attr_value("$.data.user.location") + + @property + def profile_url(self): + return "https://weibo.com" + self._get_attr_value("$.data.user.profile_url") + + @property + def user_type(self): + return self._get_attr_value("$.data.user.user_type") + + @property + def verified(self): + return self._get_attr_value("$.data.user.verified") + + @property + def vvip(self): + return self._get_attr_value("$.data.user.vvip") + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + +class UserDetailFilter(JSONModel): + + @property + def status(self): + return self._get_attr_value("$.ok") + + @property + def message(self): + return self._get_attr_value("$.message") + + @property + def birthday(self): + return self._get_attr_value("$.data.birthday") + + @property + def description(self): + return replaceT(self._get_attr_value("$.data.description")) + + @property + def description_raw(self): + return self._get_attr_value("$.data.description") + + @property + def location(self): + return self._get_attr_value("$.data.ip_location") + + @property + def gender(self): + return self._get_attr_value("$.data.gender") + + @property + def create_at(self): + return self._get_attr_value("$.data.created_at").replace(":", "-") + + @property + def video_play_count(self): + return self._get_attr_value("$.data.label_desc[0].name") + + @property + def real_name(self): + return self._get_attr_value("$.data.real_name.name") + + @property + def sunshine_credit(self): + return self._get_attr_value("$.data.sunshine_credit.level") + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + +class WeiboDetailFilter(JSONModel): + + @property + def status(self): + return self._get_attr_value("$.ok") + + @property + def message(self): + return self._get_attr_value("$.message") + + @property + def error_code(self): + return self._get_attr_value("$.error_code") + + @property + def weibo_id(self): + return self._get_attr_value("$.idstr") + + @property + def weibo_blog_id(self): + return self._get_attr_value("$.mblogid") + + @property + def weibo_type(self): + return self._get_attr_value("$.mblogtype") + + @property + def rid(self): + return self._get_attr_value("$.rid") + + @property + def create_time(self): + return timestamp_2_str(self._get_attr_value("$.created_at")) + + @property + def desc(self): + return replaceT(self._get_attr_value("$.text")) + + @property + def descLength(self): + return self._get_attr_value("$.textLength") + + @property + def descRaw(self): + return replaceT(self._get_attr_value("$.text_raw")) + + @property + def descRaw_raw(self): + return self._get_attr_value("$.text_raw") + + @property + def digg_count(self): + return self._get_attr_value("$.attitudes_count") + + @property + def comments_count(self): + return self._get_attr_value("$.comments_count") + + @property + def share_count(self): + return self._get_attr_value("$.reposts_count") + + # IMG + @property + def pic_ids(self): + return self._get_attr_value("$.pic_ids") + + @property + def pic_num(self): + return self._get_attr_value("$.pic_num") + + @property + def pic_infos(self): + # 每个图片的信息都是pic_ids作为下标的 + return self._get_attr_value("$.pic_infos") + + # VIDEO + @property + def bitrate_list(self): + return self._get_list_attr_value( + "$.page_info.media_info.playback_list[*].play_info.bitrate" + ) + + @property + def playback_list(self): + return self._get_list_attr_value( + "$.page_info.media_info.playback_list[*].play_info.url" + ) + + @property + def quality_list(self): + return self._get_list_attr_value( + "$.page_info.media_info.playback_list[*].play_info.quality_class" + ) + + @property + def region(self): + return self._get_attr_value("$.region_name") + + @property + def source(self): + return self._get_attr_value("$.source") + + @property + def isLongText(self): + return self._get_attr_value("$.isLongText") + + @property + def is_paid(self): + return self._get_attr_value("$.is_paid") + + @property + def is_public(self): + return self._get_attr_value("$.title.text") + + @property + def is_visible(self): + return self._get_attr_value("$.visible.type") + + # user + @property + def user_id(self): + return self._get_attr_value("$.user.idstr") + + @property + def nickname(self): + return replaceT(self._get_attr_value("$.user.screen_name")) + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } + + +class UserWeiboFilter(JSONModel): + + @property + def status(self): + return self._get_attr_value("$.ok") + + @property + def message(self): + return self._get_attr_value("$.message") + + @property + def weibo_total(self): + return self._get_attr_value("$.data.total") + + # Weibo + @property + def weibo_visible(self): + return self._get_attr_value("$.data.list[*].visible.type") + + @property + def weibo_created_at(self): + return self._get_attr_value("$.data.list[*].created_at") + + @property + def weibo_id(self): + return self._get_attr_value("$.data.list[*].idstr") + + @property + def weibo_isLongText(self): + return self._get_attr_value("$.data.list[*].isLongText") + + @property + def weibo_is_paid(self): + return self._get_attr_value("$.data.list[*].is_paid") + + @property + def weibo_mblogid(self): + return self._get_attr_value("$.data.list[*].mblogid") + + @property + def weibo_views(self): + return self._get_attr_value( + "$.data.list[*].number_display_strategy.display_text" + ) + + @property + def weibo_digg_count(self): + return self._get_attr_value("$.data.list[*].attitudes_count") + + @property + def weibo_read_count(self): + return self._get_attr_value("$.data.list[*].reads_count") + + @property + def weibo_pic_ids(self): + return self._get_attr_value("$.data.list[*].pic_ids") + + @property + def weibo_pic_num(self): + return self._get_attr_value("$.data.list[*].pic_num") + + @property + def weibo_location(self): + return self._get_attr_value("$.data.list[*].region_name") + + @property + def weibo_reposts_count(self): + return self._get_attr_value("$.data.list[*].reposts_count") + + @property + def weibo_showFeedComment(self): + return self._get_attr_value("$.data.list[*].showFeedComment") + + @property + def weibo_showFeedRepost(self): + return self._get_attr_value("$.data.list[*].showFeedRepost") + + @property + def weibo_showPictureViewer(self): + return self._get_attr_value("$.data.list[*].showPictureViewer") + + @property + def weibo_desc(self): + return replaceT(self._get_attr_value("$.data.list[*].text_raw")) + + @property + def weibo_desc_raw(self): + return self._get_attr_value("$.data.list[*].text_raw") + + @property + def weibo_sorce(self): + return self._get_attr_value("$.data.list[*].source") + + # 需要用#包裹并quote + @property + def weibo_topic_title(self): + return self._get_attr_value("$.data.list[*].topic_struct[*].topic_title") + + # User + @property + def weibo_user_name(self): + return replaceT(self._get_attr_value("$.data.list[*].user.screen_name")) + + @property + def weibo_user_name_raw(self): + return self._get_attr_value("$.data.list[*].user.screen_name") + + @property + def weibo_user_uid(self): + return self._get_attr_value("$.data.list[*].user.idstr") + + @property + def weibo_user_domain(self): + return self._get_attr_value("$.data.list[*].user.domain") + + @property + def weibo_user_avatar_hd(self): + return self._get_attr_value("$.data.list[*].user.avatar_hd") + + def _to_raw(self) -> dict: + return self._data + + def _to_dict(self) -> dict: + return { + prop_name: getattr(self, prop_name) + for prop_name in dir(self) + if not prop_name.startswith("__") and not prop_name.startswith("_") + } diff --git a/f2/apps/weibo/handler.py b/f2/apps/weibo/handler.py new file mode 100644 index 00000000..5edf1d4b --- /dev/null +++ b/f2/apps/weibo/handler.py @@ -0,0 +1,228 @@ +# path: f2/apps/weibo/handler.py + +from pathlib import Path +from typing import AsyncGenerator, Union, Dict, Any, List + +from f2.log.logger import logger +from f2.i18n.translator import _ +from f2.utils.decorators import mode_handler, mode_function_map +from f2.apps.weibo.db import AsyncUserDB +from f2.apps.weibo.crawler import WeiboCrawler +from f2.apps.weibo.dl import WeiboDownloader +from f2.apps.weibo.model import UserInfo, UserDetail, UserWeibo, WeiboDetail +from f2.apps.weibo.filter import UserInfoFilter, UserDetailFilter, WeiboDetailFilter +from f2.apps.weibo.utils import ( + WeiboIdFetcher, + WeiboUidFetcher, + create_or_rename_user_folder, +) +from f2.exceptions.api_exceptions import APIResponseError +from f2.cli.cli_console import RichConsoleManager + +rich_console = RichConsoleManager().rich_console +rich_prompt = RichConsoleManager().rich_prompt + + +class WeiboHandler: + + # 需要忽略的字段 + user_ignore_fields = ["status"] + + def __init__(self, kwargs) -> None: + self.kwargs = kwargs + self.downloader = WeiboDownloader(kwargs) + + async def fetch_user_info(self, uid: str = "", custom: str = "") -> UserInfoFilter: + """ + 获取用户个人信息 + (Get user personal info) + + Args: + uid (str): 用户ID (User ID) + custom (str): 用户自定义id (Custom ID) + + Returns: + UserInfoFilter: 用户信息过滤器 (User info filter) + + Note: + uid和custom只需传入一个 (Only need to pass in one of uid and custom) + """ + + async with WeiboCrawler(self.kwargs) as crawler: + params = UserInfo(uid=uid, custom=custom) + response = await crawler.fetch_user_info(params) + user = UserInfoFilter(response) + if user.nickname is None: + raise APIResponseError( + _("`fetch_user_info`请求失败,请更换cookie或稍后再试") + ) + return user + + async def fetch_user_detail(self, uid: str) -> UserDetailFilter: + """ + 获取用户详细信息 + (Get user detail info) + + Args: + uid (str): 用户ID (User ID) + + Returns: + UserDetailFilter: 用户详细信息 (User detail info) + """ + + async with WeiboCrawler(self.kwargs) as crawler: + params = UserDetail(uid=uid) + response = await crawler.fetch_user_detail(params) + user = UserDetailFilter(response) + if user.create_at is None: + raise APIResponseError( + _("`fetch_user_detail`请求失败,请更换cookie或稍后再试") + ) + return user + + async def get_or_add_user_data( + self, + kwargs: dict, + user_id: str, + db: AsyncUserDB, + ) -> Path: + """ + 获取或创建用户数据同时创建用户目录 + (Get or create user data and create user directory) + + Args: + kwargs (dict): 配置参数 (Conf parameters) + user_id (str): 用户ID (User ID) + db (AsyncUserDB): 用户数据库 (User database) + + Returns: + user_path (Path): 用户目录路径 (User directory path) + """ + + # 尝试从数据库中获取用户数据 + local_user_data = await db.get_user_info(user_id) + + # 从服务器获取当前用户最新数据 + current_user_data = await self.fetch_user_info(user_id) + + # 获取当前用户最新昵称 + current_nickname = current_user_data.nickname + + # 设置用户目录 + user_path = create_or_rename_user_folder( + kwargs, local_user_data, current_nickname + ) + + # 如果用户不在数据库中,将其添加到数据库 + if not local_user_data: + await db.add_user_info( + self.user_ignore_fields, **current_user_data._to_dict() + ) + + return user_path + + @mode_handler("one") + async def handle_one_weibo(self): + """ + 用于处理单个微博。 + (Used to process a single weibo.) + + Args: + kwargs: dict: 参数字典 (Parameter dictionary) + """ + + weibo_id = await WeiboIdFetcher.get_weibo_id(self.kwargs.get("url")) + + weibo = await self.fetch_one_weibo(weibo_id) + + # 检查是否有查看权限 + if weibo.error_code == 20112: + logger.error(_("微博 {0} 无查看权限,请配置Cookie").format(weibo_id)) + await self.downloader.close() + return + else: + logger.info( + f"微博ID: {weibo.weibo_id}, 微博文案: {weibo.descRaw}, 作者昵称: {weibo.nickname}, 发布时间: {weibo.create_time}" + ) + + async with AsyncUserDB("weibo_users.db") as db: + user_path = await self.get_or_add_user_data(self.kwargs, weibo.user_id, db) + + # async with AsyncUserDB("douyin_users.db") as db: + # user_path = await self.get_or_add_user_data( + # self.kwargs, weibo_data.get("sec_user_id"), db + # ) + + await self.downloader.create_download_tasks( + self.kwargs, weibo._to_dict(), user_path + ) + + async def fetch_one_weibo(self, weibo_id: str) -> WeiboDetailFilter: + """ + 用于获取单个微博。 + + Args: + weibo_id: str: 微博ID + + Return: + weibo_data: dict: 微博数据字典,包含微博ID、微博文案、作者昵称 + """ + + logger.info(_("开始爬取微博: {0}").format(weibo_id)) + + async with WeiboCrawler(self.kwargs) as crawler: + params = WeiboDetail(id=weibo_id) + response = await crawler.fetch_weibo_detail(params) + weibo = WeiboDetailFilter(response) + return weibo + + @mode_handler("post") + async def handle_user_weibo(self): + """ + 用于处理用户微博。 + (Used to process user weibo.) + + Args: + kwargs: dict: 参数字典 (Parameter dictionary) + """ + + user_id = await WeiboUidFetcher.get_weibo_uid(self.kwargs.get("url")) + + async with AsyncUserDB("weibo_users.db") as db: + user_path = await self.get_or_add_user_data(self.kwargs, user_id, db) + + # 获取用户微博数据 + weibo_data = await self.fetch_user_weibo(user_id) + + # 获取用户昵称 + user_nickname = weibo_data.get("nickname") + + logger.info( + f"用户ID: {user_id}, 用户昵称: {user_nickname}, 微博数量: {weibo_data.get('total')}" + ) + + await self.downloader.create_download_tasks(self.kwargs, weibo_data, user_path) + + async def fetch_user_weibo(self, user_id: str) -> Dict[str, Any]: + """ + 用于获取用户微博数据。 + + Args: + user_id: str: 用户ID + + Return: + weibo_data: dict: 用户微博数据字典 + """ + + async with WeiboCrawler(self.kwargs) as crawler: + params = UserWeibo(uid=user_id) + response = await crawler.fetch_user_weibo(params) + return response + + +async def main(kwargs): + mode = kwargs.get("mode") + if mode in mode_function_map: + await mode_function_map[mode](WeiboHandler(kwargs)) + else: + logger.error(_("不存在该模式: {0}").format(mode)) diff --git a/f2/apps/weibo/help.py b/f2/apps/weibo/help.py new file mode 100644 index 00000000..3c842c49 --- /dev/null +++ b/f2/apps/weibo/help.py @@ -0,0 +1,64 @@ +# path: f2/apps/weibo/help.py + +from rich.console import Console +from rich.panel import Panel +from rich.table import Table + +from f2.i18n.translator import _ + + +def help() -> None: + # 真彩 + console = Console(color_system="truecolor") + table = Table(highlight=True, box=None, show_header=False) + table.add_column("OPTIONS", no_wrap=True, justify="left", style="bold") + table.add_column("Type", no_wrap=True, justify="left", style="bold") + table.add_column("Description", no_wrap=True) + + options = [ + ("-c --config", "[dark_cyan]Path", _("配置文件的路径,最低优先")), + ("-u --url", "[dark_cyan]str", _("除了单个微博外,其他URL都需要用户主页URL")), + ("-p --path", "[dark_cyan]str", _("微博保存位置,默认为 'Download'")), + ( + "-f --folderize", + "[dark_cyan]Choice", + _("是否将微博保存到单独的文件夹,默认为 'yes'"), + ), + ( + "-M --mode", + "[dark_cyan]Choice", + _("下载模式:单个微博(one),主页微博(post)"), + ), + ("-n --naming", "[dark_cyan]str", _("全局微博文件命名方式")), + ( + "--auto-cookie", + "[dark_cyan]Choice", + _( + "自动从浏览器获取[yellow]cookie[/yellow],使用该命令前请确保关闭所选的浏览器" + ), + ), + ("-k --cookie", "[dark_cyan]str", _("登录后的cookie")), + ("-e --timeout", "[dark_cyan]int", _("网络请求超时时间,默认为 10")), + ("-r --max-retries", "[dark_cyan]int", _("网络请求超时重试数,默认为 5")), + ("-x --max-connections", "[dark_cyan]int", _("网络请求并发连接数,默认为 5")), + ("-t --max-tasks", "[dark_cyan]int", _("异步的任务数,默认为 10")), + ("-o --max-counts", "[dark_cyan]int", _("最大微博下载数 默认为 0,表示无限制")), + ("-s --page-counts", "[dark_cyan]int", _("每页微博数,默认为 20个微博/页")), + ("-l --languages", "[dark_cyan]Choice", _("语言设置,默认为 'zh_CN'")), + ( + "-P --proxies", + "[dark_cyan]str", + _( + "代理服务器,最多 2 个参数,http://与https://。空格区分 2 个参数 http://x.x.x.x https://x.x.x.x" + ), + ), + ("--update-config", "[dark_cyan]Flag", _("使用命令行选项更新配置文件")), + ("--init-config", "[dark_cyan]Flag", _("初始化配置文件")), + ] + + for option in options: + table.add_row(*option) + + console.print( + Panel(table, border_style="bold", title="[WeiBo]", title_align="left") + ) diff --git a/f2/apps/weibo/model.py b/f2/apps/weibo/model.py new file mode 100644 index 00000000..7cc760e5 --- /dev/null +++ b/f2/apps/weibo/model.py @@ -0,0 +1,26 @@ +# path: f2/apps/weibo/model.py + +from typing import Any +from pydantic import BaseModel + + +# Model +class UserInfo(BaseModel): + uid: str + custom: str + + +class UserDetail(BaseModel): + uid: str + + +class UserWeibo(BaseModel): + uid: str + page: int = 1 + feature: int = 0 + since_id: str = "" + + +class WeiboDetail(BaseModel): + id: str # like `O8DM0BLLm` or `5020595169001740` + locale: str = "zh-CN" diff --git a/f2/apps/weibo/test/test_gen_visitor.py b/f2/apps/weibo/test/test_gen_visitor.py new file mode 100644 index 00000000..c811e458 --- /dev/null +++ b/f2/apps/weibo/test/test_gen_visitor.py @@ -0,0 +1,27 @@ +import pytest +from f2.apps.weibo.utils import VisitorManager + + +@pytest.mark.asyncio +async def test_gen_visitor(): + # 设置测试用的 visitor_conf + VisitorManager.visitor_conf = { + "cb": "visitor_gray_callback", + "tid": "", + "from": "weibo", + "url": "https://passport.weibo.com/visitor/genvisitor2", + } + + # 设置假的 proxies 和 user agent + VisitorManager.proxies = { + "http://": None, + "https://": None, + } + + visitor_cookie = await VisitorManager.gen_visitor() + + # 断言生成的 cookie 是否正确 + assert "SUB=" in visitor_cookie + assert "SUBP=" in visitor_cookie + assert "SRT=" in visitor_cookie + assert "SRF=" in visitor_cookie diff --git a/f2/apps/weibo/test/test_handler.py b/f2/apps/weibo/test/test_handler.py new file mode 100644 index 00000000..d296f92a --- /dev/null +++ b/f2/apps/weibo/test/test_handler.py @@ -0,0 +1,33 @@ +import pytest +from f2.apps.weibo.handler import WeiboHandler +from f2.utils.conf_manager import TestConfigManager + + +@pytest.fixture +def kwargs_fixture(): + return TestConfigManager.get_test_config("weibo") + + +@pytest.mark.asyncio +async def test_fetch_user_info(kwargs_fixture): + handler = WeiboHandler(kwargs_fixture) + user_info = await handler.fetch_user_info(uid="2265830070") + assert user_info is not None + assert user_info.uid == "2265830070" + + +@pytest.mark.asyncio +async def test_fetch_user_detail(kwargs_fixture): + handler = WeiboHandler(kwargs_fixture) + user_detail = await handler.fetch_user_detail(uid="2265830070") + + assert user_detail is not None + + +@pytest.mark.asyncio +async def test_handle_one_weibo(kwargs_fixture): + handler = WeiboHandler(kwargs_fixture) + weibo = await handler.fetch_one_weibo(weibo_id="LvFY288c0") + assert weibo is not None + assert weibo.weibo_id == "4775494941938120" + assert weibo.weibo_blog_id == "LvFY288c0" diff --git a/f2/apps/weibo/test/test_weibo_id.py b/f2/apps/weibo/test/test_weibo_id.py new file mode 100644 index 00000000..c73a7ebe --- /dev/null +++ b/f2/apps/weibo/test/test_weibo_id.py @@ -0,0 +1,207 @@ +import pytest +from f2.apps.weibo.utils import WeiboIdFetcher +from f2.exceptions.api_exceptions import ( + APINotFoundError, +) + + +@pytest.mark.asyncio +class TestWeiboIdFetcher: + async def test_get_weibo_id(self): + weibo_link = "https://weibo.com/u/2265830070/O8DM0BLLm" + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "https://weibo.com/u/2265830070/O8DM0BLLm?test=123" + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "https://weibo.com/u/2265830070/O8DM0BLLm/" + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "https://weibo.com/u/2265830070/O8DM0BLLm/?test=123" + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "weibo.com/2265830070/O8DM0BLLm" + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "O8DM0BLLm" + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm/" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "O8DM0BLLm" + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm/?test=123" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "O8DM0BLLm" + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm/%$#" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "O8DM0BLLm" + + weibo_link = "https://www.weibo.com/2265830070/5020595169001740" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://www.weibo.com/2265830070/5020595169001740?test=123" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://www.weibo.com/2265830070/5020595169001740/" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://www.weibo.com/2265830070/5020595169001740/?test=123" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://m.weibo.cn/2265830070/5020595169001740" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://m.weibo.cn/2265830070/5020595169001740?test=123" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://m.weibo.cn/2265830070/5020595169001740/" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://m.weibo.cn/2265830070/5020595169001740/?test=123" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://weibo.cn/2265830070/O8DM0BLLm/" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "O8DM0BLLm" + + weibo_link = "https://weibo.cn/2265830070/O8DM0BLLm/?test=123" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "O8DM0BLLm" + + weibo_link = "https://weibo.cn/2265830070/O8DM0BLLm" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "O8DM0BLLm" + + weibo_link = "https://weibo.cn/2265830070/O8DM0BLLm?test=123" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "O8DM0BLLm" + + weibo_link = "https://weibo.cn/status/5020595169001740" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://weibo.cn/status/5020595169001740?test=123" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://weibo.cn/status/5020595169001740/" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://weibo.cn/status/5020595169001740/?test=123" + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "5020595169001740" + + weibo_link = "https://weibo.com/2265830070" + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "https://weibo.com/2265830070/" + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "https://weibo.com/2265830070/?test=123" + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "https://weibo.com/userid/postid/" + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "" + with pytest.raises(ValueError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = None + with pytest.raises(ValueError): + await WeiboIdFetcher.get_weibo_id(weibo_link) + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm/" + "a" * 2048 + weibo_id = await WeiboIdFetcher.get_weibo_id(weibo_link) + assert weibo_id == "O8DM0BLLm" + + +@pytest.mark.asyncio +class TestWeiboAllIdFetcher: + async def test_get_all_weibo_id(self): + weibo_links = [] + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_all_weibo_id(weibo_links) + + weibo_links = [ + "https://weibo.com/u/2265830070/O8DM0BLLm", + "https://weibo.com/u/2265830070/O8DM0BLLm/", + "https://weibo.com/u/2265830070/O8DM0BLLm/?test=123", + ] + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_all_weibo_id(weibo_links) + + weibo_links = [ + "https://weibo.com/2265830070/O8DM0BLLm", + "https://weibo.com/2265830070/O8DM0BLLm/", + "https://weibo.com/2265830070/O8DM0BLLm/?test=123", + "https://weibo.com/2265830070/O8DM0BLLm/%$#", + "https://weibo.com/2265830070/O8DM0BLLm/" + "a" * 2048, + ] + weibo_ids = await WeiboIdFetcher.get_all_weibo_id(weibo_links) + assert weibo_ids == [ + "O8DM0BLLm", + "O8DM0BLLm", + "O8DM0BLLm", + "O8DM0BLLm", + "O8DM0BLLm", + ] + + weibo_links = [ + "https://www.weibo.com/2265830070/5020595169001740", + "https://www.weibo.com/2265830070/5020595169001740/", + "https://www.weibo.com/2265830070/5020595169001740/?test=123", + "https://www.weibo.com/2265830070/5020595169001740/%$#", + "https://www.weibo.com/2265830070/5020595169001740/" + "a" * 2048, + "https://weibo.cn/status/5020595169001740", + "https://weibo.cn/status/5020595169001740?test=123", + "https://m.weibo.cn/status/5020595169001740/", + "https://m.weibo.cn/status/5020595169001740/?test=123", + "https://m.weibo.cn/status/5020595169001740/%$#", + "https://m.weibo.cn/status/5020595169001740/" + "a" * 2048, + ] + weibo_ids = await WeiboIdFetcher.get_all_weibo_id(weibo_links) + assert weibo_ids == [ + "5020595169001740", + "5020595169001740", + "5020595169001740", + "5020595169001740", + "5020595169001740", + "5020595169001740", + "5020595169001740", + "5020595169001740", + "5020595169001740", + "5020595169001740", + "5020595169001740", + ] + + weibo_links = [ + "weibo.com/2265830070", + "https://weibo.com/2265830070", + "https://weibo.com/2265830070/", + "https://weibo.com/2265830070/?test=123", + "https://weibo.com/userid/postid/", + ] + with pytest.raises(APINotFoundError): + await WeiboIdFetcher.get_all_weibo_id(weibo_links) diff --git a/f2/apps/weibo/test/test_weibo_uid.py b/f2/apps/weibo/test/test_weibo_uid.py new file mode 100644 index 00000000..a6503cf5 --- /dev/null +++ b/f2/apps/weibo/test/test_weibo_uid.py @@ -0,0 +1,167 @@ +import pytest +from f2.apps.weibo.utils import WeiboUidFetcher +from f2.exceptions.api_exceptions import ( + APINotFoundError, +) + + +@pytest.mark.asyncio +class TestWeiboUidFetcher: + async def test_get_weibo_uid(self): + weibo_id = "https://weibo.com/u/2265830070" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_id) + assert weibo_uid == "2265830070" + + weibo_id = "https://weibo.com/u/2265830070?test=123" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_id) + assert weibo_uid == "2265830070" + + weibo_id = "https://weibo.com/u/2265830070/" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_id) + assert weibo_uid == "2265830070" + + weibo_id = "https://weibo.com/u/2265830070/?test=123" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_id) + assert weibo_uid == "2265830070" + + weibo_link = "https://weibo.com/2265830070" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_uid == "2265830070" + + weibo_link = "https://weibo.com/2265830070/" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_uid == "2265830070" + + weibo_link = "https://weibo.com/2265830070/?test=123" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_uid == "2265830070" + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_uid == "2265830070" + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm/" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_uid == "2265830070" + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm/?test=123" + weibo_uid = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_uid == "2265830070" + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm/%$#" + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + weibo_link = "https://m.weibo.cn/2265830070/5020595169001740" + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + weibo_link = "https://m.weibo.cn/2265830070/5020595169001740?test=123" + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + weibo_link = "https://m.weibo.cn/2265830070/5020595169001740/" + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + weibo_link = "https://m.weibo.cn/2265830070/5020595169001740/?test=123" + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + weibo_link = "https://weibo.cn/2265830070/5020595169001740" + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + weibo_link = "https://weibo.cn/2265830070/5020595169001740?test=123" + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + weibo_link = "https://weibo.cn/2265830070/5020595169001740/" + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + weibo_link = "https://weibo.cn/2265830070/5020595169001740/?test=123" + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + weibo_link = "weibo.com/2265830070/O8DM0BLLm" + with pytest.raises(APINotFoundError): + await WeiboUidFetcher.get_weibo_uid(weibo_link) + + weibo_link = "https://weibo.com/O8DM0BLLm" + with pytest.raises(APINotFoundError): + await WeiboUidFetcher.get_weibo_uid(weibo_link) + + weibo_link = "https://weibo.com/O8DM0BLLm/" + with pytest.raises(APINotFoundError): + await WeiboUidFetcher.get_weibo_uid(weibo_link) + + weibo_link = "https://weibo.com/O8DM0BLLm/?test=123" + with pytest.raises(APINotFoundError): + await WeiboUidFetcher.get_weibo_uid(weibo_link) + + weibo_link = "https://weibo.com/userid/postid/" + with pytest.raises(APINotFoundError): + await WeiboUidFetcher.get_weibo_uid(weibo_link) + + weibo_link = "" + with pytest.raises(ValueError): + await WeiboUidFetcher.get_weibo_uid(weibo_link) + + weibo_link = None + with pytest.raises(ValueError): + await WeiboUidFetcher.get_weibo_uid(weibo_link) + + weibo_link = "https://weibo.com/2265830070/O8DM0BLLm/" + "a" * 2048 + weibo_id = await WeiboUidFetcher.get_weibo_uid(weibo_link) + assert weibo_id == "2265830070" + + +@pytest.mark.asyncio +class TestWeiboAllUidFetcher: + async def test_get_all_weibo_uid(self): + weibo_links = [ + "https://weibo.com/u/2265830070", + "https://weibo.com/u/2265830070/", + "https://weibo.com/u/2265830070/?test=123", + "https://weibo.com/2265830070", + "https://weibo.com/2265830070/", + "https://weibo.com/2265830070/?test=123", + "https://weibo.com/2265830070/O8DM0BLLm", + "https://weibo.com/2265830070/O8DM0BLLm/", + "https://weibo.com/2265830070/O8DM0BLLm/?test=123", + "https://weibo.com/2265830070/O8DM0BLLm/%$#", + "https://weibo.com/2265830070/O8DM0BLLm/" + "a" * 2048, + "https://m.weibo.cn/2265830070/5020595169001740", + "https://m.weibo.cn/2265830070/5020595169001740?test=123", + "https://m.weibo.cn/2265830070/5020595169001740/", + "https://m.weibo.cn/2265830070/5020595169001740/?test=123", + ] + weibo_uids = await WeiboUidFetcher.get_all_weibo_uid(weibo_links) + assert weibo_uids == [ + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + "2265830070", + ] + + weibo_links = [ + "weibo.com/O8DM0BLLm", + "https://weibo.com/O8DM0BLLm", + "https://weibo.com/O8DM0BLLm/", + "https://weibo.com/O8DM0BLLm/?test=123", + "https://weibo.com/userid/postid/", + ] + with pytest.raises(APINotFoundError): + await WeiboUidFetcher.get_all_weibo_uid(weibo_links) diff --git a/f2/apps/weibo/utils.py b/f2/apps/weibo/utils.py new file mode 100644 index 00000000..ce5c01a9 --- /dev/null +++ b/f2/apps/weibo/utils.py @@ -0,0 +1,490 @@ +# path: f2/apps/weibo/utils.py + +import f2 +import re +import httpx +import asyncio + +from typing import Union +from pathlib import Path + +from f2.i18n.translator import _ +from f2.log.logger import logger +from f2.utils.conf_manager import ConfigManager +from f2.utils.utils import extract_valid_urls, split_filename, split_set_cookie +from f2.exceptions.api_exceptions import ( + APIError, + APIConnectionError, + APIResponseError, + APIUnavailableError, + APIUnauthorizedError, + APINotFoundError, +) + + +class ClientConfManager: + """ + 用于管理客户端配置 (Used to manage client configuration) + """ + + client_conf = ConfigManager(f2.F2_CONFIG_FILE_PATH).get_config("f2") + weibo_conf = client_conf.get("weibo", {}) + + @classmethod + def client(cls) -> dict: + return cls.weibo_conf + + @classmethod + def version(cls) -> str: + return cls.client_conf.get("version", "unknown") + + @classmethod + def proxies(cls) -> dict: + return cls.weibo_conf.get("proxies", {}) + + @classmethod + def visitor(cls) -> dict: + return cls.weibo_conf.get("visitor", {}) + + @classmethod + def headers(cls) -> dict: + return cls.weibo_conf.get("headers", {}) + + @classmethod + def user_agent(cls) -> str: + return cls.headers().get("User-Agent", "") + + @classmethod + def referer(cls) -> str: + return cls.headers().get("Referer", "") + + +class ModelManager: + + @classmethod + def model_2_endpoint(cls, base_endpoint: str, params: dict = ...) -> str: + if not params: + return base_endpoint + + if not isinstance(params, dict): + raise ValueError("参数必须是字典类型") + + param_str = "&".join([f"{k}={v}" for k, v in params.items()]) + # 检查base_endpoint是否已有查询参数 (Check if base_endpoint already has query parameters) + separator = "&" if "?" in base_endpoint else "?" + + final_endpoint = f"{base_endpoint}{separator}{param_str}" + + return final_endpoint + + +class VisitorManager: + """ + 用于管理访客信息 (Used to manage visitor information) + """ + + visitor_conf = ClientConfManager.visitor() + proxies = ClientConfManager.proxies() + + @classmethod + async def gen_visitor(cls) -> str: + """ + 生成访客信息 (Generate visitor information) + + Args: + kwargs (dict): 配置参数 (Conf parameters) + + Returns: + str: 访客cookie (Visitor cookie) + """ + + payload = { + "cb": cls.visitor_conf["cb"], + "tid": cls.visitor_conf["tid"], + "from": cls.visitor_conf["from"], + } + headers = { + "User-Agent": ClientConfManager.user_agent(), + "Content-Type": "application/x-www-form-urlencoded", + } + transport = httpx.AsyncHTTPTransport(retries=5) + async with httpx.AsyncClient( + transport=transport, proxies=cls.proxies, timeout=10 + ) as aclient: + try: + response = await aclient.post( + url=cls.visitor_conf["url"], + data=payload, + headers=headers, + ) + response.raise_for_status() + + visitor_cookie = split_set_cookie( + response.headers.get("set-cookie", "") + ) + + return visitor_cookie + + except httpx.RequestError as exc: + # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) + raise APIConnectionError( + _( + "请求端点失败,请检查当前网络环境。 链接:{0},代理:{1},异常类名:{2},异常详细信息:{3}" + ).format(cls.visitor_conf["url"], cls.proxies, cls.__name__, exc) + ) + + except httpx.HTTPStatusError as e: + # 捕获 httpx 的状态代码错误 (captures specific status code errors from httpx) + if e.response.status_code == 401: + raise APIUnauthorizedError( + _( + "参数验证失败,请更新 F2 配置文件中的 {0},以匹配 {1} 新规则" + ).format("visitor", "weibo") + ) + + elif e.response.status_code == 404: + raise APINotFoundError(_("{0} 无法找到API端点").format("visitor")) + else: + raise APIResponseError( + _("链接:{0},状态码 {1}:{2} ").format( + e.response.url, e.response.status_code, e.response.text + ) + ) + + +class WeiboIdFetcher: + # 预编译正则表达式 + # (Pre-compile regular expression) + + # weibo.com/2265830070/O8DM0BLLm + # https://weibo.com/2265830070/O8DM0BLLm + # https://weibo.com/2265830070/O8DM0BLLm/ + # https://weibo.com/2265830070/O8DM0BLLm/?test=123 + + # https://www.weibo.com/2265830070/5020595169001740 + # https://www.weibo.com/2265830070/5020595169001740?test=123 + # https://www.weibo.com/2265830070/5020595169001740/ + # https://www.weibo.com/2265830070/5020595169001740/?test=123 + # www.weibo.com/2265830070/5020595169001740 + _WEIBO_ID_PATTERN = re.compile( + r"(?:https?://)?(?:www\.)?(?:weibo\.com|weibo\.cn|m\.weibo\.cn)/(?:\d{10}|status)/(\w{9}|\w{16})(?:/|\?|#.*$|$)" + ) + + @classmethod + async def get_weibo_id(cls, url: str) -> str: + """ + 从微博链接中提取微博ID + (Extract weibo ID from weibo link) + + Args: + url (str): 微博链接 (weibo link) + + Returns: + str: 微博ID (weibo ID) + """ + + if not url: + raise ValueError(_("微博链接不能为空")) + + if not isinstance(url, str): + raise TypeError(_("参数必须是字符串类型")) + + # 提取有效URL + url = extract_valid_urls(url) + + if url is None: + raise APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) + + match = cls._WEIBO_ID_PATTERN.search(url) + + if match: + return match.group(1) + else: + raise APINotFoundError( + _( + "未在响应的地址中找到weibo_id,检查链接是否为微博链接。类名:{0}" + ).format(cls.__name__) + ) + + @classmethod + async def get_all_weibo_id(cls, urls: list) -> list: + """ + 从微博链接列表中提取微博ID + (Extract weibo ID from weibo link) + + Args: + urls (list): 微博链接 (Weibo link list) + + Returns: + list: 微博ID列表 (Weibo ID list) + """ + + if not isinstance(urls, list): + raise TypeError(_("参数必须是列表类型")) + + # 提取有效URL + urls = extract_valid_urls(urls) + + # 从链接中提取微博ID + if urls == []: + raise ( + APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) + ) + ) + + weibo_ids = [cls.get_weibo_id(url) for url in urls] + return await asyncio.gather(*weibo_ids) + + +class WeiboUidFetcher: + # 预编译正则表达式 + # (Pre-compile regular expression) + + # https://weibo.com/u/2265830070 + # https://weibo.com/u/2265830070/ + # https://weibo.com/u/2265830070?test=123 + # https://weibo.com/2265830070 + # https://weibo.com/2265830070?test=123 + # https://weibo.com/2265830070/ + # https://weibo.com/2265830070/?test=123 + _WEIBO_COM_UID_PATTERN = re.compile( + r"(?:https?://)?(?:www\.)?(?:weibo\.com|weibo\.cn|m\.weibo\.cn)/(?:u/)?(\d{10})(?:/|\?|$)" + ) + + @classmethod + async def get_weibo_uid(cls, url: str) -> str: + """ + 从微博主页链接中提取微博UID + (Extract weibo UID from weibo link) + + Args: + url (str): 微博链接 (Weibo link) + + Returns: + str: 微博UID (Weibo UID) + """ + + if not url: + raise ValueError(_("微博主页链接不能为空")) + + if not isinstance(url, str): + raise TypeError(_("参数必须是字符串类型")) + + # 提取有效URL + url = extract_valid_urls(url) + + if url is None: + raise ( + APINotFoundError(_("输入的URL不合法。类名:{0}").format(cls.__name__)) + ) + + match = cls._WEIBO_COM_UID_PATTERN.search(url) + if match: + return match.group(1) + else: + raise APINotFoundError( + _( + "未在响应的地址中找到weibo_uid,检查链接是否为微博主页链接。类名:{0}" + ).format(cls.__name__) + ) + + @classmethod + async def get_all_weibo_uid(cls, urls: list) -> list: + """ + 从微博主页链接列表中提取微博UID + (Extract weibo UID from weibo link) + + Args: + urls (list): 微博链接 (Weibo link list) + + Returns: + list: 微博UID列表 (Weibo UID list) + """ + + if not urls: + raise ValueError(_("微博链接列表不能为空")) + + if not isinstance(urls, list): + raise TypeError(_("参数必须是列表类型")) + + # 提取有效URL + urls = extract_valid_urls(urls) + + # 从链接中提取微博ID + if urls == []: + raise ( + APINotFoundError( + _("输入的URL List不合法。类名:{0}").format(cls.__name__) + ) + ) + + weibo_uids = [cls.get_weibo_uid(url) for url in urls] + return await asyncio.gather(*weibo_uids) + + +def format_file_name( + naming_template: str, + weibo_data: dict = {}, + custom_fields: dict = {}, +) -> str: + """ + 根据配置文件的全局格式化文件名 + (Format file name according to the global conf file) + + Args: + weibo_data (dict): 微博数据的字典 (dict of douyin data) + naming_template (str): 文件的命名模板, 如 "{create}_{desc}" (Naming template for files, such as "{create}_{desc}") + custom_fields (dict): 用户自定义字段, 用于替代默认的字段值 (Custom fields for replacing default field values) + + Note: + windows 文件名长度限制为 255 个字符, 开启了长文件名支持后为 32,767 个字符 + (Windows file name length limit is 255 characters, 32,767 characters after long file name support is enabled) + Unix 文件名长度限制为 255 个字符 + (Unix file name length limit is 255 characters) + 取去除后的50个字符, 加上后缀, 一般不会超过255个字符 + (Take the removed 50 characters, add the suffix, and generally not exceed 255 characters) + 详细信息请参考: https://en.wikipedia.org/wiki/Filename#Length + (For more information, please refer to: https://en.wikipedia.org/wiki/Filename#Length) + + Returns: + str: 格式化的文件名 (Formatted file name) + """ + + # 为不同系统设置不同的文件名长度限制 + os_limit = { + "win32": 200, + "cygwin": 60, + "darwin": 60, + "linux": 60, + } + + fields = { + "create": weibo_data.get("create_time", ""), # 长度固定19 + "nickname": weibo_data.get("nickname", ""), # 最长30 + "weibo_id": weibo_data.get("weibo_id", ""), # 长度固定19 + "desc": split_filename(weibo_data.get("descRaw", ""), os_limit), + "uid": weibo_data.get("uid", ""), # 固定10 + } + + if custom_fields: + # 更新自定义字段 + fields.update(custom_fields) + + try: + return naming_template.format(**fields) + except KeyError as e: + raise KeyError(_("文件名模板字段 {0} 不存在,请检查".format(e))) + + +def create_or_rename_user_folder( + kwargs: dict, local_user_data: dict, current_nickname: str +) -> Path: + """ + 创建或重命名用户目录 (Create or rename user directory) + + Args: + kwargs (dict): 配置参数 (Conf parameters) + local_user_data (dict): 本地用户数据 (Local user data) + current_nickname (str): 当前用户昵称 (Current user nickname) + + Returns: + user_path (Path): 用户目录路径 (User directory path) + """ + user_path = create_user_folder(kwargs, current_nickname) + + if not local_user_data: + return user_path + + if local_user_data.get("nickname") != current_nickname: + # 昵称不一致,触发目录更新操作 + user_path = rename_user_folder(user_path, current_nickname) + + return user_path + + +def create_user_folder(kwargs: dict, nickname: Union[str, int]) -> Path: + """ + 根据提供的配置文件和昵称,创建对应的保存目录。 + (Create the corresponding save directory according to the provided conf file and nickname.) + + Args: + kwargs (dict): 配置文件,字典格式。(Conf file, dict format) + nickname (Union[str, int]): 用户的昵称,允许字符串或整数。 (User nickname, allow strings or integers) + + Note: + 如果未在配置文件中指定路径,则默认为 "Download"。 + (If the path is not specified in the conf file, it defaults to "Download".) + 支持绝对与相对路径。 + (Support absolute and relative paths) + + Raises: + TypeError: 如果 kwargs 不是字典格式,将引发 TypeError。 + (If kwargs is not in dict format, TypeError will be raised.) + """ + + # 确定函数参数是否正确 + if not isinstance(kwargs, dict): + raise TypeError("kwargs 参数必须是字典") + + # 创建基础路径 + base_path = Path(kwargs.get("path", "Download")) + + # 添加下载模式和用户名 + user_path = ( + base_path / "weibo" / kwargs.get("mode", "PLEASE_SETUP_MODE") / str(nickname) + ) + + # 获取绝对路径并确保它存在 + resolve_user_path = user_path.resolve() + + # 创建目录 + resolve_user_path.mkdir(parents=True, exist_ok=True) + + return resolve_user_path + + +def rename_user_folder(old_path: Path, new_nickname: str) -> Path: + """ + 重命名用户目录 (Rename User Folder). + + Args: + old_path (Path): 旧的用户目录路径 (Path of the old user folder) + new_nickname (str): 新的用户昵称 (New user nickname) + + Returns: + Path: 重命名后的用户目录路径 (Path of the renamed user folder) + """ + # 获取目标目录的父目录 (Get the parent directory of the target folder) + parent_directory = old_path.parent + + # 构建新目录路径 (Construct the new directory path) + new_path = old_path.rename(parent_directory / new_nickname).resolve() + + return new_path + + +def create_or_rename_user_folder( + kwargs: dict, local_user_data: dict, current_nickname: str +) -> Path: + """ + 创建或重命名用户目录 (Create or rename user directory) + + Args: + kwargs (dict): 配置参数 (Conf parameters) + local_user_data (dict): 本地用户数据 (Local user data) + current_nickname (str): 当前用户昵称 (Current user nickname) + + Returns: + user_path (Path): 用户目录路径 (User directory path) + """ + user_path = create_user_folder(kwargs, current_nickname) + + if not local_user_data: + return user_path + + if local_user_data.get("nickname") != current_nickname: + # 昵称不一致,触发目录更新操作 + user_path = rename_user_folder(user_path, current_nickname) + + return user_path diff --git a/f2/cli/cli_commands.py b/f2/cli/cli_commands.py index a1da310d..c6a6933f 100644 --- a/f2/cli/cli_commands.py +++ b/f2/cli/cli_commands.py @@ -5,12 +5,14 @@ import typing import asyncio import importlib +import traceback from f2 import helps from f2.apps import __apps__ as apps_module from f2.exceptions import APIError from f2.cli.cli_console import RichConsoleManager from f2.utils._signal import SignalManager +from f2.utils.utils import get_latest_version from f2.i18n.translator import _ from f2.log.logger import logger @@ -48,12 +50,47 @@ def handle_debug( ) -> None: if not value or ctx.resilient_parsing: return + from rich.traceback import install install() logger.setLevel(value) - logger.debug("开启调试模式 (Debug mode on)") + logger.debug(_("调试模式:{0}").format(value)) + + +# 版本检测 +def handle_version( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + if not value or ctx.resilient_parsing: + return + + asyncio.run(check_version()) + + ctx.exit() + + +async def check_version(): + """用于检查F2的版本是否最新""" + + latest_version = await get_latest_version("f2") + + if latest_version: + if f2.__version__ == latest_version: + logger.warning( + _( + "您当前使用的版本 {0} 可能已过时,请考虑及时升级到最新版本 {1},请使用 pip install -U f2 更新" + ).format(f2.__version__, latest_version) + ) + elif f2.__version__ == latest_version: + logger.info(_("您当前使用的是最新版本:{0}").format(f2.__version__)) + else: + logger.error(_("无法获取最新版本信息")) + + return # 应用映射 @@ -67,24 +104,26 @@ def handle_debug( class DynamicGroup(click.Group): - def get_command(self, ctx, cmd_name): + def get_command(self, ctx: click.Context, cmd_name: str): app_name = ( cmd_name if cmd_name in APP_MAPPINGS else REVERSE_APP_MAPPINGS.get(cmd_name, None) ) if not app_name: - return None + ctx.fail(_("没有找到 {0} 应用").format(cmd_name)) try: if app_name: + # 检查版本 + asyncio.run(check_version()) # 动态导入app的cli模块 module = importlib.import_module(f"f2.apps.{app_name}.cli") - logger.info("App: %s" % app_name) + logger.info(_("应用:{0}").format(app_name)) command = getattr(module, app_name) return command - except (ImportError, AttributeError) as e: - logger.error("Error: %s" % e) - return None + except (ImportError, AttributeError): + logger.error(traceback.format_exc()) + return @click.command(cls=DynamicGroup) @@ -113,6 +152,14 @@ def get_command(self, ctx, cmd_name): expose_value=False, callback=handle_debug, ) +@click.option( + "--check-version", + is_flag=True, + expose_value=False, + is_eager=True, + callback=handle_version, + help=_("检查F2版本"), +) def main(**kwargs): pass @@ -138,7 +185,6 @@ def set_cli_config(ctx, **kwargs): async def run_app(kwargs): - logger.info(f"Version {f2.__version__}") app_name = kwargs["app_name"] app_module = importlib.import_module(f"f2.apps.{app_name}.handler") await app_module.main(kwargs) diff --git a/f2/conf/app.yaml b/f2/conf/app.yaml index fae14ec8..a1ebc517 100644 --- a/f2/conf/app.yaml +++ b/f2/conf/app.yaml @@ -22,3 +22,33 @@ tiktok: page_counts: 5 path: Download timeout: 10 + +twitter: + + path: Download + folderize: yes + mode: one + naming: '{create}_{desc}' + cookie: + interval: all + timeout: 10 + max_retries: 5 + max_connections: 5 + max_counts: 0 + max_tasks: 5 + page_counts: 20 + +weibo: + + path: Download + folderize: yes + mode: post + naming: '{create}_{desc}' + cookie: + interval: all + timeout: 10 + max_retries: 5 + max_connections: 5 + max_counts: 0 + max_tasks: 5 + page_counts: 20 diff --git a/f2/conf/conf.yaml b/f2/conf/conf.yaml index 550e357c..7e235174 100644 --- a/f2/conf/conf.yaml +++ b/f2/conf/conf.yaml @@ -1,13 +1,40 @@ f2: + version: "0.0.1.6" douyin: + encryption: ab + + BaseRequestModel: + version: + code: "190500" + name: "19.5.0" + browser: + language: zh-CN + platform: Win32 + name: Edge + version: "122.0.0.0" + engine: + name: Blink + version: "122.0.0.0" + os: + name: Windows + version: "10" + + BaseLiveModel: + language: zh-CN + browser: + language: zh-CN + platform: Win32 + name: Edge + version: "119.0.0.0" + headers: - User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0 Referer: https://www.douyin.com/ proxies: - http: - https: + http://: + https://: msToken: url: https://mssdk.bytedance.com/web/report @@ -15,34 +42,85 @@ f2: version: 1 dataType: 8 strData: fWOdJTQR3/jwmZqBBsPO6tdNEc1jX7YTwPg0Z8CT+j3HScLFbj2Zm1XQ7/lqgSutntVKLJWaY3Hc/+vc0h+So9N1t6EqiImu5jKyUa+S4NPy6cNP0x9CUQQgb4+RRihCgsn4QyV8jivEFOsj3N5zFQbzXRyOV+9aG5B5EAnwpn8C70llsWq0zJz1VjN6y2KZiBZRyonAHE8feSGpwMDeUTllvq6BG3AQZz7RrORLWNCLEoGzM6bMovYVPRAJipuUML4Hq/568bNb5vqAo0eOFpvTZjQFgbB7f/CtAYYmnOYlvfrHKBKvb0TX6AjYrw2qmNNEer2ADJosmT5kZeBsogDui8rNiI/OOdX9PVotmcSmHOLRfw1cYXTgwHXr6cJeJveuipgwtUj2FNT4YCdZfUGGyRDz5bR5bdBuYiSRteSX12EktobsKPksdhUPGGv99SI1QRVmR0ETdWqnKWOj/7ujFZsNnfCLxNfqxQYEZEp9/U01CHhWLVrdzlrJ1v+KJH9EA4P1Wo5/2fuBFVdIz2upFqEQ11DJu8LSyD43qpTok+hFG3Moqrr81uPYiyPHnUvTFgwA/TIE11mTc/pNvYIb8IdbE4UAlsR90eYvPkI+rK9KpYN/l0s9ti9sqTth12VAw8tzCQvhKtxevJRQntU3STeZ3coz9Dg8qkvaSNFWuBDuyefZBGVSgILFdMy33//l/eTXhQpFrVc9OyxDNsG6cvdFwu7trkAENHU5eQEWkFSXBx9Ml54+fa3LvJBoacfPViyvzkJworlHcYYTG392L4q6wuMSSpYUconb+0c5mwqnnLP6MvRdm/bBTaY2Q6RfJcCxyLW0xsJMO6fgLUEjAg/dcqGxl6gDjUVRWbCcG1NAwPCfmYARTuXQYbFc8LO+r6WQTWikO9Q7Cgda78pwH07F8bgJ8zFBbWmyrghilNXENNQkyIzBqOQ1V3w0WXF9+Z3vG3aBKCjIENqAQM9qnC14WMrQkfCHosGbQyEH0n/5R2AaVTE/ye2oPQBWG1m0Gfcgs/96f6yYrsxbDcSnMvsA+okyd6GfWsdZYTIK1E97PYHlncFeOjxySjPpfy6wJc4UlArJEBZYmgveo1SZAhmXl3pJY3yJa9CmYImWkhbpwsVkSmG3g11JitJXTGLIfqKXSAhh+7jg4HTKe+5KNir8xmbBI/DF8O/+diFAlD+BQd3cV0G4mEtCiPEhOvVLKV1pE+fv7nKJh0t38wNVdbs3qHtiQNN7JhY4uWZAosMuBXSjpEtoNUndI+o0cjR8XJ8tSFnrAY8XihiRzLMfeisiZxWCvVwIP3kum9MSHXma75cdCQGFBfFRj0jPn1JildrTh2vRgwG+KeDZ33BJ2VGw9PgRkztZ2l/W5d32jc7H91FftFFhwXil6sA23mr6nNp6CcrO7rOblcm5SzXJ5MA601+WVicC/g3p6A0lAnhjsm37qP+xGT+cbCFOfjexDYEhnqz0QZm94CCSnilQ9B/HBLhWOddp9GK0SABIk5i3xAH701Xb4HCcgAulvfO5EK0RL2eN4fb+CccgZQeO1Zzo4qsMHc13UG0saMgBEH8SqYlHz2S0CVHuDY5j1MSV0nsShjM01vIynw6K0T8kmEyNjt1eRGlleJ5lvE8vonJv7rAeaVRZ06rlYaxrMT6cK3RSHd2liE50Z3ik3xezwWoaY6zBXvCzljyEmqjNFgAPU3gI+N1vi0MsFmwAwFzYqqWdk3jwRoWLp//FnawQX0g5T64CnfAe/o2e/8o5/bvz83OsAAwZoR48GZzPu7KCIN9q4GBjyrePNx5Csq2srblifmzSKwF5MP/RLYsk6mEE15jpCMKOVlHcu0zhJybNP3AKMVllF6pvn+HWvUnLXNkt0A6zsfvjAva/tbLQiiiYi6vtheasIyDz3HpODlI+BCkV6V8lkTt7m8QJ1IcgTfqjQBummyjYTSwsQji3DdNCnlKYd13ZQa545utqu837FFAzOZQhbnC3bKqeJqO2sE3m7WBUMbRWLflPRqp/PsklN+9jBPADKxKPl8g6/NZVq8fB1w68D5EJlGExdDhglo4B0aihHhb1u3+zJ2DqkxkPCGBAZ2AcuFIDzD53yS4NssoWb4HJ7YyzPaJro+tgG9TshWRBtUw8Or3m0OtQtX+rboYn3+GxvD1O8vWInrg5qxnepelRcQzmnor4rHF6ZNhAJZAf18Rjncra00HPJBugY5rD+EwnN9+mGQo43b01qBBRYEnxy9JJYuvXxNXxe47/MEPOw6qsxN+dmyIWZSuzkw8K+iBM/anE11yfU4qTFt0veCaVprK6tXaFK0ZhGXDOYJd70sjIP4UrPhatp8hqIXSJ2cwi70B+TvlDk/o19CA3bH6YxrAAVeag1P9hmNlfJ7NxK3Jp7+Ny1Vd7JHWVF+R6rSJiXXPfsXi3ZEy0klJAjI51NrDAnzNtgIQf0V8OWeEVv7F8Rsm3/GKnjdNOcDKymi9agZUgtctENWbCXGFnI40NHuVHtBRZeYAYtwfV7v6U0bP9s7uZGpkp+OETHMv3AyV0MVbZwQvarnjmct4Z3Vma+DvT+Z4VlMVnkC2x2FLt26K3SIMz+KV2XLv5ocEdPFSn1vMR7zruCWC8XqAG288biHo/soldmb/nlw8o8qlfZj4h296K3hfdFubGIUtqgsrZCrLCkkRC08Cv1ozEX/y6t2YrQepwiNmwDVk5IufStVvJMj+y2r9TcYLv7UKWXx3P6aySvM2ZHPaZhv+6Z/A/jIMBSvOizn4qG11iK7Oo6JYhxCSMJZsetjsnL4ecSIAufEmoFlAScWBh6nFArRpVLvkAZ3tej7H2lWFRXIU7x7mdBfGqU82PpM6znKMMZCpEsvHqpkSPSL+Kwz2z1f5wW7BKcKK4kNZ8iveg9VzY1NNjs91qU8DJpUnGyM04C7KNMpeilEmoOxvyelMQdi85ndOVmigVKmy5JYlODNX744sHpeqmMEK/ux3xY5O406lm7dZlyGPSMrFWbm4rzqvSEIskP43+9xVP8L84GeHE4RpOHg3qh/shx+/WnT1UhKuKpByHCpLoEo144udpzZswCYSMp58uPrlwdVF31//AacTRk8dUP3tBlnSQPa1eTpXWFCn7vIiqOTXaRL//YQK+e7ssrgSUnwhuGKJ8aqNDgdsL+haVZnV9g5Qrju643adyNixvYFEp0uxzOzVkekOMh2FYnFVIL2mJYGpZEXlAIC0zQbb54rSP89j0G7soJ2HcOkD0NmMEWj/7hUdTuMin1lRNde/qmHjwhbhqL8Z9MEO/YG3iLMgFTgSNQQhyE8AZAAKnehmzjORJfbK+qxyiJ07J843EDduzOoYt9p/YLqyTFmAgpdfK0uYrtAJ47cbl5WWhVXp5/XUxwWdL7TvQB0Xh6ir1/XBRcsVSDrR7cPE221ThmW1EPzD+SPf2L2gS0WromZqj1PhLgk92YnnR9s7/nLBXZHPKy+fDbJT16QqabFKqAl9G0blyf+R5UGX2kN+iQp4VGXEoH5lXxNNTlgRskzrW7KliQXcac20oimAHUE8Phf+rXXglpmSv4XN3eiwfXwvOaAMVjMRmRxsKitl5iZnwpcdbsC4jt16g2r/ihlKzLIYju+XZej4dNMlkftEidyNg24IVimJthXY1H15RZ8Hm7mAM/JZrsxiAVI0A49pWEiUk3cyZcBzq/vVEjHUy4r6IZnKkRvLjqsvqWE95nAGMor+F0GLHWfBCVkuI51EIOknwSB1eTvLgwgRepV4pdy9cdp6iR8TZndPVCikflXYVMlMEJ2bJ2c0Swiq57ORJW6vQwnkxtPudpFRc7tNNDzz4LKEznJxAwGi6pBR7/co2IUgRw1ijLFTHWHQJOjgc7KaduHI0C6a+BJb4Y8IWuIk2u2qCMF1HNKFAUn/J1gTcqtIJcvK5uykpfJFCYc899TmUc8LMKI9nu57m0S44Y2hPPYeW4XSakScsg8bJHMkcXk3Tbs9b4eqiD+kHUhTS2BGfsHadR3d5j8lNhBPzA5e+mE== - User-Agent: 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.47 ttwid: url: https://ttwid.bytedance.com/ttwid/union/register/ data: '{"region":"cn","aid":1768,"needFid":false,"service":"www.ixigua.com","migrate_info":{"ticket":"","source":"node"},"cbUrlProtocol":"https","union":true}' + webid: + url: https://mcs.zijieapi.com/webid?aid=6383&sdk_version=5.1.18_zip&device_platform=web + body: + app_id: 6383 + referer: https://www.douyin.com/ + url: https://www.douyin.com/ + user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0 + user_unique_id: "" tiktok: + + BaseRequestModel: + browser: + language: zh-CN + name: Mozilla + platform: Win32 + version: 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 + + device: + id: "7377772863376426514" + platform: web_pc + + os: windows + region: SG + priority_region: "" + webcast_language: zh-Hans + tz_name: Asia/Hong_Kong + headers: - User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Referer: https://www.tiktok.com/ proxies: - http: - https: + http://: + https://: msToken: - url: https://mssdk-sg.tiktok.com/web/common?msToken=1Ab-7YxR9lUHSem0PraI_XzdKmpHb6j50L8AaXLAd2aWTdoJCYLfX_67rVQFE4UwwHVHmyG_NfIipqrlLT3kCXps-5PYlNAqtdwEg7TrDyTAfCKyBrOLmhMUjB55oW8SPZ4_EkNxNFUdV7MquA== + url: https://mssdk-sg.tiktok.com/web/common?msToken=QnC7zMMh1cpaDTxHDHnabNOrqaWv49JwA1IAq3AIFvrdaqQi8Rs_YlXSya1vN-4b6C1MgpWpS2cL1oakaUEDe3pUDMLpCbdSc3b3V98Fux0AuwXn_9Ns3FyMTnFRmSOOOVeGg6bVXMSGoMG6dq3k + url2: https://mssdk-sg.tiktok.com/web/report?msToken=QnC7zMMh1cpaDTxHDHnabNOrqaWv49JwA1IAq3AIFvrdaqQi8Rs_YlXSya1vN-4b6C1MgpWpS2cL1oakaUEDe3pUDMLpCbdSc3b3V98Fux0AuwXn_9Ns3FyMTnFRmSOOOVeGg6bVXMSGoMG6dq3k magic: 538969122 version: 1 dataType: 8 - strData: 3BvqYbNXLLOcZehvxZVbjpAu7vq82RoWmFSJHLFwzDwJIZevE0AeilQfP55LridxmdGGjknoksqIsLqlMHMif0IFK/Br7JWqxOHnYuMwVCnttFc0Y4MFvdVWM5FECiEulJC0Dc+eeVsNSrFnAc9K7fazqdglyJgGLSfXIJmgyCvvQ4pg0u5HBVVugLSWs242X42fjoWymaUCLZJQo6vi6WLyuV7l5IC3Mg+lelr5xBQD6Q7hBIFEw8zzxJ1n2DyA4xLbOHTQdKvEtsK7XzyWwjpRnojPTbBl69Zosnuru+lOBIl+tFu/+hCQ1m0jYZwTP4rVE75L3Du6+KZ5v/9TyFYjq7y3y9bGLP4d7yQueJbF90G1yrZ6htElrZ2vqZKDrIqBVbmOZr/nph12k2JKrITtN0R/pMsp0sJ4gesQnXxcD/pLOFAINHk7umgbe6LzJ7+TLUdGuO4M7xiEg/jCqhjgJX1izZ4NPoBDp35zRxj6Y6OrcstlTN/cv5sz663+Nco/mEwhGq2VwrL4gAIAPycndIsb48dPdtngmLqNDNN0ZyVRjgqVIDXXrxigXCkR9CH89Dlrrb7QQqWVgRXz9/k5ihEM43BR3sd3mMU/XgFLN1Aoxf6GzzdxP2QPBI75/ZoHoAmu54v8gTmA3ntCGlEF0zgaFGTdpkGdb+oZgyQM4pw1aAyxmFINXkpD3IKKoGev9kD9gTFnhiQMGCMemhZS7ZYdbuGu0Cb+lQKaL/QTt80FMyGmW8kzVy9xW/ja9BcdEJYRoaufuFRkBFG5ay8x4WHLR6hEapXqQial/cREbLL4sQytpjtmnndFqvT7xN5DhgsLY2Z7451MJhD6NJXKNrMafGZSbItzQWY= - User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 + strData: 3g+ZrAO5JQgfqCRzb689RAbiueodAexjAjn2plyPVBdOArbRM66UOTnZPoVTDDw8GZ8TL/CO13bWedlvWSA8kCODHA6mjO1Af5hgak+1NHbb8EvfuahWFDL8x8hruWn4t6qb79DhOy64REx1nsO6ub4SX4cKdgs+ZXTjUItB2WKddXo7CKikuywT8SJ0TcwZ+z/9hSsawqJXawopS5H8bOF0o3qPn+CwkoWkXBUu4iwkI5UzL9/k18Gwq1BxAiCbyl2kCHJaox7md1H+KfQ0lU9fBhfVBuXa3jNkT5GfyASaBNeJXdhHwuPfEGI6a+4FOaO0MEGzOWDzhu6joo89ooEPo1XV+UJE2KLC5tStxSBibUvn+wZSfGmcpPfYl0jB0H2vLqyaveYraOx8WRE/G/Y9DkbBLJvz7E8sDZ/v1fCUVs1JDDewE/yn2aqOHr3NxUjOnylKl5WWSCPeSR4ZTXRR1ZJhvpGKP7XYc3tkOjBSQ70cXnl53dAQ6aouUVMcztSlkXh4goI46tgJDjTiJJILHBr03dM/1KwVzqoEEO8f7JBhfQo4OR6Y48PXomshJmZhDiEmtkqLndpZY2I/B8q0EShuPlDCFRW7iob6kFahyqDfpOmK15kzCX2n7eu7zqDGgpJuHEtULF9803lR5QBt104ET49RbnDJ+ipbz1bFvS2FJQ2rfktNlwjqdBT8UzfFPHyZk6z4cSxYIfPRj1lB8fDwTwv3PUSRxUZSwkuGoZbc6j/6hxwZaY0xxIEbb9DlOmQZY4qyiqSziZ8HGWsQ9uD95dO4lX6GlZaeh7mKsjLSKSdUt13YeGAciqjTvSPyO/pZ5xc3+i+5hY4A290/JiTmIyFBOrRrlmIhPEQe9OEJn0DPXYgliYUO1nljEi6q/4HMFdXFmlw6nC8/kL9EeinisX7g6PbdHuRcKONXZpXxBevL8xD5Z8SvFKH/uQmYHrJlybiKHjDIMq7vc59NBw2VDQBmRqa8NTh2XLwIryjoBIHuNSBngSZwJE== ttwid: url: https://www.tiktok.com/ttwid/check/ data: '{"aid":1988,"service":"www.tiktok.com","union":false,"unionHost":"","needFid":false,"fid":"","migrate_priority":0}' - cookie: ttwid=1%7CovVQu2St-HXSHAdEfZ7tljPe151SZ88AbrlTirlaC6w%7C1701072604%7C49b17849da69bafc3638e794f3f26b30fe9677c5253e65a2a5f615489846ce02 + cookie: ttwid=1%7C3uOVjidbOFBmdS6Aci5oeBb8Ta-HWpeyp2dvhl2Ib2E%7C1716637053%7C462867ee452aecd60a854225b854ff4498e9b99c13d4826a3e2a4e58a4c55134; odin_tt: - url: https://www.tiktok.com/passport/web/account/info/?aid=1459&app_language=zh-Hans&app_name=tiktok_web&browser_language=zh-CN&browser_name=Mozilla&browser_online=true&browser_platform=Win32&browser_version=5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F119.0.0.0%20Safari%2F537.36&channel=tiktok_web&cookie_enabled=true&device_id=7306060721837852167&root_referer=https%3A%2F%2Fwww.tiktok.com%2Flogin%2F + url: https://www.tiktok.com/passport/web/account/info/?WebIdLastTime=1716637053&aid=1459&app_language=zh-Hans&app_name=tiktok_web&browser_language=zh-CN&browser_name=Mozilla&browser_online=true&browser_platform=Win32&browser_version=5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F124.0.0.0%20Safari%2F537.36&channel=tiktok_web&cookie_enabled=true&device_id=7372899909097571857&device_platform=web_pc&focus_state=true&from_page=fyp&history_len=2&is_fullscreen=false&is_page_visible=true&odinId=7372898697492972561&os=windows&priority_region=&referer=®ion=SG&screen_height=1080&screen_width=1920&tz_name=Asia%2FHong_Kong&webcast_language=zh-Hans + + twitter: + headers: + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0 + Referer: https://twitter.com/ + Authorization: Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA + X-Csrf-Token: "" + + proxies: + http://: + https://: + + weibo: + headers: + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0 + Referer: https://weibo.com/ + + proxies: + http://: + https://: + + visitor: + url: https://passport.weibo.com/visitor/genvisitor2 + cb: visitor_gray_callback + tid: + from: weibo diff --git a/f2/conf/defaults.yaml b/f2/conf/defaults.yaml index 2f7c0c1d..7d9f3d14 100644 --- a/f2/conf/defaults.yaml +++ b/f2/conf/defaults.yaml @@ -28,6 +28,7 @@ tiktok: mode: naming: cookie: + keyword: interval: timeout: max_connections: @@ -36,3 +37,36 @@ tiktok: max_tasks: page_counts: languages: + +weibo: + url: + path: + folderize: + mode: + naming: + cookie: + keyword: + interval: + timeout: + max_retries: + max_connections: + max_counts: + max_tasks: + page_counts: + languages: + +x: + url: + path: + folderize: + mode: + naming: + cookie: + interval: + timeout: + max_retries: + max_connections: + max_counts: + max_tasks: + page_counts: + languages: \ No newline at end of file diff --git a/f2/conf/test.yaml b/f2/conf/test.yaml index 699dcf3d..328aa3d1 100644 --- a/f2/conf/test.yaml +++ b/f2/conf/test.yaml @@ -1,17 +1,35 @@ douyin: headers: - User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0 Referer: https://www.douyin.com/ - cookie: __ac_nonce=066126d7400831d6b5c4e; __ac_signature=_02B4Z6wo00f01Y.vKPwAAIDC-cD1sEsc6ZmPzyxAAAXq04; ttwid=1%7Crk-8HHHCVERrpnGqN5PSPeaEwNAo02w-MzX5peKwBnI%7C1712483701%7C16399ab279d221e1f50ef17c96b5215641887b7a739cec048347e0c45221a491; douyin.com; device_web_cpu_core=12; device_web_memory_size=8; architecture=amd64; IsDouyinActive=true; home_can_add_dy_2_desktop=%220%22; dy_swidth=1920; dy_sheight=1080; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1920%2C%5C%22screen_height%5C%22%3A1080%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A12%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; csrf_session_id=3d3dbde628fcd833106552c68320f88b; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; strategyABtestKey=%221712483704.155%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.7%7D; stream_player_status_params=%22%7B%5C%22is_auto_play%5C%22%3A0%2C%5C%22is_full_screen%5C%22%3A0%2C%5C%22is_full_webscreen%5C%22%3A0%2C%5C%22is_mute%5C%22%3A0%2C%5C%22is_speed%5C%22%3A1%2C%5C%22is_visible%5C%22%3A1%7D%22; passport_csrf_token=b2ef0d72ded3b759412fe41b62111cc9; passport_csrf_token_default=b2ef0d72ded3b759412fe41b62111cc9; bd_ticket_guard_client_data=eyJiZC10aWNrZXQtZ3VhcmQtdmVyc2lvbiI6MiwiYmQtdGlja2V0LWd1YXJkLWl0ZXJhdGlvbi12ZXJzaW9uIjoxLCJiZC10aWNrZXQtZ3VhcmQtcmVlLXB1YmxpYy1rZXkiOiJCRWFLenNuYW8vUHhnajB2L1NRYUU4U29QVGZhS1YxVnpnYnZEMnFlektXdVhqcDNXOTNjZXB6b1hoU0MxQ05YNVNRTUg1QWRlakd5UzBNNld1RjRBZVE9IiwiYmQtdGlja2V0LWd1YXJkLXdlYi12ZXJzaW9uIjoxfQ%3D%3D; bd_ticket_guard_client_web_domain=2; msToken=mnRmihU-xb0ZsLkb-itjzad_A7ZQyDmKNrcczdt6Jj_7IXz0gdwCT8uN8XYukG1Zj5-JFxDv4yeRKsiC_WxSeXrcbYJYC0Uwyuz0C-Oe; odin_tt=509a427e7fae6b0f4498c2bbed2ead86ae99d77284740ea0f1ab337321cb107858575a289f02b0d4e81f660aeec9405be135e8361c3726ca00857825c875944955ed204c58993e6c6e879dcd024a0733 + cookie: ttwid=1%7CmA00PxWh-zoEXR6YIC77BSmyQ96T7QextS6YFZ9Ocgg%7C1717076775%7C22722d2a9b78b258483ab482ed7d58c0cdd69c75a951b2bb67ec58d00c6b57d6; home_can_add_dy_2_desktop=%220%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1920%2C%5C%22screen_height%5C%22%3A1080%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A12%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A200%7D%22; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; strategyABtestKey=%221717076781.199%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.5%7D; passport_csrf_token=28a44ba980807da79b895df1ac4e16ea; passport_csrf_token_default=28a44ba980807da79b895df1ac4e16ea; bd_ticket_guard_client_data=eyJiZC10aWNrZXQtZ3VhcmQtdmVyc2lvbiI6MiwiYmQtdGlja2V0LWd1YXJkLWl0ZXJhdGlvbi12ZXJzaW9uIjoxLCJiZC10aWNrZXQtZ3VhcmQtcmVlLXB1YmxpYy1rZXkiOiJCSFVoWGRMd0dkaVUvQmdOU2xXa2U3b2tmS0NlRkxOQ0o0Vk9ieGFMclNwNWhkTzdTK3RpazdZZU0xQmpCYWVhZmJFeVA2YjZndm1seDBxTktTVC9hWUU9IiwiYmQtdGlja2V0LWd1YXJkLXdlYi12ZXJzaW9uIjoxfQ%3D%3D; bd_ticket_guard_client_web_domain=2; download_guide=%221%2F20240530%2F0%22; IsDouyinActive=true; stream_player_status_params=%22%7B%5C%22is_auto_play%5C%22%3A0%2C%5C%22is_full_screen%5C%22%3A0%2C%5C%22is_full_webscreen%5C%22%3A0%2C%5C%22is_mute%5C%22%3A0%2C%5C%22is_speed%5C%22%3A1%2C%5C%22is_visible%5C%22%3A1%7D%22; odin_tt=9e666e08aa7c209164e2c3192087a9b9d9b72860dfa94785cd0d5d24d812e598b06848fa210bc71c3ed59f8ea381839a18a47c99dda8b3b6a28c95f9b3e52663336fec7ab8aeea181e5be0287392027f; msToken=fxUIQB8U7zUO7ec0ZI4OsvWRqHUExFN-bPUapHIvRDvL1s3F4Ywzz8fDj5MWoJnutglkgdxV2AZux90jpq1cBwELFWrQu9myVyylg94= proxies: - http: - https: + http://: + https://: tiktok: headers: - User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Referer: https://www.tiktok.com/ - cookie: tt_csrf_token=OFxPH3PE-pSbFptiHW4iy-z4yo3u6gMDt3H0; tt_chain_token=mCH+/C/4flkJarNC1WciiQ==; ak_bmsc=91F3094B27728C66727FD8F413E3CFDA~000000000000000000000000000000~YAAQDfN0aMJLgrOOAQAAfOkFuBeR4cRsy06d9RdcxcQPWGV8lJwTK79/VYCCwvjeEtlU2mCQalUnDU1R2p6Put0FonN6gVhoG2cCdwocRl87ItHMeEQpQxAH0640eFwLYG7OBS5ydU9DUbYX3pCINlGp3AbB7wbAxQb75svv1eEALa0ba8YvA85s1ingqnpt4WmKuSeyR9LkCIg9oABnKJHbPaR2FAWA982tsSdQYIeKlsUc5nyotjtrmgDubgD+v624hs2ilwVQ61HKiCQ7P15mH0M9FL8ciGsDpTKotxZ8sG2phoylvmn0ajD8QV2YdDOegI9ss6RC79POVI5xaFnpoV8TQWLyPzdEl/E6S1TVqaqRHMJsF1De/tdQcLC9//aCKt3NZTqDGg==; tiktok_webapp_theme=light; ttwid=1%7C92fxGgEo0Z8ZDy0YLeqOnKTMHMCPieG0L8UVR1j1oI8%7C1712484381%7Ccb6cbac4d746f5a6acaf73301b34642d1358092dce19a9cb97a4adc978e9d383; odin_tt=d739e385ecc07c0682ee2c2824e464e42d6760e26ec3df8c5b4a0ed1b0bcf02fb36a3eed2a0012a70b3f8ac84a79d5278e23b2ead40ed7aaf85c8ca8b611a8f5819324df5b2ecc7e1d45725a1f5f3ed9; msToken=iA6SqoX-3ggC9aEGWcxqgX0GoRG_1z-3JA9jecj8NwngM3ivhpnl6113Xc72cEc1RhP5YXXeblbo5OOrjfH6tyqtVhD68pnoxVHFTwONHFKwxGkryp3AP2ipQ8Y4; msToken=-RWw93z8QG4ppPftuXXV0NXTs3KN74Jc-eZ7G8Yw0GOWvLoCXB4upq-lPpbKVhIOxp_wJ-hDkWHj364_BTIJUmFLhFLIOaTzgjD4tazEVOEQUBdyG3IbsEZnHxcR + cookie: tt_csrf_token=NZdFHVu4-KtYghBEQDgHh_JKjgAdFhKIcgDU; ak_bmsc=165BFF3A4E2C445E031EEB9B347F3F99~000000000000000000000000000000~YAAQ3pPMF8pAiaiPAQAAWY/dyReR/C9ddS4mWedMGCN7W2a+ascE+7v5dTEEgGTrODWuBLEcqqPZyYRBBiVm99rjTDUSJi2P+0NziV9Ffe2olLI8uakawsAZkpKpZp0dsMpUlWCZ1lIlNgiLQ4OorRsxCrosSr6A56MOf6Q+X1cfFW5b4f3slHhUAgeUHJhNLZc2tN+zdBpIqRrK6Dc96MNa7IKVi6CNoYR/NRksJfINehDW3K3d2bDi//Et1nUcHFcKuw7L2hFhJQoILib/WjJmWOVomf3LSPfGEYZLHkm35SgskCTBlOMDX7oKaELbi98m5z00flHkvHwuyb0yltQjkvIZDA4DIvqBwqENAPesJQpIOULclJI5OIm69tYQGcE+sZ5u9ME=; tt_chain_token=zccwp7dvS61dC917u98Qdg==; bm_sv=2C218A189C99A8C0A7322DC4A6B52B2B~YAAQ3pPMF8tAiaiPAQAAz5HdyReH0vsS+BfXu7TAPBrd5wGaSHIGCvQj0n3LR/iub7ol4bhsXslOpYHY0pg/n6ZhD/yThWgU5wYI4InC/XCE85uuompY2T82hCGIC4zdMsyZ7JJpqerUagxgDTjTbxqPByQDNweuF7cLkzQisuCqfLNVoeId8LB7d2Jg+skOJoaUtDvY4r42wQH0pV/tZ0L7kRBfdJcbzbp2zGHKTNSrpgiPuPN550RtYLLv44QM~1; tiktok_webapp_theme_auto_dark_ab=1; tiktok_webapp_theme=system; ttwid=1%7C3skh2Vk1U1g6e8OS4OPTE7jZOHBW7MakDYrcBah14Ws%7C1717078699%7C449ad7f65ea5e1f1b014fbf3b4674e7bac68db6fd10201b6b3302c74274059ae; msToken=TxDdrpwkvfiXZJYTL4mrRxUhHJhSBKyd9cwgsI7zxYL4Cs9yyMXPRLGbw1nyaUI2iXjURwZaGT6fy3uztCVp6AIL8O9lqLxKerC6RxCdR9zh2umdd9JoAXSxK-EY; msToken=TxDdrpwkvfiXZJYTL4mrRxUhHJhSBKyd9cwgsI7zxYL4Cs9yyMXPRLGbw1nyaUI2iXjURwZaGT6fy3uztCVp6AIL8O9lqLxKerC6RxCdR9zh2umdd9JoAXSxK-EY proxies: - http: - https: \ No newline at end of file + http://: + https://: + +weibo: + headers: + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0 + Referer: https://www.weibo.com/ + cookie: XSRF-TOKEN=GxMcbrwRytI2afYndoarLKjJ; SUB=_2AkMRJL-Ef8NxqwFRmfwQz2rmZIVywg3EieKneE5fJRMxHRl-yT8XqlEmtRB6OqSRa3vF6f2fdNtQwdZNlAKdlpN1p7dT; SUBP=0033WrSXqPxfM72-Ws9jqgMF55529P9D9W5d3LX86ys6FPw-ljjWXw25; WBPSESS=Wk6CxkYDejV3DDBcnx2LOUWDIoRAGwNtFtgdJsHeFORJjYAOaFsShugP8UQ0ESPFYVi4zv3bd-yDJ_nOUlU6r5tEaTqi9GsMdESNWiZKcFssXVdv8LPGCfF_x1G4jj6N906MaQArmD8_aUiW5-mQXkubESA5BsVyta1YqRA0Jjg= + proxies: + http://: + https://: + +twitter: + headers: + User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/12 + Referer: https://twitter.com/ + cookie: guest_id=171949133021733050; night_mode=2; guest_id_marketing=v1%3A171949133021733050; guest_id_ads=v1%3A171949133021733050; gt=1806303677675958490; personalization_id="v1_jd7qHm6d2VrljV2ulxzSMw==" + proxies: + http://: + https://: diff --git a/f2/crawlers/base_crawler.py b/f2/crawlers/base_crawler.py index bd8cb1be..24c714c1 100644 --- a/f2/crawlers/base_crawler.py +++ b/f2/crawlers/base_crawler.py @@ -3,8 +3,11 @@ import httpx import json import asyncio +import traceback +import websockets from httpx import Response +from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK from f2.i18n.translator import _ from f2.log.logger import logger @@ -28,18 +31,28 @@ class BaseCrawler: def __init__( self, - proxies: dict = {}, + proxies: dict = ..., max_retries: int = 5, max_connections: int = 10, timeout: int = 10, max_tasks: int = 10, crawler_headers: dict = {}, ): + # 设置代理 (Set proxy) + self.proxies = proxies if isinstance(proxies, dict): - self.proxies = proxies - # [f"{k}://{v}" for k, v in proxies.items()] - else: - self.proxies = None + # 底层连接重试次数 / Underlying connection retry count + self.sync_transport = httpx.HTTPTransport( + proxy=proxies.get("http://", None), + retries=max_retries, + local_address="0.0.0.0", + ) + # 底层连接重试次数 / Underlying connection retry count + self.async_transport = httpx.AsyncHTTPTransport( + proxy=proxies.get("http://", None), + retries=max_retries, + local_address="0.0.0.0", + ) # 爬虫请求头 / Crawler request header self.crawler_headers = crawler_headers or {} @@ -54,20 +67,38 @@ def __init__( # 业务逻辑重试次数 / Business logic retry count self._max_retries = max_retries - # 底层连接重试次数 / Underlying connection retry count - self.atransport = httpx.AsyncHTTPTransport(retries=max_retries) # 超时等待时间 / Timeout waiting time self._timeout = timeout self.timeout = httpx.Timeout(timeout) + # 异步客户端 / Asynchronous client - self.aclient = httpx.AsyncClient( - headers=self.crawler_headers, - proxies=self.proxies, - timeout=self.timeout, - limits=self.limits, - transport=self.atransport, - ) + self._aclient = None + + # 同步客户端 / Synchronous client + self._client = None + + @property + def aclient(self): + if self._aclient is None: + self._aclient = httpx.AsyncClient( + headers=self.crawler_headers, + verify=False, + timeout=self.timeout, + limits=self.limits, + ) + return self._aclient + + @property + def client(self): + if self._client is None: + self._client = httpx.Client( + headers=self.crawler_headers, + verify=False, + timeout=self.timeout, + limits=self.limits, + ) + return self._client async def _fetch_response(self, endpoint: str) -> Response: """获取数据 (Get data) @@ -121,14 +152,16 @@ def parse_json(self, response: Response) -> dict: try: return response.json() except json.JSONDecodeError as e: - logger.error(_("解析 {0} 接口 JSON 失败: {1}").format(response.url, e)) + logger.error(_("解析 {0} 接口 JSON 失败:{1}").format(response.url, e)) + except UnicodeDecodeError as e: + logger.error(_("解析 {0} 接口 JSON 失败:{1}").format(response.url, e)) else: if isinstance(response, Response): logger.error( _("获取数据失败。状态码: {0}").format(response.status_code) ) else: - logger.error(_("无效响应类型。响应类型: {0}").format(type(response))) + logger.error(_("无效响应类型")) return {} @@ -164,15 +197,68 @@ async def get_fetch_data(self, url: str): response.raise_for_status() return response - except httpx.RequestError: + # 捕获所有与 httpx 请求相关的异常情况 (Captures all httpx request-related exceptions) + except httpx.TimeoutException as exc: + raise APITimeoutError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求端点超时"), + url, + self.proxies, + self.__class__.__name__, + exc, + ) + ) + + except httpx.NetworkError as exc: raise APIConnectionError( _( - "连接端点失败,检查网络环境或代理:{0} 代理:{1} 类名:{2}" - ).format(url, self.proxies, self.__class__.__name__) + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("网络连接失败,请检查当前网络环境"), + url, + self.proxies, + self.__class__.__name__, + exc, + ) ) - except httpx.HTTPStatusError as http_error: - self.handle_http_status_error(http_error, url, attempt + 1) + except httpx.ProtocolError as exc: + raise APIUnauthorizedError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求协议错误"), + url, + self.proxies, + self.__class__.__name__, + exc, + ) + ) + + except httpx.ProxyError as exc: + raise APIConnectionError( + _( + "{0}。 链接:{1},代理:{2},异常类名:{3},异常详细信息:{4}" + ).format( + _("请求代理错误"), + url, + self.proxies, + self.__class__.__name__, + exc, + ) + ) + + except httpx.HTTPStatusError as exc: + self.handle_http_status_error(exc, url, attempt + 1) + + except httpx.RequestError as req_err: + raise APIConnectionError( + _( + "连接端点失败,检查网络环境或代理:{0} 代理:{1} 类名:{2} 异常详细信息:{3}" + ).format(url, self.proxies, self.__class__.__name__, req_err) + ) except APIError as e: logger.error(e) @@ -212,11 +298,11 @@ async def post_fetch_data(self, url: str, params: dict = {}): response.raise_for_status() return response - except httpx.RequestError: + except httpx.RequestError as req_err: raise APIConnectionError( _( - "连接端点失败,检查网络环境或代理:{0} 代理:{1} 类名:{2}" - ).format(url, self.proxies, self.__class__.__name__) + "连接端点失败,检查网络环境或代理:{0} 代理:{1} 类名:{2} 异常详细信息:{3}" + ).format(url, self.proxies, self.__class__.__name__, req_err) ) except httpx.HTTPStatusError as http_error: @@ -241,11 +327,11 @@ async def head_fetch_data(self, url: str): response.raise_for_status() return response - except httpx.RequestError: + except httpx.RequestError as req_err: raise APIConnectionError( - _("连接端点失败,检查网络环境或代理:{0} 代理:{1} 类名:{2}").format( - url, self.proxies, self.__class__.__name__ - ) + _( + "连接端点失败,检查网络环境或代理:{0} 代理:{1} 类名:{2} 异常详细信息:{3}" + ).format(url, self.proxies, self.__class__.__name__, req_err) ) except httpx.HTTPStatusError as http_error: @@ -306,10 +392,158 @@ def handle_http_status_error(self, http_error, url: str, attempt): raise APIResponseError(_("HTTP状态码错误:"), status_code) async def close(self): - await self.aclient.aclose() + # 如果没有初始化客户端,则不关闭 (If the client is not initialized, do not close) + if self._client: + self.client.close() + if self._aclient: + await self.aclient.aclose() async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): - await self.aclient.aclose() + await self.close() + + +class WebSocketCrawler: + """ + WebSocket爬虫客户端 (WebSocket crawler client) + """ + + def __init__(self, wss_headers: dict, callbacks: dict = None, timeout: int = 10): + """ + 初始化 WebSocketCrawler 实例 + + Args: + wss_headers: WebSocket 连接头信息 + callbacks: WebSocket 回调函数 + timeout: WebSocket 超时时间 + """ + self.websocket = None + self.wss_headers = wss_headers + self.callbacks = callbacks or {} # 存储回调函数 + self.timeout = timeout + + async def connect_websocket( + self, + websocket_uri: str, + ): + """ + 连接 WebSocket + + Args: + websocket_uri: WebSocket URI (ws:// or wss://) + """ + try: + # https://websockets.readthedocs.io/en/stable/reference/features.html#client 暂不支持代理 + self.websocket = await websockets.connect( + websocket_uri, extra_headers=self.wss_headers + ) + logger.info(_("已连接 WebSocket")) + except ConnectionRefusedError as exc: + logger.error(traceback.format_exc()) + logger.error(_("WebSocket 连接被拒绝:{0}").format(exc)) + raise APIConnectionError(_("连接 WebSocket 失败:{0}").format(exc)) + + except websockets.InvalidStatusCode as exc: + logger.error(traceback.format_exc()) + logger.error(_("WebSocket 连接状态码无效:{0}").format(exc)) + await asyncio.sleep(2) + await self.connect_websocket(websocket_uri) + + async def receive_messages(self): + """ + 接收 WebSocket 消息并处理 + """ + timeout_count = 0 + try: + while True: + try: + # 为wss连接设置10秒超时机制 + logger.info( + _("等待接收消息,超时时间:{0} 秒").format(self.timeout) + ) + message = await asyncio.wait_for( + self.websocket.recv(), timeout=self.timeout + ) + timeout_count = 0 # 重置超时计数 + await self.on_message(message) + except asyncio.TimeoutError: + logger.warning(_("接收消息超时")) + timeout_count += 1 + if timeout_count >= 3: + await self.on_close(_("即将关闭 WebSocket 连接")) + return "closed" + if self.websocket.closed: + await self.on_close(_("即将关闭 WebSocket 连接")) + return "closed" + except ConnectionClosedError as exc: + logger.error(traceback.format_exc()) + await self.on_close(_("WebSocket 连接被关闭:{0}").format(exc)) + return "closed" + except ConnectionClosedOK: + await self.on_close(_("WebSocket 连接正常关闭")) + return "closed" + except Exception as exc: + logger.error(traceback.format_exc()) + logger.error(_("处理消息时出错:{0}").format(exc)) + await self.on_error(exc) + return "error" + + except Exception as e: + logger.error(traceback.format_exc()) + logger.error(_("接收消息过程中出错:{0}").format(e)) + return "error" + + async def close_websocket(self): + """ + 关闭 WebSocket 连接 + """ + if self.websocket: + await self.websocket.close() + logger.info(_("已关闭 WebSocket 连接")) + + async def on_message(self, message): + """ + 处理 WebSocket 消息 + + Args: + message: WebSocket 消息 + """ + logger.debug(_("收到消息:{0}").format(message)) + + async def on_error(self, message): + """ + 处理 WebSocket 错误 + + Args: + message: WebSocket 错误 + """ + logger.error(_("WebSocket 错误:{0}").format(message)) + + async def on_close(self, message): + """ + 处理 WebSocket 关闭 + + Args: + message: WebSocket 关闭消息 + """ + logger.warning(message) + + async def on_open(self): + """ + 处理 WebSocket 打开 + """ + logger.info(_("WebSocket 连接已打开")) + + async def __aenter__(self): + """ + 进入异步上下文:连接WebSocket + """ + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """ + 退出异步上下文:关闭WebSocket连接 + """ + await self.close_websocket() diff --git a/f2/dl/base_downloader.py b/f2/dl/base_downloader.py index ec12d483..bec09485 100644 --- a/f2/dl/base_downloader.py +++ b/f2/dl/base_downloader.py @@ -6,6 +6,7 @@ import aiofiles import traceback from pathlib import Path +from urllib.error import HTTPError as urllib_HTTPError from rich.progress import TaskID from typing import Union, Optional, Any, List @@ -22,24 +23,19 @@ get_segments_from_m3u8, ) +# 最大片段缓存数量,超过这个数量就会进行清理 +# (Maximum segment cache count, clear when it exceeds this count) +MAX_SEGMENT_COUNT = 1000 + class BaseDownloader(BaseCrawler): """基础下载器 (Base Downloader Class)""" def __init__(self, kwargs: dict = ...): - proxies_conf = kwargs.get("proxies", {"http": None, "https": None}) - proxies = { - "http://": proxies_conf.get("http", None), - "https://": proxies_conf.get("https", None), - } - - self.headers = { - "User-Agent": kwargs["headers"]["User-Agent"], - "Referer": kwargs["headers"]["Referer"], - "Cookie": kwargs["cookie"], - } - + proxies = kwargs.get("proxies", {"http://": None, "https://": None}) + self.headers = kwargs.get("headers") | {"Cookie": kwargs["cookie"]} super().__init__(proxies=proxies, crawler_headers=self.headers) + self.progress = RichConsoleManager().progress self.download_tasks = [] @@ -49,7 +45,6 @@ def _ensure_path(path: Union[str, Path]) -> Path: async def _download_chunks( self, - client: httpx.AsyncClient, request: httpx.Request, file: Any, content_length: int, @@ -59,7 +54,6 @@ async def _download_chunks( 为给定的任务ID下载块 (Download chunks for a given task ID) Args: - client (httpx.AsyncClient): HTTP客户端 (HTTP client) request (httpx.Request): HTTP请求对象 (HTTP request object) file: 文件对象 (File object) content_length (int): 内容长度 (Content length) @@ -67,7 +61,7 @@ async def _download_chunks( """ try: - response = await client.send(request, stream=True) + response = await self.aclient.send(request, stream=True) async for chunk in response.aiter_bytes(get_chunk_size(content_length)): if SignalManager.is_shutdown_signaled(): break @@ -75,10 +69,27 @@ async def _download_chunks( await self.progress.update( task_id, advance=len(chunk), total=int(content_length) ) - except httpx.ReadTimeout as e: - logger.warning(_("文件区块下载超时:{0}").format(e)) + except httpx.TimeoutException as e: + logger.error(traceback.format_exc()) + logger.error(_("文件区块超时:{0}").format(e)) + except httpx.NetworkError as e: + logger.error(traceback.format_exc()) + logger.error(_("文件区块网络错误:{0}").format(e)) + except httpx.HTTPStatusError as e: + logger.error(traceback.format_exc()) + logger.error(_("文件区块HTTP错误:{0}").format(e)) + except httpx.ProxyError as e: + logger.error(traceback.format_exc()) + logger.error(_("文件区块代理错误:{0}").format(e)) + except httpx.UnsupportedProtocol as e: + logger.error(traceback.format_exc()) + logger.error(_("文件区块协议错误:{0}").format(e)) + except httpx.StreamError as e: + logger.error(traceback.format_exc()) + logger.error(_("文件区块流错误:{0}").format(e)) except Exception as e: - logger.error(_("文件区块下载失败:{0}").format(e)) + logger.error(traceback.format_exc()) + logger.error(_("文件区块下载失败:{0} Exception:{1}").format(request, e)) async def download_file( self, @@ -111,7 +122,9 @@ async def download_file( for link in urls: # 获取文件内容大小 (Get the size of the file content) content_length = await get_content_length( - link, self.headers, self.proxies + link, + self.headers, + self.proxies, ) logger.debug( @@ -158,7 +171,7 @@ async def download_file( tmp_path, "ab" if start_byte else "wb" ) as file: await self._download_chunks( - self.aclient, range_request, file, content_length, task_id + range_request, file, content_length, task_id ) # 下载完成后重命名文件 (Rename file after download is complete) @@ -189,7 +202,7 @@ async def download_file( await self.progress.update( task_id, - description=_("[ 完成 ]:"), + description=_("[ 完成 ]:"), filename=trim_filename(full_path.name, 45), state="completed", ) @@ -203,7 +216,7 @@ async def download_file( logger.warning("所有链接都无法下载") await self.progress.update( task_id, - description=_("[ 丢失 ]:所有链接都无法下载"), + description=_("[ 丢失 ]:"), filename=trim_filename(full_path.name, 45), state="error", ) @@ -259,8 +272,13 @@ async def download_m3u8_stream( """ async with self.semaphore: full_path = self._ensure_path(full_path) - total_downloaded = 1024000 + # 设置默认下载总量 (Set default total download) + total_downloaded = 10240000 + # 默认块大小 (Default chunk size) default_chunks = 409600 + # 记录已经下载的片段序号 + # (Record the segment number that has been downloaded) + downloaded_segments = set() while not SignalManager.is_shutdown_signaled(): try: @@ -269,7 +287,7 @@ async def download_m3u8_stream( if not segments: await self.progress.update( task_id, - description=_("[ 丢失 ]:"), + description=_("[ 丢失 ]:"), filename=trim_filename(full_path.name, 45), state="completed", ) @@ -283,57 +301,77 @@ async def download_m3u8_stream( if SignalManager.is_shutdown_signaled(): break - ts_url = segment.absolute_uri - ts_content_length = await get_content_length( - ts_url, self.headers - ) - if ts_content_length == 0: - ts_content_length = default_chunks - logger.warning( - _( - "无法读取该TS文件字节长度,将使用默认400kb块大小处理数据" + # 检查是否已经下载过该片段 (Check if the segment has been downloaded) + if segment.absolute_uri not in downloaded_segments: + ts_url = segment.absolute_uri + ts_content_length = await get_content_length( + ts_url, + self.headers, + self.proxies, + ) + if ts_content_length == 0: + ts_content_length = default_chunks + logger.warning( + _( + "无法读取该TS文件字节长度,将使用默认400kb块大小处理数据" + ) ) + ts_request = self.aclient.build_request( + "GET", ts_url, headers=self.headers + ) + ts_response = await self.aclient.send( + ts_request, stream=True ) - ts_request = self.aclient.build_request( - "GET", ts_url, headers=self.headers - ) - ts_response = await self.aclient.send( - ts_request, stream=True - ) - - try: - async for chunk in ts_response.aiter_bytes( - get_chunk_size(ts_content_length) - ): - if SignalManager.is_shutdown_signaled(): - break - - # 直播流分块下载,每次下载后更新进度条 - # (Live stream block download, update progress bar after each download) - await file.write(chunk) - total_downloaded += len(chunk) - await self.progress.update( - task_id, - advance=len(chunk), - total=total_downloaded, + + try: + async for chunk in ts_response.aiter_bytes( + get_chunk_size(ts_content_length) + ): + if SignalManager.is_shutdown_signaled(): + break + + # 直播流分块下载,每次下载后更新进度条 + # (Live stream block download, update progress bar after each download) + await file.write(chunk) + total_downloaded += len(chunk) + await self.progress.update( + task_id, + advance=len(chunk), + total=total_downloaded, + ) + + # 记录已经下载的片段序号 + # (Record the segment number that has been downloaded) + downloaded_segments.add(segment.absolute_uri) + + except httpx.ReadTimeout as e: + logger.warning(_("TS文件下载超时: {0}").format(e)) + except Exception as e: + logger.error(_("TS文件下载失败: {0}").format(e)) + logger.error(traceback.format_exc()) + finally: + await ts_response.aclose() + else: + logger.debug( + _("为你跳过已下载的片段,URI: {0}").format( + segment.absolute_uri ) + ) + + # 每下载一定数量的片段后,清理一次集合 + # (After downloading a certain number of segments, clean up the collection) + if len(downloaded_segments) > MAX_SEGMENT_COUNT: + downloaded_segments = set() - except httpx.ReadTimeout as e: - logger.warning(_("TS文件下载超时: {0}").format(e)) - except Exception as e: - logger.error(_("TS文件下载失败: {0}").format(e)) - logger.error(traceback.format_exc()) - finally: - await ts_response.aclose() # 等待一段时间后再次请求更新 (Request update again after waiting for a while) - await asyncio.sleep(5) + await asyncio.sleep(segment.duration) except httpx.HTTPStatusError as e: if e.response.status_code == 404: logger.warning(_("m3u8文件或ts文件未找到,可能直播结束")) await self.progress.update( task_id, - description=_("[ 丢失 ]:"), + description=_("[ 丢失 ]:"), filename=trim_filename(full_path.name, 45), state="completed", ) @@ -342,18 +380,38 @@ async def download_m3u8_stream( logger.error(_("HTTP错误: {0}").format(e)) await self.progress.update( task_id, - description=_("[ 失败 ]:"), + description=_("[ 失败 ]:"), + filename=trim_filename(full_path.name, 45), + state="completed", + ) + return + + except urllib_HTTPError as e: + if e.code == 404: + logger.warning(_("m3u8文件或ts文件未找到,直播已结束")) + await self.progress.update( + task_id, + description=_("[ 完成 ]:"), filename=trim_filename(full_path.name, 45), state="completed", ) return + logger.error(_("m3u8文件下载失败:{0},但文件已保存").format(e)) + logger.error(traceback.format_exc()) + await self.progress.update( + task_id, + description=_("[ 失败 ]:"), + filename=trim_filename(full_path.name, 45), + state="completed", + ) + return except Exception as e: logger.error(_("m3u8文件解析失败: {0}").format(e)) logger.error(traceback.format_exc()) await self.progress.update( task_id, - description=_("[ 失败 ]:"), + description=_("[ 失败 ]:"), filename=trim_filename(full_path.name, 45), state="completed", ) @@ -514,7 +572,10 @@ async def execute_tasks(self): async def close(self) -> None: """关闭下载器 (Close the downloader)""" - await self.aclient.aclose() + if self.client: + self.client.close() + if self.aclient: + await self.aclient.aclose() async def __aenter__(self) -> "BaseDownloader": """进入上下文管理器 (Enter the context manager)""" @@ -524,4 +585,4 @@ async def __aenter__(self) -> "BaseDownloader": async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: """退出上下文管理器 (Exit the context manager)""" self.progress.__exit__(exc_type, exc_val, exc_tb) - await self.aclient.aclose() + await self.close() diff --git a/f2/exceptions/api_exceptions.py b/f2/exceptions/api_exceptions.py index 9241f791..0a3f87c8 100644 --- a/f2/exceptions/api_exceptions.py +++ b/f2/exceptions/api_exceptions.py @@ -1,15 +1,12 @@ # path: f2/exceptions/api_exceptions.py - -from f2.cli.cli_console import RichConsoleManager - -exception_console = RichConsoleManager().exception_console +from f2.log.logger import logger class APIError(Exception): """基本API异常类,其他API异常都会继承这个类""" def __init__(self, message=None, status_code=None): - exception_console.print( + logger.error( "请前往QA文档 https://johnserf-seed.github.io/f2/question-answer/qa.html 查看相关帮助" ) self.status_code = status_code diff --git a/f2/exceptions/db_exceptions.py b/f2/exceptions/db_exceptions.py index 1edb871f..a023a365 100644 --- a/f2/exceptions/db_exceptions.py +++ b/f2/exceptions/db_exceptions.py @@ -1,15 +1,12 @@ # path: f2/exceptions/db_exceptions.py - -from f2.cli.cli_console import RichConsoleManager - -exception_console = RichConsoleManager().exception_console +from f2.log.logger import logger class DatabaseError(Exception): """基本数据库异常类,其他数据库异常都会继承这个类""" def __init__(self, message=None, db=None): - exception_console.print( + logger.error( "请前往QA文档 https://johnserf-seed.github.io/f2/question-answer/qa.html 查看相关帮助" ) self.db = db diff --git a/f2/exceptions/file_exceptions.py b/f2/exceptions/file_exceptions.py index 6edb2b80..5269f216 100644 --- a/f2/exceptions/file_exceptions.py +++ b/f2/exceptions/file_exceptions.py @@ -1,15 +1,12 @@ # path: f2/exceptions/file_exceptions.py - -from f2.cli.cli_console import RichConsoleManager - -exception_console = RichConsoleManager().exception_console +from f2.log.logger import logger class FileError(Exception): """基本的文件错误异常类,其他文件异常都会继承这个类""" def __init__(self, message, filepath=None): - exception_console.print( + logger.error( "请前往QA文档 https://johnserf-seed.github.io/f2/question-answer/qa.html 查看相关帮助" ) self.filepath = filepath diff --git a/f2/helps.py b/f2/helps.py index ece0bc36..afed0acc 100644 --- a/f2/helps.py +++ b/f2/helps.py @@ -4,7 +4,7 @@ @Description:helps.py @Date :2023/02/06 17:36:41 @Author :JohnserfSeed -@version :0.0.1.5 +@version :0.0.1.6 @License :Apache License 2.0 @Github :https://github.com/johnserf-seed @Mail :johnserf-seed@foxmail.com @@ -66,14 +66,14 @@ def main() -> None: table.add_row( _("douyin 或 dy"), _( - "- 单个作品,主页作品,点赞作品,收藏作品,合辑作品,图文,文案,封面,直播,原声。" + "- 单个作品,主页作品,点赞作品,收藏作品,合集作品,图文,文案,封面,直播,原声。" ), _("✔"), ) table.add_row( _("tiktok 或 tk"), _( - "- 单个作品,主页作品,点赞作品,收藏作品,播放列表(合辑)作品,文案,封面,原声。" + "- 单个作品,主页作品,点赞作品,收藏作品,播放列表(合集)作品,文案,封面,原声。" ), _("✔"), ) diff --git a/f2/languages/en_US/LC_MESSAGES/en_US.mo b/f2/languages/en_US/LC_MESSAGES/en_US.mo index 55aebf4f..b4ddd802 100644 Binary files a/f2/languages/en_US/LC_MESSAGES/en_US.mo and b/f2/languages/en_US/LC_MESSAGES/en_US.mo differ diff --git a/f2/languages/zh_CN/LC_MESSAGES/zh_CN.mo b/f2/languages/zh_CN/LC_MESSAGES/zh_CN.mo index 4aa11a19..cc587695 100644 Binary files a/f2/languages/zh_CN/LC_MESSAGES/zh_CN.mo and b/f2/languages/zh_CN/LC_MESSAGES/zh_CN.mo differ diff --git a/f2/utils/_dl.py b/f2/utils/_dl.py index d38d9c35..2b97e9ab 100644 --- a/f2/utils/_dl.py +++ b/f2/utils/_dl.py @@ -2,6 +2,7 @@ import m3u8 import httpx +import traceback from pathlib import Path from typing import Union @@ -10,21 +11,33 @@ from f2.i18n.translator import _ -async def get_content_length(url: str, headers: dict = {}, proxies: dict = {}) -> int: +async def get_content_length(url: str, headers: dict = ..., proxies: dict = ...) -> int: """ 获取给定URL的Content-Length (Retrieve the Content-Length for a given URL) Args: url (str): 目标URL (Target URL) + headers (dict): 请求头 (Request headers) + proxies (dict): 代理 (Proxies) Returns: int: Content-Length的值,如果获取失败则返回0 (Value of Content-Length, or 0 if retrieval fails) """ + + if proxies is ... or proxies is None: + proxies = {"all://": None} + + proxy_url = ( + proxies.get("http://") or proxies.get("https://") or proxies.get("all://") + ) + async with httpx.AsyncClient( - timeout=10.0, transport=httpx.AsyncHTTPTransport(retries=5), proxies=proxies - ) as client: + timeout=10.0, + transport=httpx.AsyncHTTPTransport(retries=5, proxy=proxy_url), + verify=False, + ) as aclient: try: - response = await client.head(url, headers=headers, follow_redirects=True) + response = await aclient.head(url, headers=headers, follow_redirects=True) # 当head请求被禁止时,释放status异常被捕获 (When head requests are forbidden, release status exceptions are caught) response.raise_for_status() @@ -33,15 +46,18 @@ async def get_content_length(url: str, headers: dict = {}, proxies: dict = {}) - and int(response.headers.get("Content-Length")) == 0 ): # 如果head请求无法获取Content-Length, 则使用GET请求再次尝试获取 - response = await client.get(url, headers=headers, follow_redirects=True) + response = await aclient.get( + url, headers=headers, follow_redirects=True + ) response.raise_for_status() except httpx.ConnectTimeout: # 连接超时错误处理 (Handling connection timeout errors) + logger.error(traceback.format_exc()) logger.error(_("连接超时错误: {0}".format(url))) - logger.error("===================================") - logger.error(f"headers:{headers}, proxies:{proxies}") - logger.error("===================================") + logger.debug("===================================") + logger.debug(f"headers:{headers}, proxies:{proxies}") + logger.debug("===================================") return 0 # 对HTTP状态错误进行处理 (Handling HTTP status errors) except httpx.HTTPStatusError as exc: @@ -50,12 +66,13 @@ async def get_content_length(url: str, headers: dict = {}, proxies: dict = {}) - try: # 使用GET请求尝试再次获取Content-Length # (Trying to retrieve Content-Length using GET request) - request = client.build_request("GET", url, headers=headers) + request = aclient.build_request("GET", url, headers=headers) # 使用stream=True来避免下载整个内容 # (Using stream=True to avoid downloading the entire content) - response = await client.send(request, stream=True) + response = await aclient.send(request, stream=True) response.raise_for_status() except Exception as e: + logger.error(traceback.format_exc()) logger.error( _( "HTTP状态错误, 尝试GET请求失败: {0}, 错误详情: {1}".format( @@ -74,10 +91,12 @@ async def get_content_length(url: str, headers: dict = {}, proxies: dict = {}) - ) return 0 except httpx.RequestError as e: - logger.error(f"httpx 请求错误: {0}, 错误详情: {1}".format(url, e)) + logger.error(traceback.format_exc()) + logger.error(_("httpx 请求错误:{0},错误详情:{1}".format(url, e))) return 0 except Exception as e: # 处理未知错误 (Handling unknown errors) + logger.error(traceback.format_exc()) logger.error( _( "f2 请求 Content-Length 时发生未知错误: {0}, 错误详情: {1}".format( @@ -141,7 +160,7 @@ def get_chunk_size(file_size: int) -> int: return 1 * 1024 * 1024 # 使用1MB的块大小 (Use a chunk size of 1MB) -async def get_segments_from_m3u8(url: str): +async def get_segments_from_m3u8(url: str) -> Union[list, str, None]: """ 从给定的m3u8文件中获取segments @@ -170,3 +189,17 @@ async def get_segments_from_m3u8(url: str): ) ) return segments + + +async def get_segments_duration(url: str) -> Union[list, int, float, None]: + """ + 从给定的m3u8文件中获取segments的duration + + Args: + url (str): m3u8文件的URL + + Returns: + segments的duration列表 + """ + segments = await get_segments_from_m3u8(url) + return [segment.duration for segment in segments] diff --git a/f2/utils/_signal.py b/f2/utils/_signal.py index 0b4be5be..fd69acb0 100644 --- a/f2/utils/_signal.py +++ b/f2/utils/_signal.py @@ -2,10 +2,10 @@ import signal import asyncio -# from f2.crawlers.base_crawler import BaseCrawler from f2.utils._singleton import Singleton from f2.cli.cli_console import RichConsoleManager + class SignalManager(metaclass=Singleton): def __init__(self): self._shutdown_event = asyncio.Event() @@ -15,25 +15,30 @@ def shutdown_event(self): """提供对shutdown_event的只读访问""" return self._shutdown_event - # async def _cleanup_resources(self): - # await BaseCrawler.close() - def _handle_signal(self, received_signal, frame): """内部处理接收到的信号""" self._shutdown_event.set() - RichConsoleManager().rich_console.print("exiting f2...") + RichConsoleManager().rich_console.print("\nexiting f2...") + # 取消所有运行中的asyncio任务 - for task in asyncio.all_tasks(): - task.cancel() + loop = asyncio.get_event_loop() + if loop.is_running(): + try: + for task in asyncio.all_tasks(loop): + task.cancel() + loop.stop() + except Exception: + pass # 执行资源清理操作 - # asyncio.run(self._cleanup_resources()) sys.exit(0) def register_shutdown_signal(self): """注册一个处理程序来捕获关闭信号""" signal.signal(signal.SIGINT, self._handle_signal) - signal.signal(signal.SIGTERM, self._handle_signal) # 捕获SIGTERM信号,确保更好的跨平台兼容性 + signal.signal( + signal.SIGTERM, self._handle_signal + ) # 捕获SIGTERM信号,确保更好的跨平台兼容性 @classmethod def is_shutdown_signaled(cls): diff --git a/f2/utils/_singleton.py b/f2/utils/_singleton.py index af210fe4..f1bdb8b5 100644 --- a/f2/utils/_singleton.py +++ b/f2/utils/_singleton.py @@ -1,3 +1,5 @@ +# path: f2/utils/_singleton.py + import threading diff --git a/f2/utils/abogus.py b/f2/utils/abogus.py new file mode 100644 index 00000000..d0aa787d --- /dev/null +++ b/f2/utils/abogus.py @@ -0,0 +1,777 @@ +# path: f2/utils/abogus.py + +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +""" +@Description:abogus.py +@Date :2024/06/16 11:21:14 +@Author :JohnserfSeed +@version :0.0.1 +@License :Apache License 2.0 +@Github :https://github.com/johnserf-seed +@Mail :johnserf-seed@foxmail.com +------------------------------------------------- +Change Log : +2024/06/16 17:27:47 - Create ABogus algorithm & black style +2024/06/16 17:27:47 - Limit custom ua late open source full version +------------------------------------------------- +""" + +import time +import random + +from gmssl import sm3, func +from typing import Union, Callable, List, Dict + + +class StringProcessor: + """ + StringProcessor 类用于计算ABogus算法中所需的字符串处理方法。 + 包括字符串和 ASCII 码之间的转换、无符号右移运算等。 + + 类方法: + + to_ord_str(s: str) -> str: + 将字符串转换为 ASCII 码字符串。 + + to_ord_array(s: str) -> List[int]: + 将字符串转换为 ASCII 码列表。 + + to_char_str(s: str) -> str: + 将 ASCII 码列表转换为字符串。 + + to_char_array(s: str) -> List[int]: + 将字符串转换为 ASCII 码列表。 + + js_shift_right(val: int, n: int) -> int: + 实现 JavaScript 中的无符号右移运算。 + + generate_random_bytes(length: int = 3) -> str: + 生成一组伪随机字节字符串,用于混淆数据。 + + 使用示例: + # 将字符串转换为 ASCII 码字符串 + ord_str = StringProcessor.to_ord_str("Hello, World!") + print(ord_str) + + # 将字符串转换为 ASCII 码列表 + ord_array = StringProcessor.to_ord_array("Hello, World!") + print(ord_array) + + # 将 ASCII 码列表转换为字符串 + char_str = StringProcessor.to_char_str(ord_array) + print(char_str) + + # 将字符串转换为 ASCII 码列表 + char_array = StringProcessor.to_char_array("Hello, World!") + print(char_array) + + # 实现 JavaScript 中的无符号右移运算 + shifted_val = StringProcessor.js_shift_right(10, 2) + print(shifted_val) + + # 生成一组伪随机字节字符串 + random_bytes = StringProcessor.generate_random_bytes(3) + print(random_bytes) + """ + + @staticmethod + def to_ord_str(s: str) -> str: + """ + 将字符串转换为 ASCII 码字符串 (Convert a string to an ASCII code string). + + Args: + s (str): 输入字符串 (Input string). + + Returns: + str: 转换后的 ASCII 码字符串 (Converted ASCII code string). + """ + return "".join([chr(i) for i in s]) + + @staticmethod + def to_ord_array(s: str) -> List[int]: + """ + 将字符串转换为 ASCII 码列表 (Convert a string to a list of ASCII codes). + + Args: + s (str): 输入字符串 (Input string). + + Returns: + List[int]: 转换后的 ASCII 码列表 (Converted list of ASCII codes). + """ + return [ord(char) for char in s] + + @staticmethod + def to_char_str(s: str) -> str: + """ + 将 ASCII 码列表转换为字符串 (Convert a list of ASCII codes to a string). + + Args: + s (str): ASCII 码列表 (List of ASCII codes). + + Returns: + str: 转换后的字符串 (Converted string). + """ + return "".join([chr(i) for i in s]) + + @staticmethod + def to_char_array(s: str) -> List[int]: + """ + 将字符串转换为 ASCII 码列表 (Convert a string to a list of ASCII codes). + + Args: + s (str): 输入字符串 (Input string). + + Returns: + List[int]: 转换后的 ASCII 码列表 (Converted list of ASCII codes). + """ + return [ord(char) for char in s] + + @staticmethod + def js_shift_right(val: int, n: int) -> int: + """ + 实现 JavaScript 中的无符号右移运算 (Implement the unsigned right shift operation in JavaScript). + + Args: + val (int): 输入值 (Input value). + n (int): 右移位数 (Number of bits to shift right). + + Returns: + int: 右移后的值 (Value after right shift). + """ + return (val % 0x100000000) >> n + + @staticmethod + def generate_random_bytes(length: int = 3) -> str: + """ + 生成一组伪随机字节字符串,用于混淆数据 (Generate a pseudo-random byte string to obfuscate the data). + + Args: + length (int): 生成的字节序列长度 (Length of the byte sequence to generate). + + Returns: + str: 生成的伪随机字节字符串 (Generated pseudo-random byte string). + """ + + def generate_byte_sequence() -> List[str]: + _rd = int(random.random() * 10000) + return [ + chr(((_rd & 255) & 170) | 1), + chr(((_rd & 255) & 85) | 2), + chr((StringProcessor.js_shift_right(_rd, 8) & 170) | 5), + chr((StringProcessor.js_shift_right(_rd, 8) & 85) | 40), + ] + + result = [] + for _ in range(length): + result.extend(generate_byte_sequence()) + + return "".join(result) + + +class CryptoUtility: + """ + CryptoUtility 类用于提供加密和编码的工具方法,包括 SM3 哈希、添加盐值、Base64 编码和 RC4 加密等。 + """ + + def __init__(self, salt: str, custom_base64_alphabet: List[str]): + """ + 初始化 CryptoUtility 类 + Initialize the CryptoUtility class. + + Args: + salt (str): 加密盐值 (Encryption salt). + custom_base64_alphabet (List[str]): 自定义 Base64 字符表 (Custom Base64 alphabet). + """ + self.salt = salt + self.base64_alphabet = custom_base64_alphabet + + # fmt: off + self.big_array = [ + 121, 243, 55, 234, 103, 36, 47, 228, 30, 231, 106, 6, 115, 95, 78, 101, 250, 207, 198, 50, + 139, 227, 220, 105, 97, 143, 34, 28, 194, 215, 18, 100, 159, 160, 43, 8, 169, 217, 180, 120, + 247, 45, 90, 11, 27, 197, 46, 3, 84, 72, 5, 68, 62, 56, 221, 75, 144, 79, 73, 161, + 178, 81, 64, 187, 134, 117, 186, 118, 16, 241, 130, 71, 89, 147, 122, 129, 65, 40, 88, 150, + 110, 219, 199, 255, 181, 254, 48, 4, 195, 248, 208, 32, 116, 167, 69, 201, 17, 124, 125, 104, + 96, 83, 80, 127, 236, 108, 154, 126, 204, 15, 20, 135, 112, 158, 13, 1, 188, 164, 210, 237, + 222, 98, 212, 77, 253, 42, 170, 202, 26, 22, 29, 182, 251, 10, 173, 152, 58, 138, 54, 141, + 185, 33, 157, 31, 252, 132, 233, 235, 102, 196, 191, 223, 240, 148, 39, 123, 92, 82, 128, 109, + 57, 24, 38, 113, 209, 245, 2, 119, 153, 229, 189, 214, 230, 174, 232, 63, 52, 205, 86, 140, + 66, 175, 111, 171, 246, 133, 238, 193, 99, 60, 74, 91, 225, 51, 76, 37, 145, 211, 166, 151, + 213, 206, 0, 200, 244, 176, 218, 44, 184, 172, 49, 216, 93, 168, 53, 21, 183, 41, 67, 85, + 224, 155, 226, 242, 87, 177, 146, 70, 190, 12, 162, 19, 137, 114, 25, 165, 163, 192, 23, 59, + 9, 94, 179, 107, 35, 7, 142, 131, 239, 203, 149, 136, 61, 249, 14, 156 + ] + # fmt: on + + @staticmethod + def sm3_to_array(input_data: Union[str, List[int]]) -> List[int]: + """ + 计算请求体的 SM3 哈希值,并将结果转换为整数数组 (Calculate the SM3 hash value of the request body and convert the result to an array of integers). + + Args: + input_data (Union[str, List[int]]): 输入数据 (Input data). + + Returns: + List[int]: 哈希值的整数数组 (Array of integers representing the hash value). + """ + # 如果输入是字符串,则将其编码为字节数组 + if isinstance(input_data, str): + input_data_bytes = input_data.encode("utf-8") + else: + input_data_bytes = bytes(input_data) # 将 List[int] 转换为字节数组 + + # 将字节数组转换为适合 sm3.sm3_hash 函数处理的列表格式 + hex_result = sm3.sm3_hash(func.bytes_to_list(input_data_bytes)) + + # 将十六进制字符串结果转换为十进制整数列表 + return [int(hex_result[i : i + 2], 16) for i in range(0, len(hex_result), 2)] + + def add_salt(self, param: str) -> str: + """ + 为字符串参数添加盐值 (Add salt to the string parameter). + + Args: + param (str): 输入字符串 (Input string). + + Returns: + str: 添加盐值后的字符串 (String with added salt). + """ + return param + self.salt + + def process_param( + self, param: Union[str, List[int]], add_salt: bool + ) -> Union[str, List[int]]: + """ + 处理输入参数,根据需要添加盐值 (Process input parameter and add salt if needed). + + Args: + param (Union[str, List[int]]): 输入参数 (Input parameter). + add_salt (bool): 是否添加盐值 (Whether to add salt). + + Returns: + Union[str, List[int]]: 处理后的参数 (Processed parameter). + """ + if isinstance(param, str) and add_salt: + param = self.add_salt(param) + return param + + def params_to_array( + self, param: Union[str, List[int]], add_salt: bool = True + ) -> List[int]: + """ + 获取输入参数的哈希数组 (Get the hash array of the input parameter). + + Args: + param (Union[str, List[int]]): 输入参数 (Input parameter). + add_salt (bool): 是否添加盐值 (Whether to add salt). + + Returns: + List[int]: 哈希数组 (Hash array). + """ + processed_param = self.process_param(param, add_salt) + return self.sm3_to_array(processed_param) + + def transform_bytes(self, bytes_list: List[int]) -> str: + """ + 对输入的字节列表进行加密/解密操作,返回处理后的字符串 (Encrypt/decrypt the input byte list and return the processed string). + + Args: + bytes_list (List[int]): 输入的字节列表 (Input byte list). + + Returns: + str: 处理后的字符串 (Processed string). + """ + # 将字节列表转换为字符字符串 + bytes_str = StringProcessor.to_char_str(bytes_list) + result_str = [] + index_b = self.big_array[1] + initial_value = 0 + + for index, char in enumerate(bytes_str): + if index == 0: + initial_value = self.big_array[index_b] + sum_initial = index_b + initial_value + + self.big_array[1] = initial_value + self.big_array[index_b] = index_b + else: + sum_initial = initial_value + value_e + + char_value = ord(char) + sum_initial %= len(self.big_array) + value_f = self.big_array[sum_initial] + encrypted_char = char_value ^ value_f + result_str.append(chr(encrypted_char)) + + # 交换数组元素 + value_e = self.big_array[(index + 2) % len(self.big_array)] + sum_initial = (index_b + value_e) % len(self.big_array) + initial_value = self.big_array[sum_initial] + self.big_array[sum_initial] = self.big_array[ + (index + 2) % len(self.big_array) + ] + self.big_array[(index + 2) % len(self.big_array)] = initial_value + index_b = sum_initial + + return "".join(result_str) + + def base64_encode(self, input_string: str, selected_alphabet: int = 0) -> str: + """ + 使用自定义字符表对输入字符串进行 Base64 编码 (Encode the input string using a custom Base64 alphabet). + + Args: + input_string (str): 输入字符串 (Input string). + selected_alphabet (int): 选择的自定义 Base64 字符表索引 (Selected custom Base64 alphabet index). + + Returns: + str: 编码后的字符串 (Encoded string). + """ + + # 将输入字符串转换为ASCII码的二进制形式 + binary_string = "".join(["{:08b}".format(ord(char)) for char in input_string]) + + # 补全二进制字符串使其长度为6的倍数 + padding_length = (6 - len(binary_string) % 6) % 6 + binary_string += "0" * padding_length + + # 将二进制字符串分割为6位一组 + base64_indices = [ + int(binary_string[i : i + 6], 2) for i in range(0, len(binary_string), 6) + ] + + # 根据自定义字符表生成输出字符串 + output_string = "".join( + [self.base64_alphabet[selected_alphabet][index] for index in base64_indices] + ) + + # 添加等号填充,使符合 Base64 编码规范 + output_string += "=" * (padding_length // 2) + + return output_string + + def abogus_encode(self, abogus_bytes_str: str, selected_alphabet: int) -> str: + """ + 对输入的字节字符串进行自定义 Base64 编码,并添加位移和填充 (Encode the input byte string using a custom Base64 alphabet, and add shifts and padding). + + Args: + abogus_bytes_str (str): 输入的字节字符串 (Input byte string). + selected_alphabet (int): 选择的自定义 Base64 字符表索引 (Selected custom Base64 alphabet index). + + Returns: + str: 编码后的字符串 (Encoded string). + """ + abogus = [] + + for i in range(0, len(abogus_bytes_str), 3): + if i + 2 < len(abogus_bytes_str): + n = ( + (ord(abogus_bytes_str[i]) << 16) + | (ord(abogus_bytes_str[i + 1]) << 8) + | ord(abogus_bytes_str[i + 2]) + ) + elif i + 1 < len(abogus_bytes_str): + n = (ord(abogus_bytes_str[i]) << 16) | ( + ord(abogus_bytes_str[i + 1]) << 8 + ) + else: + n = ord(abogus_bytes_str[i]) << 16 + + for j, k in zip(range(18, -1, -6), (0xFC0000, 0x03F000, 0x0FC0, 0x3F)): + if j == 6 and i + 1 >= len(abogus_bytes_str): + break + if j == 0 and i + 2 >= len(abogus_bytes_str): + break + abogus.append(self.base64_alphabet[selected_alphabet][(n & k) >> j]) + + abogus.append("=" * ((4 - len(abogus) % 4) % 4)) + return "".join(abogus) + + @staticmethod + def rc4_encrypt(key: bytes, plaintext: str) -> bytes: + """ + 使用 RC4 算法加密数据 (Encrypt data using the RC4 algorithm). + + Args: + key (bytes): 加密密钥 (Encryption key). + plaintext (str): 明文数据 (Plaintext data). + + Returns: + bytes: 加密后的数据 (Encrypted data). + """ + S = list(range(256)) + j = 0 + for i in range(256): + j = (j + S[i] + key[i % len(key)]) % 256 + S[i], S[j] = S[j], S[i] + + i = j = 0 + ciphertext = [] + for char in plaintext: + i = (i + 1) % 256 + j = (j + S[i]) % 256 + S[i], S[j] = S[j], S[i] + K = S[(S[i] + S[j]) % 256] + ciphertext.append(ord(char) ^ K) + + return bytes(ciphertext) + + +class BrowserFingerprintGenerator: + """ + BrowserFingerprintGenerator 用于生成模拟的浏览器指纹信息,用于在不同浏览器环境中进行测试和数据采集。 + + 类属性: + browsers (Dict[str, Callable[[], str]]): 浏览器类型和生成浏览器指纹的映射关系。 + + 方法: + generate_fingerprint(browser_type="Edge"): + 根据指定的浏览器类型生成浏览器指纹。 + + generate_chrome_fingerprint(): + 生成 Chrome 浏览器指纹。 + + generate_firefox_fingerprint(): + 生成 Firefox 浏览器指纹。 + + generate_safari_fingerprint(): + 生成 Safari 浏览器指纹。 + + generate_edge_fingerprint(): + 生成 Edge 浏览器指纹。 + + _generate_fingerprint(platform="Win32"): + 根据给定的参数生成浏览器指纹字符串。 + + 使用示例: + chrome_fp = BrowserFingerprintGenerator.generate_fingerprint("Chrome") + print(chrome_fp) + """ + + @classmethod + def generate_fingerprint(cls, browser_type: str = "Edge") -> str: + """ + 根据指定的浏览器类型生成浏览器指纹。 (Generate a browser fingerprint based on the specified browser type.) + + Args: + browser_type (str): 浏览器类型 (Browser type). + + Returns: + str: 生成的浏览器指纹字符串 (Generated browser fingerprint string). + """ + cls.browsers: Dict[str, Callable[[], str]] = { + "Chrome": cls.generate_chrome_fingerprint, + "Firefox": cls.generate_firefox_fingerprint, + "Safari": cls.generate_safari_fingerprint, + "Edge": cls.generate_edge_fingerprint, + } + return cls.browsers.get(browser_type, cls.generate_chrome_fingerprint)() + + @classmethod + def generate_chrome_fingerprint(cls) -> str: + return cls._generate_fingerprint(platform="Win32") + + @classmethod + def generate_firefox_fingerprint(cls) -> str: + return cls._generate_fingerprint(platform="Win32") + + @classmethod + def generate_safari_fingerprint(cls) -> str: + return cls._generate_fingerprint(platform="MacIntel") + + @classmethod + def generate_edge_fingerprint(cls) -> str: + return cls._generate_fingerprint(platform="Win32") + + @staticmethod + def _generate_fingerprint(platform: str) -> str: + """ + 根据给定的参数生成浏览器指纹字符串。 (Generate a browser fingerprint string based on the given parameters.) + + Args: + platform (str): 操作系统平台 (Operating system platform). + + Returns: + str: 生成的浏览器指纹字符串 (Generated browser fingerprint string). + """ + inner_width = random.randint(1024, 1920) + inner_height = random.randint(768, 1080) + outer_width = inner_width + random.randint(24, 32) + outer_height = inner_height + random.randint(75, 90) + screen_x = 0 + screen_y = random.choice([0, 30]) + avail_width = random.randint(1280, 1920) + avail_height = random.randint(800, 1080) + + fingerprint = ( + f"{inner_width}|{inner_height}|{outer_width}|{outer_height}|" + f"{screen_x}|{screen_y}|0|0|{avail_width}|{avail_height}|" + f"{avail_width}|{avail_height}|{inner_width}|{inner_height}|24|24|{platform}" + ) + return fingerprint + + +class ABogus: + """ + ABogus 类用于生成 ABogus 参数。 + + 类属性: + array1 (List[int]): 加密请求体 (Encrypted request body). + array2 (List[int]): 加密请求头 (Encrypted request header). + array3 (List[int]): 加密 UA (Encrypted User-Agent). + aid (int): AID 值 (AID value). + pageId (int): 页面 ID (Page ID). + salt (str): 加密盐值 (Encryption salt). + options (List[int]): 请求选项 (Request options). + ua_key (bytes): UA 加密密钥 (UA encryption key). + character (str): 自定义 Base64 字符表 (Custom Base64 alphabet). + character2 (str): 自定义 Base64 字符表 (Custom Base64 alphabet). + character_list (List[str]): 自定义 Base64 字符表列表 (List of custom Base64 alphabets). + crypto_utility (CryptoUtility): 加密工具类 (Encryption utility). + user_agent (str): 自定义 UA (Custom User-Agent). + browser_fp (str): 浏览器指纹 (Browser fingerprint). + sort_index (List[int]): 排序索引 (Sort index). + sort_index_2 (List[int]): 排序索引 (Sort index). + + 方法: + encode_data(data: str, alphabet_index: int = 0) -> str: + 使用指定的字符表对数据进行 Base64 编码 (Encode the data using the specified Base64 alphabet). + + generate_abogus(params: str, request: str = "") -> str: + 生成 ABogus 参数 (Generate the ABogus parameter). + + 使用示例: + # 生成 ABogus 参数,置空使用默认 UA 和浏览器指纹 + abogus = ABogus(user_agent="xxx", fp="xxx") + abogus_param = abogus.generate_abogus("device_platform=webapp&aid=6383&channel=channel_pc_web&aweme_id=7380308675841297704……省略……") + print(abogus_param[1]) + """ + + def __init__(self, fp: str = "", user_agent: str = ""): + self.aid = 6383 + self.pageId = 0 + self.salt = "cus" # 加密盐 + self.array1 = [] # 加密请求体 + self.array2 = [] # 加密请求头 为空 + # fmt: off + self.array3 = [] # 加密UA + # fmt: on + self.options = [0, 1, 14] # GET, POST, JSON + + self.character = ( + "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe" + ) + self.character2 = ( + "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe" + ) + self.character_list = [self.character, self.character2] # 自定义base64字符表 + + self.crypto_utility = CryptoUtility( + self.salt, self.character_list + ) # 加密工具类 + + self.user_agent = ( + user_agent + if user_agent is not None and user_agent != "" + else "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0" + ) # 自定义ua,为空则设置一个默认ua + + self.browser_fp = ( + fp + if fp is not None and fp != "" + else BrowserFingerprintGenerator.generate_fingerprint("Edge") + ) # 自定义浏览器指纹,为空则生成Edge指纹 + + # fmt: off + self.sort_index = [ + 18, 20, 52, 26, 30, 34, 58, 38, 40, 53, 42, 21, 27, 54, 55, 31, 35, 57, 39, 41, 43, 22, 28, + 32, 60, 36, 23, 29, 33, 37, 44, 45, 59, 46, 47, 48, 49, 50, 24, 25, 65, 66, 70, 71 + ] + self.sort_index_2 = [ + 18, 20, 26, 30, 34, 38, 40, 42, 21, 27, 31, 35, 39, 41, 43, 22, 28, 32, 36, 23, 29, 33, 37, + 44, 45, 46, 47, 48, 49, 50, 24, 25, 52, 53, 54, 55, 57, 58, 59, 60, 65, 66, 70, 71 + ] + # fmt: on + + def encode_data(self, data: str, alphabet_index: int = 0) -> str: + """ + 使用指定的字符表对数据进行 Base64 编码 (Encode the data using the specified Base64 alphabet). + + Args: + data (str): 输入数据 (Input data). + alphabet_index (int): 自定义字符表索引 (Custom alphabet index). + + Returns: + str: 编码后的数据 (Encoded data). + """ + return self.crypto_utility.abogus_encode(data, alphabet_index) + + def generate_abogus(self, params: str, request: str = "") -> tuple: + """ + 生成 abogus 参数 (Generate the ABogus parameter). + + Args: + params (str): 请求参数 (Request parameters). + request (str): 请求方法,不明确则为空 (Request method, empty if unclear). + + Returns: + tuple: params 生成的 abogus 参数 和 ua (ABogus parameter generated by params and ua). + """ + ab_dir = { + 8: 3, # 固定 + 15: { + "aid": self.aid, + "pageId": self.pageId, + "boe": False, + "ddrt": 7, + "paths": { + "include": [{} for _ in range(7)], + "exclude": [], + }, + "track": {"mode": 0, "delay": 300, "paths": []}, + "dump": True, + "rpU": "", + }, + 18: 44, + 19: [1, 0, 1, 5], + 66: 0, # 固定 + 69: 0, # 固定 + 70: 0, # 固定 + 71: 0, # 固定 + } + + # 开始加密时间 + start_encryption = int(time.time() * 1000) + + array1 = self.crypto_utility.params_to_array( + self.crypto_utility.params_to_array(params) + ) + array2 = self.crypto_utility.params_to_array( + self.crypto_utility.params_to_array(request) + ) + # fmt: off + # 24/06/16 晚点开源自定义ua + # 配置文件请使用该ua "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0" + array3 = [ + 212, 61, 87, 195, 104, 163, 124, 28, 92, 126, 187, + 53, 218, 38, 254, 253, 252, 73, 83, 197, 194, 142, + 113, 37, 9, 67, 166, 36, 56, 72, 56, 64, + ] + # fmt: on + + # 结束加密时间 + end_encryption = int(time.time() * 1000) + + # 插入加密开始时间 + ab_dir[20] = (start_encryption >> 24) & 255 + ab_dir[21] = (start_encryption >> 16) & 255 + ab_dir[22] = (start_encryption >> 8) & 255 + ab_dir[23] = start_encryption & 255 + ab_dir[24] = int(start_encryption / 256 / 256 / 256 / 256) >> 0 + ab_dir[25] = int(start_encryption / 256 / 256 / 256 / 256 / 256) >> 0 + + # 插入请求头配置 + ab_dir[26] = (self.options[0] >> 24) & 255 + ab_dir[27] = (self.options[0] >> 16) & 255 + ab_dir[28] = (self.options[0] >> 8) & 255 + ab_dir[29] = self.options[0] & 255 + + # 插入请求方法 + ab_dir[30] = int(self.options[1] / 256) & 255 + ab_dir[31] = (self.options[1] % 256) & 255 + ab_dir[32] = (self.options[1] >> 24) & 255 + ab_dir[33] = (self.options[1] >> 16) & 255 + + # 插入请求头加密 + ab_dir[34] = (self.options[2] >> 24) & 255 + ab_dir[35] = (self.options[2] >> 16) & 255 + ab_dir[36] = (self.options[2] >> 8) & 255 + ab_dir[37] = self.options[2] & 255 + + # 插入请求体加密 + ab_dir[38] = array1[21] + ab_dir[39] = array1[22] + # 插入body加密 + ab_dir[40] = array2[21] + ab_dir[41] = array2[22] + # 插入ua加密 + ab_dir[42] = array3[23] + ab_dir[43] = array3[24] + + # 插入加密结束时间 + ab_dir[44] = (end_encryption >> 24) & 255 + ab_dir[45] = (end_encryption >> 16) & 255 + ab_dir[46] = (end_encryption >> 8) & 255 + ab_dir[47] = end_encryption & 255 + ab_dir[48] = ab_dir[8] + ab_dir[49] = int(end_encryption / 256 / 256 / 256 / 256) >> 0 + ab_dir[50] = int(end_encryption / 256 / 256 / 256 / 256 / 256) >> 0 + + # 插入固定值 + ab_dir[51] = (self.pageId >> 24) & 255 + ab_dir[52] = (self.pageId >> 16) & 255 + ab_dir[53] = (self.pageId >> 8) & 255 + ab_dir[54] = self.pageId & 255 + ab_dir[55] = self.pageId + ab_dir[56] = self.aid + ab_dir[57] = self.aid & 255 + ab_dir[58] = (self.aid >> 8) & 255 + ab_dir[59] = (self.aid >> 16) & 255 + ab_dir[60] = (self.aid >> 24) & 255 + + # 插入浏览器指纹 + ab_dir[64] = len(self.browser_fp) + ab_dir[65] = len(self.browser_fp) + + # 获取 ab_dir 中 sort_index 的值 + sorted_values = [ab_dir.get(i, 0) for i in self.sort_index] + + # 将浏览器指纹转换为 ASCII 码列表 + edge_fp_array = StringProcessor.to_char_array(self.browser_fp) + + # 将浏览器指纹长度的低 8 位作为异或值 + ab_xor = (len(self.browser_fp) & 255) >> 8 & 255 + + # 进行异或计算 + for index in range(len(self.sort_index_2) - 1): + if index == 0: + ab_xor = ab_dir.get(self.sort_index_2[index], 0) + ab_xor ^= ab_dir.get(self.sort_index_2[index + 1], 0) + + sorted_values.extend(edge_fp_array) + sorted_values.append(ab_xor) + + abogus_bytes_str = ( + StringProcessor.generate_random_bytes() + + self.crypto_utility.transform_bytes(sorted_values) + ) + + abogus = self.crypto_utility.abogus_encode(abogus_bytes_str, 0) + params = "%s&a_bogus=%s" % (params, abogus) + return (params, abogus, self.user_agent) + + +if __name__ == "__main__": + # 24/06/16 晚点开源自定义ua + # 配置文件请使用该ua "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0" + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0" + url = "https://www.douyin.com/aweme/v1/web/aweme/detail/?" + params = "device_platform=webapp&aid=6383&channel=channel_pc_web&aweme_id=7380308675841297704&update_version_code=170400&pc_client_type=1&version_code=190500&version_name=19.5.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=125.0.0.0&browser_online=true&engine_name=Blink&engine_version=125.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7376294349792396827" + request = "GET" + + chrome_fp = BrowserFingerprintGenerator.generate_fingerprint("Chrome") + abogus = ABogus(user_agent=user_agent, fp=chrome_fp) + print(abogus.generate_abogus(params=params, request=request)) + + # # 测试生成100个abogus参数 和 100个指纹所需时间 + # start = time.time() + # for _ in range(100): + # abogus.generate_abogus(params=params, request=request) + # end = time.time() + # print("生成100个abogus参数和指纹所需时间:", end - start) # 生成100个abogus参数和指纹所需时间: 2.203000783920288 + + # start = time.time() + # for _ in range(100): + # BrowserFingerprintGenerator.generate_fingerprint("Chrome") + # end = time.time() + # print("生成100个指纹所需时间:", end - start) # 生成100个指纹所需时间: 0.00400090217590332 diff --git a/f2/utils/mode_handler.py b/f2/utils/decorators.py similarity index 100% rename from f2/utils/mode_handler.py rename to f2/utils/decorators.py diff --git a/f2/utils/utils.py b/f2/utils/utils.py index 09c5a340..0cb2ffd8 100644 --- a/f2/utils/utils.py +++ b/f2/utils/utils.py @@ -1,16 +1,21 @@ # path: f2/utils/utils.py +import f2 import re import sys +import httpx import random import secrets import datetime +import traceback import browser_cookie3 import importlib_resources from typing import Union, Any from pathlib import Path +from f2.log.logger import logger + # 生成一个 16 字节的随机字节串 (Generate a random byte string of 16 bytes) seed_bytes = secrets.token_bytes(16) @@ -38,7 +43,7 @@ def gen_random_str(randomlength: int) -> str: def get_timestamp(unit: str = "milli"): """ - 根据给定的单位获取当前时间 (Get the current time based on the given unit) + 根据给定的单位获取当前时区的时间戳 (Get the current time based on the given unit) Args: unit (str): 时间单位,可以是 "milli"、"sec"、"min" 等 @@ -48,7 +53,9 @@ def get_timestamp(unit: str = "milli"): int: 根据给定单位的当前时间 (The current time based on the given unit) """ - now = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1) + now = datetime.datetime.now(datetime.timezone.utc) - datetime.datetime( + 1970, 1, 1, tzinfo=datetime.timezone.utc + ) if unit == "milli": return int(now.total_seconds() * 1000) elif unit == "sec": @@ -60,10 +67,12 @@ def get_timestamp(unit: str = "milli"): def timestamp_2_str( - timestamp: Union[str, int, float], format: str = "%Y-%m-%d %H-%M-%S" + timestamp: Union[str, int, float], + format: str = "%Y-%m-%d %H-%M-%S", + tz: datetime.timezone = datetime.timezone(datetime.timedelta(hours=8)), ) -> str: """ - 将 UNIX 时间戳转换为格式化字符串 (Convert a UNIX timestamp to a formatted string) + 将 UNIX 时间戳转换为东八区北京时间格式化字符串使用 Args: timestamp (int): 要转换的 UNIX 时间戳 (The UNIX timestamp to be converted) @@ -71,18 +80,24 @@ def timestamp_2_str( 默认为 '%Y-%m-%d %H-%M-%S'。 (The format for the returned date-time string Defaults to '%Y-%m-%d %H-%M-%S') + tz (datetime.timezone, optional): 时区,默认为东八区北京时间。 Returns: str: 格式化的日期时间字符串 (The formatted date-time string) """ if timestamp is None or timestamp == "None": - return "" + return "Invalid timestamp" if isinstance(timestamp, str): if len(timestamp) == 30: - return datetime.datetime.strptime(timestamp, "%a %b %d %H:%M:%S %z %Y") + date_obj = datetime.datetime.strptime(timestamp, "%a %b %d %H:%M:%S %z %Y") + else: + date_obj = datetime.datetime.fromtimestamp(float(timestamp), tz=tz) - return datetime.datetime.fromtimestamp(float(timestamp)).strftime(format) + elif isinstance(timestamp, (int, float)): + date_obj = datetime.datetime.fromtimestamp(float(timestamp), tz=tz) + + return date_obj.strftime(format) def num_to_base36(num: int) -> str: @@ -348,3 +363,71 @@ def merge_config( merged_conf[key] = value # CLI 参数会覆盖自定义配置和主配置中的同名参数 return merged_conf + + +def unescape_json(json_text: str) -> dict: + """ + 反转义 JSON 文本 + + Args: + json_text (str): 带有转义字符的 JSON 文本 + + Returns: + json_obj (dict): 反转义后的 JSON 对象 + """ + + # 反转义 JSON 文本 + json_text = re.sub(r"\\{2}", r"\\", json_text) + json_text = re.sub(r"\\\"", r"\"", json_text) + json_text = re.sub(r"\\", r"", json_text) + json_text = re.sub(r"\"{", r"{", json_text) + json_text = re.sub(r"}\"", r"}", json_text) + json_text = re.sub(r"\&", r"&", json_text) + json_text = re.sub(r"\\n", r"", json_text) + json_text = re.sub(r"\\t", r"", json_text) + json_text = re.sub(r"\\r", r"", json_text) + + try: + # 尝试解析 JSON 文本 + import json + + json_obj = json.loads(json_text) + except Exception as e: + print(f"`unescape_json` error: {e}, raw_json: {json_text}") + json_obj = {} + + return json_obj + + +async def get_latest_version(package_name: str) -> str: + """ + 获取 Python 包的最新版本号 + + Args: + package_name (str): Python 包名 + + Returns: + str: Python 包的最新版本号 + """ + async with httpx.AsyncClient( + timeout=10.0, + transport=httpx.AsyncHTTPTransport(retries=5), + verify=False, + ) as aclient: + try: + response = await aclient.get(f"{f2.PYPI_URL}/{package_name}/json") + response.raise_for_status() + package_data = response.json() + latest_version = package_data["info"]["version"] + return latest_version + except (httpx.HTTPStatusError, httpx.RequestError, KeyError) as e: + logger.error(traceback.format_exc()) + return "0.0.0.0" + + +class BaseEndpointManager: + @classmethod + def model_2_endpoint(cls, base_endpoint: str, params: dict) -> str: + param_str = "&".join([f"{k}={v}" for k, v in params.items()]) + separator = "&" if "?" in base_endpoint else "?" + return f"{base_endpoint}{separator}{param_str}" diff --git a/f2/utils/xbogus.py b/f2/utils/xbogus.py index d23aac7c..56fa09ae 100644 --- a/f2/utils/xbogus.py +++ b/f2/utils/xbogus.py @@ -1,10 +1,12 @@ +# path: f2/utils/xbogus.py + #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description:xbogus.py @Date :2023/02/09 00:29:30 @Author :JohnserfSeed -@version :0.0.2 +@version :0.0.3 @License :Apache License 2.0 @Github :https://github.com/johnserf-seed @Mail :johnserf-seed@foxmail.com @@ -13,6 +15,7 @@ 2023/02/09 00:29:30 - Create XBogus class 2023/06/07 17:26:02 - Refactor the XB algorithm using Python. 2024/04/01 00:32:30 - Black Code Style & Support custom ua +2024/04/13 14:42:10 - Correction examples ------------------------------------------------- """ @@ -22,7 +25,7 @@ class XBogus: - def __init__(self, user_agent: str = None) -> None: + def __init__(self, user_agent: str = "") -> None: # fmt: off self.Array = [ None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, @@ -59,15 +62,15 @@ def md5_str_to_array(self, md5_str): idx += 2 return array - def md5_encrypt(self, url_path): + def md5_encrypt(self, url_params): """ 使用多轮md5哈希算法对URL路径进行加密。 Encrypt the URL path using multiple rounds of md5 hashing. """ - hashed_url_path = self.md5_str_to_array( - self.md5(self.md5_str_to_array(self.md5(url_path))) + hashed_url_params = self.md5_str_to_array( + self.md5(self.md5_str_to_array(self.md5(url_params))) ) - return hashed_url_path + return hashed_url_params def md5(self, input_data): """ @@ -147,7 +150,7 @@ def calculation(self, a1, a2, a3): + self.character[x3 & 63] ) - def getXBogus(self, url_path): + def getXBogus(self, url_params): """ 获取 X-Bogus 值。 Get the X-Bogus value. @@ -164,7 +167,7 @@ def getXBogus(self, url_path): array2 = self.md5_str_to_array( self.md5(self.md5_str_to_array("d41d8cd98f00b204e9800998ecf8427e")) ) - url_path_array = self.md5_encrypt(url_path) + url_params_array = self.md5_encrypt(url_params) timer = int(time.time()) ct = 536919696 @@ -174,7 +177,7 @@ def getXBogus(self, url_path): # fmt: off new_array = [ 64, 0.00390625, 1, 12, - url_path_array[14], url_path_array[15], array2[14], array2[15], array1[14], array1[15], + url_params_array[14], url_params_array[15], array2[14], array2[15], array1[14], array1[15], timer >> 24 & 255, timer >> 16 & 255, timer >> 8 & 255, timer & 255, ct >> 24 & 255, ct >> 16 & 255, ct >> 8 & 255, ct & 255 ] @@ -216,16 +219,21 @@ def getXBogus(self, url_path): ord(garbled_code[idx + 2]), ) idx += 3 - self.params = "%s&X-Bogus=%s" % (url_path, xb_) + self.params = "%s&X-Bogus=%s" % (url_params, xb_) self.xb = xb_ return (self.params, self.xb, self.user_agent) if __name__ == "__main__": - url_path = "https://www.douyin.com/aweme/v1/web/aweme/post/?device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAAW9FWcqS7RdQAWPd2AA5fL_ilmqsIFUCQ_Iym6Yh9_cUa6ZRqVLjVQSUjlHrfXY1Y&max_cursor=0&locate_query=false&show_live_replay_strategy=1&need_time_list=1&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=122.0.0.0&browser_online=true&engine_name=Blink&engine_version=122.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7335414539335222835&msToken=p9Y7fUBuq9DKvAuN27Peml6JbaMqG2ZcXfFiyDv1jcHrCN00uidYqUgSuLsKl1onC-E_n82m-aKKYE0QGEmxIWZx9iueQ6WLbvzPfqnMk4GBAlQIHcDzxb38FLXXQxAm" # ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0" - ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36" - + ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" XB = XBogus(user_agent=ua) - xbogus = XB.getXBogus(url_path) - print(f"url: {xbogus[0]}, xbogus:{xbogus[1]}, ua: {xbogus[2]}") + + dy_url_params = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAAW9FWcqS7RdQAWPd2AA5fL_ilmqsIFUCQ_Iym6Yh9_cUa6ZRqVLjVQSUjlHrfXY1Y&max_cursor=0&locate_query=false&show_live_replay_strategy=1&need_time_list=1&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=122.0.0.0&browser_online=true&engine_name=Blink&engine_version=122.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7335414539335222835&msToken=p9Y7fUBuq9DKvAuN27Peml6JbaMqG2ZcXfFiyDv1jcHrCN00uidYqUgSuLsKl1onC-E_n82m-aKKYE0QGEmxIWZx9iueQ6WLbvzPfqnMk4GBAlQIHcDzxb38FLXXQxAm" + tk_url_params = "WebIdLastTime=1713796127&abTestVersion=%5Bobject%20Object%5D&aid=1988&appType=t&app_language=zh-Hans&app_name=tiktok_web&browser_name=Mozilla&browser_online=true&browser_platform=Win32&browser_version=5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F123.0.0.0%20Safari%2F537.36&channel=tiktok_web&device_id=7360698239018452498&odinId=7360698115047851026®ion=TW&tz_name=Asia%2FHong_Kong&uniqueId=rei_toy625" + + dy_xbogus = XB.getXBogus(dy_url_params) + print(f"url: {dy_xbogus[0]}, xbogus:{dy_xbogus[1]}, ua: {dy_xbogus[2]}") + + tk_xbogus = XB.getXBogus(tk_url_params) + print(f"url: {tk_xbogus[0]}, xbogus:{tk_xbogus[1]}, ua: {tk_xbogus[2]}") diff --git a/pyproject.toml b/pyproject.toml index b5c7ed3c..bd29ded8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,18 +28,22 @@ classifiers = [ dependencies = [ "click==8.1.7", "rich==13.7.1", - "httpx==0.25.0", + "httpx==0.27.0", "aiofiles==22.1.0", - "aiosqlite==0.19.0", + "aiosqlite==0.20.0", "pyyaml==6.0.1", "jsonpath-ng==1.6.0", "importlib_resources==6.1.0", "m3u8==3.6.0", - "pytest==7.4.2", + "pytest==8.2.1", "pytest-asyncio==0.21.1", "browser_cookie3==0.19.1", - "pydantic==1.10.12", + "pydantic==2.6.4", "qrcode==7.4.2", + "websockets>=11.0", + "PyExecJS==1.5.1", + "protobuf==4.23.0", + "gmssl==3.2.2", ] [project.scripts] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..b7ca0bd8 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +markers = + apps: 与 apps 模块相关的测试 + asyncio: 异步相关的测试 diff --git a/tests/test_dl.py b/tests/test_dl.py index f3065b54..9b71e15b 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.py @@ -3,7 +3,11 @@ import pytest from f2.dl.base_downloader import BaseDownloader -kwargs = {"headers": {"User-Agent": "", "Referer": ""}, "cookie": ""} +kwargs = { + "headers": {"User-Agent": "", "Referer": ""}, + "proxies": {"http://": None, "https://": None}, + "cookie": "", +} # 使用 Pytest 的 asyncio 装饰器标记异步测试函数 diff --git a/tests/test_timestamp.py b/tests/test_timestamp.py new file mode 100644 index 00000000..b04145f2 --- /dev/null +++ b/tests/test_timestamp.py @@ -0,0 +1,66 @@ +import pytest +from f2.utils.utils import get_timestamp, timestamp_2_str + + +class TestGetTimestamp: + def test_get_timestamp(self): + print(get_timestamp()) + assert len(str(get_timestamp())) == 13 + + def test_get_timestamp_with_unit(self): + assert len(str(get_timestamp("sec"))) == 10 + + def test_get_timestamp_with_invalid_unit(self): + with pytest.raises(ValueError): + get_timestamp("invalid_unit") + + +class TestTimestamp2Str: + def test_timestamp_2_str(self): + assert timestamp_2_str(1697889407) == "2023-10-21 19-56-47" + + def test_timestamp_2_str_with_format(self): + assert timestamp_2_str(1697889407, "%Y-%m-%d %H-%M-%S") == "2023-10-21 19-56-47" + + def test_timestamp_2_str_with_invalid_format(self): + assert timestamp_2_str(1697889407, "%Y-%m-%d") == "2023-10-21" + + def test_timestamp_2_str_with_invalid_timestamp(self): + assert timestamp_2_str("1620000000") == "2021-05-03 08-00-00" + + def test_long_timestamp_2_str(self): + assert ( + timestamp_2_str("Sun Apr 07 18:43:48 +0800 2024") == "2024-04-07 18-43-48" + ) + + def test_long_timestamp_2_str_with_format(self): + assert ( + timestamp_2_str("Sun Apr 07 18:43:48 +0800 2024", "%Y-%m-%d %H-%M-%S") + == "2024-04-07 18-43-48" + ) + + def test_long_timestamp_2_str_with_format_ymd(self): + assert ( + timestamp_2_str("Sun Apr 07 18:43:48 +0800 2024", "%Y-%m-%d") + == "2024-04-07" + ) + + def test_long_timestamp_2_str_with_format_abd_hms_zy(self): + assert ( + timestamp_2_str("Sun Apr 07 18:43:48 +0800 2024", "%a %b %d %H:%M:%S %z %Y") + == "Sun Apr 07 18:43:48 +0800 2024" + ) + + def test_long_timestamp_2_str_with_format_abd_hms(self): + assert ( + timestamp_2_str("Sun Apr 07 18:43:48 +0800 2024", "%a %b %d %H:%M:%S") + == "Sun Apr 07 18:43:48" + ) + + def test_invalid_timestamp_2_str(self): + with pytest.raises(ValueError): + timestamp_2_str("invalid_timestamp") + + def test_invalid_timestamp_2_str_with_format(self): + with pytest.raises(ValueError): + timestamp_2_str("invalid_timestamp", "%Y-%m-%d %H-%M-%S")