wx-downloder 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wx_downloder-0.1.1/.gitignore +13 -0
- wx_downloder-0.1.1/LICENSE +35 -0
- wx_downloder-0.1.1/PKG-INFO +162 -0
- wx_downloder-0.1.1/README.md +114 -0
- wx_downloder-0.1.1/examples/links.json +6 -0
- wx_downloder-0.1.1/pyproject.toml +41 -0
- wx_downloder-0.1.1/src/wx_channels_cli/__init__.py +1 -0
- wx_downloder-0.1.1/src/wx_channels_cli/__main__.py +3 -0
- wx_downloder-0.1.1/src/wx_channels_cli/auth.py +130 -0
- wx_downloder-0.1.1/src/wx_channels_cli/cli.py +167 -0
- wx_downloder-0.1.1/src/wx_channels_cli/client.py +141 -0
- wx_downloder-0.1.1/src/wx_channels_cli/config.py +82 -0
- wx_downloder-0.1.1/src/wx_channels_cli/download.py +147 -0
- wx_downloder-0.1.1/src/wx_channels_cli/inputs.py +56 -0
- wx_downloder-0.1.1/tests/test_browser_login.py +84 -0
- wx_downloder-0.1.1/tests/test_cli.py +293 -0
- wx_downloder-0.1.1/tests/test_login.py +122 -0
- wx_downloder-0.1.1/uv.lock +379 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"Commons Clause" License Condition v1.0
|
|
2
|
+
|
|
3
|
+
License: MIT License
|
|
4
|
+
Licensor: ltaoo
|
|
5
|
+
Software: wx_channels_download (including binaries and distributions under the names "wx_channels_download.exe", "wx_channel", "wx_video_download", and any substantially similar names)
|
|
6
|
+
|
|
7
|
+
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
|
8
|
+
|
|
9
|
+
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
|
|
10
|
+
|
|
11
|
+
For the purposes of the foregoing, "Sell" means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration, a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any licensee who wishes to Sell the Software must obtain a separate license from the Licensor.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
MIT License
|
|
16
|
+
|
|
17
|
+
Copyright (c) 2025 ltaoo
|
|
18
|
+
|
|
19
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
20
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
21
|
+
in the Software without restriction, including without limitation the rights
|
|
22
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
23
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
24
|
+
furnished to do so, subject to the following conditions:
|
|
25
|
+
|
|
26
|
+
The above copyright notice and this permission notice shall be included in all
|
|
27
|
+
copies or substantial portions of the Software.
|
|
28
|
+
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
32
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
33
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
34
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
35
|
+
SOFTWARE.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: wx-downloder
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: 通过元宝登录解析并下载微信视频号分享链接的 Python CLI
|
|
5
|
+
Project-URL: Homepage, https://github.com/QinGeneral/wx-downloder
|
|
6
|
+
Project-URL: Repository, https://github.com/QinGeneral/wx-downloder
|
|
7
|
+
Project-URL: Issues, https://github.com/QinGeneral/wx-downloder/issues
|
|
8
|
+
License: "Commons Clause" License Condition v1.0
|
|
9
|
+
|
|
10
|
+
License: MIT License
|
|
11
|
+
Licensor: ltaoo
|
|
12
|
+
Software: wx_channels_download (including binaries and distributions under the names "wx_channels_download.exe", "wx_channel", "wx_video_download", and any substantially similar names)
|
|
13
|
+
|
|
14
|
+
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
|
15
|
+
|
|
16
|
+
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
|
|
17
|
+
|
|
18
|
+
For the purposes of the foregoing, "Sell" means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration, a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any licensee who wishes to Sell the Software must obtain a separate license from the Licensor.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
MIT License
|
|
23
|
+
|
|
24
|
+
Copyright (c) 2025 ltaoo
|
|
25
|
+
|
|
26
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
27
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
28
|
+
in the Software without restriction, including without limitation the rights
|
|
29
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
30
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
31
|
+
furnished to do so, subject to the following conditions:
|
|
32
|
+
|
|
33
|
+
The above copyright notice and this permission notice shall be included in all
|
|
34
|
+
copies or substantial portions of the Software.
|
|
35
|
+
|
|
36
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
37
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
38
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
39
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
40
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
41
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
42
|
+
SOFTWARE.
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Requires-Dist: httpx<1,>=0.28
|
|
46
|
+
Requires-Dist: playwright<2,>=1.51
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# wx-downloder
|
|
50
|
+
|
|
51
|
+
独立 Python CLI:打开腾讯元宝登录、保存登录凭据、解析微信视频号分享链接并下载 MP4。基于原项目 [wx_channels_download](https://github.com/ltaoo/wx_channels_download) 的元宝分享链接下载流程迁移;运行时无需 Go、微信客户端、代理抓包服务或 Cloudflare Worker。
|
|
52
|
+
|
|
53
|
+
## 安装
|
|
54
|
+
|
|
55
|
+
需要 Python 3.10+,以及 Chrome 或 Playwright Chromium。
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv tool install wx-downloder
|
|
59
|
+
wx-downloder --help
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
固定安装本次版本:`uv tool install wx-downloder==0.1.1`。也支持 `python -m pip install wx-downloder`。
|
|
63
|
+
|
|
64
|
+
如未安装 Chrome,安装 Playwright Chromium:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv tool run --from playwright playwright install chromium
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
源码开发安装:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/QinGeneral/wx-downloder.git
|
|
74
|
+
cd wx-downloder
|
|
75
|
+
uv sync
|
|
76
|
+
uv run wx-downloder --help
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
从旧版 `wx-channels` 更新:先安装新版并确认 `wx-downloder --version` 输出 `0.1.1`,再运行 `uv tool uninstall wx-channels-cli` 移除旧命令。配置目录、`WX_CHANNELS_HOME` 环境变量和登录凭据位置保持兼容,已经登录的用户无需再次扫码。
|
|
80
|
+
|
|
81
|
+
0.1.1 修复登录成功后浏览器关闭时请求回调反复抛出 `TargetClosedError`:请求监听使用本地缓存的请求头,并在关闭前解除监听。
|
|
82
|
+
|
|
83
|
+
项目默认使用清华 PyPI 镜像以改善国内安装速度;需要官方源时可执行 `uv sync --default-index https://pypi.org/simple`。
|
|
84
|
+
|
|
85
|
+
## 登录和下载
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# 启动独立浏览器窗口,扫码或手机号登录,检测到凭据后自动保存
|
|
89
|
+
wx-downloder login
|
|
90
|
+
|
|
91
|
+
# 单个链接;未保存登录凭据时会自动打开登录窗口
|
|
92
|
+
wx-downloder download 'https://weixin.qq.com/sph/实际分享ID'
|
|
93
|
+
|
|
94
|
+
# 多个链接 / JSON 文件 / 内联 JSON
|
|
95
|
+
wx-downloder download 'https://weixin.qq.com/sph/ID1' 'https://weixin.qq.com/sph/ID2'
|
|
96
|
+
wx-downloder download --input examples/links.json
|
|
97
|
+
wx-downloder download links.json
|
|
98
|
+
wx-downloder download '["https://weixin.qq.com/sph/ID1", {"url":"https://weixin.qq.com/sph/ID2"}]'
|
|
99
|
+
cat links.json | wx-downloder download - --no-login --json
|
|
100
|
+
|
|
101
|
+
# 不传参数:终端交互输入;管道输入则读取标准输入
|
|
102
|
+
wx-downloder download
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
必须使用微信分享生成的 `https://weixin.qq.com/sph/...` 链接。会自动去除分享参数和重复项。暂不支持公众号文章、直播、图集或微信内部短口令。
|
|
106
|
+
|
|
107
|
+
JSON 支持字符串数组、带 `url` / `share_url` / `shareUrl` / `sourceUrl` / `link` 字段的对象,以及 `links` / `urls` / `videos` / `items` / `data` 包装的列表。文件支持 UTF-8 和 UTF-8 BOM。示例链接需替换成真实链接。开始下载前验证全部输入,避免错格式时弹出浏览器。
|
|
108
|
+
|
|
109
|
+
## 目录配置
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# 成品目录,默认 ~/Downloads/wx-channels
|
|
113
|
+
wx-downloder config set download_dir '/Volumes/Data/微信视频'
|
|
114
|
+
|
|
115
|
+
# 临时下载缓存目录,默认 ~/.cache/wx-channels(遵循 XDG_CACHE_HOME)
|
|
116
|
+
wx-downloder config set cache_dir '/Volumes/Data/视频缓存'
|
|
117
|
+
|
|
118
|
+
# 单次覆盖配置;支持缓存和成品在不同磁盘
|
|
119
|
+
wx-downloder download --input links.json --output './videos' --cache-dir './cache'
|
|
120
|
+
|
|
121
|
+
wx-downloder config set quality h265
|
|
122
|
+
wx-downloder config set timeout 120
|
|
123
|
+
wx-downloder config set retries 2
|
|
124
|
+
wx-downloder config set browser chrome
|
|
125
|
+
wx-downloder config show
|
|
126
|
+
wx-downloder status
|
|
127
|
+
wx-downloder logout
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`quality` 默认为 h264;优先所选编码,缺失时尝试通用地址和另一编码。`timeout` 是单次网络读写超时(秒),`retries` 是网络错误、HTTP 429/5xx 的重试次数(0–10)。重试重新解析链接并重新下载,不续传。批量任务中某个视频失败会继续后续项,并返回退出码 1;全部下载或跳过成功返回 0;取消返回 130。
|
|
131
|
+
|
|
132
|
+
视频先写入缓存目录 `.part` 文件,校验长度和 MP4 文件头后再保存成品。正常结束、失败或 Ctrl+C 时清理本次临时文件。强制终止进程留下的 `.part` 可手动删除。已有文件不会覆盖;匹配下载记录且大小一致的文件自动跳过。成品名称包含视频标题和链接哈希,避免同名冲突;成品目录中的 `.wx-*.json` 保存标题、作者和去重记录,请保留。
|
|
133
|
+
|
|
134
|
+
## 登录凭据
|
|
135
|
+
|
|
136
|
+
元宝接口实际使用完整 Cookie,而非单个 token。CLI 在独立浏览器上下文中等待 `hy_user` 和 `hy_token`,保存适用于元宝的 Cookie、浏览器登录状态和相关请求头。保存后关闭本次浏览器,不读取日常浏览器配置文件。
|
|
137
|
+
|
|
138
|
+
配置默认位于 `~/.config/wx-channels/config.json`,凭据位于同目录的 `auth.json`。可用 `WX_CHANNELS_HOME` 指定其他配置目录。凭据原子写入,文件权限为 `0600`,不打印到终端;下载 CDN 和视频号预览请求不携带元宝 Cookie。`logout` 删除本地凭据,不注销服务器会话。`status` 只反映本地保存情况;服务端凭据过期需重新 `login`。
|
|
139
|
+
|
|
140
|
+
使用 [Playwright 浏览器登录状态保存机制](https://playwright.dev/python/docs/auth)。元宝和视频号接口并非稳定公开 API,接口或登录方式变化时可能需要更新。真实扫码必须由用户完成;自动测试使用模拟接口,不等同于真实账号端到端验证。
|
|
141
|
+
|
|
142
|
+
## 开发验证
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uv sync
|
|
146
|
+
uv run pytest
|
|
147
|
+
uv run ruff check .
|
|
148
|
+
uv build
|
|
149
|
+
|
|
150
|
+
# 可选:真实 Chrome 回归测试,模拟登录成功后仍有请求进行的场景
|
|
151
|
+
WX_DOWNLODER_BROWSER_TEST=1 uv run pytest tests/test_browser_login.py -q
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
原项目许可证为 MIT + Commons Clause,保留在 `LICENSE`;本项目沿用该许可条件。
|
|
155
|
+
|
|
156
|
+
验收记录:39 项常规自动化测试和 1 项真实 Chrome 登录关闭回归测试通过,Ruff 检查及格式检查通过,wheel/源码包构建通过。使用源项目已有凭据实测下载了一个 8,155,895 字节的视频,ffprobe 确认含 H.264 视频流和 AAC 音频流,时长 73.45 秒。该实测不替代用户首次扫码登录;新项目未预填源项目的凭据。
|
|
157
|
+
|
|
158
|
+
## 发布流程
|
|
159
|
+
|
|
160
|
+
发布使用 GitHub Actions 的 `.github/workflows/release.yml`,绑定 PyPI Trusted Publisher:`QinGeneral / wx-downloder / release.yml / pypi`。构建任务检查版本标签、运行测试、构建 wheel 和源码包并验证 CLI;独立发布任务通过 OIDC 上传,只有发布任务拥有 `id-token: write` 权限。
|
|
161
|
+
|
|
162
|
+
维护者更新版本并提交后,创建与版本一致的 `v<版本>` 标签并推送即可发布。PyPI 版本不可覆盖。PyPI 页面:[wx-downloder](https://pypi.org/project/wx-downloder/)。
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# wx-downloder
|
|
2
|
+
|
|
3
|
+
独立 Python CLI:打开腾讯元宝登录、保存登录凭据、解析微信视频号分享链接并下载 MP4。基于原项目 [wx_channels_download](https://github.com/ltaoo/wx_channels_download) 的元宝分享链接下载流程迁移;运行时无需 Go、微信客户端、代理抓包服务或 Cloudflare Worker。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
需要 Python 3.10+,以及 Chrome 或 Playwright Chromium。
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv tool install wx-downloder
|
|
11
|
+
wx-downloder --help
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
固定安装本次版本:`uv tool install wx-downloder==0.1.1`。也支持 `python -m pip install wx-downloder`。
|
|
15
|
+
|
|
16
|
+
如未安装 Chrome,安装 Playwright Chromium:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool run --from playwright playwright install chromium
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
源码开发安装:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/QinGeneral/wx-downloder.git
|
|
26
|
+
cd wx-downloder
|
|
27
|
+
uv sync
|
|
28
|
+
uv run wx-downloder --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
从旧版 `wx-channels` 更新:先安装新版并确认 `wx-downloder --version` 输出 `0.1.1`,再运行 `uv tool uninstall wx-channels-cli` 移除旧命令。配置目录、`WX_CHANNELS_HOME` 环境变量和登录凭据位置保持兼容,已经登录的用户无需再次扫码。
|
|
32
|
+
|
|
33
|
+
0.1.1 修复登录成功后浏览器关闭时请求回调反复抛出 `TargetClosedError`:请求监听使用本地缓存的请求头,并在关闭前解除监听。
|
|
34
|
+
|
|
35
|
+
项目默认使用清华 PyPI 镜像以改善国内安装速度;需要官方源时可执行 `uv sync --default-index https://pypi.org/simple`。
|
|
36
|
+
|
|
37
|
+
## 登录和下载
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 启动独立浏览器窗口,扫码或手机号登录,检测到凭据后自动保存
|
|
41
|
+
wx-downloder login
|
|
42
|
+
|
|
43
|
+
# 单个链接;未保存登录凭据时会自动打开登录窗口
|
|
44
|
+
wx-downloder download 'https://weixin.qq.com/sph/实际分享ID'
|
|
45
|
+
|
|
46
|
+
# 多个链接 / JSON 文件 / 内联 JSON
|
|
47
|
+
wx-downloder download 'https://weixin.qq.com/sph/ID1' 'https://weixin.qq.com/sph/ID2'
|
|
48
|
+
wx-downloder download --input examples/links.json
|
|
49
|
+
wx-downloder download links.json
|
|
50
|
+
wx-downloder download '["https://weixin.qq.com/sph/ID1", {"url":"https://weixin.qq.com/sph/ID2"}]'
|
|
51
|
+
cat links.json | wx-downloder download - --no-login --json
|
|
52
|
+
|
|
53
|
+
# 不传参数:终端交互输入;管道输入则读取标准输入
|
|
54
|
+
wx-downloder download
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
必须使用微信分享生成的 `https://weixin.qq.com/sph/...` 链接。会自动去除分享参数和重复项。暂不支持公众号文章、直播、图集或微信内部短口令。
|
|
58
|
+
|
|
59
|
+
JSON 支持字符串数组、带 `url` / `share_url` / `shareUrl` / `sourceUrl` / `link` 字段的对象,以及 `links` / `urls` / `videos` / `items` / `data` 包装的列表。文件支持 UTF-8 和 UTF-8 BOM。示例链接需替换成真实链接。开始下载前验证全部输入,避免错格式时弹出浏览器。
|
|
60
|
+
|
|
61
|
+
## 目录配置
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 成品目录,默认 ~/Downloads/wx-channels
|
|
65
|
+
wx-downloder config set download_dir '/Volumes/Data/微信视频'
|
|
66
|
+
|
|
67
|
+
# 临时下载缓存目录,默认 ~/.cache/wx-channels(遵循 XDG_CACHE_HOME)
|
|
68
|
+
wx-downloder config set cache_dir '/Volumes/Data/视频缓存'
|
|
69
|
+
|
|
70
|
+
# 单次覆盖配置;支持缓存和成品在不同磁盘
|
|
71
|
+
wx-downloder download --input links.json --output './videos' --cache-dir './cache'
|
|
72
|
+
|
|
73
|
+
wx-downloder config set quality h265
|
|
74
|
+
wx-downloder config set timeout 120
|
|
75
|
+
wx-downloder config set retries 2
|
|
76
|
+
wx-downloder config set browser chrome
|
|
77
|
+
wx-downloder config show
|
|
78
|
+
wx-downloder status
|
|
79
|
+
wx-downloder logout
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`quality` 默认为 h264;优先所选编码,缺失时尝试通用地址和另一编码。`timeout` 是单次网络读写超时(秒),`retries` 是网络错误、HTTP 429/5xx 的重试次数(0–10)。重试重新解析链接并重新下载,不续传。批量任务中某个视频失败会继续后续项,并返回退出码 1;全部下载或跳过成功返回 0;取消返回 130。
|
|
83
|
+
|
|
84
|
+
视频先写入缓存目录 `.part` 文件,校验长度和 MP4 文件头后再保存成品。正常结束、失败或 Ctrl+C 时清理本次临时文件。强制终止进程留下的 `.part` 可手动删除。已有文件不会覆盖;匹配下载记录且大小一致的文件自动跳过。成品名称包含视频标题和链接哈希,避免同名冲突;成品目录中的 `.wx-*.json` 保存标题、作者和去重记录,请保留。
|
|
85
|
+
|
|
86
|
+
## 登录凭据
|
|
87
|
+
|
|
88
|
+
元宝接口实际使用完整 Cookie,而非单个 token。CLI 在独立浏览器上下文中等待 `hy_user` 和 `hy_token`,保存适用于元宝的 Cookie、浏览器登录状态和相关请求头。保存后关闭本次浏览器,不读取日常浏览器配置文件。
|
|
89
|
+
|
|
90
|
+
配置默认位于 `~/.config/wx-channels/config.json`,凭据位于同目录的 `auth.json`。可用 `WX_CHANNELS_HOME` 指定其他配置目录。凭据原子写入,文件权限为 `0600`,不打印到终端;下载 CDN 和视频号预览请求不携带元宝 Cookie。`logout` 删除本地凭据,不注销服务器会话。`status` 只反映本地保存情况;服务端凭据过期需重新 `login`。
|
|
91
|
+
|
|
92
|
+
使用 [Playwright 浏览器登录状态保存机制](https://playwright.dev/python/docs/auth)。元宝和视频号接口并非稳定公开 API,接口或登录方式变化时可能需要更新。真实扫码必须由用户完成;自动测试使用模拟接口,不等同于真实账号端到端验证。
|
|
93
|
+
|
|
94
|
+
## 开发验证
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
uv sync
|
|
98
|
+
uv run pytest
|
|
99
|
+
uv run ruff check .
|
|
100
|
+
uv build
|
|
101
|
+
|
|
102
|
+
# 可选:真实 Chrome 回归测试,模拟登录成功后仍有请求进行的场景
|
|
103
|
+
WX_DOWNLODER_BROWSER_TEST=1 uv run pytest tests/test_browser_login.py -q
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
原项目许可证为 MIT + Commons Clause,保留在 `LICENSE`;本项目沿用该许可条件。
|
|
107
|
+
|
|
108
|
+
验收记录:39 项常规自动化测试和 1 项真实 Chrome 登录关闭回归测试通过,Ruff 检查及格式检查通过,wheel/源码包构建通过。使用源项目已有凭据实测下载了一个 8,155,895 字节的视频,ffprobe 确认含 H.264 视频流和 AAC 音频流,时长 73.45 秒。该实测不替代用户首次扫码登录;新项目未预填源项目的凭据。
|
|
109
|
+
|
|
110
|
+
## 发布流程
|
|
111
|
+
|
|
112
|
+
发布使用 GitHub Actions 的 `.github/workflows/release.yml`,绑定 PyPI Trusted Publisher:`QinGeneral / wx-downloder / release.yml / pypi`。构建任务检查版本标签、运行测试、构建 wheel 和源码包并验证 CLI;独立发布任务通过 OIDC 上传,只有发布任务拥有 `id-token: write` 权限。
|
|
113
|
+
|
|
114
|
+
维护者更新版本并提交后,创建与版本一致的 `v<版本>` 标签并推送即可发布。PyPI 版本不可覆盖。PyPI 页面:[wx-downloder](https://pypi.org/project/wx-downloder/)。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wx-downloder"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "通过元宝登录解析并下载微信视频号分享链接的 Python CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
dependencies = ["httpx>=0.28,<1", "playwright>=1.51,<2"]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
wx-downloder = "wx_channels_cli.cli:main"
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/QinGeneral/wx-downloder"
|
|
19
|
+
Repository = "https://github.com/QinGeneral/wx-downloder"
|
|
20
|
+
Issues = "https://github.com/QinGeneral/wx-downloder/issues"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = ["pytest>=8,<10", "ruff>=0.11"]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/wx_channels_cli"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.sdist]
|
|
29
|
+
include = ["/src", "/tests", "/examples", "/README.md", "/LICENSE", "/pyproject.toml", "/uv.lock"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
target-version = "py310"
|
|
36
|
+
line-length = 100
|
|
37
|
+
|
|
38
|
+
[[tool.uv.index]]
|
|
39
|
+
name = "tuna"
|
|
40
|
+
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
41
|
+
default = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from urllib.parse import urlsplit
|
|
5
|
+
|
|
6
|
+
from .client import ORIGIN
|
|
7
|
+
from .config import AppError, app_home, read_json, write_json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cookie_header(cookies: list[dict]) -> str:
|
|
11
|
+
now = time.time()
|
|
12
|
+
valid = []
|
|
13
|
+
for cookie in cookies:
|
|
14
|
+
domain = cookie.get("domain", "").lstrip(".")
|
|
15
|
+
expiry = cookie.get("expires", -1)
|
|
16
|
+
if (
|
|
17
|
+
domain
|
|
18
|
+
and ("yuanbao.tencent.com" == domain or "yuanbao.tencent.com".endswith("." + domain))
|
|
19
|
+
and (expiry == -1 or expiry > now)
|
|
20
|
+
and cookie.get("value")
|
|
21
|
+
and "/api/weixin/get_parse_result".startswith(cookie.get("path", "/"))
|
|
22
|
+
):
|
|
23
|
+
valid.append(cookie)
|
|
24
|
+
valid.sort(key=lambda c: len(c.get("path", "/")), reverse=True)
|
|
25
|
+
return "; ".join(f"{c['name']}={c['value']}" for c in valid)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_auth() -> dict:
|
|
29
|
+
auth = read_json(app_home() / "auth.json", {})
|
|
30
|
+
if not isinstance(auth, dict):
|
|
31
|
+
raise AppError("登录凭据文件格式错误,请运行 wx-downloder login")
|
|
32
|
+
cookie = cookie_header(auth.get("cookies", []))
|
|
33
|
+
names = {part.split("=", 1)[0] for part in cookie.split("; ")}
|
|
34
|
+
if not cookie or not {"hy_user", "hy_token"}.issubset(names):
|
|
35
|
+
raise AppError("没有可用的元宝登录凭据,请先运行 wx-downloder login")
|
|
36
|
+
return auth | {"cookie": cookie}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def login(browser_name: str = "auto", timeout: int = 300, emit=print) -> None:
|
|
40
|
+
from playwright.sync_api import Error as BrowserError
|
|
41
|
+
from playwright.sync_api import sync_playwright
|
|
42
|
+
|
|
43
|
+
with sync_playwright() as pw:
|
|
44
|
+
browser = None
|
|
45
|
+
choices = ("chrome", "chromium") if browser_name == "auto" else (browser_name,)
|
|
46
|
+
for choice in choices:
|
|
47
|
+
try:
|
|
48
|
+
browser = pw.chromium.launch(
|
|
49
|
+
headless=False, **({"channel": choice} if choice != "chromium" else {})
|
|
50
|
+
)
|
|
51
|
+
break
|
|
52
|
+
except BrowserError:
|
|
53
|
+
continue
|
|
54
|
+
if browser is None:
|
|
55
|
+
raise AppError(
|
|
56
|
+
"无法启动浏览器。请安装 Chrome,或运行 python -m playwright install chromium"
|
|
57
|
+
)
|
|
58
|
+
context = None
|
|
59
|
+
capture = None
|
|
60
|
+
try:
|
|
61
|
+
# Always authenticate afresh: reusing locally unexpired cookies here can
|
|
62
|
+
# immediately re-save a session that the server has already revoked.
|
|
63
|
+
context = browser.new_context(locale="zh-CN")
|
|
64
|
+
page = context.new_page()
|
|
65
|
+
captured = {}
|
|
66
|
+
|
|
67
|
+
def capture(request):
|
|
68
|
+
if (
|
|
69
|
+
urlsplit(request.url).netloc != "yuanbao.tencent.com"
|
|
70
|
+
or "/api/" not in request.url
|
|
71
|
+
):
|
|
72
|
+
return
|
|
73
|
+
# This property reads the event's cached headers without issuing a
|
|
74
|
+
# browser RPC. all_headers() can outlive the page during shutdown.
|
|
75
|
+
for key, value in request.headers.items():
|
|
76
|
+
if key in (
|
|
77
|
+
"authorization",
|
|
78
|
+
"t-userid",
|
|
79
|
+
"x-id",
|
|
80
|
+
"x-device-id",
|
|
81
|
+
"x-hy92",
|
|
82
|
+
"x-hy93",
|
|
83
|
+
):
|
|
84
|
+
captured[key] = value
|
|
85
|
+
|
|
86
|
+
context.on("request", capture)
|
|
87
|
+
page.goto(ORIGIN, wait_until="domcontentloaded", timeout=60000)
|
|
88
|
+
emit("已打开元宝,请在浏览器中完成扫码或手机号登录;检测到登录凭据后自动保存。")
|
|
89
|
+
# Site labels vary; failure to auto-open is harmless, user can click 登录.
|
|
90
|
+
for label in ("登录", "立即登录", "登录 / 注册", "Log In", "Not logged in"):
|
|
91
|
+
try:
|
|
92
|
+
button = page.get_by_text(label, exact=True).first
|
|
93
|
+
if button.is_visible():
|
|
94
|
+
button.click(timeout=1500)
|
|
95
|
+
break
|
|
96
|
+
except BrowserError:
|
|
97
|
+
pass
|
|
98
|
+
deadline = time.monotonic() + timeout
|
|
99
|
+
while time.monotonic() < deadline:
|
|
100
|
+
if page.is_closed():
|
|
101
|
+
raise AppError("浏览器已关闭,未保存新的登录凭据")
|
|
102
|
+
cookies = context.cookies(ORIGIN + "/api/weixin/get_parse_result")
|
|
103
|
+
names = {
|
|
104
|
+
c["name"].lower(): c["value"]
|
|
105
|
+
for c in cookies
|
|
106
|
+
if c.get("value") and (c.get("expires", -1) == -1 or c["expires"] > time.time())
|
|
107
|
+
}
|
|
108
|
+
# Yuanbao creates anonymous tracking cookies before login; those do not count.
|
|
109
|
+
if names.get("hy_user") and names.get("hy_token"):
|
|
110
|
+
state = context.storage_state()
|
|
111
|
+
write_json(
|
|
112
|
+
app_home() / "auth.json",
|
|
113
|
+
{
|
|
114
|
+
"cookies": cookies,
|
|
115
|
+
"storage_state": state,
|
|
116
|
+
"headers": captured,
|
|
117
|
+
"user_agent": page.evaluate("navigator.userAgent"),
|
|
118
|
+
"saved_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
emit("元宝登录凭据已保存(仅当前用户可读写)。")
|
|
122
|
+
return
|
|
123
|
+
page.wait_for_timeout(1000)
|
|
124
|
+
raise AppError("等待登录超时,未检测到 hy_user/hy_token,请重新运行 wx-downloder login")
|
|
125
|
+
except BrowserError as exc:
|
|
126
|
+
raise AppError("元宝浏览器登录失败,请检查网络后重试") from exc
|
|
127
|
+
finally:
|
|
128
|
+
if context is not None and capture is not None:
|
|
129
|
+
context.remove_listener("request", capture)
|
|
130
|
+
browser.close()
|