parsehub 2.1.2__tar.gz → 2.1.4__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 (73) hide show
  1. {parsehub-2.1.2/src/parsehub.egg-info → parsehub-2.1.4}/PKG-INFO +2 -2
  2. {parsehub-2.1.2 → parsehub-2.1.4}/pyproject.toml +3 -2
  3. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/base/ytdlp.py +2 -2
  4. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/bilibili.py +6 -5
  5. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/douyin.py +28 -14
  6. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/bilibili.py +4 -4
  7. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/coolapk.py +4 -4
  8. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/douyin.py +1 -1
  9. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/instagram.py +5 -5
  10. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/twitter.py +1 -1
  11. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/zhihu.py +4 -4
  12. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/types/result.py +13 -13
  13. {parsehub-2.1.2 → parsehub-2.1.4/src/parsehub.egg-info}/PKG-INFO +2 -2
  14. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub.egg-info/requires.txt +1 -1
  15. {parsehub-2.1.2 → parsehub-2.1.4}/test/test_core_offline.py +1 -2
  16. {parsehub-2.1.2 → parsehub-2.1.4}/LICENSE +0 -0
  17. {parsehub-2.1.2 → parsehub-2.1.4}/README.md +0 -0
  18. {parsehub-2.1.2 → parsehub-2.1.4}/setup.cfg +0 -0
  19. {parsehub-2.1.2 → parsehub-2.1.4}/src/__init__.py +0 -0
  20. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/__init__.py +0 -0
  21. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/cli.py +0 -0
  22. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/cli_config.py +0 -0
  23. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/config/__init__.py +0 -0
  24. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/config/config.py +0 -0
  25. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/errors.py +0 -0
  26. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/__init__.py +0 -0
  27. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/base/__init__.py +0 -0
  28. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/base/base.py +0 -0
  29. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/__init__.py +0 -0
  30. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/coolapk.py +0 -0
  31. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/facebook.py +0 -0
  32. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/instagram.py +0 -0
  33. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/kuaishou.py +0 -0
  34. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/pipix.py +0 -0
  35. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/snapchat.py +0 -0
  36. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/threads.py +0 -0
  37. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/tieba.py +0 -0
  38. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/tiktok.py +0 -0
  39. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/twitter.py +0 -0
  40. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/weibo.py +0 -0
  41. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/weixin.py +0 -0
  42. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/xhs.py +0 -0
  43. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
  44. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/youtube.py +0 -0
  45. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/zhihu.py +0 -0
  46. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/parsers/parser/zuiyou.py +0 -0
  47. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/__init__.py +0 -0
  48. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/kuaishou.py +0 -0
  49. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/pipix.py +0 -0
  50. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/threads.py +0 -0
  51. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/tieba.py +0 -0
  52. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/tiktok.py +0 -0
  53. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/weibo.py +0 -0
  54. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/weixin.py +0 -0
  55. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/xhs.py +0 -0
  56. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/xiaoheihe.py +0 -0
  57. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/provider_api/zuiyou.py +0 -0
  58. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/types/__init__.py +0 -0
  59. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/types/callback.py +0 -0
  60. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/types/media_file.py +0 -0
  61. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/types/media_ref.py +0 -0
  62. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/types/platform.py +0 -0
  63. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/types/post.py +0 -0
  64. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/utils/downloader.py +0 -0
  65. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/utils/helpers.py +0 -0
  66. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub/utils/media_info.py +0 -0
  67. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub.egg-info/SOURCES.txt +0 -0
  68. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub.egg-info/dependency_links.txt +0 -0
  69. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub.egg-info/entry_points.txt +0 -0
  70. {parsehub-2.1.2 → parsehub-2.1.4}/src/parsehub.egg-info/top_level.txt +0 -0
  71. {parsehub-2.1.2 → parsehub-2.1.4}/test/test_cli.py +0 -0
  72. {parsehub-2.1.2 → parsehub-2.1.4}/test/test_cli_config.py +0 -0
  73. {parsehub-2.1.2 → parsehub-2.1.4}/test/test_downloader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
@@ -34,7 +34,7 @@ Requires-Dist: python-slugify[unidecode]>=8.0.4
34
34
  Requires-Dist: opencv-python-headless>=4.13.0.92
35
35
  Requires-Dist: cryptography>=49.0.0
36
36
  Requires-Dist: gmssl>=3.2.2
37
- Requires-Dist: SignerPy>=0.12.0
37
+ Requires-Dist: signerpy>=0.12.0
38
38
  Provides-Extra: cli
39
39
  Requires-Dist: argcomplete>=3.6.3; extra == "cli"
40
40
  Requires-Dist: platformdirs>=4.5.1; extra == "cli"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "parsehub"
3
- version = "2.1.2"
3
+ version = "2.1.4"
4
4
  description = "轻量、异步、开箱即用的社交媒体聚合解析库"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12.0"
@@ -37,7 +37,7 @@ dependencies = [
37
37
  "opencv-python-headless>=4.13.0.92",
38
38
  "cryptography>=49.0.0",
39
39
  "gmssl>=3.2.2",
40
- "SignerPy>=0.12.0",
40
+ "signerpy>=0.12.0",
41
41
  ]
42
42
 
43
43
  [project.scripts]
@@ -66,6 +66,7 @@ Issues = "https://github.com/z-mio/parsehub/issues"
66
66
 
67
67
  [tool.ruff]
68
68
  line-length = 120
69
+ exclude = ["test/_*.py"]
69
70
 
70
71
  [tool.ruff.lint]
71
72
  select = [
@@ -445,9 +445,9 @@ class YtVideoParseResult(VideoParseResult):
445
445
  def __init__(
446
446
  self,
447
447
  dl: "YtVideoInfo",
448
- title: str | None,
448
+ title: str = "",
449
449
  video: VideoRef | None = None,
450
- content: str | None = None,
450
+ content: str = "",
451
451
  ):
452
452
  """dl: yt-dlp解析结果"""
453
453
  self.dl = dl
@@ -106,7 +106,6 @@ class BiliParse(BaseParser):
106
106
  view = data["View"]
107
107
 
108
108
  cid = view["cid"]
109
- part = ""
110
109
  duration = view["duration"]
111
110
  dimension = view["dimension"]
112
111
  desc = view["desc"]
@@ -114,7 +113,6 @@ class BiliParse(BaseParser):
114
113
  if p != 1 and (pages := view.get("pages")):
115
114
  if page_info := next((i for i in pages if i["page"] == p), None):
116
115
  cid = page_info["cid"]
117
- part = page_info["part"]
118
116
  duration = page_info["duration"]
119
117
  dimension = page_info["dimension"]
120
118
 
@@ -123,9 +121,12 @@ class BiliParse(BaseParser):
123
121
 
124
122
  durl = video_playurl["data"]["durl"][0]
125
123
  video_url = self.change_source(durl["backup_url"][0]) if durl.get("backup_url") else durl["url"]
124
+ content = desc.strip()
125
+ if content == "-":
126
+ content = ""
126
127
  return BiliVideoParseResult(
127
128
  title=data["View"]["title"],
128
- content=f"P{p}: {part}" if part else (desc.strip() or ""),
129
+ content=content,
129
130
  video=VideoRef(
130
131
  url=video_url,
131
132
  thumb_url=data["View"]["pic"],
@@ -147,9 +148,9 @@ class BiliParse(BaseParser):
147
148
  )
148
149
 
149
150
  @staticmethod
150
- def hashtag_handler(desc: str | None) -> str | None:
151
+ def hashtag_handler(desc: str) -> str:
151
152
  if not desc:
152
- return None
153
+ return ""
153
154
  hashtags = re.findall(r" ?#[^#]+# ?", desc)
154
155
  for hashtag in hashtags:
155
156
  desc = desc.replace(hashtag, f" {hashtag.strip().removesuffix('#')} ")
@@ -117,13 +117,18 @@ def remove_video_watermark(url: str) -> str:
117
117
  return url.replace("playwm", "play")
118
118
 
119
119
 
120
- def parse_video_info(video_data: dict) -> dict:
121
- bit_rates = video_data.get("bit_rate")
122
- if not bit_rates:
123
- raise ParseError("抖音解析失败: 未获取到视频下载地址")
124
-
125
- # 按分辨率、文件大小、码率降序排列,选择最高质量。
126
- # Story/日常的移动端 default 播放地址通常没有 br 参数,但 data_size 最大。
120
+ def _p_play_addr(video_data: dict) -> tuple[str | None, int, int]:
121
+ play_addr = video_data["play_addr"]
122
+ url_list = play_addr["url_list"]
123
+ if not url_list:
124
+ return None, 0, 0
125
+ return url_list[0], play_addr.get("width", 0), play_addr.get("height", 0)
126
+
127
+
128
+ def _p_bit_rates(video_data: dict, filter_bytevc1: bool = False) -> tuple[str, int, int]:
129
+ bit_rates: list = video_data.get("bit_rate", [])
130
+ if filter_bytevc1:
131
+ bit_rates = list(filter(lambda i: i.get("is_bytevc1", 0) == 0, bit_rates))
127
132
  bit_rates.sort(
128
133
  key=lambda x: (
129
134
  x.get("play_addr", {}).get("width", 0) * x.get("play_addr", {}).get("height", 0),
@@ -135,11 +140,20 @@ def parse_video_info(video_data: dict) -> dict:
135
140
  best_quality = bit_rates[0]
136
141
 
137
142
  play_addr = best_quality.get("play_addr", {})
138
- video_url_list = play_addr.get("url_list", [])
139
- if not video_url_list:
140
- raise ParseError("抖音解析失败: 视频下载地址为空")
143
+ url_list = play_addr.get("url_list", [])
144
+ return url_list[0], play_addr.get("width", 0), play_addr.get("height", 0)
145
+
146
+
147
+ def parse_video_info(video_data: dict) -> dict:
148
+ is_app = video_data.get("is_bytevc1", None) is not None # 只有 app 接口带 is_bytevc1 参数
149
+ if is_app:
150
+ u, w, h = _p_play_addr(video_data)
151
+ if not u:
152
+ u, w, h = _p_bit_rates(video_data, True)
153
+ else:
154
+ u, w, h = _p_bit_rates(video_data)
141
155
 
142
- video_url = remove_video_watermark(video_url_list[0])
156
+ video_url = remove_video_watermark(u)
143
157
 
144
158
  cover = video_data.get("cover", {})
145
159
  thumb_url_list = cover.get("url_list", [])
@@ -148,9 +162,9 @@ def parse_video_info(video_data: dict) -> dict:
148
162
  return {
149
163
  "video_url": video_url,
150
164
  "thumb_url": thumb_url,
151
- "duration": best_quality.get("duration", 0),
152
- "width": play_addr.get("width", 0),
153
- "height": play_addr.get("height", 0),
165
+ "duration": video_data.get("duration", 0),
166
+ "width": w,
167
+ "height": h,
154
168
  }
155
169
 
156
170
 
@@ -245,8 +245,8 @@ class BiliImage:
245
245
 
246
246
  @dataclass(kw_only=True)
247
247
  class BiliDynamic:
248
- title: str | None = None
249
- content: str | None = None
248
+ title: str = ""
249
+ content: str = ""
250
250
  images: list[BiliImage] | None = None
251
251
 
252
252
  @classmethod
@@ -361,10 +361,10 @@ class BiliDynamic:
361
361
  return cls(title=title)
362
362
 
363
363
  @staticmethod
364
- def _get_desc_text(module_dynamic: dict) -> str | None:
364
+ def _get_desc_text(module_dynamic: dict) -> str:
365
365
  if desc := module_dynamic["desc"]:
366
366
  return str(desc["text"]).strip()
367
- return None
367
+ return ""
368
368
 
369
369
  @staticmethod
370
370
  def _get_major_cover(major_content: dict) -> list[BiliImage] | None:
@@ -10,9 +10,9 @@ from ..utils.helpers import UA
10
10
 
11
11
  @dataclass
12
12
  class Coolapk:
13
- title: str | None = None
14
- markdown_content: str | None = None
15
- text_content: str | None = None
13
+ title: str = ""
14
+ markdown_content: str = ""
15
+ text_content: str = ""
16
16
  imgs: list[str] | None = None
17
17
 
18
18
  @classmethod
@@ -37,6 +37,6 @@ class Coolapk:
37
37
  if feed_element and (feed_content := feed_element.text.strip()):
38
38
  message_image_group = soup.find(class_="message-image-group")
39
39
  imgs = [f"https:{i['src']}" for i in message_image_group.find_all("img")] if message_image_group else []
40
- return cls(None, None, feed_content, imgs)
40
+ return cls("", "", feed_content, imgs)
41
41
 
42
42
  raise ValueError("获取内容失败, 分享时请保留 shareKey 或 s 参数")
@@ -1147,7 +1147,7 @@ class DouyinMobileCrawler:
1147
1147
  return cast(dict[str, Any], payload)
1148
1148
  last_error = f"{host} missing aweme_detail: {payload.get('status_msg') or payload}"
1149
1149
  await asyncio.sleep(0.15)
1150
- raise ParseError(f"获取抖音 Story/日常失败: {last_error}")
1150
+ raise ParseError(f"获取抖音作品失败: {last_error}")
1151
1151
 
1152
1152
  async def parse(self, raw_url: str) -> dict:
1153
1153
  aweme_id = await self.get_aweme_id(raw_url)
@@ -65,17 +65,17 @@ class InstagramPost:
65
65
  return bool(self._field("is_video"))
66
66
 
67
67
  @property
68
- def title(self) -> str | None:
69
- return self._node.get("title")
68
+ def title(self) -> str:
69
+ return str(self._node.get("title", ""))
70
70
 
71
71
  @property
72
- def caption(self) -> str | None:
72
+ def caption(self) -> str:
73
73
  caption_edges = self._node.get("edge_media_to_caption", {}).get("edges") or []
74
74
  if caption_edges:
75
75
  if text := caption_edges[0].get("node", {}).get("text"):
76
76
  return str(text)
77
- return None
78
- return self._node.get("caption")
77
+ return ""
78
+ return str(self._node.get("caption", ""))
79
79
 
80
80
  @property
81
81
  def url(self) -> str:
@@ -173,7 +173,7 @@ class TwitterTweet:
173
173
  def __init__(
174
174
  self,
175
175
  tweet_id: str,
176
- full_text: str | None = None,
176
+ full_text: str = "",
177
177
  media: list[TwitterVideo | TwitterPhoto | TwitterAni] | None = None,
178
178
  article: TwitterArticle | None = None,
179
179
  ):
@@ -91,8 +91,8 @@ class ZhihuQA:
91
91
  class ZhihuZhuanLan:
92
92
  title: str
93
93
  imgs: list[str]
94
- markdown_content: str | None = None
95
- plaintext_content: str | None = None
94
+ markdown_content: str = ""
95
+ plaintext_content: str = ""
96
96
 
97
97
  @classmethod
98
98
  def parse(cls, data: dict) -> Self:
@@ -122,8 +122,8 @@ class ZhihuPin:
122
122
  type: ZhihuPinType
123
123
  title: str
124
124
  media: list[ZhihuMedia]
125
- markdown_content: str | None = None
126
- plaintext_content: str | None = None
125
+ markdown_content: str = ""
126
+ plaintext_content: str = ""
127
127
 
128
128
  @classmethod
129
129
  def parse(cls, result: dict) -> "ZhihuPin":
@@ -30,8 +30,8 @@ class ParseResult(ABC): # noqa: B024
30
30
 
31
31
  def __init__(
32
32
  self,
33
- title: str | None = "",
34
- content: str | None = "",
33
+ title: str = "",
34
+ content: str = "",
35
35
  media: Sequence[AnyMediaRef] | AnyMediaRef | None = None,
36
36
  platform: Platform | None = None,
37
37
  ):
@@ -42,8 +42,8 @@ class ParseResult(ABC): # noqa: B024
42
42
  :param platform: 平台
43
43
  """
44
44
  self.raw_url: str = ""
45
- self.title = (title or "").strip()
46
- self.content = (content or "").strip()
45
+ self.title = title.strip()
46
+ self.content = content.strip()
47
47
  self.media = media
48
48
  self.platform = platform
49
49
  self.name = slugify(
@@ -294,9 +294,9 @@ class VideoParseResult(ParseResult):
294
294
 
295
295
  def __init__(
296
296
  self,
297
- title: str | None = "",
297
+ title: str = "",
298
298
  video: str | VideoRef | None = None,
299
- content: str | None = "",
299
+ content: str = "",
300
300
  ):
301
301
  video = VideoRef(url=video) if isinstance(video, str) else video
302
302
  super().__init__(
@@ -313,9 +313,9 @@ class ImageParseResult(ParseResult):
313
313
 
314
314
  def __init__(
315
315
  self,
316
- title: str | None = "",
316
+ title: str = "",
317
317
  photo: Sequence[str | ImageRef | AniRef | LivePhotoRef] | None = None,
318
- content: str | None = "",
318
+ content: str = "",
319
319
  ):
320
320
  media = [ImageRef(url=p) if isinstance(p, str) else p for p in photo] if photo else None
321
321
  super().__init__(title=title, media=media, content=content)
@@ -328,9 +328,9 @@ class MultimediaParseResult(ParseResult):
328
328
 
329
329
  def __init__(
330
330
  self,
331
- title: str | None = "",
331
+ title: str = "",
332
332
  media: Sequence[AnyMediaRef] | None = None,
333
- content: str | None = "",
333
+ content: str = "",
334
334
  ):
335
335
  super().__init__(title=title, media=media, content=content)
336
336
 
@@ -342,16 +342,16 @@ class RichTextParseResult(ParseResult):
342
342
 
343
343
  def __init__(
344
344
  self,
345
- title: str | None = "",
345
+ title: str = "",
346
346
  media: Sequence[AnyMediaRef] | None = None,
347
- markdown_content: str | None = "",
347
+ markdown_content: str = "",
348
348
  ):
349
349
  """
350
350
  :param title: 标题
351
351
  :param media: 文章中的媒体
352
352
  :param markdown_content: markdown 格式正文
353
353
  """
354
- self.markdown_content = markdown_content or ""
354
+ self.markdown_content = markdown_content
355
355
  super().__init__(title=title, media=media, content=self.plaintext_content)
356
356
 
357
357
  def __repr__(self) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
@@ -34,7 +34,7 @@ Requires-Dist: python-slugify[unidecode]>=8.0.4
34
34
  Requires-Dist: opencv-python-headless>=4.13.0.92
35
35
  Requires-Dist: cryptography>=49.0.0
36
36
  Requires-Dist: gmssl>=3.2.2
37
- Requires-Dist: SignerPy>=0.12.0
37
+ Requires-Dist: signerpy>=0.12.0
38
38
  Provides-Extra: cli
39
39
  Requires-Dist: argcomplete>=3.6.3; extra == "cli"
40
40
  Requires-Dist: platformdirs>=4.5.1; extra == "cli"
@@ -17,7 +17,7 @@ python-slugify[unidecode]>=8.0.4
17
17
  opencv-python-headless>=4.13.0.92
18
18
  cryptography>=49.0.0
19
19
  gmssl>=3.2.2
20
- SignerPy>=0.12.0
20
+ signerpy>=0.12.0
21
21
 
22
22
  [cli]
23
23
  argcomplete>=3.6.3
@@ -265,11 +265,11 @@ class TestDouyinStorySupport(unittest.TestCase):
265
265
 
266
266
  def test_parse_video_info_prefers_story_default_play_url_by_data_size(self):
267
267
  video_data = {
268
+ "duration": 9682,
268
269
  "cover": {"url_list": ["https://cdn.example/thumb.jpg"]},
269
270
  "bit_rate": [
270
271
  {
271
272
  "bit_rate": 0,
272
- "duration": 9682,
273
273
  "play_addr": {
274
274
  "url_list": ["https://cdn.example/story-default.mp4"],
275
275
  "width": 720,
@@ -279,7 +279,6 @@ class TestDouyinStorySupport(unittest.TestCase):
279
279
  },
280
280
  {
281
281
  "bit_rate": 632,
282
- "duration": 9682,
283
282
  "play_addr": {
284
283
  "url_list": ["https://cdn.example/720p.mp4"],
285
284
  "width": 720,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes