rogerxavier commited on
Commit
0aee47a
1 Parent(s): 97d3f10

Upload 258 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. bilibili_api/__init__.py +166 -0
  2. bilibili_api/__pycache__/__init__.cpython-38.pyc +0 -0
  3. bilibili_api/__pycache__/app.cpython-38.pyc +0 -0
  4. bilibili_api/__pycache__/article.cpython-38.pyc +0 -0
  5. bilibili_api/__pycache__/article_category.cpython-38.pyc +0 -0
  6. bilibili_api/__pycache__/ass.cpython-38.pyc +0 -0
  7. bilibili_api/__pycache__/audio.cpython-38.pyc +0 -0
  8. bilibili_api/__pycache__/audio_uploader.cpython-38.pyc +0 -0
  9. bilibili_api/__pycache__/bangumi.cpython-38.pyc +0 -0
  10. bilibili_api/__pycache__/black_room.cpython-38.pyc +0 -0
  11. bilibili_api/__pycache__/channel_series.cpython-38.pyc +0 -0
  12. bilibili_api/__pycache__/cheese.cpython-38.pyc +0 -0
  13. bilibili_api/__pycache__/client.cpython-38.pyc +0 -0
  14. bilibili_api/__pycache__/comment.cpython-38.pyc +0 -0
  15. bilibili_api/__pycache__/creative_center.cpython-38.pyc +0 -0
  16. bilibili_api/__pycache__/dynamic.cpython-38.pyc +0 -0
  17. bilibili_api/__pycache__/emoji.cpython-38.pyc +0 -0
  18. bilibili_api/__pycache__/favorite_list.cpython-38.pyc +0 -0
  19. bilibili_api/__pycache__/festival.cpython-38.pyc +0 -0
  20. bilibili_api/__pycache__/game.cpython-38.pyc +0 -0
  21. bilibili_api/__pycache__/homepage.cpython-38.pyc +0 -0
  22. bilibili_api/__pycache__/hot.cpython-38.pyc +0 -0
  23. bilibili_api/__pycache__/interactive_video.cpython-38.pyc +0 -0
  24. bilibili_api/__pycache__/live.cpython-38.pyc +0 -0
  25. bilibili_api/__pycache__/live_area.cpython-38.pyc +0 -0
  26. bilibili_api/__pycache__/login.cpython-38.pyc +0 -0
  27. bilibili_api/__pycache__/login_func.cpython-38.pyc +0 -0
  28. bilibili_api/__pycache__/manga.cpython-38.pyc +0 -0
  29. bilibili_api/__pycache__/music.cpython-38.pyc +0 -0
  30. bilibili_api/__pycache__/note.cpython-38.pyc +0 -0
  31. bilibili_api/__pycache__/opus.cpython-38.pyc +0 -0
  32. bilibili_api/__pycache__/rank.cpython-38.pyc +0 -0
  33. bilibili_api/__pycache__/search.cpython-38.pyc +0 -0
  34. bilibili_api/__pycache__/session.cpython-38.pyc +0 -0
  35. bilibili_api/__pycache__/settings.cpython-38.pyc +0 -0
  36. bilibili_api/__pycache__/show.cpython-38.pyc +0 -0
  37. bilibili_api/__pycache__/topic.cpython-38.pyc +0 -0
  38. bilibili_api/__pycache__/user.cpython-38.pyc +0 -0
  39. bilibili_api/__pycache__/video.cpython-38.pyc +0 -0
  40. bilibili_api/__pycache__/video_tag.cpython-38.pyc +0 -0
  41. bilibili_api/__pycache__/video_uploader.cpython-38.pyc +0 -0
  42. bilibili_api/__pycache__/video_zone.cpython-38.pyc +0 -0
  43. bilibili_api/__pycache__/vote.cpython-38.pyc +0 -0
  44. bilibili_api/__pycache__/watchroom.cpython-38.pyc +0 -0
  45. bilibili_api/_pyinstaller/__pycache__/entry_points.cpython-38.pyc +0 -0
  46. bilibili_api/_pyinstaller/__pycache__/hook-bilibili_api.cpython-38.pyc +0 -0
  47. bilibili_api/_pyinstaller/entry_points.py +6 -0
  48. bilibili_api/_pyinstaller/hook-bilibili_api.py +5 -0
  49. bilibili_api/app.py +123 -0
  50. bilibili_api/article.py +1078 -0
bilibili_api/__init__.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ bilibili_api
3
+
4
+ 哔哩哔哩的各种 API 调用便捷整合(视频、动态、直播等),另外附加一些常用的功能。
5
+ """
6
+
7
+ import asyncio
8
+ import platform
9
+
10
+ from .utils.sync import sync
11
+ from .utils.credential_refresh import Credential
12
+ from .utils.picture import Picture
13
+ from .utils.short import get_real_url
14
+ from .utils.parse_link import ResourceType, parse_link
15
+ from .utils.aid_bvid_transformer import aid2bvid, bvid2aid
16
+ from .utils.danmaku import DmMode, Danmaku, DmFontSize, SpecialDanmaku
17
+ from .utils.network import (
18
+ HEADERS,
19
+ get_session,
20
+ set_session,
21
+ get_aiohttp_session,
22
+ set_aiohttp_session,
23
+ get_httpx_sync_session,
24
+ set_httpx_sync_session
25
+ )
26
+ from .errors import (
27
+ LoginError,
28
+ ApiException,
29
+ ArgsException,
30
+ LiveException,
31
+ NetworkException,
32
+ ResponseException,
33
+ VideoUploadException,
34
+ ResponseCodeException,
35
+ DanmakuClosedException,
36
+ CredentialNoBuvid3Exception,
37
+ CredentialNoBiliJctException,
38
+ DynamicExceedImagesException,
39
+ CredentialNoSessdataException,
40
+ CredentialNoDedeUserIDException,
41
+ )
42
+ from . import (
43
+ app,
44
+ ass,
45
+ hot,
46
+ game,
47
+ live,
48
+ note,
49
+ rank,
50
+ show,
51
+ user,
52
+ vote,
53
+ audio,
54
+ emoji,
55
+ login,
56
+ manga,
57
+ music,
58
+ topic,
59
+ video,
60
+ cheese,
61
+ client,
62
+ search,
63
+ article,
64
+ bangumi,
65
+ comment,
66
+ dynamic,
67
+ session,
68
+ festival,
69
+ homepage,
70
+ settings,
71
+ watchroom,
72
+ live_area,
73
+ video_tag,
74
+ black_room,
75
+ login_func,
76
+ video_zone,
77
+ favorite_list,
78
+ channel_series,
79
+ video_uploader,
80
+ creative_center,
81
+ article_category,
82
+ interactive_video,
83
+ audio_uploader,
84
+ )
85
+
86
+ BILIBILI_API_VERSION = "16.2.0"
87
+
88
+ # 如果系统为 Windows,则修改默认策略,以解决代理报错问题
89
+ if "windows" in platform.system().lower():
90
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # type: ignore
91
+
92
+ __all__ = [
93
+ "ApiException",
94
+ "ArgsException",
95
+ "BILIBILI_API_VERSION",
96
+ "Credential",
97
+ "CredentialNoBiliJctException",
98
+ "CredentialNoBuvid3Exception",
99
+ "CredentialNoDedeUserIDException",
100
+ "CredentialNoSessdataException",
101
+ "Danmaku",
102
+ "DanmakuClosedException",
103
+ "DmFontSize",
104
+ "DmMode",
105
+ "DynamicExceedImagesException",
106
+ "HEADERS",
107
+ "LiveException",
108
+ "LoginError",
109
+ "NetworkException",
110
+ "Picture",
111
+ "ResourceType",
112
+ "ResponseCodeException",
113
+ "ResponseException",
114
+ "SpecialDanmaku",
115
+ "VideoUploadException",
116
+ "aid2bvid",
117
+ "app",
118
+ "article",
119
+ "article_category",
120
+ "ass",
121
+ "audio",
122
+ "audio_uploader",
123
+ "bangumi",
124
+ "black_room",
125
+ "bvid2aid",
126
+ "channel_series",
127
+ "cheese",
128
+ "client",
129
+ "comment",
130
+ "creative_center",
131
+ "dynamic",
132
+ "emoji",
133
+ "favorite_list",
134
+ "festival",
135
+ "game",
136
+ "get_aiohttp_session",
137
+ "get_real_url",
138
+ "get_session",
139
+ "homepage",
140
+ "hot",
141
+ "interactive_video",
142
+ "live",
143
+ "live_area",
144
+ "login",
145
+ "login_func",
146
+ "manga",
147
+ "music",
148
+ "note",
149
+ "parse_link",
150
+ "rank",
151
+ "search",
152
+ "session",
153
+ "set_aiohttp_session",
154
+ "set_session",
155
+ "settings",
156
+ "show",
157
+ "sync",
158
+ "topic",
159
+ "user",
160
+ "video",
161
+ "video_tag",
162
+ "video_uploader",
163
+ "video_zone",
164
+ "vote",
165
+ "watchroom",
166
+ ]
bilibili_api/__pycache__/__init__.cpython-38.pyc ADDED
Binary file (3.07 kB). View file
 
bilibili_api/__pycache__/app.cpython-38.pyc ADDED
Binary file (2.92 kB). View file
 
bilibili_api/__pycache__/article.cpython-38.pyc ADDED
Binary file (33.1 kB). View file
 
bilibili_api/__pycache__/article_category.cpython-38.pyc ADDED
Binary file (3.96 kB). View file
 
bilibili_api/__pycache__/ass.cpython-38.pyc ADDED
Binary file (8.16 kB). View file
 
bilibili_api/__pycache__/audio.cpython-38.pyc ADDED
Binary file (5.76 kB). View file
 
bilibili_api/__pycache__/audio_uploader.cpython-38.pyc ADDED
Binary file (21 kB). View file
 
bilibili_api/__pycache__/bangumi.cpython-38.pyc ADDED
Binary file (41.3 kB). View file
 
bilibili_api/__pycache__/black_room.cpython-38.pyc ADDED
Binary file (10.2 kB). View file
 
bilibili_api/__pycache__/channel_series.cpython-38.pyc ADDED
Binary file (7.9 kB). View file
 
bilibili_api/__pycache__/cheese.cpython-38.pyc ADDED
Binary file (19.3 kB). View file
 
bilibili_api/__pycache__/client.cpython-38.pyc ADDED
Binary file (587 Bytes). View file
 
bilibili_api/__pycache__/comment.cpython-38.pyc ADDED
Binary file (12 kB). View file
 
bilibili_api/__pycache__/creative_center.cpython-38.pyc ADDED
Binary file (20.9 kB). View file
 
bilibili_api/__pycache__/dynamic.cpython-38.pyc ADDED
Binary file (26.4 kB). View file
 
bilibili_api/__pycache__/emoji.cpython-38.pyc ADDED
Binary file (732 Bytes). View file
 
bilibili_api/__pycache__/favorite_list.cpython-38.pyc ADDED
Binary file (16.2 kB). View file
 
bilibili_api/__pycache__/festival.cpython-38.pyc ADDED
Binary file (1.44 kB). View file
 
bilibili_api/__pycache__/game.cpython-38.pyc ADDED
Binary file (6.14 kB). View file
 
bilibili_api/__pycache__/homepage.cpython-38.pyc ADDED
Binary file (2.05 kB). View file
 
bilibili_api/__pycache__/hot.cpython-38.pyc ADDED
Binary file (2.34 kB). View file
 
bilibili_api/__pycache__/interactive_video.cpython-38.pyc ADDED
Binary file (35.9 kB). View file
 
bilibili_api/__pycache__/live.cpython-38.pyc ADDED
Binary file (36.7 kB). View file
 
bilibili_api/__pycache__/live_area.cpython-38.pyc ADDED
Binary file (3.93 kB). View file
 
bilibili_api/__pycache__/login.cpython-38.pyc ADDED
Binary file (16.2 kB). View file
 
bilibili_api/__pycache__/login_func.cpython-38.pyc ADDED
Binary file (6.74 kB). View file
 
bilibili_api/__pycache__/manga.cpython-38.pyc ADDED
Binary file (15.1 kB). View file
 
bilibili_api/__pycache__/music.cpython-38.pyc ADDED
Binary file (6.02 kB). View file
 
bilibili_api/__pycache__/note.cpython-38.pyc ADDED
Binary file (27.8 kB). View file
 
bilibili_api/__pycache__/opus.cpython-38.pyc ADDED
Binary file (3.9 kB). View file
 
bilibili_api/__pycache__/rank.cpython-38.pyc ADDED
Binary file (11.1 kB). View file
 
bilibili_api/__pycache__/search.cpython-38.pyc ADDED
Binary file (10.2 kB). View file
 
bilibili_api/__pycache__/session.cpython-38.pyc ADDED
Binary file (15 kB). View file
 
bilibili_api/__pycache__/settings.cpython-38.pyc ADDED
Binary file (1.03 kB). View file
 
bilibili_api/__pycache__/show.cpython-38.pyc ADDED
Binary file (7.26 kB). View file
 
bilibili_api/__pycache__/topic.cpython-38.pyc ADDED
Binary file (4.94 kB). View file
 
bilibili_api/__pycache__/user.cpython-38.pyc ADDED
Binary file (40.8 kB). View file
 
bilibili_api/__pycache__/video.cpython-38.pyc ADDED
Binary file (67.8 kB). View file
 
bilibili_api/__pycache__/video_tag.cpython-38.pyc ADDED
Binary file (3.76 kB). View file
 
bilibili_api/__pycache__/video_uploader.cpython-38.pyc ADDED
Binary file (35.8 kB). View file
 
bilibili_api/__pycache__/video_zone.cpython-38.pyc ADDED
Binary file (13.1 kB). View file
 
bilibili_api/__pycache__/vote.cpython-38.pyc ADDED
Binary file (6.41 kB). View file
 
bilibili_api/__pycache__/watchroom.cpython-38.pyc ADDED
Binary file (9.88 kB). View file
 
bilibili_api/_pyinstaller/__pycache__/entry_points.cpython-38.pyc ADDED
Binary file (407 Bytes). View file
 
bilibili_api/_pyinstaller/__pycache__/hook-bilibili_api.cpython-38.pyc ADDED
Binary file (375 Bytes). View file
 
bilibili_api/_pyinstaller/entry_points.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List
3
+
4
+
5
+ def get_hook_dirs() -> List[str]:
6
+ return [os.path.abspath(os.path.dirname(__file__))]
bilibili_api/_pyinstaller/hook-bilibili_api.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from typing import List, Tuple
2
+
3
+ from PyInstaller.utils.hooks import collect_data_files
4
+
5
+ datas: List[Tuple[str, str]] = collect_data_files("bilibili_api")
bilibili_api/app.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ bilibili_api.app
3
+
4
+ 手机 APP 相关
5
+ """
6
+
7
+ import time
8
+ from hashlib import md5
9
+ from typing import Union
10
+
11
+ from .utils.utils import get_api
12
+ from .utils.credential import Credential
13
+ from .utils.network import Api
14
+
15
+ API = get_api("app")
16
+
17
+
18
+ async def get_loading_images(
19
+ mobi_app: str = "android",
20
+ platform: str = "android",
21
+ height: int = 1920,
22
+ width: int = 1080,
23
+ build: int = 999999999,
24
+ birth: str = "",
25
+ credential: Union[Credential, None] = None,
26
+ ):
27
+ """
28
+ 获取开屏启动画面
29
+
30
+ Args:
31
+ build (int, optional) : 客户端内部版本号
32
+
33
+ mobi_app (str, optional) : android / iphone / ipad
34
+
35
+ platform (str, optional) : android / ios / ios
36
+
37
+ height (int, optional) : 屏幕高度
38
+
39
+ width (int, optional) : 屏幕宽度
40
+
41
+ birth (str, optional) : 生日日期(四位数,例 0101)
42
+
43
+ credential (Credential | None, optional): 凭据. Defaults to None.
44
+
45
+ Returns:
46
+ dict: 调用 API 返回的结果
47
+ """
48
+ credential = credential if credential is not None else Credential()
49
+
50
+ api = API["splash"]["list"]
51
+ params = {
52
+ "build": build,
53
+ "mobi_app": mobi_app,
54
+ "platform": platform,
55
+ "height": height,
56
+ "width": width,
57
+ "birth": birth,
58
+ }
59
+ return await Api(**api, credential=credential).update_params(**params).result
60
+
61
+
62
+ async def get_loading_images_special(
63
+ mobi_app: str = "android",
64
+ platform: str = "android",
65
+ height: int = 1920,
66
+ width: int = 1080,
67
+ credential: Union[Credential, None] = None,
68
+ ):
69
+ """
70
+ 获取特殊开屏启动画面
71
+
72
+ Args:
73
+ mobi_app (str, optional) : android / iphone / ipad
74
+
75
+ platform (str, optional) : android / ios / ios
76
+
77
+ height (str, optional) : 屏幕高度
78
+
79
+ width (str, optional) : 屏幕宽度
80
+
81
+ credential (Credential | None, optional): 凭据. Defaults to None.
82
+
83
+ Returns:
84
+ dict: 调用 API 返回的结果
85
+ """
86
+ APPKEY = "1d8b6e7d45233436"
87
+ APPSEC = "560c52ccd288fed045859ed18bffd973"
88
+
89
+ ts = int(time.time())
90
+
91
+ credential = credential if credential is not None else Credential()
92
+
93
+ api = API["splash"]["brand"]
94
+ sign_params = (
95
+ "appkey="
96
+ + APPKEY
97
+ + "&mobi_app="
98
+ + mobi_app
99
+ + "&platform="
100
+ + platform
101
+ + "&screen_height="
102
+ + str(height)
103
+ + "&screen_width="
104
+ + str(width)
105
+ + "&ts="
106
+ + str(ts)
107
+ + APPSEC
108
+ )
109
+
110
+ sign = md5()
111
+ sign.update(sign_params.encode(encoding="utf-8"))
112
+ sign = sign.hexdigest()
113
+
114
+ params = {
115
+ "appkey": APPKEY,
116
+ "mobi_app": mobi_app,
117
+ "platform": platform,
118
+ "screen_height": height,
119
+ "screen_width": width,
120
+ "ts": ts,
121
+ "sign": sign,
122
+ }
123
+ return await Api(**api, credential=credential).update_params(**params).result
bilibili_api/article.py ADDED
@@ -0,0 +1,1078 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ bilibili_api.article
3
+
4
+ 专栏相关
5
+ """
6
+
7
+ import re
8
+ import json
9
+ from copy import copy
10
+ from enum import Enum
11
+ from html import unescape
12
+ from datetime import datetime
13
+ from urllib.parse import unquote
14
+ from typing import List, Union, TypeVar, overload
15
+
16
+ import yaml
17
+ import httpx
18
+ from yarl import URL
19
+ from bs4 import BeautifulSoup, element
20
+
21
+ from .utils.initial_state import get_initial_state, get_initial_state_sync
22
+ from .utils.utils import get_api, raise_for_statement
23
+ from .utils.credential import Credential
24
+ from .utils.network import Api, get_session
25
+ from .utils import cache_pool
26
+ from .exceptions.NetworkException import ApiException, NetworkException
27
+ from .video import get_cid_info_sync
28
+ from . import note
29
+ from . import opus
30
+
31
+ API = get_api("article")
32
+
33
+ # 文章颜色表
34
+ ARTICLE_COLOR_MAP = {
35
+ "default": "222222",
36
+ "blue-01": "56c1fe",
37
+ "lblue-01": "73fdea",
38
+ "green-01": "89fa4e",
39
+ "yellow-01": "fff359",
40
+ "pink-01": "ff968d",
41
+ "purple-01": "ff8cc6",
42
+ "blue-02": "02a2ff",
43
+ "lblue-02": "18e7cf",
44
+ "green-02": "60d837",
45
+ "yellow-02": "fbe231",
46
+ "pink-02": "ff654e",
47
+ "purple-02": "ef5fa8",
48
+ "blue-03": "0176ba",
49
+ "lblue-03": "068f86",
50
+ "green-03": "1db100",
51
+ "yellow-03": "f8ba00",
52
+ "pink-03": "ee230d",
53
+ "purple-03": "cb297a",
54
+ "blue-04": "004e80",
55
+ "lblue-04": "017c76",
56
+ "green-04": "017001",
57
+ "yellow-04": "ff9201",
58
+ "pink-04": "b41700",
59
+ "purple-04": "99195e",
60
+ "gray-01": "d6d5d5",
61
+ "gray-02": "929292",
62
+ "gray-03": "5f5f5f",
63
+ }
64
+
65
+
66
+ class ArticleType(Enum):
67
+ """
68
+ 专栏类型
69
+
70
+ - ARTICLE : 普通专栏,不与 opus 图文兼容。
71
+ - NOTE : 公开笔记
72
+ - SPECIAL_ARTICLE: 特殊专栏,采用笔记格式,且与 opus 图文完全兼容。
73
+ """
74
+
75
+ ARTICLE = 0
76
+ NOTE = 2
77
+ SPECIAL_ARTICLE = 3
78
+
79
+
80
+ class ArticleRankingType(Enum):
81
+ """
82
+ 专栏排行榜类型枚举。
83
+
84
+ + MONTH: 月榜
85
+ + WEEK: 周榜
86
+ + DAY_BEFORE_YESTERDAY: 前日榜
87
+ + YESTERDAY: 昨日榜
88
+ """
89
+
90
+ MONTH = 1
91
+ WEEK = 2
92
+ DAY_BEFORE_YESTERDAY = 4
93
+ YESTERDAY = 3
94
+
95
+
96
+ ArticleT = TypeVar("ArticleT", bound="Article")
97
+
98
+
99
+ async def get_article_rank(
100
+ rank_type: ArticleRankingType = ArticleRankingType.YESTERDAY,
101
+ ):
102
+ """
103
+ 获取专栏排行榜
104
+
105
+ Args:
106
+ rank_type (ArticleRankingType): 排行榜类别. Defaults to ArticleRankingType.YESTERDAY.
107
+
108
+ Returns:
109
+ dict: 调用 API 返回的结果
110
+ """
111
+ api = API["info"]["rank"]
112
+ params = {"cid": rank_type.value}
113
+ return await Api(**api).update_params(**params).result
114
+
115
+
116
+ class ArticleList:
117
+ """
118
+ 文集类
119
+
120
+ Attributes:
121
+ credential (Credential): 凭据类
122
+ """
123
+
124
+ def __init__(self, rlid: int, credential: Union[Credential, None] = None):
125
+ """
126
+ Args:
127
+ rlid (int) : 文集 id
128
+
129
+ credential (Credential | None, optional): 凭据类. Defaults to None.
130
+ """
131
+ self.__rlid = rlid
132
+ self.credential = credential
133
+
134
+ def get_rlid(self) -> int:
135
+ return self.__rlid
136
+
137
+ async def get_content(self) -> dict:
138
+ """
139
+ 获取专栏文集文章列表
140
+
141
+ Returns:
142
+ dict: 调用 API 返回的结果
143
+ """
144
+ credential = self.credential if self.credential is not None else Credential()
145
+
146
+ api = API["info"]["list"]
147
+ params = {"id": self.__rlid}
148
+ return await Api(**api, credential=credential).update_params(**params).result
149
+
150
+
151
+ class Article:
152
+ """
153
+ 专栏类
154
+
155
+ Attributes:
156
+ credential (Credential): 凭据类
157
+ """
158
+
159
+ def __init__(self, cvid: int, credential: Union[Credential, None] = None):
160
+ """
161
+ Args:
162
+ cvid (int) : cv 号
163
+
164
+ credential (Credential | None, optional): 凭据. Defaults to None.
165
+ """
166
+ self.__children: List[Node] = []
167
+ self.credential: Credential = (
168
+ credential if credential is not None else Credential()
169
+ )
170
+ self.__meta = None
171
+ self.__cvid = cvid
172
+ self.__has_parsed: bool = False
173
+
174
+ # 设置专栏类别
175
+ if cache_pool.article_is_opus.get(self.__cvid):
176
+ self.__type = ArticleType.SPECIAL_ARTICLE
177
+ else:
178
+ api = API["info"]["view"]
179
+ params = {"id": self.__cvid}
180
+ resp = Api(**api).update_params(**params).request_sync(raw=True)
181
+
182
+ if resp["code"] != 0:
183
+ self.__type = ArticleType.ARTICLE
184
+ elif resp["data"]["type"] == 0:
185
+ self.__type = ArticleType.ARTICLE
186
+ elif resp["data"]["type"] == 2:
187
+ self.__type = ArticleType.NOTE
188
+ else:
189
+ self.__type = ArticleType.SPECIAL_ARTICLE
190
+
191
+ if cache_pool.article_dyn_id.get(self.__cvid):
192
+ self.__dyn_id = cache_pool.article_dyn_id[self.__cvid]
193
+ else:
194
+ initial_state = get_initial_state_sync(
195
+ f"https://www.bilibili.com/read/cv{self.__cvid}"
196
+ )
197
+ self.__dyn_id = int(initial_state[0]["readInfo"]["dyn_id_str"])
198
+
199
+ def get_cvid(self) -> int:
200
+ return self.__cvid
201
+
202
+ def get_type(self) -> ArticleType:
203
+ """
204
+ 获取专栏类型(专栏/笔记)
205
+
206
+ Returns:
207
+ ArticleType: 专栏类型
208
+ """
209
+ return self.__type
210
+
211
+ def is_note(self) -> bool:
212
+ """
213
+ 检查专栏是否笔记
214
+
215
+ Returns:
216
+ bool: 是否笔记
217
+ """
218
+ return self.__type == ArticleType.NOTE
219
+
220
+ def turn_to_note(self) -> "note.Note":
221
+ """
222
+ 对于完全与 opus 兼容的部分的特殊专栏,将 Article 对象转换为 Dynamic 对象。
223
+
224
+ Returns:
225
+ Note: 笔记类
226
+ """
227
+ raise_for_statement(
228
+ self.__type == ArticleType.NOTE, "仅支持公开笔记 (ArticleType.NOTE)"
229
+ )
230
+ return note.Note(
231
+ cvid=self.__cvid, note_type=note.NoteType.PUBLIC, credential=self.credential
232
+ )
233
+
234
+ def turn_to_opus(self) -> "opus.Opus":
235
+ """
236
+ 对于 SPECIAL_ARTICLE,将其转为图文
237
+ """
238
+ raise_for_statement(
239
+ self.__type == ArticleType.SPECIAL_ARTICLE, "仅支持图文专栏"
240
+ )
241
+ cache_pool.opus_type[self.__dyn_id] = 1
242
+ cache_pool.opus_info[self.__dyn_id] = {"basic": {"rid_str": str(self.__cvid)}}
243
+ return opus.Opus(self.__dyn_id, credential=self.credential)
244
+
245
+ def markdown(self) -> str:
246
+ """
247
+ 转换为 Markdown
248
+
249
+ 请先调用 fetch_content()
250
+
251
+ Returns:
252
+ str: Markdown 内容
253
+ """
254
+ if not self.__has_parsed:
255
+ raise ApiException("请先调用 fetch_content()")
256
+
257
+ content = ""
258
+
259
+ for node in self.__children:
260
+ try:
261
+ markdown_text = node.markdown()
262
+ except:
263
+ continue
264
+ else:
265
+ content += markdown_text
266
+
267
+ meta_yaml = yaml.safe_dump(self.__meta, allow_unicode=True)
268
+ content = f"---\n{meta_yaml}\n---\n\n{content}"
269
+ return content
270
+
271
+ def json(self) -> dict:
272
+ """
273
+ 转换为 JSON 数据
274
+
275
+ 请先调用 fetch_content()
276
+
277
+ Returns:
278
+ dict: JSON 数据
279
+ """
280
+ if not self.__has_parsed:
281
+ raise ApiException("请先调用 fetch_content()")
282
+
283
+ return {
284
+ "type": "Article",
285
+ "meta": self.__meta,
286
+ "children": list(map(lambda x: x.json(), self.__children)),
287
+ }
288
+
289
+ async def fetch_content(self) -> None:
290
+ """
291
+ 获取并解析专栏内容
292
+
293
+ 该返回不会返回任何值,调用该方法后请再调用 `self.markdown()` 或 `self.json()` 来获取你需要的值。
294
+ """
295
+
296
+ resp = await self.get_all()
297
+
298
+ document = BeautifulSoup(f"<div>{resp['readInfo']['content']}</div>", "lxml")
299
+
300
+ async def parse(el: BeautifulSoup):
301
+ node_list = []
302
+
303
+ for e in el.contents: # type: ignore
304
+ if type(e) == element.NavigableString:
305
+ # 文本节点
306
+ node = TextNode(e) # type: ignore
307
+ node_list.append(node)
308
+ continue
309
+
310
+ e: BeautifulSoup = e
311
+ if e.name == "p":
312
+ # 段落
313
+ node = ParagraphNode()
314
+ node_list.append(node)
315
+
316
+ if "style" in e.attrs:
317
+ if "text-align: center" in e.attrs["style"]:
318
+ node.align = "center"
319
+
320
+ elif "text-align: right" in e.attrs["style"]:
321
+ node.align = "right"
322
+
323
+ else:
324
+ node.align = "left"
325
+
326
+ node.children = await parse(e)
327
+
328
+ elif e.name == "h1":
329
+ # 标题
330
+ node = HeadingNode()
331
+ node_list.append(node)
332
+
333
+ node.children = await parse(e)
334
+
335
+ elif e.name == "strong":
336
+ # 粗体
337
+ node = BoldNode()
338
+ node_list.append(node)
339
+
340
+ node.children = await parse(e)
341
+
342
+ elif e.name == "span":
343
+ # 各种样式
344
+ if "style" in e.attrs:
345
+ style = e.attrs["style"]
346
+
347
+ if "text-decoration: line-through" in style:
348
+ # 删除线
349
+ node = DelNode()
350
+ node_list.append(node)
351
+
352
+ node.children = await parse(e)
353
+
354
+ elif "class" in e.attrs:
355
+ className = e.attrs["class"][0]
356
+
357
+ if "font-size" in className:
358
+ # 字体大小
359
+ node = FontSizeNode()
360
+ node_list.append(node)
361
+
362
+ node.size = int(re.search("font-size-(\d\d)", className)[1]) # type: ignore
363
+ node.children = await parse(e)
364
+
365
+ elif "color" in className:
366
+ # 字体颜色
367
+ node = ColorNode()
368
+ node_list.append(node)
369
+
370
+ color_text = re.search("color-(.*);?", className)[1] # type: ignore
371
+ node.color = ARTICLE_COLOR_MAP[color_text]
372
+
373
+ node.children = await parse(e)
374
+ else:
375
+ if e.text != "":
376
+ # print(e.text.replace("\n", ""))
377
+ # print()
378
+ node = TextNode(e.text)
379
+ # print("Add a text node: ", e.text)
380
+ node_list.append(node)
381
+ node.children = parse(e) # type: ignore
382
+
383
+ elif e.name == "blockquote":
384
+ # 引用块
385
+ # print(e.text)
386
+ node = BlockquoteNode()
387
+ node_list.append(node)
388
+ node.children = await parse(e)
389
+
390
+ elif e.name == "figure":
391
+ if "class" in e.attrs:
392
+ className = e.attrs["class"]
393
+
394
+ if "img-box" in className:
395
+ img_el: BeautifulSoup = e.find("img") # type: ignore
396
+ if img_el == None:
397
+ pass
398
+ elif "class" in img_el.attrs:
399
+ className = img_el.attrs["class"]
400
+
401
+ if "cut-off" in className:
402
+ # 分割线
403
+ node = SeparatorNode()
404
+ node_list.append(node)
405
+
406
+ if "aid" in img_el.attrs:
407
+ # 各种卡片
408
+ aid = img_el.attrs["aid"]
409
+
410
+ if "video-card" in className:
411
+ # 视频卡片,考虑有两列视频
412
+ for a in aid.split(","):
413
+ node = VideoCardNode()
414
+ node_list.append(node)
415
+
416
+ node.aid = int(a)
417
+
418
+ elif "article-card" in className:
419
+ # 文章卡片
420
+ node = ArticleCardNode()
421
+ node_list.append(node)
422
+
423
+ node.cvid = int(aid)
424
+
425
+ elif "fanju-card" in className:
426
+ # 番剧卡片
427
+ node = BangumiCardNode()
428
+ node_list.append(node)
429
+
430
+ node.epid = int(aid[2:])
431
+
432
+ elif "music-card" in className:
433
+ # 音乐卡片
434
+ node = MusicCardNode()
435
+ node_list.append(node)
436
+
437
+ node.auid = int(aid[2:])
438
+
439
+ elif "shop-card" in className:
440
+ # 会员购卡片
441
+ node = ShopCardNode()
442
+ node_list.append(node)
443
+
444
+ node.pwid = int(aid[2:])
445
+
446
+ elif "caricature-card" in className:
447
+ # 漫画卡片,考虑有两列
448
+
449
+ for i in aid.split(","):
450
+ node = ComicCardNode()
451
+ node_list.append(node)
452
+
453
+ node.mcid = int(i)
454
+
455
+ elif "live-card" in className:
456
+ # 直播卡片
457
+ node = LiveCardNode()
458
+ node_list.append(node)
459
+
460
+ node.room_id = int(aid)
461
+ else:
462
+ # 图片节点
463
+ node = ImageNode()
464
+ node_list.append(node)
465
+
466
+ node.url = "https:" + e.find("img").attrs["data-src"] # type: ignore
467
+
468
+ figcaption_el: BeautifulSoup = e.find("figcaption") # type: ignore
469
+
470
+ if figcaption_el:
471
+ if figcaption_el.contents:
472
+ node.alt = figcaption_el.contents[0] # type: ignore
473
+
474
+ elif "code-box" in className:
475
+ # 代码块
476
+ node = CodeNode()
477
+ node_list.append(node)
478
+
479
+ pre_el: BeautifulSoup = e.find("pre") # type: ignore
480
+ node.lang = pre_el.attrs["data-lang"].split("@")[0].lower()
481
+ node.code = unquote(pre_el.attrs["codecontent"])
482
+
483
+ elif e.name == "ol":
484
+ # 有序列表
485
+ node = OlNode()
486
+ node_list.append(node)
487
+
488
+ node.children = await parse(e)
489
+
490
+ elif e.name == "li":
491
+ # 列表元素
492
+ node = LiNode()
493
+ node_list.append(node)
494
+
495
+ node.children = await parse(e)
496
+
497
+ elif e.name == "ul":
498
+ # 无序列表
499
+ node = UlNode()
500
+ node_list.append(node)
501
+
502
+ node.children = await parse(e)
503
+
504
+ elif e.name == "a":
505
+ # 超链接
506
+ if len(e.contents) == 0:
507
+ from .utils.parse_link import ResourceType, parse_link
508
+
509
+ parse_link_res = await parse_link(e.attrs["href"])
510
+ if parse_link_res[1] == ResourceType.VIDEO:
511
+ node = VideoCardNode()
512
+ node.aid = parse_link_res[0].get_aid()
513
+ node_list.append(node)
514
+ elif parse_link_res[1] == ResourceType.AUDIO:
515
+ node = MusicCardNode()
516
+ node.auid = parse_link_res[0].get_auid()
517
+ node_list.append(node)
518
+ elif parse_link_res[1] == ResourceType.LIVE:
519
+ node = LiveCardNode()
520
+ node.room_id = parse_link_res[0].room_display_id
521
+ node_list.append(node)
522
+ elif parse_link_res[1] == ResourceType.ARTICLE:
523
+ node = ArticleCardNode()
524
+ node.cvid = parse_link_res[0].get_cvid()
525
+ node_list.append(node)
526
+ else:
527
+ # XXX: 暂不支持其他的站内链接
528
+ pass
529
+ else:
530
+ node = AnchorNode()
531
+ node_list.append(node)
532
+
533
+ node.url = e.attrs["href"]
534
+ node.text = e.contents[0] # type: ignore
535
+
536
+ elif e.name == "img":
537
+ className = e.attrs.get("class")
538
+
539
+ if not className:
540
+ # 图片
541
+ node = ImageNode()
542
+ node.url = e.attrs.get("data-src") # type: ignore
543
+ node_list.append(node)
544
+
545
+ elif "latex" in className:
546
+ # 公式
547
+ node = LatexNode()
548
+ node_list.append(node)
549
+
550
+ node.code = unquote(e["alt"]) # type: ignore
551
+
552
+ return node_list
553
+
554
+ def parse_note(data: List[dict]):
555
+ for field in data:
556
+ if not isinstance(field["insert"], str):
557
+ if "tag" in field["insert"].keys():
558
+ node = VideoCardNode()
559
+ node.aid = get_cid_info_sync(field["insert"]["tag"]["cid"])[
560
+ "cid"
561
+ ]
562
+ self.__children.append(node)
563
+ elif "imageUpload" in field["insert"].keys():
564
+ node = ImageNode()
565
+ node.url = field["insert"]["imageUpload"]["url"]
566
+ self.__children.append(node)
567
+ elif "cut-off" in field["insert"].keys():
568
+ node = ImageNode()
569
+ node.url = field["insert"]["cut-off"]["url"]
570
+ self.__children.append(node)
571
+ elif "native-image" in field["insert"].keys():
572
+ node = ImageNode()
573
+ node.url = field["insert"]["native-image"]["url"]
574
+ self.__children.append(node)
575
+ else:
576
+ raise Exception()
577
+ else:
578
+ node = TextNode(field["insert"])
579
+ if "attributes" in field.keys():
580
+ if field["attributes"].get("bold") == True:
581
+ bold = BoldNode()
582
+ bold.children = [node]
583
+ node = bold
584
+ if field["attributes"].get("strike") == True:
585
+ delete = DelNode()
586
+ delete.children = [node]
587
+ node = delete
588
+ if field["attributes"].get("underline") == True:
589
+ underline = UnderlineNode()
590
+ underline.children = [node]
591
+ node = underline
592
+ if field["attributes"].get("background") == True:
593
+ # FIXME: 暂不支持背景颜色
594
+ pass
595
+ if field["attributes"].get("color") != None:
596
+ color = ColorNode()
597
+ color.color = field["attributes"]["color"].replace("#", "")
598
+ color.children = [node]
599
+ node = color
600
+ if field["attributes"].get("size") != None:
601
+ size = FontSizeNode()
602
+ size.size = field["attributes"]["size"]
603
+ size.children = [node]
604
+ node = size
605
+ else:
606
+ pass
607
+ self.__children.append(node)
608
+
609
+ # 文章元数据
610
+ self.__meta = copy(resp["readInfo"])
611
+ del self.__meta["content"]
612
+
613
+ # 解析正文
614
+ if self.__type != ArticleType.SPECIAL_ARTICLE:
615
+ self.__children = await parse(document.find("div")) # type: ignore
616
+ else:
617
+ s = resp["readInfo"]["content"]
618
+ s = unescape(s)
619
+ parse_note(json.loads(s)["ops"])
620
+
621
+ self.__has_parsed = True
622
+
623
+ async def get_info(self) -> dict:
624
+ """
625
+ 获取专栏信息
626
+
627
+ Returns:
628
+ dict: 调用 API 返回的结果
629
+ """
630
+
631
+ api = API["info"]["view"]
632
+ params = {"id": self.__cvid}
633
+ return (
634
+ await Api(**api, credential=self.credential).update_params(**params).result
635
+ )
636
+
637
+ async def get_all(self) -> dict:
638
+ """
639
+ 一次性获取专栏尽可能详细数据,包括原始内容、标签、发布时间、标题、相关专栏推荐等
640
+
641
+ Returns:
642
+ dict: 调用 API 返回的结果
643
+ """
644
+ return (
645
+ await get_initial_state(f"https://www.bilibili.com/read/cv{self.__cvid}")
646
+ )[0]
647
+
648
+ async def set_like(self, status: bool = True) -> dict:
649
+ """
650
+ 设置专栏点赞状态
651
+
652
+ Args:
653
+ status (bool, optional): 点赞状态. Defaults to True
654
+
655
+ Returns:
656
+ dict: 调用 API 返回的结果
657
+ """
658
+ self.credential.raise_for_no_sessdata()
659
+
660
+ api = API["operate"]["like"]
661
+ data = {"id": self.__cvid, "type": 1 if status else 2}
662
+ return await Api(**api, credential=self.credential).update_data(**data).result
663
+
664
+ async def set_favorite(self, status: bool = True) -> dict:
665
+ """
666
+ 设置专栏收藏状态
667
+
668
+ Args:
669
+ status (bool, optional): 收藏状态. Defaults to True
670
+
671
+ Returns:
672
+ dict: 调用 API 返回的结果
673
+ """
674
+ self.credential.raise_for_no_sessdata()
675
+
676
+ api = (
677
+ API["operate"]["add_favorite"] if status else API["operate"]["del_favorite"]
678
+ )
679
+
680
+ data = {"id": self.__cvid}
681
+ return await Api(**api, credential=self.credential).update_data(**data).result
682
+
683
+ async def add_coins(self) -> dict:
684
+ """
685
+ 给专栏投币,目前只能投一个
686
+
687
+ Returns:
688
+ dict: 调用 API 返回的结果
689
+ """
690
+ self.credential.raise_for_no_sessdata()
691
+
692
+ upid = (await self.get_info())["mid"]
693
+ api = API["operate"]["coin"]
694
+ data = {"aid": self.__cvid, "multiply": 1, "upid": upid, "avtype": 2}
695
+ return await Api(**api, credential=self.credential).update_data(**data).result
696
+
697
+ # TODO: 专栏上传/编辑/删除
698
+
699
+
700
+ class Node:
701
+ def __init__(self):
702
+ pass
703
+
704
+ @overload
705
+ def markdown(self) -> str: # type: ignore
706
+ pass
707
+
708
+ @overload
709
+ def json(self) -> dict: # type: ignore
710
+ pass
711
+
712
+
713
+ class ParagraphNode(Node):
714
+ def __init__(self):
715
+ self.children = []
716
+ self.align = "left"
717
+
718
+ def markdown(self):
719
+ content = "".join([node.markdown() for node in self.children])
720
+ return content + "\n\n"
721
+
722
+ def json(self):
723
+ return {
724
+ "type": "ParagraphNode",
725
+ "children": list(map(lambda x: x.json(), self.children)),
726
+ }
727
+
728
+
729
+ class HeadingNode(Node):
730
+ def __init__(self):
731
+ self.children = []
732
+
733
+ def markdown(self):
734
+ text = "".join([node.markdown() for node in self.children])
735
+ if len(text) == 0:
736
+ return ""
737
+ return f"## {text}\n\n"
738
+
739
+ def json(self):
740
+ return {
741
+ "type": "HeadingNode",
742
+ "children": list(map(lambda x: x.json(), self.children)),
743
+ }
744
+
745
+
746
+ class BlockquoteNode(Node):
747
+ def __init__(self):
748
+ self.children = []
749
+
750
+ def markdown(self):
751
+ t = "".join([node.markdown() for node in self.children])
752
+ # 填补空白行的 > 并加上标识符
753
+ t = "\n".join(["> " + line for line in t.split("\n")]) + "\n\n"
754
+
755
+ return t
756
+
757
+ def json(self):
758
+ return {
759
+ "type": "BlockquoteNode",
760
+ "children": list(map(lambda x: x.json(), self.children)),
761
+ }
762
+
763
+
764
+ class ItalicNode(Node):
765
+ def __init__(self):
766
+ self.children = []
767
+
768
+ def markdown(self):
769
+ text = "".join([node.markdown() for node in self.children])
770
+ if len(text) == 0:
771
+ return ""
772
+ return f" *{text}* "
773
+
774
+ def json(self):
775
+ return {
776
+ "type": "ItalicNode",
777
+ "children": list(map(lambda x: x.json(), self.children)),
778
+ }
779
+
780
+
781
+ class BoldNode(Node):
782
+ def __init__(self):
783
+ self.children = []
784
+
785
+ def markdown(self):
786
+ t = "".join([node.markdown() for node in self.children])
787
+ if len(t) == 0:
788
+ return ""
789
+ return f" **{t.lstrip().rstrip()}** "
790
+
791
+ def json(self):
792
+ return {
793
+ "type": "BoldNode",
794
+ "children": list(map(lambda x: x.json(), self.children)),
795
+ }
796
+
797
+
798
+ class DelNode(Node):
799
+ def __init__(self):
800
+ self.children = []
801
+
802
+ def markdown(self):
803
+ text = "".join([node.markdown() for node in self.children])
804
+ if len(text) == 0:
805
+ return ""
806
+ return f" ~~{text}~~ "
807
+
808
+ def json(self):
809
+ return {
810
+ "type": "DelNode",
811
+ "children": list(map(lambda x: x.json(), self.children)),
812
+ }
813
+
814
+
815
+ class UnderlineNode(Node):
816
+ def __init__(self):
817
+ self.children = []
818
+
819
+ def markdown(self):
820
+ text = "".join([node.markdown() for node in self.children])
821
+ if len(text) == 0:
822
+ return ""
823
+ return " $\\underline{" + text + "}$ "
824
+
825
+ def json(self):
826
+ return {
827
+ "type": "UnderlineNode",
828
+ "children": list(map(lambda x: x.json(), self.children)),
829
+ }
830
+
831
+
832
+ class UlNode(Node):
833
+ def __init__(self):
834
+ self.children = []
835
+
836
+ def markdown(self):
837
+ return "\n".join(["- " + node.markdown() for node in self.children])
838
+
839
+ def json(self):
840
+ return {
841
+ "type": "UlNode",
842
+ "children": list(map(lambda x: x.json(), self.children)),
843
+ }
844
+
845
+
846
+ class OlNode(Node):
847
+ def __init__(self):
848
+ self.children = []
849
+
850
+ def markdown(self):
851
+ t = []
852
+ for i, node in enumerate(self.children):
853
+ t.append(f"{i + 1}. {node.markdown()}")
854
+ return "\n".join(t)
855
+
856
+ def json(self):
857
+ return {
858
+ "type": "OlNode",
859
+ "children": list(map(lambda x: x.json(), self.children)),
860
+ }
861
+
862
+
863
+ class LiNode(Node):
864
+ def __init__(self):
865
+ self.children = []
866
+
867
+ def markdown(self):
868
+ return "".join([node.markdown() for node in self.children])
869
+
870
+ def json(self):
871
+ return {
872
+ "type": "LiNode",
873
+ "children": list(map(lambda x: x.json(), self.children)),
874
+ }
875
+
876
+
877
+ class ColorNode(Node):
878
+ def __init__(self):
879
+ self.color = "000000"
880
+ self.children = []
881
+
882
+ def markdown(self):
883
+ return "".join([node.markdown() for node in self.children])
884
+
885
+ def json(self):
886
+ return {
887
+ "type": "ColorNode",
888
+ "color": self.color,
889
+ "children": list(map(lambda x: x.json(), self.children)),
890
+ }
891
+
892
+
893
+ class FontSizeNode(Node):
894
+ def __init__(self):
895
+ self.size = 16
896
+ self.children = []
897
+
898
+ def markdown(self):
899
+ return "".join([node.markdown() for node in self.children])
900
+
901
+ def json(self):
902
+ return {
903
+ "type": "FontSizeNode",
904
+ "size": self.size,
905
+ "children": list(map(lambda x: x.json(), self.children)),
906
+ }
907
+
908
+
909
+ # 特殊节点,即无子节点
910
+
911
+
912
+ class TextNode(Node):
913
+ def __init__(self, text: str):
914
+ self.text = text
915
+
916
+ def markdown(self):
917
+ txt = self.text
918
+ txt = txt.replace("\t", " ")
919
+ txt = txt.replace(" ", "&emsp;")
920
+ txt = txt.replace(chr(160), "&emsp;")
921
+ special_chars = ["\\", "*", "$", "<", ">", "|"]
922
+ for c in special_chars:
923
+ txt = txt.replace(c, "\\" + c)
924
+ return txt
925
+
926
+ def json(self):
927
+ return {"type": "TextNode", "text": self.text}
928
+
929
+
930
+ class ImageNode(Node):
931
+ def __init__(self):
932
+ self.url = ""
933
+ self.alt = ""
934
+
935
+ def markdown(self):
936
+ if URL(self.url).scheme == "":
937
+ self.url = "https:" + self.url
938
+ alt = self.alt.replace("[", "\\[")
939
+ return f"![{alt}]({self.url})\n\n"
940
+
941
+ def json(self):
942
+ if URL(self.url).scheme == "":
943
+ self.url = "https:" + self.url
944
+ return {"type": "ImageNode", "url": self.url, "alt": self.alt}
945
+
946
+
947
+ class LatexNode(Node):
948
+ def __init__(self):
949
+ self.code = ""
950
+
951
+ def markdown(self):
952
+ if "\n" in self.code:
953
+ # 块级公式
954
+ return f"$$\n{self.code}\n$$"
955
+ else:
956
+ # 行内公式
957
+ return f"${self.code}$"
958
+
959
+ def json(self):
960
+ return {"type": "LatexNode", "code": self.code}
961
+
962
+
963
+ class CodeNode(Node):
964
+ def __init__(self):
965
+ self.code = ""
966
+ self.lang = ""
967
+
968
+ def markdown(self):
969
+ return f"```{self.lang if self.lang else ''}\n{self.code}\n```\n\n"
970
+
971
+ def json(self):
972
+ return {"type": "CodeNode", "code": self.code, "lang": self.lang}
973
+
974
+
975
+ # 卡片
976
+
977
+
978
+ class VideoCardNode(Node):
979
+ def __init__(self):
980
+ self.aid = 0
981
+
982
+ def markdown(self):
983
+ return f"[视频 av{self.aid}](https://www.bilibili.com/av{self.aid})\n\n"
984
+
985
+ def json(self):
986
+ return {"type": "VideoCardNode", "aid": self.aid}
987
+
988
+
989
+ class ArticleCardNode(Node):
990
+ def __init__(self):
991
+ self.cvid = 0
992
+
993
+ def markdown(self):
994
+ return f"[文章 cv{self.cvid}](https://www.bilibili.com/read/cv{self.cvid})\n\n"
995
+
996
+ def json(self):
997
+ return {"type": "ArticleCardNode", "cvid": self.cvid}
998
+
999
+
1000
+ class BangumiCardNode(Node):
1001
+ def __init__(self):
1002
+ self.epid = 0
1003
+
1004
+ def markdown(self):
1005
+ return f"[番剧 ep{self.epid}](https://www.bilibili.com/bangumi/play/ep{self.epid})\n\n"
1006
+
1007
+ def json(self):
1008
+ return {"type": "BangumiCardNode", "epid": self.epid}
1009
+
1010
+
1011
+ class MusicCardNode(Node):
1012
+ def __init__(self):
1013
+ self.auid = 0
1014
+
1015
+ def markdown(self):
1016
+ return f"[音乐 au{self.auid}](https://www.bilibili.com/audio/au{self.auid})\n\n"
1017
+
1018
+ def json(self):
1019
+ return {"type": "MusicCardNode", "auid": self.auid}
1020
+
1021
+
1022
+ class ShopCardNode(Node):
1023
+ def __init__(self):
1024
+ self.pwid = 0
1025
+
1026
+ def markdown(self):
1027
+ return f"[会员购 {self.pwid}](https://show.bilibili.com/platform/detail.html?id={self.pwid})\n\n"
1028
+
1029
+ def json(self):
1030
+ return {"type": "ShopCardNode", "pwid": self.pwid}
1031
+
1032
+
1033
+ class ComicCardNode(Node):
1034
+ def __init__(self):
1035
+ self.mcid = 0
1036
+
1037
+ def markdown(self):
1038
+ return (
1039
+ f"[漫画 mc{self.mcid}](https://manga.bilibili.com/m/detail/mc{self.mcid})\n\n"
1040
+ )
1041
+
1042
+ def json(self):
1043
+ return {"type": "ComicCardNode", "mcid": self.mcid}
1044
+
1045
+
1046
+ class LiveCardNode(Node):
1047
+ def __init__(self):
1048
+ self.room_id = 0
1049
+
1050
+ def markdown(self):
1051
+ return f"[直播 {self.room_id}](https://live.bilibili.com/{self.room_id})\n\n"
1052
+
1053
+ def json(self):
1054
+ return {"type": "LiveCardNode", "room_id": self.room_id}
1055
+
1056
+
1057
+ class AnchorNode(Node):
1058
+ def __init__(self):
1059
+ self.url = ""
1060
+ self.text = ""
1061
+
1062
+ def markdown(self):
1063
+ text = self.text.replace("[", "\\[")
1064
+ return f"[{text}]({self.url})"
1065
+
1066
+ def json(self):
1067
+ return {"type": "AnchorNode", "url": self.url, "text": self.text}
1068
+
1069
+
1070
+ class SeparatorNode(Node):
1071
+ def __init__(self):
1072
+ pass
1073
+
1074
+ def markdown(self):
1075
+ return "\n------\n"
1076
+
1077
+ def json(self):
1078
+ return {"type": "SeparatorNode"}