nonebot-plugin-parser 2.6.7__tar.gz → 2.6.9__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.
Files changed (83) hide show
  1. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/PKG-INFO +1 -1
  2. nonebot_plugin_parser-2.6.9/pyproject.toml +224 -0
  3. nonebot_plugin_parser-2.6.7/pyproject.toml → nonebot_plugin_parser-2.6.9/pyproject.toml.orig +2 -2
  4. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/download/__init__.py +19 -44
  5. nonebot_plugin_parser-2.6.9/src/nonebot_plugin_parser/download/rich.py +40 -0
  6. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +40 -4
  7. nonebot_plugin_parser-2.6.9/src/nonebot_plugin_parser/parsers/douyin/__init__.py +118 -0
  8. nonebot_plugin_parser-2.6.9/src/nonebot_plugin_parser/parsers/douyin/aweme.py +82 -0
  9. nonebot_plugin_parser-2.6.7/src/nonebot_plugin_parser/parsers/douyin/__init__.py +0 -137
  10. nonebot_plugin_parser-2.6.7/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -63
  11. nonebot_plugin_parser-2.6.7/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -103
  12. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/README.md +0 -0
  13. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/__init__.py +0 -0
  14. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/config.py +0 -0
  15. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/constants.py +0 -0
  16. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/download/task.py +0 -0
  17. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  18. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/exception.py +0 -0
  19. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/helper.py +0 -0
  20. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/matchers/__init__.py +0 -0
  21. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  22. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/matchers/rule.py +0 -0
  23. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/__init__.py +0 -0
  24. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/acfun/__init__.py +0 -0
  25. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/acfun/video.py +0 -0
  26. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/base.py +0 -0
  27. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  28. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  29. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
  30. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  31. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  32. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  33. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  34. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  35. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/data.py +0 -0
  36. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py +0 -0
  37. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/kuaishou/states.py +0 -0
  38. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
  39. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/task.py +0 -0
  40. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/tiktok.py +0 -0
  41. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/twitter.py +0 -0
  42. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/utils.py +0 -0
  43. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/weibo/__init__.py +0 -0
  44. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/weibo/article.py +0 -0
  45. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/weibo/common.py +0 -0
  46. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/weibo/show.py +0 -0
  47. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py +0 -0
  48. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/xiaohongshu/common.py +0 -0
  49. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/xiaohongshu/discovery.py +0 -0
  50. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/xiaohongshu/explore.py +0 -0
  51. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/youtube/__init__.py +0 -0
  52. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/parsers/youtube/meta.py +0 -0
  53. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/__init__.py +0 -0
  54. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/base.py +0 -0
  55. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/common/__init__.py +0 -0
  56. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/common/assets.py +0 -0
  57. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/common/font.py +0 -0
  58. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/common/renderer.py +0 -0
  59. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/default.py +0 -0
  60. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/htmlrender.py +0 -0
  61. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW.ttf +0 -0
  62. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/__init__.py +0 -0
  63. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/avatar.png +0 -0
  64. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  65. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  66. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/fallback_pic/1.jpg +0 -0
  67. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/fallback_pic/2.jpg +0 -0
  68. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/fallback_pic/3.jpg +0 -0
  69. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/fallback_pic/4.jpg +0 -0
  70. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/fallback_pic/5.jpg +0 -0
  71. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/fallback_pic/6.jpg +0 -0
  72. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/fallback_pic/7.jpg +0 -0
  73. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/fallback_pic/8.jpg +0 -0
  74. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/fallback_pic/9.jpg +0 -0
  75. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  76. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/play.png +0 -0
  77. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  78. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  79. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  80. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  81. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/resources/youtube.png +0 -0
  82. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/renders/templates/card.html.jinja2 +0 -0
  83. {nonebot_plugin_parser-2.6.7 → nonebot_plugin_parser-2.6.9}/src/nonebot_plugin_parser/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nonebot-plugin-parser
3
- Version: 2.6.7
3
+ Version: 2.6.9
4
4
  Summary: NoneBot2 链接分享解析 Alconna 版, 现支持B站|抖音|快手|微博|小红书|YouTube|TikTok|Twitter|AcFun|NGA
5
5
  Keywords: acfun,bilibili,douyin,kuaishou,nga,nonebot,nonebot2,tiktok,twitter,video,weibo,xiaohongshu,youtube
6
6
  Author: fllesser
@@ -0,0 +1,224 @@
1
+ [project]
2
+ name = "nonebot-plugin-parser"
3
+ version = "2.6.9"
4
+ description = "NoneBot2 链接分享解析 Alconna 版, 现支持B站|抖音|快手|微博|小红书|YouTube|TikTok|Twitter|AcFun|NGA"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ keywords = [
8
+ "acfun",
9
+ "bilibili",
10
+ "douyin",
11
+ "kuaishou",
12
+ "nga",
13
+ "nonebot",
14
+ "nonebot2",
15
+ "tiktok",
16
+ "twitter",
17
+ "video",
18
+ "weibo",
19
+ "xiaohongshu",
20
+ "youtube",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 5 - Production/Stable",
24
+ "Framework :: AsyncIO",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3 :: Only",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
33
+ "Topic :: Communications :: Chat",
34
+ "Topic :: Internet :: WWW/HTTP",
35
+ "Topic :: Multimedia :: Video",
36
+ ]
37
+ dependencies = [
38
+ "rich>=13.0.0",
39
+ "pillow>=11.0.0",
40
+ "aiofiles>=25.1.0",
41
+ "httpx>=0.27.2,<1.0.0",
42
+ "msgspec>=0.20.0,<1.0.0",
43
+ "nonebot2>=2.5.0,<3.0.0",
44
+ "apilmoji[rich]>=0.3.1,<1.0.0",
45
+ "beautifulsoup4>=4.12.0,<5.0.0",
46
+ "curl_cffi>=0.13.0,<1.0.0,!=0.14.0",
47
+ "bilibili-api-python>=17.4.2,<18.0.0",
48
+ "nonebot-plugin-uninfo>=0.10.1,<1.0.0",
49
+ "nonebot-plugin-alconna>=0.60.4,<1.0.0",
50
+ "nonebot-plugin-localstore>=0.7.4,<1.0.0",
51
+ "nonebot-plugin-apscheduler>=0.5.0,<1.0.0",
52
+ ]
53
+
54
+ [[project.authors]]
55
+ name = "fllesser"
56
+ email = "fllessive@gmail.com"
57
+
58
+ [project.urls]
59
+ IssueTracker = "https://github.com/fllesser/nonebot-plugin-parser/issues"
60
+ Release = "https://github.com/fllesser/nonebot-plugin-parser/releases"
61
+ Repository = "https://github.com/fllesser/nonebot-plugin-parser"
62
+
63
+ [project.optional-dependencies]
64
+ htmlkit = ["nonebot-plugin-htmlkit>=0.1.0rc4"]
65
+ htmlrender = ["nonebot-plugin-htmlrender>=0.6.7"]
66
+ ytdlp = ["yt-dlp[default]>=2025.2.21"]
67
+ emosvg = ["emosvg>=0.1.7"]
68
+ all = [
69
+ "nonebot-plugin-htmlkit>=0.1.0rc4",
70
+ "nonebot-plugin-htmlrender>=0.6.7",
71
+ "yt-dlp[default]>=2026.3.13",
72
+ "emosvg>=0.1.7",
73
+ ]
74
+
75
+ [dependency-groups]
76
+ dev = [
77
+ "nonebot2[fastapi]>=2.5.0,<3.0.0",
78
+ "nonebot-adapter-onebot>=2.4.6",
79
+ "ruff>=0.15.6,<1.0.0",
80
+ { include-group = "extras" },
81
+ { include-group = "test" },
82
+ ]
83
+ extras = [
84
+ "emosvg>=0.1.7",
85
+ "nonebot-plugin-htmlkit>=0.1.0rc4",
86
+ "nonebot-plugin-htmlrender>=0.6.7",
87
+ "yt-dlp[default]>=2026.3.13",
88
+ "types-yt-dlp>=2026.3.13.20260314",
89
+ ]
90
+ test = [
91
+ "nonebug>=0.4.4,<1.0.0",
92
+ "poethepoet>=0.42.1,<1.0.0",
93
+ "pytest-asyncio>=1.3.0,<1.4.0",
94
+ "pytest-cov>=7.0.0",
95
+ "pytest-xdist>=3.8.0,<4.0.0",
96
+ "respx>=0.22.0",
97
+ ]
98
+ pydantic-v1 = ["pydantic<2.0.0"]
99
+ pydantic-v2 = ["pydantic>=2.0.0"]
100
+ telegram = ["nonebot-adapter-telegram>=0.1.0b20"]
101
+
102
+ [build-system]
103
+ requires = ["uv_build>=0.11.0,<0.12.0"]
104
+ build-backend = "uv_build"
105
+
106
+ [tool.uv]
107
+ conflicts = [[
108
+ { group = "pydantic-v1" },
109
+ { group = "pydantic-v2" },
110
+ { group = "telegram" },
111
+ ]]
112
+
113
+ [tool.bumpversion]
114
+ tag = true
115
+ commit = true
116
+ current_version = "2.6.9"
117
+ message = "release: bump vesion from {current_version} to {new_version}"
118
+
119
+ [[tool.bumpversion.files]]
120
+ filename = "uv.lock"
121
+ search = """
122
+ name = "nonebot-plugin-parser"
123
+ version = "{current_version}""""
124
+ replace = """
125
+ name = "nonebot-plugin-parser"
126
+ version = "{new_version}""""
127
+
128
+ [tool.coverage.report]
129
+ show_missing = true
130
+ exclude_lines = [
131
+ "raise NotImplementedError",
132
+ "if TYPE_CHECKING:",
133
+ "@overload",
134
+ "except",
135
+ "raise",
136
+ ]
137
+
138
+ [tool.nonebot]
139
+ plugins = ["nonebot_plugin_parser"]
140
+
141
+ [[tool.nonebot.adapters]]
142
+ name = "Onebot V11"
143
+ module_name = "nonebot.adapters.onebot.v11"
144
+
145
+ [tool.poe.tasks]
146
+ test-other = "pytest tests/others --cov=src --cov-report=xml --junitxml=junit.xml -n auto"
147
+ test-parser = "pytest tests/parsers --cov=src --cov-report=xml --junitxml=junit.xml -n auto"
148
+ test-render = "pytest tests/renders --cov=src --cov-report=xml --junitxml=junit.xml"
149
+ bump = "bump-my-version bump"
150
+ show-bump = "bump-my-version show-bump"
151
+
152
+ [tool.basedpyright]
153
+ pythonVersion = "3.10"
154
+ pythonPlatform = "All"
155
+ typeCheckingMode = "standard"
156
+ disableBytesTypePromotions = true
157
+
158
+ [tool.basedpyright.defineConstant]
159
+ PYDANTIC_V2 = true
160
+
161
+ [[tool.basedpyright.executionEnvironments]]
162
+ root = "./tests"
163
+ extraPaths = ["./src"]
164
+
165
+ [[tool.basedpyright.executionEnvironments]]
166
+ root = "./src"
167
+
168
+ [tool.pytest]
169
+ addopts = [
170
+ "--import-mode=prepend",
171
+ "--strict-markers",
172
+ "--tb=short",
173
+ "-ra",
174
+ "-s",
175
+ "-v",
176
+ ]
177
+ pythonpath = ["src"]
178
+ asyncio_mode = "auto"
179
+ asyncio_default_fixture_loop_scope = "session"
180
+
181
+ [tool.ruff]
182
+ line-length = 120
183
+
184
+ [tool.ruff.format]
185
+ line-ending = "lf"
186
+
187
+ [tool.ruff.lint]
188
+ select = [
189
+ "F",
190
+ "W",
191
+ "E",
192
+ "I",
193
+ "UP",
194
+ "Q",
195
+ "PYI",
196
+ "T10",
197
+ "T20",
198
+ "C4",
199
+ "PT",
200
+ "TID",
201
+ "ASYNC",
202
+ "RUF",
203
+ ]
204
+ ignore = [
205
+ "E402",
206
+ "UP037",
207
+ "RUF001",
208
+ "RUF002",
209
+ "RUF003",
210
+ "W191",
211
+ "TID252",
212
+ "ASYNC240",
213
+ ]
214
+
215
+ [tool.ruff.lint.isort]
216
+ length-sort = true
217
+ known-first-party = [
218
+ "nonebot_plugin_parser",
219
+ "tests/*",
220
+ ]
221
+ extra-standard-library = ["typing_extensions"]
222
+
223
+ [tool.ruff.lint.pyupgrade]
224
+ keep-runtime-typing = true
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parser"
3
- version = "2.6.7"
3
+ version = "2.6.9"
4
4
  description = "NoneBot2 链接分享解析 Alconna 版, 现支持B站|抖音|快手|微博|小红书|YouTube|TikTok|Twitter|AcFun|NGA"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -115,7 +115,7 @@ conflicts = [
115
115
  [tool.bumpversion]
116
116
  tag = true
117
117
  commit = true
118
- current_version = "2.6.7"
118
+ current_version = "2.6.9"
119
119
  message = "release: bump vesion from {current_version} to {new_version}"
120
120
 
121
121
  [[tool.bumpversion.files]]
@@ -1,20 +1,13 @@
1
1
  import asyncio
2
2
  from pathlib import Path
3
- from functools import partial
4
- from contextlib import contextmanager
5
3
  from urllib.parse import urljoin
6
4
 
7
5
  import httpx
8
6
  import aiofiles
9
7
  import curl_cffi
10
8
  from nonebot import logger, get_driver
11
- from rich.progress import (
12
- Progress,
13
- BarColumn,
14
- TextColumn,
15
- DownloadColumn,
16
- )
17
9
 
10
+ from .rich import add_progress_task
18
11
  from .task import auto_task
19
12
  from ..utils import merge_av, safe_unlink, generate_file_name, is_module_available
20
13
  from ..config import pconfig
@@ -31,22 +24,6 @@ class StreamDownloader:
31
24
  async def aclose(self):
32
25
  await self.client.aclose()
33
26
 
34
- @staticmethod
35
- @contextmanager
36
- def rich_progress(
37
- desc: str,
38
- total: int | None = None,
39
- ):
40
- with Progress(
41
- TextColumn("[bold blue]{task.description}", justify="right"),
42
- BarColumn(bar_width=None),
43
- "[progress.percentage]{task.percentage:>3.1f}%",
44
- "•",
45
- DownloadColumn(),
46
- ) as progress:
47
- task_id = progress.add_task(description=desc, total=total)
48
- yield partial(progress.update, task_id)
49
-
50
27
  @staticmethod
51
28
  def _validate_content_length(
52
29
  response: httpx.Response | curl_cffi.Response,
@@ -84,14 +61,14 @@ class StreamDownloader:
84
61
  response.raise_for_status()
85
62
  content_length = self._validate_content_length(response)
86
63
 
87
- with self.rich_progress(
64
+ update_progress = add_progress_task(
88
65
  f"httpx | {file_path.name}",
89
66
  content_length,
90
- ) as update_progress:
91
- async with aiofiles.open(file_path, "wb") as file:
92
- async for chunk in response.aiter_bytes(chunk_size):
93
- await file.write(chunk)
94
- update_progress(advance=len(chunk))
67
+ )
68
+ async with aiofiles.open(file_path, "wb") as file:
69
+ async for chunk in response.aiter_bytes(chunk_size):
70
+ await file.write(chunk)
71
+ update_progress(advance=len(chunk))
95
72
 
96
73
  return file_path
97
74
 
@@ -112,14 +89,14 @@ class StreamDownloader:
112
89
  response.raise_for_status()
113
90
  content_length = self._validate_content_length(response)
114
91
 
115
- with self.rich_progress(
92
+ update_progress = add_progress_task(
116
93
  f"curl_cffi | {file_path.name}",
117
94
  content_length,
118
- ) as update_progress:
119
- async with aiofiles.open(file_path, "wb") as file:
120
- async for chunk in response.aiter_content(chunk_size=8192):
121
- await file.write(chunk)
122
- update_progress(advance=len(chunk))
95
+ )
96
+ async with aiofiles.open(file_path, "wb") as file:
97
+ async for chunk in response.aiter_content(chunk_size=8192):
98
+ await file.write(chunk)
99
+ update_progress(advance=len(chunk))
123
100
 
124
101
  return file_path
125
102
 
@@ -242,14 +219,12 @@ class StreamDownloader:
242
219
 
243
220
  try:
244
221
  async with aiofiles.open(video_path, "wb") as f:
245
- total_size = 0
246
- with self.rich_progress(desc=video_name) as update_progress:
247
- for url in await self._get_m3u8_slices(m3u8_url):
248
- async with self.client.stream("GET", url, headers=ext_headers) as response:
249
- async for chunk in response.aiter_bytes(chunk_size=1024 * 1024):
250
- await f.write(chunk)
251
- total_size += len(chunk)
252
- update_progress(advance=len(chunk), total=total_size)
222
+ update_progress = add_progress_task(desc=video_name)
223
+ for url in await self._get_m3u8_slices(m3u8_url):
224
+ async with self.client.stream("GET", url, headers=ext_headers) as response:
225
+ async for chunk in response.aiter_bytes(chunk_size=1024 * 1024):
226
+ await f.write(chunk)
227
+ update_progress(advance=len(chunk))
253
228
  except httpx.HTTPError:
254
229
  await safe_unlink(video_path)
255
230
  logger.exception("m3u8 视频下载失败")
@@ -0,0 +1,40 @@
1
+ from functools import partial
2
+
3
+ from rich.progress import (
4
+ Progress,
5
+ BarColumn,
6
+ TextColumn,
7
+ DownloadColumn,
8
+ )
9
+
10
+ progress_bar: Progress = Progress(
11
+ TextColumn("[bold blue]{task.description}", justify="right"),
12
+ BarColumn(bar_width=None),
13
+ "[progress.percentage]{task.percentage:>3.1f}%",
14
+ "•",
15
+ DownloadColumn(),
16
+ )
17
+
18
+
19
+ def add_progress_task(
20
+ desc: str,
21
+ total: int | None = None,
22
+ ):
23
+ task_id = progress_bar.add_task(description=desc, total=total)
24
+ progress_bar.start_task(task_id)
25
+ return partial(progress_bar.update, task_id)
26
+
27
+
28
+ from nonebot import get_driver
29
+
30
+ driver = get_driver()
31
+
32
+
33
+ @driver.on_startup
34
+ async def enter_progress_bar():
35
+ progress_bar.start()
36
+
37
+
38
+ @driver.on_shutdown
39
+ async def exit_progress_bar():
40
+ progress_bar.stop()
@@ -6,10 +6,16 @@ from collections.abc import AsyncGenerator
6
6
 
7
7
  from msgspec import convert
8
8
  from nonebot import logger
9
- from bilibili_api import HEADERS, Credential, select_client, request_settings
9
+ from bilibili_api import (
10
+ HEADERS,
11
+ Credential,
12
+ get_client,
13
+ select_client,
14
+ request_settings,
15
+ )
10
16
  from bilibili_api.opus import Opus
11
17
  from bilibili_api.video import Video
12
- from bilibili_api.login_v2 import QrCodeLogin, QrCodeLoginEvents
18
+ from bilibili_api.login_v2 import QrCodeLogin, QrCodeLoginEvents, QrCodeLoginChannel
13
19
 
14
20
  from ..base import (
15
21
  BaseParser,
@@ -353,18 +359,48 @@ class BilibiliParser(BaseParser):
353
359
 
354
360
  async def login_with_qrcode(self) -> bytes:
355
361
  """通过二维码登录获取哔哩哔哩登录凭证"""
356
- self._qr_login = QrCodeLogin()
362
+ self._qr_login = QrCodeLogin(platform=QrCodeLoginChannel.WEB)
357
363
  await self._qr_login.generate_qrcode()
358
364
 
359
365
  qr_pic = self._qr_login.get_qrcode_picture()
360
366
  return qr_pic.content
361
367
 
368
+ @staticmethod
369
+ async def check_state(qr: QrCodeLogin):
370
+ client = get_client()
371
+ resp = await client.request(
372
+ method="GET",
373
+ url="https://passport.bilibili.com/x/passport-login/web/qrcode/poll",
374
+ params={"qrcode_key": qr._QrCodeLogin__qr_key}, # pyright: ignore[reportAttributeAccessIssue]
375
+ )
376
+ result: dict = resp.json()["data"] # pyright: ignore[reportIndexIssue]
377
+ code = result["code"]
378
+ if code == 86101:
379
+ return QrCodeLoginEvents.SCAN
380
+ elif code == 86090:
381
+ return QrCodeLoginEvents.CONF
382
+ elif code == 86038:
383
+ return QrCodeLoginEvents.TIMEOUT
384
+ else:
385
+ ac_time_value = result["refresh_token"]
386
+ cookies = resp.cookies
387
+ sessdata = cookies["SESSDATA"]
388
+ bili_jct = cookies["bili_jct"]
389
+ dedeuserid = cookies["DedeUserID"]
390
+ qr._QrCodeLogin__credential = Credential( # pyright: ignore[reportAttributeAccessIssue]
391
+ sessdata=sessdata,
392
+ bili_jct=bili_jct,
393
+ dedeuserid=dedeuserid,
394
+ ac_time_value=ac_time_value,
395
+ )
396
+ return QrCodeLoginEvents.DONE
397
+
362
398
  async def check_qr_state(self) -> AsyncGenerator[str]:
363
399
  """检查二维码登录状态"""
364
400
  scan_tip_pending = True
365
401
 
366
402
  for _ in range(30):
367
- state = await self._qr_login.check_state()
403
+ state = await self.check_state(self._qr_login)
368
404
  match state:
369
405
  case QrCodeLoginEvents.DONE:
370
406
  yield "登录成功"
@@ -0,0 +1,118 @@
1
+ import re
2
+ import json
3
+ from typing import ClassVar
4
+
5
+ from httpx import AsyncClient
6
+
7
+ from ..base import (
8
+ COMMON_TIMEOUT,
9
+ Platform,
10
+ BaseParser,
11
+ PlatformEnum,
12
+ ParseException,
13
+ handle,
14
+ )
15
+ from .aweme import decoder
16
+
17
+
18
+ class DouyinParser(BaseParser):
19
+ platform: ClassVar[Platform] = Platform(name=PlatformEnum.DOUYIN, display_name="抖音")
20
+
21
+ def __init__(self):
22
+ super().__init__()
23
+ self.headers.update(
24
+ {
25
+ "Origin": "https://open.douyin.com",
26
+ "Referer": "https://open.douyin.com/",
27
+ }
28
+ )
29
+
30
+ # https://v.douyin.com/_2ljF4AmKL8
31
+ @handle("v.douyin", r"v\.douyin\.com/[a-zA-Z0-9_\-]+")
32
+ @handle("jx.douyin", r"jx\.douyin\.com/[a-zA-Z0-9_\-]+")
33
+ async def _parse_short_link(self, searched: re.Match[str]):
34
+ url = f"https://{searched.group(0)}"
35
+ return await self.parse_with_redirect(url)
36
+
37
+ # https://www.douyin.com/video/7521023890996514083
38
+ # https://www.douyin.com/note/7469411074119322899
39
+ @handle("douyin", r"douyin\.com/[a-z]+/(?P<aweme_id>\d+)")
40
+ @handle("iesdouyin", r"iesdouyin\.com/share/[a-z]+/(?P<aweme_id>\d+)")
41
+ @handle("m.douyin", r"m\.douyin\.com/share/[a-z]+/(?P<aweme_id>\d+)")
42
+ # https://jingxuan.douyin.com/m/video/7574300896016862490?app=yumme&utm_source=copy_link
43
+ @handle("jingxuan.douyin", r"jingxuan\.douyin.com/m/[a-z]+/(?P<aweme_id>\d+)")
44
+ async def _parse_douyin(self, searched: re.Match[str]):
45
+ aweme_id = searched.group("aweme_id")
46
+ return await self.parse_aweme(aweme_id)
47
+
48
+ async def parse_aweme(self, aweme_id: str):
49
+ async with AsyncClient(
50
+ headers=self.headers,
51
+ timeout=COMMON_TIMEOUT,
52
+ follow_redirects=True,
53
+ verify=False,
54
+ ) as client:
55
+ response = await client.get(
56
+ "https://www.douyin.com/aweme/v1/web/aweme/detail/",
57
+ params={"aweme_id": aweme_id, "aid": "6383"},
58
+ )
59
+ if response.status_code != 200:
60
+ raise ParseException(f"status: {response.status_code}")
61
+ aweme = decoder.decode(response.content).aweme_detail
62
+
63
+ # 作者
64
+ author = self.create_author(
65
+ aweme.author.nickname,
66
+ aweme.author.avatar_thumb.url_list[-1],
67
+ aweme.author.signature,
68
+ )
69
+
70
+ # 先以部分数据构建结果,后续再填充内容,避免使用临时变量
71
+ result = self.result(
72
+ text=aweme.share_info.text,
73
+ author=author,
74
+ timestamp=aweme.create_time,
75
+ url=aweme.share_url.split("?")[0],
76
+ )
77
+ if music := aweme.music:
78
+ if not music.is_original_sound:
79
+ if music.play_url.uri == "":
80
+ extra = json.loads(music.extra)
81
+ music_url = extra.get("original_song_url")
82
+ else:
83
+ music_url = music.play_url.uri
84
+ if music_url:
85
+ result.contents.append(
86
+ self.create_audio(
87
+ url_or_task=music_url,
88
+ duration=music.duration,
89
+ )
90
+ )
91
+
92
+ # 添加图片内容
93
+ if images := aweme.images:
94
+ for image in images:
95
+ if image.clip_type == 2 or image.clip_type is None:
96
+ result.contents.append(
97
+ self.create_image(
98
+ url_or_task=image.url_list[-1],
99
+ )
100
+ )
101
+ elif image_video := image.video:
102
+ result.contents.extend(
103
+ [
104
+ self.create_image(
105
+ url_or_task=image_video.cover.url_list[-1],
106
+ ),
107
+ self.create_video(url_or_task=image_video.play_addr.url),
108
+ ]
109
+ )
110
+ # 添加视频内容
111
+ elif video := aweme.video:
112
+ result.video = self.create_video(
113
+ video.play_addr.url,
114
+ video.cover_original_scale.url_list[-1] if video.cover_original_scale else video.cover.url_list[-1],
115
+ video.duration // 1000,
116
+ )
117
+
118
+ return result
@@ -0,0 +1,82 @@
1
+ from msgspec import Struct, field
2
+ from msgspec.json import Decoder
3
+
4
+
5
+ class Addr(Struct):
6
+ uri: str
7
+
8
+ @property
9
+ def url(self) -> str:
10
+ return f"https://aweme.snssdk.com/aweme/v1/play/?video_id={self.uri}&ratio=1080p&line=0"
11
+
12
+
13
+ class UrlList(Struct):
14
+ url_list: list[str]
15
+
16
+
17
+ class Author(Struct):
18
+ uid: str
19
+ nickname: str
20
+ avatar_thumb: UrlList
21
+ """头像"""
22
+ signature: str | None = None
23
+
24
+
25
+ class Video(Struct):
26
+ cover: UrlList
27
+ duration: int
28
+ """视频时长(/1000)"""
29
+ play_addr: Addr
30
+ cover_original_scale: UrlList | None = None
31
+
32
+ @property
33
+ def url(self) -> str:
34
+ return self.play_addr.url
35
+
36
+
37
+ class Image(Struct):
38
+ url_list: list[str]
39
+ uri: str = field(default="")
40
+ clip_type: int | None = field(default=None)
41
+ """=2 or None 是普通图片"""
42
+ video: Video | None = field(default=None)
43
+ """Live Photo 视频"""
44
+
45
+
46
+ class MusicPlayUrl(Struct):
47
+ uri: str
48
+
49
+
50
+ class Music(Struct):
51
+ duration: int
52
+ mid: str
53
+ play_url: MusicPlayUrl
54
+ extra: str
55
+ is_original_sound: bool
56
+
57
+
58
+ class ShareInfo(Struct):
59
+ share_desc: str
60
+ share_desc_info: str
61
+
62
+ @property
63
+ def text(self) -> str:
64
+ return self.share_desc_info.replace(f"#{self.share_desc}#", "", 1)
65
+
66
+
67
+ class Aweme(Struct):
68
+ aweme_id: str
69
+ author: Author
70
+ share_info: ShareInfo
71
+ create_time: int
72
+ share_url: str
73
+ images: list[Image] | None = field(default=None)
74
+ music: Music | None = field(default=None)
75
+ video: Video | None = field(default=None)
76
+
77
+
78
+ class Response(Struct):
79
+ aweme_detail: Aweme
80
+
81
+
82
+ decoder = Decoder(Response)
@@ -1,137 +0,0 @@
1
- import re
2
- from typing import ClassVar
3
-
4
- from httpx import AsyncClient
5
- from nonebot import logger
6
-
7
- from ..base import (
8
- COMMON_TIMEOUT,
9
- Platform,
10
- BaseParser,
11
- PlatformEnum,
12
- ParseException,
13
- handle,
14
- )
15
-
16
-
17
- class DouyinParser(BaseParser):
18
- platform: ClassVar[Platform] = Platform(name=PlatformEnum.DOUYIN, display_name="抖音")
19
-
20
- # https://v.douyin.com/_2ljF4AmKL8
21
- @handle("v.douyin", r"v\.douyin\.com/[a-zA-Z0-9_\-]+")
22
- @handle("jx.douyin", r"jx\.douyin\.com/[a-zA-Z0-9_\-]+")
23
- async def _parse_short_link(self, searched: re.Match[str]):
24
- url = f"https://{searched.group(0)}"
25
- return await self.parse_with_redirect(url)
26
-
27
- # https://www.douyin.com/video/7521023890996514083
28
- # https://www.douyin.com/note/7469411074119322899
29
- @handle("douyin", r"douyin\.com/(?P<ty>video|note)/(?P<vid>\d+)")
30
- @handle("iesdouyin", r"iesdouyin\.com/share/(?P<ty>slides|video|note)/(?P<vid>\d+)")
31
- @handle("m.douyin", r"m\.douyin\.com/share/(?P<ty>slides|video|note)/(?P<vid>\d+)")
32
- # https://jingxuan.douyin.com/m/video/7574300896016862490?app=yumme&utm_source=copy_link
33
- @handle("jingxuan.douyin", r"jingxuan\.douyin.com/m/(?P<ty>slides|video|note)/(?P<vid>\d+)")
34
- async def _parse_douyin(self, searched: re.Match[str]):
35
- ty, vid = searched.group("ty"), searched.group("vid")
36
- if ty == "slides":
37
- return await self.parse_slides(vid)
38
-
39
- for url in (self._build_m_douyin_url(ty, vid), self._build_iesdouyin_url(ty, vid)):
40
- try:
41
- return await self.parse_video(url)
42
- except ParseException as e:
43
- logger.warning(f"failed to parse {url}, error: {e}")
44
- continue
45
- raise ParseException("分享已删除或资源直链提取失败, 请稍后再试")
46
-
47
- @staticmethod
48
- def _build_iesdouyin_url(ty: str, vid: str) -> str:
49
- return f"https://www.iesdouyin.com/share/{ty}/{vid}"
50
-
51
- @staticmethod
52
- def _build_m_douyin_url(ty: str, vid: str) -> str:
53
- return f"https://m.douyin.com/share/{ty}/{vid}"
54
-
55
- async def parse_video(self, url: str):
56
- from . import video
57
-
58
- async with AsyncClient(
59
- headers=self.ios_headers,
60
- timeout=COMMON_TIMEOUT,
61
- follow_redirects=False,
62
- verify=False,
63
- ) as client:
64
- response = await client.get(url)
65
- if response.status_code != 200:
66
- raise ParseException(f"status: {response.status_code}")
67
- text = response.text
68
-
69
- pattern = re.compile(
70
- pattern=r"window\._ROUTER_DATA\s*=\s*(.*?)</script>",
71
- flags=re.DOTALL,
72
- )
73
- matched = pattern.search(text)
74
-
75
- if not matched or not matched.group(1):
76
- raise ParseException("can't find _ROUTER_DATA in html")
77
-
78
- video_data = video.decoder.decode(matched.group(1).strip()).video_data
79
-
80
- # 作者
81
- author = self.create_author(
82
- video_data.author.nickname,
83
- video_data.avatar_url,
84
- )
85
-
86
- # 先以部分数据构建结果,后续再填充内容,避免使用临时变量
87
- result = self.result(
88
- title=video_data.desc,
89
- author=author,
90
- timestamp=video_data.create_time,
91
- )
92
-
93
- # 添加图片内容
94
- if image_urls := video_data.image_urls:
95
- result.contents.extend(self.create_images(image_urls))
96
- # 添加视频内容
97
- elif video_url := video_data.video_url:
98
- result.video = self.create_video(
99
- video_url,
100
- video_data.cover_url,
101
- video_data.duration,
102
- )
103
-
104
- return result
105
-
106
- async def parse_slides(self, video_id: str):
107
- from . import slides
108
-
109
- url = "https://www.iesdouyin.com/web/api/v2/aweme/slidesinfo/"
110
- params = {
111
- "aweme_ids": f"[{video_id}]",
112
- "request_source": "200",
113
- }
114
- async with AsyncClient(headers=self.android_headers, verify=False) as client:
115
- response = await client.get(url, params=params)
116
- response.raise_for_status()
117
-
118
- slides_data = slides.decoder.decode(response.content).aweme_details[0]
119
-
120
- # 作者
121
- author = self.create_author(slides_data.name, slides_data.avatar_url)
122
-
123
- # 先以部分数据构建结果,后续再填充内容,避免使用临时变量
124
- result = self.result(
125
- title=slides_data.desc,
126
- author=author,
127
- timestamp=slides_data.create_time,
128
- )
129
-
130
- # 优先取动图
131
- if dynamic_urls := slides_data.dynamic_urls:
132
- for dynamic_url in dynamic_urls:
133
- result.contents.append(self.create_gif(dynamic_url))
134
- elif image_urls := slides_data.image_urls:
135
- result.contents.extend(self.create_images(image_urls))
136
-
137
- return result
@@ -1,63 +0,0 @@
1
- from random import choice
2
-
3
- from msgspec import Struct, field
4
- from msgspec.json import Decoder
5
-
6
-
7
- class PlayAddr(Struct):
8
- url_list: list[str]
9
-
10
-
11
- class Cover(Struct):
12
- url_list: list[str]
13
-
14
-
15
- class Video(Struct):
16
- play_addr: PlayAddr
17
- cover: Cover
18
- duration: int
19
-
20
-
21
- class Image(Struct):
22
- video: Video | None = None
23
- url_list: list[str] = field(default_factory=list)
24
-
25
-
26
- class Avatar(Struct):
27
- url_list: list[str]
28
-
29
-
30
- class Author(Struct):
31
- nickname: str
32
- # avatar_larger: Avatar
33
- avatar_thumb: Avatar
34
-
35
-
36
- class SlidesData(Struct):
37
- author: Author
38
- desc: str
39
- create_time: int
40
- images: list[Image]
41
-
42
- @property
43
- def name(self) -> str:
44
- return self.author.nickname
45
-
46
- @property
47
- def avatar_url(self) -> str:
48
- return choice(self.author.avatar_thumb.url_list)
49
-
50
- @property
51
- def image_urls(self) -> list[str]:
52
- return [choice(image.url_list) for image in self.images]
53
-
54
- @property
55
- def dynamic_urls(self) -> list[str]:
56
- return [choice(image.video.play_addr.url_list) for image in self.images if image.video]
57
-
58
-
59
- class SlidesInfo(Struct):
60
- aweme_details: list[SlidesData] = field(default_factory=list)
61
-
62
-
63
- decoder = Decoder(SlidesInfo)
@@ -1,103 +0,0 @@
1
- from random import choice
2
- from typing import Any
3
-
4
- from msgspec import Struct, field
5
- from msgspec.json import Decoder
6
-
7
- from ..base import ParseException
8
-
9
-
10
- class Avatar(Struct):
11
- url_list: list[str]
12
-
13
-
14
- class Author(Struct):
15
- nickname: str
16
- avatar_thumb: Avatar | None = None
17
- avatar_medium: Avatar | None = None
18
-
19
-
20
- class PlayAddr(Struct):
21
- url_list: list[str]
22
-
23
-
24
- class Cover(Struct):
25
- url_list: list[str]
26
-
27
-
28
- class Video(Struct):
29
- play_addr: PlayAddr
30
- cover: Cover
31
- duration: int
32
-
33
-
34
- class Image(Struct):
35
- video: Video | None = None
36
- url_list: list[str] = field(default_factory=list)
37
-
38
-
39
- class VideoData(Struct):
40
- create_time: int
41
- author: Author
42
- desc: str
43
- images: list[Image] | None = None
44
- video: Video | None = None
45
-
46
- @property
47
- def image_urls(self) -> list[str]:
48
- return [choice(image.url_list) for image in self.images] if self.images else []
49
-
50
- @property
51
- def video_url(self) -> str | None:
52
- return choice(self.video.play_addr.url_list).replace("playwm", "play") if self.video else None
53
-
54
- @property
55
- def cover_url(self) -> str | None:
56
- return choice(self.video.cover.url_list) if self.video else None
57
-
58
- @property
59
- def duration(self) -> int | None:
60
- return self.video.duration // 1000 if self.video else None
61
-
62
- @property
63
- def avatar_url(self) -> str | None:
64
- if avatar := self.author.avatar_thumb:
65
- return choice(avatar.url_list)
66
- elif avatar := self.author.avatar_medium:
67
- return choice(avatar.url_list)
68
- return None
69
-
70
-
71
- class VideoInfoRes(Struct):
72
- item_list: list[VideoData] = field(default_factory=list)
73
-
74
- @property
75
- def video_data(self) -> VideoData:
76
- if len(self.item_list) == 0:
77
- raise ParseException("can't find data in videoInfoRes")
78
- return choice(self.item_list)
79
-
80
-
81
- class VideoOrNotePage(Struct):
82
- video_info_res: VideoInfoRes = field(name="videoInfoRes", default_factory=VideoInfoRes)
83
-
84
-
85
- class LoaderData(Struct):
86
- video_page: VideoOrNotePage | None = field(name="video_(id)/page", default=None)
87
- note_page: VideoOrNotePage | None = field(name="note_(id)/page", default=None)
88
-
89
-
90
- class RouterData(Struct):
91
- loader_data: LoaderData = field(name="loaderData", default_factory=LoaderData)
92
- errors: dict[str, Any] | None = None
93
-
94
- @property
95
- def video_data(self) -> VideoData:
96
- if page := self.loader_data.video_page:
97
- return page.video_info_res.video_data
98
- elif page := self.loader_data.note_page:
99
- return page.video_info_res.video_data
100
- raise ParseException("can't find video_(id)/page or note_(id)/page in router data")
101
-
102
-
103
- decoder = Decoder(RouterData)