nonebot-plugin-parser 2.0.12__tar.gz → 2.0.13__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 (54) hide show
  1. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/PKG-INFO +1 -2
  2. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/pyproject.toml +16 -7
  3. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/__init__.py +1 -1
  4. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/config.py +5 -6
  5. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +27 -8
  6. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/douyin/slides.py +7 -6
  7. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/douyin/video.py +13 -12
  8. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/kuaishou.py +4 -4
  9. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/README.md +0 -0
  10. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/constants.py +0 -0
  11. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/download/__init__.py +0 -0
  12. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/download/task.py +0 -0
  13. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  14. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/exception.py +0 -0
  15. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/helper.py +0 -0
  16. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/matchers/__init__.py +0 -0
  17. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  18. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/matchers/preprocess.py +0 -0
  19. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/__init__.py +0 -0
  20. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/acfun.py +0 -0
  21. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/base.py +0 -0
  22. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +0 -0
  23. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  24. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  25. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
  26. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  27. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  28. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  29. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  30. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  31. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/data.py +0 -0
  32. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
  33. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/tiktok.py +0 -0
  34. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/twitter.py +0 -0
  35. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/weibo.py +0 -0
  36. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/xiaohongshu.py +0 -0
  37. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/parsers/youtube.py +0 -0
  38. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/__init__.py +0 -0
  39. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/base.py +0 -0
  40. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/common.py +0 -0
  41. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/default.py +0 -0
  42. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW-1.ttf +0 -0
  43. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  44. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  45. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  46. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/media_button.png +0 -0
  47. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  48. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  49. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  50. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  51. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/resources/youtube.png +0 -0
  52. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -0
  53. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/src/nonebot_plugin_parser/renders/weibo.py +0 -0
  54. {nonebot_plugin_parser-2.0.12 → nonebot_plugin_parser-2.0.13}/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.0.12
3
+ Version: 2.0.13
4
4
  Summary: NoneBot2 链接分享解析 Alconna 版, 通用媒体卡片渲染(PIL 实现), 支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga
5
5
  Keywords: nonebot,nonebot2,video,bilibili,youtube,tiktok,twitter,kuaishou,acfun,weibo,xiaohongshu,nga,douyin
6
6
  Author: fllesser
@@ -20,7 +20,6 @@ Requires-Dist: nonebot-plugin-apscheduler>=0.5.0,<1.0.0
20
20
  Requires-Dist: nonebot-plugin-alconna>=0.59.4
21
21
  Requires-Dist: nonebot-plugin-uninfo>=0.9.0
22
22
  Requires-Dist: nonebot-plugin-htmlkit>=0.1.0rc3 ; extra == 'htmlkit'
23
- Requires-Dist: jinja2>=3.1.6 ; extra == 'htmlkit'
24
23
  Requires-Python: >=3.10
25
24
  Project-URL: IssueTracker, https://github.com/fllesser/nonebot-plugin-parser/issues
26
25
  Project-URL: Release, https://github.com/fllesser/nonebot-plugin-parser/releases
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parser"
3
- version = "2.0.12"
3
+ version = "2.0.13"
4
4
  description = "NoneBot2 链接分享解析 Alconna 版, 通用媒体卡片渲染(PIL 实现), 支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga"
5
5
  authors = [{ "name" = "fllesser", "email" = "fllessive@gmail.com" }]
6
6
  readme = "README.md"
@@ -41,7 +41,7 @@ dependencies = [
41
41
  ]
42
42
 
43
43
  [project.optional-dependencies]
44
- htmlkit = ["nonebot-plugin-htmlkit>=0.1.0rc3", "jinja2>=3.1.6"]
44
+ htmlkit = ["nonebot-plugin-htmlkit>=0.1.0rc3"]
45
45
 
46
46
  [project.urls]
47
47
  Repository = "https://github.com/fllesser/nonebot-plugin-parser"
@@ -52,10 +52,8 @@ Release = "https://github.com/fllesser/nonebot-plugin-parser/releases"
52
52
  dev = [
53
53
  "nb-cli>=1.4.2",
54
54
  "nonebot2[fastapi]>=2.4.3,<3.0.0",
55
- "nonebot-adapter-telegram>=0.1.0b20",
56
55
  "pre-commit>=4.3.0",
57
56
  "ruff>=0.14.0,<1.0.0",
58
- "bump-my-version>=1.2.4",
59
57
  ]
60
58
 
61
59
  test = [
@@ -69,16 +67,27 @@ test = [
69
67
  "respx>=0.22.0",
70
68
  ]
71
69
 
72
- all_extras = ["nonebot-plugin-htmlkit>=0.1.0rc1", "jinja2>=3.1.6"]
70
+ telegram = ["nonebot-adapter-telegram>=0.1.0b20"]
71
+ pydantic_v1 = ["pydantic<2.0.0"]
72
+ pydantic_v2 = ["pydantic>=2.0.0"]
73
+
74
+ all_extras = ["nonebot-plugin-htmlkit>=0.1.0rc3"]
73
75
 
74
76
  [tool.uv]
75
77
  required-version = ">=0.9.2"
76
78
  default-groups = ["test", "dev", "all_extras"]
79
+ conflicts = [
80
+ [
81
+ { group = "pydantic_v1" },
82
+ { group = "pydantic_v2" },
83
+ { group = "telegram" },
84
+ ],
85
+ ]
77
86
 
78
87
  [tool.nonebot]
79
88
  adapters = [
80
89
  { "name" = "Onebot V11", module_name = "nonebot.adapters.onebot.v11" },
81
- { name = "Telegram", module_name = "nonebot.adapters.telegram" },
90
+ # { name = "Telegram", module_name = "nonebot.adapters.telegram" },
82
91
  ]
83
92
  plugins = ["nonebot_plugin_parser"]
84
93
 
@@ -186,7 +195,7 @@ build-backend = "uv_build"
186
195
 
187
196
 
188
197
  [tool.bumpversion]
189
- current_version = "2.0.12"
198
+ current_version = "2.0.13"
190
199
  commit = true
191
200
  message = "🔖 release: bump vesion from {current_version} to {new_version}"
192
201
  tag = true
@@ -12,7 +12,7 @@ from .utils import safe_unlink
12
12
 
13
13
  __plugin_meta__ = PluginMetadata(
14
14
  name="链接分享解析 Alconna 版",
15
- description="全新通用媒体卡片渲染(PIL 实现), 支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga",
15
+ description="全新通用媒体卡片渲染(PIL 实现)\n支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga",
16
16
  usage="发送支持平台的(BV号/链接/小程序/卡片)即可",
17
17
  type="application",
18
18
  homepage="https://github.com/fllesser/nonebot-plugin-parser",
@@ -1,5 +1,4 @@
1
1
  from enum import Enum
2
- from functools import cached_property
3
2
  from pathlib import Path
4
3
  from typing import Literal
5
4
 
@@ -57,22 +56,22 @@ class Config(BaseModel):
57
56
  parser_need_forward_contents: bool = True
58
57
  """是否需要转发媒体内容"""
59
58
 
60
- @cached_property
59
+ @property
61
60
  def nickname(self) -> str:
62
61
  """全局名称"""
63
62
  return _nickname
64
63
 
65
- @cached_property
64
+ @property
66
65
  def cache_dir(self) -> Path:
67
66
  """插件缓存目录"""
68
67
  return _cache_dir
69
68
 
70
- @cached_property
69
+ @property
71
70
  def config_dir(self) -> Path:
72
71
  """插件配置目录"""
73
72
  return _config_dir
74
73
 
75
- @cached_property
74
+ @property
76
75
  def data_dir(self) -> Path:
77
76
  """插件数据目录"""
78
77
  return _data_dir
@@ -132,7 +131,7 @@ class Config(BaseModel):
132
131
  """是否在解析结果中附加原始URL"""
133
132
  return self.parser_append_url
134
133
 
135
- @cached_property
134
+ @property
136
135
  def custom_font(self) -> Path | None:
137
136
  """自定义字体"""
138
137
  return (self.data_dir / self.parser_custom_font) if self.parser_custom_font else None
@@ -1,6 +1,6 @@
1
1
  import re
2
2
  from typing import ClassVar
3
- from typing_extensions import override
3
+ from typing_extensions import deprecated, override
4
4
 
5
5
  import httpx
6
6
  import msgspec
@@ -18,10 +18,7 @@ class DouyinParser(BaseParser):
18
18
  # URL 正则表达式模式(keyword, pattern)
19
19
  patterns: ClassVar[list[tuple[str, str]]] = [
20
20
  ("v.douyin", r"https://v\.douyin\.com/[a-zA-Z0-9_\-]+"),
21
- (
22
- "douyin",
23
- r"https://www\.(?:douyin|iesdouyin)\.com/(?:video|note|share/(?:video|note|slides))/[0-9]+",
24
- ),
21
+ ("douyin", r"https://www\.(?:douyin|iesdouyin)\.com/[a-zA-Z0-9_\-/]+"),
25
22
  ]
26
23
 
27
24
  def _build_iesdouyin_url(self, _type: str, video_id: str) -> str:
@@ -30,6 +27,7 @@ class DouyinParser(BaseParser):
30
27
  def _build_m_douyin_url(self, _type: str, video_id: str) -> str:
31
28
  return f"https://m.douyin.com/share/{_type}/{video_id}"
32
29
 
30
+ @deprecated("use parse instead after 2.0.12, will be removed in the future")
33
31
  async def parse_share_url(self, share_url: str):
34
32
  if matched := re.match(r"(video|note)/([0-9]+)", share_url):
35
33
  # https://www.douyin.com/video/xxxxxx
@@ -45,6 +43,7 @@ class DouyinParser(BaseParser):
45
43
  _type, video_id = matched.group(1), matched.group(2)
46
44
  if _type == "slides":
47
45
  return await self.parse_slides(video_id)
46
+
48
47
  for url in [
49
48
  self._build_m_douyin_url(_type, video_id),
50
49
  share_url,
@@ -81,7 +80,6 @@ class DouyinParser(BaseParser):
81
80
  from .video import RouterData
82
81
 
83
82
  video_data = msgspec.json.decode(matched.group(1).strip(), type=RouterData).video_data
84
-
85
83
  # 使用新的简洁构建方式
86
84
  contents = []
87
85
 
@@ -152,5 +150,26 @@ class DouyinParser(BaseParser):
152
150
  ParseException: 解析失败时抛出
153
151
  """
154
152
  # 从匹配对象中获取原始URL
155
- url = matched.group(0)
156
- return await self.parse_share_url(url)
153
+ share_url = matched.group(0)
154
+ # return await self.parse_share_url(url)
155
+ if "v.douyin" in share_url:
156
+ share_url = await self.get_redirect_url(share_url)
157
+
158
+ searched = re.search(r"(slides|video|note)/(\d+)", share_url)
159
+ if not searched:
160
+ raise ParseException(f"无法从 {share_url} 中解析出 ID")
161
+ _type, video_id = searched.group(1), searched.group(2)
162
+ if _type == "slides":
163
+ return await self.parse_slides(video_id)
164
+
165
+ for url in (
166
+ self._build_m_douyin_url(_type, video_id),
167
+ self._build_iesdouyin_url(_type, video_id),
168
+ share_url,
169
+ ):
170
+ try:
171
+ return await self.parse_video(url)
172
+ except ParseException as e:
173
+ logger.warning(f"failed to parse {url[:60]}, error: {e}")
174
+ continue
175
+ raise ParseException("分享已删除或资源直链获取失败, 请稍后再试")
@@ -1,3 +1,5 @@
1
+ from random import choice
2
+
1
3
  from msgspec import Struct, field
2
4
 
3
5
 
@@ -26,7 +28,8 @@ class Avatar(Struct):
26
28
 
27
29
  class Author(Struct):
28
30
  nickname: str
29
- avatar_larger: Avatar
31
+ # avatar_larger: Avatar
32
+ avatar_thumb: Avatar
30
33
 
31
34
 
32
35
  class SlidesData(Struct):
@@ -41,17 +44,15 @@ class SlidesData(Struct):
41
44
 
42
45
  @property
43
46
  def avatar_url(self) -> str:
44
- from random import choice
45
-
46
- return choice(self.author.avatar_larger.url_list)
47
+ return choice(self.author.avatar_thumb.url_list)
47
48
 
48
49
  @property
49
50
  def image_urls(self) -> list[str]:
50
- return [image.url_list[0] for image in self.images]
51
+ return [choice(image.url_list) for image in self.images]
51
52
 
52
53
  @property
53
54
  def dynamic_urls(self) -> list[str]:
54
- return [image.video.play_addr.url_list[0] for image in self.images if image.video]
55
+ return [choice(image.video.play_addr.url_list) for image in self.images if image.video]
55
56
 
56
57
 
57
58
  class SlidesInfo(Struct):
@@ -1,3 +1,4 @@
1
+ from random import choice
1
2
  from typing import Any
2
3
 
3
4
  from msgspec import Struct, field
@@ -43,22 +44,22 @@ class VideoData(Struct):
43
44
 
44
45
  @property
45
46
  def image_urls(self) -> list[str]:
46
- return [image.url_list[0] for image in self.images] if self.images else []
47
+ return [choice(image.url_list) for image in self.images] if self.images else []
47
48
 
48
49
  @property
49
50
  def video_url(self) -> str | None:
50
- return self.video.play_addr.url_list[0].replace("playwm", "play") if self.video else None
51
+ return choice(self.video.play_addr.url_list).replace("playwm", "play") if self.video else None
51
52
 
52
53
  @property
53
54
  def cover_url(self) -> str | None:
54
- return self.video.cover.url_list[0] if self.video else None
55
+ return choice(self.video.cover.url_list) if self.video else None
55
56
 
56
57
  @property
57
58
  def avatar_url(self) -> str | None:
58
59
  if avatar := self.author.avatar_thumb:
59
- return avatar.url_list[0]
60
+ return choice(avatar.url_list)
60
61
  elif avatar := self.author.avatar_medium:
61
- return avatar.url_list[0]
62
+ return choice(avatar.url_list)
62
63
  return None
63
64
 
64
65
 
@@ -69,11 +70,11 @@ class VideoInfoRes(Struct):
69
70
  def video_data(self) -> VideoData:
70
71
  if len(self.item_list) == 0:
71
72
  raise ParseException("can't find data in videoInfoRes")
72
- return self.item_list[0]
73
+ return choice(self.item_list)
73
74
 
74
75
 
75
76
  class VideoOrNotePage(Struct):
76
- videoInfoRes: VideoInfoRes
77
+ video_info_res: VideoInfoRes = field(name="videoInfoRes", default_factory=VideoInfoRes)
77
78
 
78
79
 
79
80
  class LoaderData(Struct):
@@ -82,13 +83,13 @@ class LoaderData(Struct):
82
83
 
83
84
 
84
85
  class RouterData(Struct):
85
- loaderData: LoaderData
86
+ loader_data: LoaderData = field(name="loaderData", default_factory=LoaderData)
86
87
  errors: dict[str, Any] | None = None
87
88
 
88
89
  @property
89
90
  def video_data(self) -> VideoData:
90
- if page := self.loaderData.video_page:
91
- return page.videoInfoRes.video_data
92
- elif page := self.loaderData.note_page:
93
- return page.videoInfoRes.video_data
91
+ if page := self.loader_data.video_page:
92
+ return page.video_info_res.video_data
93
+ elif page := self.loader_data.note_page:
94
+ return page.video_info_res.video_data
94
95
  raise ParseException("can't find video_(id)/page or note_(id)/page in router data")
@@ -1,4 +1,4 @@
1
- import random
1
+ from random import choice
2
2
  import re
3
3
  from typing import ClassVar
4
4
 
@@ -106,7 +106,7 @@ class Atlas(Struct):
106
106
  def img_urls(self):
107
107
  if len(self.cdn_list) == 0 or len(self.img_route_list) == 0:
108
108
  return []
109
- cdn = random.choice(self.cdn_list).cdn
109
+ cdn = choice(self.cdn_list).cdn
110
110
  return [f"https://{cdn}/{url}" for url in self.img_route_list]
111
111
 
112
112
 
@@ -131,11 +131,11 @@ class Photo(Struct):
131
131
 
132
132
  @property
133
133
  def cover_url(self):
134
- return random.choice(self.cover_urls).url if len(self.cover_urls) != 0 else None
134
+ return choice(self.cover_urls).url if len(self.cover_urls) != 0 else None
135
135
 
136
136
  @property
137
137
  def video_url(self):
138
- return random.choice(self.main_mv_urls).url if len(self.main_mv_urls) != 0 else None
138
+ return choice(self.main_mv_urls).url if len(self.main_mv_urls) != 0 else None
139
139
 
140
140
  @property
141
141
  def img_urls(self):